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

feat: get creating, updating, deleting Integrations working #240

Merged
merged 1 commit into from
Mar 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of this file can be DRYed

export async function updateRemoteIntegration(data: any) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

data can be typed

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>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add in integration category

</Stack>
}
action={
<Switch
checked={enabled}
onClick={() => {
trigger({ ...integration, enabled: !enabled });
}}
disabled={true}
checked={integration?.isEnabled}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

data-drive

// 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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like integration is already passed in as a prop -- why do we need to do this?

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)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ICON_SIZE?

<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: '',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this line necessary? Does it get overridden by the next line?

...integration?.config,
oauth: {
...integration?.config?.oauth,
credentials: {
oauthClientId: clientId,
oauthClientSecret: clientSecret,
},
oauthScopes: oauthScopes.split(','),
},
sync: {
periodMs: 60 * 60 * 1000,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is 1x/hour what we want?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now

},
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
Loading