Skip to content
This repository has been archived by the owner on Mar 10, 2024. It is now read-only.

Commit

Permalink
feat: get creating, updating, deleting Integrations working (#240)
Browse files Browse the repository at this point in the history
- Prisma migration for `Integration`: adds `is_enabled` and makes
`config` optional
- Use Nextjs `useSWR`'s `mutate` to fetch and make updates
- Get creating, updating, deleting Integrations working
- Update openapi spec for creating/updating integrations

SWR related reading for using mutating local and remote state:
- vercel/swr#157
- vercel/swr#1745
- https://github.com/vercel/swr-site/pull/202/files

Other:
- Remove extra /dashboard page, use root instead for dashboard
- Use `Card` for `MetricCard` rather than `Grid`
- Rename `Active` tab to `Installed`
- Rename `Application 1` to `Your application`
- others
  • Loading branch information
tomkit committed Mar 11, 2023
1 parent 983d2ba commit f3cc980
Show file tree
Hide file tree
Showing 38 changed files with 469 additions and 328 deletions.
2 changes: 2 additions & 0 deletions apps/mgmt-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@
"@types/react": "18.0.28",
"@types/react-dom": "18.0.11",
"autoprefixer": "^10.4.13",
"camelcase-keys": "^8.0.2",
"dayjs": "^1.11.7",
"eslint": "8.35.0",
"eslint-config-next": "13.2.3",
"next": "13.2.3",
"postcss": "^8.4.21",
"react": "18.2.0",
"react-dom": "18.2.0",
"snakecase-keys": "^5.4.5",
"swr": "^2.1.0",
"tailwindcss": "^3.2.7",
"typescript": "4.9.5"
Expand Down
Binary file modified apps/mgmt-ui/public/favicon.ico
Binary file not shown.
49 changes: 49 additions & 0 deletions apps/mgmt-ui/src/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { snakecaseKeys } from './utils/snakecase';

export const API_HOST = 'http://localhost:8080';

// TODO: get this on the server-side from the session
export const APPLICATION_ID = 'a4398523-03a2-42dd-9681-c91e3e2efaf4';

// TODO: use Supaglue TS client
export async function updateRemoteIntegration(data: any) {
const result = await fetch(`${API_HOST}/mgmt/v1/applications/${APPLICATION_ID}/integrations/${data.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(snakecaseKeys(data)),
});

const r = await result.json();
return r;
}

// TODO: use Supaglue TS client
export async function createRemoteIntegration(data: any) {
const result = await fetch(`${API_HOST}/mgmt/v1/applications/${APPLICATION_ID}/integrations`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(snakecaseKeys(data)),
});

const r = await result.json();
return r;
}

// TODO: use Supaglue TS client
export async function removeRemoteIntegration(data: any) {
const result = await fetch(`${API_HOST}/mgmt/v1/applications/${APPLICATION_ID}/integrations/${data.id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
});

const r = await result.json();
return r;
}

// TODO: add other calls
58 changes: 42 additions & 16 deletions apps/mgmt-ui/src/components/configuration/IntegrationCard.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
/* eslint-disable @typescript-eslint/no-floating-promises */
import { sendRequest } from '@/sendRequests';
import { Button, Card, CardContent, CardHeader, Divider, Grid, Switch } from '@mui/material';
import { useIntegration } from '@/hooks/useIntegration';
import { useIntegrations } from '@/hooks/useIntegrations';
import { Button, Card, CardContent, CardHeader, Divider, Grid, Stack, Switch, Typography } from '@mui/material';
import { Box } from '@mui/system';
import { useRouter } from 'next/router';
import useSWRMutation from 'swr/mutation';
import { Integration, IntegrationCardInfo } from './VerticalTabs';

export default function IntegrationCard(props: {
integration: Integration;
integrationInfo: IntegrationCardInfo;
enabled: boolean;
}) {
export default function IntegrationCard(props: { integration: Integration; integrationInfo: IntegrationCardInfo }) {
const router = useRouter();
const { trigger } = useSWRMutation('/mgmt/v1/integrations', sendRequest);
const { enabled, integration } = props;
const { icon, name, description, category, providerName } = props.integrationInfo;
const { integration } = props;
const { integrations: existingIntegrations = [], mutate } = useIntegrations();
const { mutate: mutateIntegration } = useIntegration(integration?.id); // TODO: run this when there's an integration only

const { icon, name, description, category, status, providerName } = props.integrationInfo;

return (
<Card
classes={{
Expand All @@ -24,13 +23,40 @@ export default function IntegrationCard(props: {
<Box>
<CardHeader
avatar={icon}
subheader={name}
subheader={
<Stack direction="column">
<Typography>{name}</Typography>
<Typography fontSize={12}>{status === 'auth-only' ? status : category.toUpperCase()}</Typography>
</Stack>
}
action={
<Switch
checked={enabled}
onClick={() => {
trigger({ ...integration, enabled: !enabled });
}}
disabled={true}
checked={integration?.isEnabled}
// onClick={() => {
// if (!integration) {
// const newIntegration = {
// authType: 'oauth2',
// category,
// providerName,
// isEnabled: true, // TODO: we need another notion of live vs enabled
// applicationId: APPLICATION_ID,
// };
// const updatedIntegrations = [...existingIntegrations, newIntegration];

// mutate(updatedIntegrations, false);
// mutateIntegration(createRemoteIntegration(newIntegration), false);
// return;
// }

// const updatedIntegration = { ...integration, isEnabled: !integration?.isEnabled };
// const updatedIntegrations = existingIntegrations.map((ei: Integration) =>
// ei.id === updatedIntegration.id ? updatedIntegration : ei
// );

// mutate(updatedIntegrations, false);
// mutateIntegration(updateRemoteIntegration(updatedIntegration), false);
// }}
></Switch>
}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
/* eslint-disable @typescript-eslint/no-floating-promises */
import { sendRequest } from '@/sendRequests';
import { removeRemoteIntegration, updateRemoteIntegration } from '@/client';
import { useIntegration } from '@/hooks/useIntegration';
import { useIntegrations } from '@/hooks/useIntegrations';
import providerToIcon from '@/utils/providerToIcon';
import { Box, Button, Stack, Switch, TextField, Typography } from '@mui/material';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import useSWRMutation from 'swr/mutation';
import { Integration, IntegrationCardInfo } from './VerticalTabs';

export type IntegrationDetailTabPanelProps = {
Expand All @@ -17,8 +19,10 @@ export default function IntegrationDetailTabPanel(props: IntegrationDetailTabPan
const [clientId, setClientId] = useState('');
const [clientSecret, setClientSecret] = useState('');
const [oauthScopes, setOauthScopes] = useState('');
const router = useRouter();

const { trigger } = useSWRMutation('/mgmt/v1/integrations', sendRequest);
const { mutate: mutateIntegration } = useIntegration(integration?.id);
const { integrations: existingIntegrations = [], mutate } = useIntegrations();

useEffect(() => {
setClientId(integration?.config?.oauth?.credentials?.oauthClientId);
Expand All @@ -29,12 +33,19 @@ export default function IntegrationDetailTabPanel(props: IntegrationDetailTabPan
return (
<Stack direction="column" className="gap-4">
<Stack direction="row" className="items-center justify-between w-full">
<Stack direction="row">
{providerToIcon(integrationCardInfo.providerName)}
<Typography variant="subtitle1">{integrationCardInfo.name}</Typography>
<Stack direction="row" className="items-center justify-center gap-2">
{providerToIcon(integrationCardInfo.providerName, 35)}
<Stack direction="column">
<Typography variant="subtitle1">{integrationCardInfo.name}</Typography>
<Typography fontSize={12}>
{integrationCardInfo.status === 'auth-only'
? integrationCardInfo.status
: integrationCardInfo.category.toUpperCase()}
</Typography>
</Stack>
</Stack>
<Box>
<Switch></Switch>
<Switch checked={integration?.isEnabled}></Switch>
</Box>
</Stack>

Expand Down Expand Up @@ -72,29 +83,62 @@ export default function IntegrationDetailTabPanel(props: IntegrationDetailTabPan
}}
/>
</Stack>
<Stack direction="row" className="gap-2">
<Button variant="outlined">Cancel</Button>{' '}
<Button
variant="contained"
onClick={() => {
trigger({
...integration,
config: {
...integration?.config,
oauth: {
credentials: {
oauthClientId: clientId,
oauthClientSecret: clientSecret,
<Stack direction="row" className="gap-2 justify-between">
<Stack direction="row" className="gap-2">
<Button
variant="outlined"
onClick={() => {
router.back();
}}
>
Cancel
</Button>
<Button
variant="contained"
onClick={() => {
const newIntegration = {
...integration,
config: {
providerAppId: '',
...integration?.config,
oauth: {
...integration?.config?.oauth,
credentials: {
oauthClientId: clientId,
oauthClientSecret: clientSecret,
},
oauthScopes: oauthScopes.split(','),
},
sync: {
periodMs: 60 * 60 * 1000,
},
oauthScopes: oauthScopes.split(','),
...integration?.config?.oauth,
},
},
});
}}
>
Save
</Button>
};
const updatedIntegrations = existingIntegrations.map((ei: Integration) =>
ei.id === newIntegration.id ? newIntegration : ei
);

mutate(updatedIntegrations, false);
mutateIntegration(updateRemoteIntegration(newIntegration), false);
}}
>
Save
</Button>
</Stack>
<Stack direction="row" className="gap-2">
<Button
variant="text"
color="error"
onClick={() => {
const updatedIntegrations = existingIntegrations.filter((ei: Integration) => ei.id !== integration.id);
mutate(updatedIntegrations, false);
mutateIntegration(removeRemoteIntegration(integration), false);
router.back();
}}
>
Delete
</Button>
</Stack>
</Stack>
</Stack>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,25 @@ import { Integration, IntegrationCardInfo } from './VerticalTabs';

export type IntegrationTabPanelProps = {
integrationCardsInfo: IntegrationCardInfo[];
activeIntegrations: Integration[];
existingIntegrations: Integration[];
status: string;
};

export default function IntegrationTabPanel(props: IntegrationTabPanelProps) {
const { integrationCardsInfo, activeIntegrations, status } = props;
const { integrationCardsInfo, existingIntegrations, status } = props;

return (
<Grid container spacing={2}>
{integrationCardsInfo
.filter((info) => info.status === status)
.map((info) => {
const activeIntegration = activeIntegrations.find(
const existingIntegration = existingIntegrations.find(
(integration: Integration) => info.providerName === integration.providerName
);

return (
<Grid key={info.name} item xs={6}>
<IntegrationCard
enabled={Boolean(activeIntegration)}
integration={activeIntegration}
integrationInfo={info}
/>
<IntegrationCard integration={existingIntegration} integrationInfo={info} />
</Grid>
);
})}
Expand Down

0 comments on commit f3cc980

Please sign in to comment.