From 47a59224bb126d7121d72f5747fc11d10e789912 Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Tue, 5 Sep 2023 14:14:03 +0200 Subject: [PATCH] Integration card component (#4557) ![image](https://github.com/Unleash/unleash/assets/2625371/42364fdb-1ff1-48c4-9756-a145a39e45b9) ## About the changes Closes # ### Important files ## Discussion points --- .../CreateIntegration/CreateIntegration.tsx | 6 +- .../EditIntegration/EditIntegration.tsx | 4 +- .../IntegrationForm/IntegrationForm.tsx | 31 ++-- .../IntegrationParameter.tsx | 13 +- .../IntegrationParameters.tsx | 6 +- .../AddonNameCell.tsx} | 10 +- .../IntegrationList/AddonsList.tsx | 21 +++ .../AvailableAddons/AvailableAddons.tsx | 19 +-- .../ConfigureAddonsButton.tsx | 4 +- .../AvailableIntegrations.tsx | 38 ++++- .../ConfiguredAddons/ConfiguredAddons.tsx | 14 +- .../ConfiguredAddonsActionsCell.tsx | 8 +- .../ConfiguredIntegrations.tsx | 61 +++++++ .../IntegrationCard/IntegrationCard.tsx | 101 ++++++++++++ .../IntegrationCardMenu.tsx | 150 ++++++++++++++++++ .../IntegrationList.styles.tsx | 8 + .../IntegrationList/IntegrationList.tsx | 36 +++-- frontend/src/component/menu/routes.ts | 3 +- .../api/actions/useAddonsApi/useAddonsApi.ts | 6 +- .../hooks/api/getters/useAddons/useAddons.ts | 9 +- frontend/src/interfaces/addons.ts | 58 ------- src/server-dev.ts | 1 + 22 files changed, 458 insertions(+), 149 deletions(-) rename frontend/src/component/integrations/IntegrationList/{IntegrationNameCell/IntegrationNameCell.tsx => AddonNameCell/AddonNameCell.tsx} (84%) create mode 100644 frontend/src/component/integrations/IntegrationList/AddonsList.tsx create mode 100644 frontend/src/component/integrations/IntegrationList/ConfiguredIntegrations/ConfiguredIntegrations.tsx create mode 100644 frontend/src/component/integrations/IntegrationList/IntegrationCard/IntegrationCard.tsx create mode 100644 frontend/src/component/integrations/IntegrationList/IntegrationCard/IntegrationCardMenu/IntegrationCardMenu.tsx create mode 100644 frontend/src/component/integrations/IntegrationList/IntegrationList.styles.tsx delete mode 100644 frontend/src/interfaces/addons.ts diff --git a/frontend/src/component/integrations/CreateIntegration/CreateIntegration.tsx b/frontend/src/component/integrations/CreateIntegration/CreateIntegration.tsx index 0158ab9957..0d827739d7 100644 --- a/frontend/src/component/integrations/CreateIntegration/CreateIntegration.tsx +++ b/frontend/src/component/integrations/CreateIntegration/CreateIntegration.tsx @@ -1,10 +1,10 @@ import useAddons from 'hooks/api/getters/useAddons/useAddons'; import { IntegrationForm } from '../IntegrationForm/IntegrationForm'; import cloneDeep from 'lodash.clonedeep'; -import { IAddon } from 'interfaces/addons'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; +import { AddonSchema } from 'openapi'; -export const DEFAULT_DATA = { +export const DEFAULT_DATA: Omit = { provider: '', description: '', enabled: true, @@ -12,7 +12,7 @@ export const DEFAULT_DATA = { events: [], projects: [], environments: [], -} as unknown as IAddon; // TODO: improve type +}; export const CreateIntegration = () => { const providerId = useRequiredPathParam('providerId'); diff --git a/frontend/src/component/integrations/EditIntegration/EditIntegration.tsx b/frontend/src/component/integrations/EditIntegration/EditIntegration.tsx index a3ca3acee6..278ae49040 100644 --- a/frontend/src/component/integrations/EditIntegration/EditIntegration.tsx +++ b/frontend/src/component/integrations/EditIntegration/EditIntegration.tsx @@ -1,9 +1,9 @@ import useAddons from 'hooks/api/getters/useAddons/useAddons'; import { IntegrationForm } from '../IntegrationForm/IntegrationForm'; import cloneDeep from 'lodash.clonedeep'; -import { IAddon } from 'interfaces/addons'; import { DEFAULT_DATA } from '../CreateIntegration/CreateIntegration'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; +import { AddonSchema } from 'openapi'; export const EditIntegration = () => { const addonId = useRequiredPathParam('addonId'); @@ -11,7 +11,7 @@ export const EditIntegration = () => { const editMode = true; const addon = addons.find( - (addon: IAddon) => addon.id === Number(addonId) + (addon: AddonSchema) => addon.id === Number(addonId) ) || { ...cloneDeep(DEFAULT_DATA) }; const provider = addon ? providers.find(provider => provider.name === addon.provider) diff --git a/frontend/src/component/integrations/IntegrationForm/IntegrationForm.tsx b/frontend/src/component/integrations/IntegrationForm/IntegrationForm.tsx index 9765535c58..6056beb14e 100644 --- a/frontend/src/component/integrations/IntegrationForm/IntegrationForm.tsx +++ b/frontend/src/component/integrations/IntegrationForm/IntegrationForm.tsx @@ -15,7 +15,7 @@ import { } from '@mui/material'; import produce from 'immer'; import { trim } from 'component/common/util'; -import { IAddon, IAddonProvider } from 'interfaces/addons'; +import type { AddonSchema, AddonTypeSchema } from 'openapi'; import { IntegrationParameters } from './IntegrationParameters/IntegrationParameters'; import { IntegrationInstall } from './IntegrationInstall/IntegrationInstall'; import cloneDeep from 'lodash.clonedeep'; @@ -49,14 +49,14 @@ import { GO_BACK } from 'constants/navigate'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { IntegrationDeleteDialog } from './IntegrationDeleteDialog/IntegrationDeleteDialog'; -interface IAddonFormProps { - provider?: IAddonProvider; - addon: IAddon; +type IntegrationFormProps = { + provider?: AddonTypeSchema; fetch: () => void; editMode: boolean; -} + addon: AddonSchema | Omit; +}; -export const IntegrationForm: VFC = ({ +export const IntegrationForm: VFC = ({ editMode, provider, addon: initialValues, @@ -77,7 +77,7 @@ export const IntegrationForm: VFC = ({ value: environment.name, label: environment.name, })); - const selectableEvents = provider?.events.map(event => ({ + const selectableEvents = provider?.events?.map(event => ({ value: event, label: event, })); @@ -97,7 +97,7 @@ export const IntegrationForm: VFC = ({ }); const submitText = editMode ? 'Update' : 'Create'; let url = `${uiConfig.unleashUrl}/api/admin/addons${ - editMode ? `/${formValues.id}` : `` + editMode ? `/${(formValues as AddonSchema).id}` : `` }`; const formatApiCode = () => { @@ -207,8 +207,9 @@ export const IntegrationForm: VFC = ({ updatedErrors.containsErrors = true; } - provider.parameters.forEach(parameterConfig => { - const value = trim(formValues.parameters[parameterConfig.name]); + provider.parameters?.forEach(parameterConfig => { + let value = formValues.parameters[parameterConfig.name]; + value = typeof value === 'string' ? trim(value) : value; if (parameterConfig.required && !value) { updatedErrors.parameters[parameterConfig.name] = 'This field is required'; @@ -223,14 +224,14 @@ export const IntegrationForm: VFC = ({ try { if (editMode) { - await updateAddon(formValues); + await updateAddon(formValues as AddonSchema); navigate('/addons'); setToastData({ type: 'success', title: 'Addon updated successfully', }); } else { - await createAddon(formValues); + await createAddon(formValues as Omit); navigate('/addons'); setToastData({ type: 'success', @@ -255,7 +256,7 @@ export const IntegrationForm: VFC = ({ documentationUrl = 'https://unleash.github.io/docs/addons', installation, alerts, - } = provider ? provider : ({} as Partial); + } = provider ? provider : ({} as Partial); return ( = ({ = ({ Delete { setDeleteOpen(false); diff --git a/frontend/src/component/integrations/IntegrationForm/IntegrationParameters/IntegrationParameter/IntegrationParameter.tsx b/frontend/src/component/integrations/IntegrationForm/IntegrationParameters/IntegrationParameter/IntegrationParameter.tsx index 8983a5b9e5..974c1d3fe4 100644 --- a/frontend/src/component/integrations/IntegrationForm/IntegrationParameters/IntegrationParameter/IntegrationParameter.tsx +++ b/frontend/src/component/integrations/IntegrationForm/IntegrationParameters/IntegrationParameter/IntegrationParameter.tsx @@ -1,7 +1,7 @@ import { TextField, Typography } from '@mui/material'; -import { IAddonConfig, IAddonProviderParams } from 'interfaces/addons'; import { ChangeEventHandler } from 'react'; import { StyledAddonParameterContainer } from '../../IntegrationForm.styles'; +import type { AddonParameterSchema, AddonSchema } from 'openapi'; const resolveType = ({ type = 'text', sensitive = false }, value: string) => { if (sensitive && value === MASKED_VALUE) { @@ -17,9 +17,9 @@ const MASKED_VALUE = '*****'; export interface IIntegrationParameterProps { parametersErrors: Record; - definition: IAddonProviderParams; + definition: AddonParameterSchema; setParameterValue: (param: string) => ChangeEventHandler; - config: IAddonConfig; + config: AddonSchema; } export const IntegrationParameter = ({ @@ -28,8 +28,11 @@ export const IntegrationParameter = ({ parametersErrors, setParameterValue, }: IIntegrationParameterProps) => { - const value = config.parameters[definition.name] || ''; - const type = resolveType(definition, value); + const value = config.parameters[definition?.name] || ''; + const type = resolveType( + definition, + typeof value === 'string' ? value : '' + ); const error = parametersErrors[definition.name]; return ( diff --git a/frontend/src/component/integrations/IntegrationForm/IntegrationParameters/IntegrationParameters.tsx b/frontend/src/component/integrations/IntegrationForm/IntegrationParameters/IntegrationParameters.tsx index 646b45ab84..d06db25713 100644 --- a/frontend/src/component/integrations/IntegrationForm/IntegrationParameters/IntegrationParameters.tsx +++ b/frontend/src/component/integrations/IntegrationForm/IntegrationParameters/IntegrationParameters.tsx @@ -1,13 +1,13 @@ import React from 'react'; -import { IAddonProvider } from 'interfaces/addons'; import { IntegrationParameter, IIntegrationParameterProps, } from './IntegrationParameter/IntegrationParameter'; import { StyledTitle } from '../IntegrationForm.styles'; +import type { AddonTypeSchema } from 'openapi'; interface IIntegrationParametersProps { - provider?: IAddonProvider; + provider?: AddonTypeSchema; parametersErrors: IIntegrationParameterProps['parametersErrors']; editMode: boolean; setParameterValue: IIntegrationParameterProps['setParameterValue']; @@ -32,7 +32,7 @@ export const IntegrationParameters = ({ when saving.

) : null} - {provider.parameters.map(parameter => ( + {provider.parameters?.map(parameter => ( ({ cursor: 'pointer', marginLeft: theme.spacing(1), })); -interface IIntegrationNameCellProps { +interface IAddonNameCellProps { provider: Pick< - IAddonProvider, + AddonTypeSchema, 'displayName' | 'description' | 'deprecated' >; } @@ -20,9 +20,7 @@ interface IIntegrationNameCellProps { /** * @deprecated Remove when integrationsRework flag is removed */ -export const IntegrationNameCell = ({ - provider, -}: IIntegrationNameCellProps) => ( +export const AddonNameCell = ({ provider }: IAddonNameCellProps) => ( { + const { providers, addons, loading } = useAddons(); + + return ( + <> + 0} + show={} + /> + + + ); +}; diff --git a/frontend/src/component/integrations/IntegrationList/AvailableAddons/AvailableAddons.tsx b/frontend/src/component/integrations/IntegrationList/AvailableAddons/AvailableAddons.tsx index 91e397622d..ab4ef87734 100644 --- a/frontend/src/component/integrations/IntegrationList/AvailableAddons/AvailableAddons.tsx +++ b/frontend/src/component/integrations/IntegrationList/AvailableAddons/AvailableAddons.tsx @@ -18,22 +18,11 @@ import { IconCell } from 'component/common/Table/cells/IconCell/IconCell'; import { ActionCell } from 'component/common/Table/cells/ActionCell/ActionCell'; import { ConfigureAddonsButton } from './ConfigureAddonButton/ConfigureAddonsButton'; import { IntegrationIcon } from '../IntegrationIcon/IntegrationIcon'; -import { IntegrationNameCell } from '../IntegrationNameCell/IntegrationNameCell'; -import { IAddonInstallation } from 'interfaces/addons'; - -interface IProvider { - name: string; - displayName: string; - description: string; - documentationUrl: string; - parameters: object[]; - events: string[]; - installation?: IAddonInstallation; - deprecated?: string; -} +import { AddonNameCell } from '../AddonNameCell/AddonNameCell'; +import type { AddonTypeSchema } from 'openapi'; interface IAvailableAddonsProps { - providers: IProvider[]; + providers: AddonTypeSchema[]; loading: boolean; } @@ -84,7 +73,7 @@ export const AvailableAddons = ({ accessor: 'name', width: '90%', Cell: ({ row: { original } }: any) => ( - + ), sortType: 'alphanumeric', }, diff --git a/frontend/src/component/integrations/IntegrationList/AvailableAddons/ConfigureAddonButton/ConfigureAddonsButton.tsx b/frontend/src/component/integrations/IntegrationList/AvailableAddons/ConfigureAddonButton/ConfigureAddonsButton.tsx index bc165e9059..74074ad8c0 100644 --- a/frontend/src/component/integrations/IntegrationList/AvailableAddons/ConfigureAddonButton/ConfigureAddonsButton.tsx +++ b/frontend/src/component/integrations/IntegrationList/AvailableAddons/ConfigureAddonButton/ConfigureAddonsButton.tsx @@ -1,10 +1,10 @@ import PermissionButton from 'component/common/PermissionButton/PermissionButton'; import { CREATE_ADDON } from 'component/providers/AccessProvider/permissions'; -import { IAddonProvider } from 'interfaces/addons'; +import type { AddonTypeSchema } from 'openapi'; import { useNavigate } from 'react-router-dom'; interface IConfigureAddonsButtonProps { - provider: IAddonProvider; + provider: AddonTypeSchema; } /** diff --git a/frontend/src/component/integrations/IntegrationList/AvailableIntegrations/AvailableIntegrations.tsx b/frontend/src/component/integrations/IntegrationList/AvailableIntegrations/AvailableIntegrations.tsx index 970f5991c4..fa75d7f0f0 100644 --- a/frontend/src/component/integrations/IntegrationList/AvailableIntegrations/AvailableIntegrations.tsx +++ b/frontend/src/component/integrations/IntegrationList/AvailableIntegrations/AvailableIntegrations.tsx @@ -1,8 +1,36 @@ +import { type VFC } from 'react'; +import type { AddonTypeSchema } from 'openapi'; +import useLoading from 'hooks/useLoading'; import { PageContent } from 'component/common/PageContent/PageContent'; -import { VFC } from 'react'; +import { PageHeader } from 'component/common/PageHeader/PageHeader'; +import { IntegrationCard } from '../IntegrationCard/IntegrationCard'; +import { StyledCardsGrid } from '../IntegrationList.styles'; -interface IAvailableIntegrationsProps {} - -export const AvailableIntegrations: VFC = () => { - return Available integrations; +interface IAvailableIntegrationsProps { + providers: AddonTypeSchema[]; + loading?: boolean; +} +export const AvailableIntegrations: VFC = ({ + providers, + loading, +}) => { + const ref = useLoading(loading || false); + return ( + } + isLoading={loading} + > + + {providers?.map(({ name, displayName, description }) => ( + + ))} + + + ); }; diff --git a/frontend/src/component/integrations/IntegrationList/ConfiguredAddons/ConfiguredAddons.tsx b/frontend/src/component/integrations/IntegrationList/ConfiguredAddons/ConfiguredAddons.tsx index 54198c7b4c..ce64f96c97 100644 --- a/frontend/src/component/integrations/IntegrationList/ConfiguredAddons/ConfiguredAddons.tsx +++ b/frontend/src/component/integrations/IntegrationList/ConfiguredAddons/ConfiguredAddons.tsx @@ -5,7 +5,6 @@ import { PageContent } from 'component/common/PageContent/PageContent'; import useAddons from 'hooks/api/getters/useAddons/useAddons'; import useToast from 'hooks/useToast'; import useAddonsApi from 'hooks/api/actions/useAddonsApi/useAddonsApi'; -import { IAddon } from 'interfaces/addons'; import { Dialogue } from 'component/common/Dialogue/Dialogue'; import { formatUnknownError } from 'utils/formatUnknownError'; import { sortTypes } from 'utils/sortTypes'; @@ -15,7 +14,8 @@ import { SortableTableHeader, TablePlaceholder } from 'component/common/Table'; import { IconCell } from 'component/common/Table/cells/IconCell/IconCell'; import { IntegrationIcon } from '../IntegrationIcon/IntegrationIcon'; import { ConfiguredAddonsActionsCell } from './ConfiguredAddonsActionCell/ConfiguredAddonsActionsCell'; -import { IntegrationNameCell } from '../IntegrationNameCell/IntegrationNameCell'; +import { AddonNameCell } from '../AddonNameCell/AddonNameCell'; +import { AddonSchema } from 'openapi'; /** * @deprecated Remove when integrationsRework flag is removed @@ -25,7 +25,7 @@ export const ConfiguredAddons = () => { const { updateAddon, removeAddon } = useAddonsApi(); const { setToastData, setToastApiError } = useToast(); const [showDelete, setShowDelete] = useState(false); - const [deletedAddon, setDeletedAddon] = useState({ + const [deletedAddon, setDeletedAddon] = useState({ id: 0, provider: '', description: '', @@ -48,7 +48,7 @@ export const ConfiguredAddons = () => { }, [addons, loading]); const toggleAddon = useCallback( - async (addon: IAddon) => { + async (addon: AddonSchema) => { try { await updateAddon({ ...addon, enabled: !addon.enabled }); refetchAddons(); @@ -91,7 +91,7 @@ export const ConfiguredAddons = () => { original: { provider, description }, }, }: any) => ( - name === provider @@ -111,7 +111,7 @@ export const ConfiguredAddons = () => { Cell: ({ row: { original }, }: { - row: { original: IAddon }; + row: { original: AddonSchema }; }) => ( { useSortBy ); - const onRemoveAddon = async (addon: IAddon) => { + const onRemoveAddon = async (addon: AddonSchema) => { try { await removeAddon(addon.id); refetchAddons(); diff --git a/frontend/src/component/integrations/IntegrationList/ConfiguredAddons/ConfiguredAddonsActionCell/ConfiguredAddonsActionsCell.tsx b/frontend/src/component/integrations/IntegrationList/ConfiguredAddons/ConfiguredAddonsActionCell/ConfiguredAddonsActionsCell.tsx index 2d3374818b..fa1af5fc52 100644 --- a/frontend/src/component/integrations/IntegrationList/ConfiguredAddons/ConfiguredAddonsActionCell/ConfiguredAddonsActionsCell.tsx +++ b/frontend/src/component/integrations/IntegrationList/ConfiguredAddons/ConfiguredAddonsActionCell/ConfiguredAddonsActionsCell.tsx @@ -8,14 +8,14 @@ import { UPDATE_ADDON, DELETE_ADDON, } from 'component/providers/AccessProvider/permissions'; -import { IAddon } from 'interfaces/addons'; +import { AddonSchema } from 'openapi'; import { useNavigate } from 'react-router-dom'; interface IConfiguredAddonsActionsCellProps { - toggleAddon: (addon: IAddon) => Promise; - original: IAddon; + toggleAddon: (addon: AddonSchema) => Promise; + original: AddonSchema; setShowDelete: React.Dispatch>; - setDeletedAddon: React.Dispatch>; + setDeletedAddon: React.Dispatch>; } /** diff --git a/frontend/src/component/integrations/IntegrationList/ConfiguredIntegrations/ConfiguredIntegrations.tsx b/frontend/src/component/integrations/IntegrationList/ConfiguredIntegrations/ConfiguredIntegrations.tsx new file mode 100644 index 0000000000..4c2f9466fc --- /dev/null +++ b/frontend/src/component/integrations/IntegrationList/ConfiguredIntegrations/ConfiguredIntegrations.tsx @@ -0,0 +1,61 @@ +import { AddonSchema, AddonTypeSchema } from 'openapi'; +import useLoading from 'hooks/useLoading'; +import { PageContent } from 'component/common/PageContent/PageContent'; +import { PageHeader } from 'component/common/PageHeader/PageHeader'; +import { StyledCardsGrid } from '../IntegrationList.styles'; +import { IntegrationCard } from '../IntegrationCard/IntegrationCard'; +import { VFC } from 'react'; + +type ConfiguredIntegrationsProps = { + loading: boolean; + addons: AddonSchema[]; + providers: AddonTypeSchema[]; +}; + +export const ConfiguredIntegrations: VFC = ({ + loading, + addons, + providers, +}) => { + const counter = addons.length ? `(${addons.length})` : ''; + const ref = useLoading(loading || false); + + return ( + } + sx={theme => ({ marginBottom: theme.spacing(2) })} + isLoading={loading} + > + + {addons + ?.sort(({ id: a }, { id: b }) => a - b) + .map(addon => { + const { + id, + enabled, + provider, + description, + // events, + // projects, + } = addon; + const providerConfig = providers.find( + item => item.name === provider + ); + + return ( + + ); + })} + + + ); +}; diff --git a/frontend/src/component/integrations/IntegrationList/IntegrationCard/IntegrationCard.tsx b/frontend/src/component/integrations/IntegrationList/IntegrationCard/IntegrationCard.tsx new file mode 100644 index 0000000000..a424c807a3 --- /dev/null +++ b/frontend/src/component/integrations/IntegrationList/IntegrationCard/IntegrationCard.tsx @@ -0,0 +1,101 @@ +import { VFC } from 'react'; +import { Link } from 'react-router-dom'; +import { styled, Typography } from '@mui/material'; +import { IntegrationIcon } from '../IntegrationIcon/IntegrationIcon'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import ChevronRightIcon from '@mui/icons-material/ChevronRight'; +import { Badge } from 'component/common/Badge/Badge'; +import { IntegrationCardMenu } from './IntegrationCardMenu/IntegrationCardMenu'; +import type { AddonSchema } from 'openapi'; + +interface IIntegrationCardProps { + id?: string | number; + icon?: string; + title: string; + description?: string; + isConfigured?: boolean; + isEnabled?: boolean; + configureActionText?: string; + link: string; + addon?: AddonSchema; +} + +const StyledLink = styled(Link)(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + padding: theme.spacing(3), + borderRadius: `${theme.shape.borderRadiusMedium}px`, + border: `1px solid ${theme.palette.divider}`, + textDecoration: 'none', + color: 'inherit', + boxShadow: theme.boxShadows.card, + ':hover': { + backgroundColor: theme.palette.action.hover, + }, +})); + +const StyledHeader = styled('div')(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + marginBottom: theme.spacing(2), +})); + +const StyledTitle = styled(Typography)(() => ({ + display: 'flex', + alignItems: 'center', + marginRight: 'auto', +})); + +const StyledAction = styled(Typography)(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + color: theme.palette.primary.main, + fontWeight: theme.typography.fontWeightBold, + marginTop: 'auto', + paddingTop: theme.spacing(1), + gap: theme.spacing(0.5), +})); + +export const IntegrationCard: VFC = ({ + icon, + title, + description, + isEnabled, + configureActionText = 'Configure', + link, + addon, +}) => { + const isConfigured = addon !== undefined; + + return ( + + + + {title} + + + Enabled + + } + /> + Disabled} + /> + } + /> + + + {description} + + + {configureActionText} + + + ); +}; diff --git a/frontend/src/component/integrations/IntegrationList/IntegrationCard/IntegrationCardMenu/IntegrationCardMenu.tsx b/frontend/src/component/integrations/IntegrationList/IntegrationCard/IntegrationCardMenu/IntegrationCardMenu.tsx new file mode 100644 index 0000000000..3e8e634bf8 --- /dev/null +++ b/frontend/src/component/integrations/IntegrationList/IntegrationCard/IntegrationCardMenu/IntegrationCardMenu.tsx @@ -0,0 +1,150 @@ +import { useCallback, useState, VFC } from 'react'; +import { + IconButton, + ListItemIcon, + ListItemText, + Menu, + MenuItem, + styled, + Tooltip, +} from '@mui/material'; +import MoreVertIcon from '@mui/icons-material/MoreVert'; + +import { Delete, PowerSettingsNew } from '@mui/icons-material'; +import { + DELETE_ADDON, + UPDATE_ADDON, +} from 'component/providers/AccessProvider/permissions'; +import { useHasRootAccess } from 'hooks/useHasAccess'; +import useAddonsApi from 'hooks/api/actions/useAddonsApi/useAddonsApi'; +import type { AddonSchema } from 'openapi'; +import useAddons from 'hooks/api/getters/useAddons/useAddons'; +import useToast from 'hooks/useToast'; +import { formatUnknownError } from 'utils/formatUnknownError'; + +interface IIntegrationCardMenuProps { + addon: AddonSchema; +} + +const StyledMenu = styled('div')(({ theme }) => ({ + marginLeft: theme.spacing(1), + marginTop: theme.spacing(-1), + marginBottom: theme.spacing(-1), + marginRight: theme.spacing(-1), + display: 'flex', + alignItems: 'center', +})); + +export const IntegrationCardMenu: VFC = ({ + addon, +}) => { + const [open, setOpen] = useState(false); + const [anchorEl, setAnchorEl] = useState(null); + const { updateAddon, removeAddon } = useAddonsApi(); + const { refetchAddons } = useAddons(); + const { setToastData, setToastApiError } = useToast(); + + const handleMenuClick = (event: React.SyntheticEvent) => { + event.preventDefault(); + if (open) { + setOpen(false); + setAnchorEl(null); + } else { + setAnchorEl(event.currentTarget); + setOpen(true); + } + }; + const updateAccess = useHasRootAccess(UPDATE_ADDON); + const deleteAccess = useHasRootAccess(DELETE_ADDON); + + const toggleIntegration = useCallback(async () => { + try { + await updateAddon({ ...addon, enabled: !addon.enabled }); + refetchAddons(); + setToastData({ + type: 'success', + title: 'Success', + text: !addon.enabled + ? 'Integration is now enabled' + : 'Integration is now disabled', + }); + } catch (error: unknown) { + setToastApiError(formatUnknownError(error)); + } + }, [setToastApiError, refetchAddons, setToastData, updateAddon]); + + const deleteIntegration = useCallback(async () => { + try { + await removeAddon(addon.id); + refetchAddons(); + setToastData({ + type: 'success', + title: 'Success', + text: 'Integration has been deleted', + }); + } catch (error: unknown) { + setToastApiError(formatUnknownError(error)); + } + }, [setToastApiError, refetchAddons, setToastData, removeAddon]); + + return ( + + + + + + + { + event.preventDefault(); + }} + onClose={handleMenuClick} + > + { + e.preventDefault(); + toggleIntegration(); + }} + disabled={!updateAccess} + > + + + + + {addon.enabled ? 'Disable' : 'Enable'} + + {' '} + { + e.preventDefault(); + deleteIntegration(); + }} + > + + + + Delete + + + + ); +}; diff --git a/frontend/src/component/integrations/IntegrationList/IntegrationList.styles.tsx b/frontend/src/component/integrations/IntegrationList/IntegrationList.styles.tsx new file mode 100644 index 0000000000..c97ab76c26 --- /dev/null +++ b/frontend/src/component/integrations/IntegrationList/IntegrationList.styles.tsx @@ -0,0 +1,8 @@ +import { styled } from '@mui/material'; + +export const StyledCardsGrid = styled('div')(({ theme }) => ({ + gridTemplateColumns: 'repeat(auto-fit, minmax(350px, 1fr))', + gridAutoRows: '1fr', + gap: theme.spacing(2), + display: 'grid', +})); diff --git a/frontend/src/component/integrations/IntegrationList/IntegrationList.tsx b/frontend/src/component/integrations/IntegrationList/IntegrationList.tsx index addbad3734..097f988617 100644 --- a/frontend/src/component/integrations/IntegrationList/IntegrationList.tsx +++ b/frontend/src/component/integrations/IntegrationList/IntegrationList.tsx @@ -1,28 +1,38 @@ -import { ConfiguredAddons } from './ConfiguredAddons/ConfiguredAddons'; -import { AvailableAddons } from './AvailableAddons/AvailableAddons'; +import { VFC } from 'react'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import useAddons from 'hooks/api/getters/useAddons/useAddons'; -import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import { AvailableIntegrations } from './AvailableIntegrations/AvailableIntegrations'; +import { ConfiguredIntegrations } from './ConfiguredIntegrations/ConfiguredIntegrations'; +import { AddonSchema } from 'openapi'; -export const IntegrationList = () => { +export const IntegrationList: VFC = () => { const { providers, addons, loading } = useAddons(); - const { uiConfig } = useUiConfig(); - const integrationsRework = uiConfig?.flags?.integrationsRework || false; + + const loadingPlaceholderAddons: AddonSchema[] = Array.from({ length: 4 }) + .fill({}) + .map((_, id) => ({ + id, + provider: 'mock', + description: 'mock integratino', + events: [], + projects: [], + parameters: {}, + enabled: false, + })); return ( <> 0} - show={} - /> - } - elseShow={ - + show={ + } /> + ); }; diff --git a/frontend/src/component/menu/routes.ts b/frontend/src/component/menu/routes.ts index 9f81bdf100..6ee0a2daca 100644 --- a/frontend/src/component/menu/routes.ts +++ b/frontend/src/component/menu/routes.ts @@ -43,6 +43,7 @@ import { LazyAdmin } from 'component/admin/LazyAdmin'; import { LazyProject } from 'component/project/Project/LazyProject'; import { LoginHistory } from 'component/loginHistory/LoginHistory'; import { FeatureTypesList } from 'component/featureTypes/FeatureTypesList'; +import { AddonsList } from 'component/integrations/IntegrationList/AddonsList'; import { TemporaryApplicationListWrapper } from 'component/application/ApplicationList/TemporaryApplicationListWrapper'; export const routes: IRoute[] = [ @@ -321,7 +322,7 @@ export const routes: IRoute[] = [ { path: '/addons', title: 'Addons', - component: IntegrationList, + component: AddonsList, // TODO: use AddonRedirect after removing `integrationsRework` menu flag hidden: false, type: 'protected', diff --git a/frontend/src/hooks/api/actions/useAddonsApi/useAddonsApi.ts b/frontend/src/hooks/api/actions/useAddonsApi/useAddonsApi.ts index 30f4e5533a..a2ffbb2cfc 100644 --- a/frontend/src/hooks/api/actions/useAddonsApi/useAddonsApi.ts +++ b/frontend/src/hooks/api/actions/useAddonsApi/useAddonsApi.ts @@ -1,6 +1,6 @@ -import { IAddon } from 'interfaces/addons'; import { useCallback } from 'react'; import useAPI from '../useApi/useApi'; +import type { AddonSchema } from 'openapi'; const useAddonsApi = () => { const { makeRequest, createRequest, errors, loading } = useAPI({ @@ -9,7 +9,7 @@ const useAddonsApi = () => { const URI = 'api/admin/addons'; - const createAddon = async (addonConfig: IAddon) => { + const createAddon = async (addonConfig: Omit) => { const path = URI; const req = createRequest(path, { method: 'POST', @@ -29,7 +29,7 @@ const useAddonsApi = () => { }; const updateAddon = useCallback( - async (addonConfig: IAddon) => { + async (addonConfig: AddonSchema) => { const path = `${URI}/${addonConfig.id}`; const req = createRequest(path, { method: 'PUT', diff --git a/frontend/src/hooks/api/getters/useAddons/useAddons.ts b/frontend/src/hooks/api/getters/useAddons/useAddons.ts index b2bc2dd1fa..f5e3b5e314 100644 --- a/frontend/src/hooks/api/getters/useAddons/useAddons.ts +++ b/frontend/src/hooks/api/getters/useAddons/useAddons.ts @@ -2,12 +2,7 @@ import useSWR, { mutate, SWRConfiguration } from 'swr'; import { useState, useEffect, useCallback } from 'react'; import { formatApiPath } from 'utils/formatPath'; import handleErrorResponses from '../httpErrorResponseHandler'; -import { IAddon, IAddonProvider } from 'interfaces/addons'; - -interface IAddonsResponse { - addons: IAddon[]; - providers: IAddonProvider[]; -} +import { AddonsSchema } from 'openapi'; const useAddons = (options: SWRConfiguration = {}) => { const fetcher = async () => { @@ -20,7 +15,7 @@ const useAddons = (options: SWRConfiguration = {}) => { const KEY = `api/admin/addons`; - const { data, error } = useSWR(KEY, fetcher, options); + const { data, error } = useSWR(KEY, fetcher, options); const [loading, setLoading] = useState(!error && !data); const refetchAddons = useCallback(() => { diff --git a/frontend/src/interfaces/addons.ts b/frontend/src/interfaces/addons.ts deleted file mode 100644 index 596980790a..0000000000 --- a/frontend/src/interfaces/addons.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { ITagType } from './tags'; - -export interface IAddon { - provider: string; - parameters: Record; - id: number; - events: string[]; - projects?: string[]; - environments?: string[]; - enabled: boolean; - description: string; -} - -export interface IAddonProvider { - description: string; - displayName: string; - documentationUrl: string; - events: string[]; - name: string; - parameters: IAddonProviderParams[]; - tagTypes: ITagType[]; - installation?: IAddonInstallation; - alerts?: IAddonAlert[]; - deprecated?: string; -} - -export interface IAddonInstallation { - url: string; - warning?: string; - title?: string; - helpText?: string; -} - -export interface IAddonAlert { - type: 'success' | 'info' | 'warning' | 'error'; - text: string; -} - -export interface IAddonProviderParams { - name: string; - displayName: string; - type: string; - required: boolean; - sensitive: boolean; - placeholder?: string; - description?: string; -} - -export interface IAddonConfig { - provider: string; - parameters: Record; - id: number; - events: string[]; - projects?: string[]; - environments?: string[]; - enabled: boolean; - description: string; -} diff --git a/src/server-dev.ts b/src/server-dev.ts index 42bd13ac8a..ad2cac1670 100644 --- a/src/server-dev.ts +++ b/src/server-dev.ts @@ -39,6 +39,7 @@ process.nextTick(async () => { responseTimeWithAppNameKillSwitch: false, slackAppAddon: true, lastSeenByEnvironment: true, + integrationsRework: true, newApplicationList: true, featureNamingPattern: true, doraMetrics: true,