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 (
+
+
+
+
+
+
+
+
+ );
+};
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,