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

Commit 92d5053

Browse files
committed
feat: get creating, updating, deleting Integrations working-ish
1 parent 3cd6151 commit 92d5053

33 files changed

+447
-308
lines changed

apps/mgmt-ui/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@
1919
"@types/react": "18.0.28",
2020
"@types/react-dom": "18.0.11",
2121
"autoprefixer": "^10.4.13",
22+
"camelcase-keys": "^8.0.2",
2223
"dayjs": "^1.11.7",
2324
"eslint": "8.35.0",
2425
"eslint-config-next": "13.2.3",
2526
"next": "13.2.3",
2627
"postcss": "^8.4.21",
2728
"react": "18.2.0",
2829
"react-dom": "18.2.0",
30+
"snakecase-keys": "^5.4.5",
2931
"swr": "^2.1.0",
3032
"tailwindcss": "^3.2.7",
3133
"typescript": "4.9.5"

apps/mgmt-ui/src/client.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { snakecaseKeys } from './utils/snakecase';
2+
3+
export const API_HOST = 'http://localhost:8080';
4+
5+
// TODO: get this on the server-side from the session
6+
export const APPLICATION_ID = 'a4398523-03a2-42dd-9681-c91e3e2efaf4';
7+
8+
// TODO: use Supaglue TS client
9+
export async function updateRemoteIntegration(data: any) {
10+
const result = await fetch(`${API_HOST}/mgmt/v1/applications/${APPLICATION_ID}/integrations/${data.id}`, {
11+
method: 'PUT',
12+
headers: {
13+
'Content-Type': 'application/json',
14+
},
15+
body: JSON.stringify(snakecaseKeys(data)),
16+
});
17+
18+
const r = await result.json();
19+
return r;
20+
}
21+
22+
// TODO: use Supaglue TS client
23+
export async function createRemoteIntegration(data: any) {
24+
const result = await fetch(`${API_HOST}/mgmt/v1/applications/${APPLICATION_ID}/integrations`, {
25+
method: 'POST',
26+
headers: {
27+
'Content-Type': 'application/json',
28+
},
29+
body: JSON.stringify(snakecaseKeys(data)),
30+
});
31+
32+
const r = await result.json();
33+
return r;
34+
}
35+
36+
// TODO: use Supaglue TS client
37+
export async function removeRemoteIntegration(data: any) {
38+
const result = await fetch(`${API_HOST}/mgmt/v1/applications/${APPLICATION_ID}/integrations/${data.id}`, {
39+
method: 'DELETE',
40+
headers: {
41+
'Content-Type': 'application/json',
42+
},
43+
});
44+
45+
const r = await result.json();
46+
return r;
47+
}
48+
49+
// TODO: add other calls

apps/mgmt-ui/src/components/configuration/IntegrationCard.tsx

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
/* eslint-disable @typescript-eslint/no-floating-promises */
2-
import { sendRequest } from '@/sendRequests';
3-
import { Button, Card, CardContent, CardHeader, Divider, Grid, Switch } from '@mui/material';
2+
import { APPLICATION_ID, createRemoteIntegration, updateRemoteIntegration } from '@/client';
3+
import { useIntegration } from '@/hooks/useIntegration';
4+
import { useIntegrations } from '@/hooks/useIntegrations';
5+
import { Button, Card, CardContent, CardHeader, Divider, Grid, Stack, Switch, Typography } from '@mui/material';
46
import { Box } from '@mui/system';
57
import { useRouter } from 'next/router';
6-
import useSWRMutation from 'swr/mutation';
78
import { Integration, IntegrationCardInfo } from './VerticalTabs';
89

9-
export default function IntegrationCard(props: {
10-
integration: Integration;
11-
integrationInfo: IntegrationCardInfo;
12-
enabled: boolean;
13-
}) {
10+
export default function IntegrationCard(props: { integration: Integration; integrationInfo: IntegrationCardInfo }) {
1411
const router = useRouter();
15-
const { trigger } = useSWRMutation('/mgmt/v1/integrations', sendRequest);
16-
const { enabled, integration } = props;
17-
const { icon, name, description, category, providerName } = props.integrationInfo;
12+
const { integration } = props;
13+
const { integrations: existingIntegrations = [], mutate } = useIntegrations();
14+
const { mutate: mutateIntegration } = useIntegration(integration?.id); // TODO: run this when there's an integration only
15+
16+
const { icon, name, description, category, status, providerName } = props.integrationInfo;
17+
1818
return (
1919
<Card
2020
classes={{
@@ -24,12 +24,38 @@ export default function IntegrationCard(props: {
2424
<Box>
2525
<CardHeader
2626
avatar={icon}
27-
subheader={name}
27+
subheader={
28+
<Stack direction="column">
29+
<Typography>{name}</Typography>
30+
<Typography fontSize={12}>{status === 'auth-only' ? status : category.toUpperCase()}</Typography>
31+
</Stack>
32+
}
2833
action={
2934
<Switch
30-
checked={enabled}
35+
checked={integration?.isEnabled}
3136
onClick={() => {
32-
trigger({ ...integration, enabled: !enabled });
37+
if (!integration) {
38+
const newIntegration = {
39+
authType: 'oauth2',
40+
category,
41+
providerName,
42+
isEnabled: true, // TODO: we need another notion of live vs enabled
43+
applicationId: APPLICATION_ID,
44+
};
45+
const updatedIntegrations = [...existingIntegrations, newIntegration];
46+
47+
mutate(updatedIntegrations, false);
48+
mutateIntegration(createRemoteIntegration(newIntegration), false);
49+
return;
50+
}
51+
52+
const updatedIntegration = { ...integration, isEnabled: !integration?.isEnabled };
53+
const updatedIntegrations = existingIntegrations.map((ei: Integration) =>
54+
ei.id === updatedIntegration.id ? updatedIntegration : ei
55+
);
56+
57+
mutate(updatedIntegrations, false);
58+
mutateIntegration(updateRemoteIntegration(updatedIntegration), false);
3359
}}
3460
></Switch>
3561
}

apps/mgmt-ui/src/components/configuration/IntegrationDetailTabPanel.tsx

Lines changed: 71 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
/* eslint-disable @typescript-eslint/no-floating-promises */
2-
import { sendRequest } from '@/sendRequests';
2+
import { removeRemoteIntegration, updateRemoteIntegration } from '@/client';
3+
import { useIntegration } from '@/hooks/useIntegration';
4+
import { useIntegrations } from '@/hooks/useIntegrations';
35
import providerToIcon from '@/utils/providerToIcon';
46
import { Box, Button, Stack, Switch, TextField, Typography } from '@mui/material';
7+
import { useRouter } from 'next/router';
58
import { useEffect, useState } from 'react';
6-
import useSWRMutation from 'swr/mutation';
79
import { Integration, IntegrationCardInfo } from './VerticalTabs';
810

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

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

2327
useEffect(() => {
2428
setClientId(integration?.config?.oauth?.credentials?.oauthClientId);
@@ -29,9 +33,16 @@ export default function IntegrationDetailTabPanel(props: IntegrationDetailTabPan
2933
return (
3034
<Stack direction="column" className="gap-4">
3135
<Stack direction="row" className="items-center justify-between w-full">
32-
<Stack direction="row">
33-
{providerToIcon(integrationCardInfo.providerName)}
34-
<Typography variant="subtitle1">{integrationCardInfo.name}</Typography>
36+
<Stack direction="row" className="items-center justify-center gap-2">
37+
{providerToIcon(integrationCardInfo.providerName, 35)}
38+
<Stack direction="column">
39+
<Typography variant="subtitle1">{integrationCardInfo.name}</Typography>
40+
<Typography fontSize={12}>
41+
{integrationCardInfo.status === 'auth-only'
42+
? integrationCardInfo.status
43+
: integrationCardInfo.category.toUpperCase()}
44+
</Typography>
45+
</Stack>
3546
</Stack>
3647
<Box>
3748
<Switch></Switch>
@@ -72,29 +83,62 @@ export default function IntegrationDetailTabPanel(props: IntegrationDetailTabPan
7283
}}
7384
/>
7485
</Stack>
75-
<Stack direction="row" className="gap-2">
76-
<Button variant="outlined">Cancel</Button>{' '}
77-
<Button
78-
variant="contained"
79-
onClick={() => {
80-
trigger({
81-
...integration,
82-
config: {
83-
...integration?.config,
84-
oauth: {
85-
credentials: {
86-
oauthClientId: clientId,
87-
oauthClientSecret: clientSecret,
86+
<Stack direction="row" className="gap-2 justify-between">
87+
<Stack direction="row" className="gap-2">
88+
<Button
89+
variant="outlined"
90+
onClick={() => {
91+
router.back();
92+
}}
93+
>
94+
Cancel
95+
</Button>{' '}
96+
<Button
97+
variant="contained"
98+
onClick={() => {
99+
const newIntegration = {
100+
...integration,
101+
config: {
102+
providerAppId: '',
103+
...integration?.config,
104+
oauth: {
105+
...integration?.config?.oauth,
106+
credentials: {
107+
oauthClientId: clientId,
108+
oauthClientSecret: clientSecret,
109+
},
110+
oauthScopes: oauthScopes.split(','),
111+
},
112+
sync: {
113+
periodMs: 60 * 60 * 1000,
88114
},
89-
oauthScopes: oauthScopes.split(','),
90-
...integration?.config?.oauth,
91115
},
92-
},
93-
});
94-
}}
95-
>
96-
Save
97-
</Button>
116+
};
117+
const updatedIntegrations = existingIntegrations.map((ei: Integration) =>
118+
ei.id === newIntegration.id ? newIntegration : ei
119+
);
120+
121+
mutate(updatedIntegrations, false);
122+
mutateIntegration(updateRemoteIntegration(newIntegration), false);
123+
}}
124+
>
125+
Save
126+
</Button>
127+
</Stack>
128+
<Stack direction="row" className="gap-2">
129+
<Button
130+
variant="text"
131+
color="error"
132+
onClick={() => {
133+
const updatedIntegrations = existingIntegrations.filter((ei: Integration) => ei.id !== integration.id);
134+
mutate(updatedIntegrations, false);
135+
mutateIntegration(removeRemoteIntegration(integration), false);
136+
router.back();
137+
}}
138+
>
139+
Delete
140+
</Button>
141+
</Stack>
98142
</Stack>
99143
</Stack>
100144
);

apps/mgmt-ui/src/components/configuration/IntegrationTabPanel.tsx

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,25 @@ import { Integration, IntegrationCardInfo } from './VerticalTabs';
44

55
export type IntegrationTabPanelProps = {
66
integrationCardsInfo: IntegrationCardInfo[];
7-
activeIntegrations: Integration[];
7+
existingIntegrations: Integration[];
88
status: string;
99
};
1010

1111
export default function IntegrationTabPanel(props: IntegrationTabPanelProps) {
12-
const { integrationCardsInfo, activeIntegrations, status } = props;
12+
const { integrationCardsInfo, existingIntegrations, status } = props;
1313

1414
return (
1515
<Grid container spacing={2}>
1616
{integrationCardsInfo
1717
.filter((info) => info.status === status)
1818
.map((info) => {
19-
const activeIntegration = activeIntegrations.find(
19+
const existingIntegration = existingIntegrations.find(
2020
(integration: Integration) => info.providerName === integration.providerName
2121
);
2222

2323
return (
2424
<Grid key={info.name} item xs={6}>
25-
<IntegrationCard
26-
enabled={Boolean(activeIntegration)}
27-
integration={activeIntegration}
28-
integrationInfo={info}
29-
/>
25+
<IntegrationCard integration={existingIntegration} integrationInfo={info} />
3026
</Grid>
3127
);
3228
})}

0 commit comments

Comments
 (0)