1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-14 00:19:16 +01:00

Integration card component (#4557)

![image](https://github.com/Unleash/unleash/assets/2625371/42364fdb-1ff1-48c4-9756-a145a39e45b9)


## About the changes
<!-- Describe the changes introduced. What are they and why are they
being introduced? Feel free to also add screenshots or steps to view the
changes if they're visual. -->

<!-- Does it close an issue? Multiple? -->
Closes #

<!-- (For internal contributors): Does it relate to an issue on public
roadmap? -->
<!--
Relates to [roadmap](https://github.com/orgs/Unleash/projects/10) item:
#
-->

### Important files
<!-- PRs can contain a lot of changes, but not all changes are equally
important. Where should a reviewer start looking to get an overview of
the changes? Are any files particularly important? -->


## Discussion points
<!-- Anything about the PR you'd like to discuss before it gets merged?
Got any questions or doubts? -->
This commit is contained in:
Tymoteusz Czech 2023-09-05 14:14:03 +02:00 committed by GitHub
parent a1e98056ec
commit 47a59224bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 458 additions and 149 deletions

View File

@ -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<AddonSchema, 'id'> = {
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');

View File

@ -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)

View File

@ -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<AddonSchema, 'id'>;
};
export const IntegrationForm: VFC<IAddonFormProps> = ({
export const IntegrationForm: VFC<IntegrationFormProps> = ({
editMode,
provider,
addon: initialValues,
@ -77,7 +77,7 @@ export const IntegrationForm: VFC<IAddonFormProps> = ({
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<IAddonFormProps> = ({
});
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<IAddonFormProps> = ({
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<IAddonFormProps> = ({
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<AddonSchema, 'id'>);
navigate('/addons');
setToastData({
type: 'success',
@ -255,7 +256,7 @@ export const IntegrationForm: VFC<IAddonFormProps> = ({
documentationUrl = 'https://unleash.github.io/docs/addons',
installation,
alerts,
} = provider ? provider : ({} as Partial<IAddonProvider>);
} = provider ? provider : ({} as Partial<AddonTypeSchema>);
return (
<FormTemplate
@ -356,7 +357,7 @@ export const IntegrationForm: VFC<IAddonFormProps> = ({
<StyledFormSection>
<IntegrationParameters
provider={provider}
config={formValues}
config={formValues as AddonSchema}
parametersErrors={errors.parameters}
editMode={editMode}
setParameterValue={setParameterValue}
@ -396,7 +397,7 @@ export const IntegrationForm: VFC<IAddonFormProps> = ({
Delete
</PermissionButton>
<IntegrationDeleteDialog
id={formValues.id}
id={(formValues as AddonSchema).id}
isOpen={isDeleteOpen}
onClose={() => {
setDeleteOpen(false);

View File

@ -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<string, string>;
definition: IAddonProviderParams;
definition: AddonParameterSchema;
setParameterValue: (param: string) => ChangeEventHandler<HTMLInputElement>;
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 (

View File

@ -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.
</p>
) : null}
{provider.parameters.map(parameter => (
{provider.parameters?.map(parameter => (
<IntegrationParameter
key={parameter.name}
definition={parameter}

View File

@ -3,16 +3,16 @@ import { Badge } from 'component/common/Badge/Badge';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell';
import { IAddonProvider } from 'interfaces/addons';
import { AddonTypeSchema } from 'openapi';
const StyledBadge = styled(Badge)(({ theme }) => ({
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) => (
<HighlightCell
value={provider.displayName}
subtitle={provider.description}

View File

@ -0,0 +1,21 @@
import { ConfiguredAddons } from './ConfiguredAddons/ConfiguredAddons';
import { AvailableAddons } from './AvailableAddons/AvailableAddons';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import useAddons from 'hooks/api/getters/useAddons/useAddons';
/**
* @deprecated remove with `integrationsRework` flag
*/
export const AddonsList = () => {
const { providers, addons, loading } = useAddons();
return (
<>
<ConditionallyRender
condition={addons.length > 0}
show={<ConfiguredAddons />}
/>
<AvailableAddons loading={loading} providers={providers} />
</>
);
};

View File

@ -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) => (
<IntegrationNameCell provider={original} />
<AddonNameCell provider={original} />
),
sortType: 'alphanumeric',
},

View File

@ -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;
}
/**

View File

@ -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<IAvailableIntegrationsProps> = () => {
return <PageContent>Available integrations</PageContent>;
interface IAvailableIntegrationsProps {
providers: AddonTypeSchema[];
loading?: boolean;
}
export const AvailableIntegrations: VFC<IAvailableIntegrationsProps> = ({
providers,
loading,
}) => {
const ref = useLoading(loading || false);
return (
<PageContent
header={<PageHeader title="Available integrations" />}
isLoading={loading}
>
<StyledCardsGrid ref={ref}>
{providers?.map(({ name, displayName, description }) => (
<IntegrationCard
key={name}
icon={name}
title={displayName || name}
description={description}
link={`/integrations/create/${name}`}
/>
))}
</StyledCardsGrid>
</PageContent>
);
};

View File

@ -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<IAddon>({
const [deletedAddon, setDeletedAddon] = useState<AddonSchema>({
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) => (
<IntegrationNameCell
<AddonNameCell
provider={{
...(providers.find(
({ name }) => name === provider
@ -111,7 +111,7 @@ export const ConfiguredAddons = () => {
Cell: ({
row: { original },
}: {
row: { original: IAddon };
row: { original: AddonSchema };
}) => (
<ConfiguredAddonsActionsCell
key={original.id}
@ -163,7 +163,7 @@ export const ConfiguredAddons = () => {
useSortBy
);
const onRemoveAddon = async (addon: IAddon) => {
const onRemoveAddon = async (addon: AddonSchema) => {
try {
await removeAddon(addon.id);
refetchAddons();

View File

@ -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<void>;
original: IAddon;
toggleAddon: (addon: AddonSchema) => Promise<void>;
original: AddonSchema;
setShowDelete: React.Dispatch<React.SetStateAction<boolean>>;
setDeletedAddon: React.Dispatch<React.SetStateAction<IAddon>>;
setDeletedAddon: React.Dispatch<React.SetStateAction<AddonSchema>>;
}
/**

View File

@ -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<ConfiguredIntegrationsProps> = ({
loading,
addons,
providers,
}) => {
const counter = addons.length ? `(${addons.length})` : '';
const ref = useLoading(loading || false);
return (
<PageContent
header={<PageHeader title={`Configured integrations ${counter}`} />}
sx={theme => ({ marginBottom: theme.spacing(2) })}
isLoading={loading}
>
<StyledCardsGrid ref={ref}>
{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 (
<IntegrationCard
key={`${id}-${provider}-${enabled}`}
addon={addon}
icon={provider}
title={providerConfig?.displayName || provider}
isEnabled={enabled}
description={description || ''}
isConfigured
link={`/integrations/edit/${id}`}
/>
);
})}
</StyledCardsGrid>
</PageContent>
);
};

View File

@ -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<IIntegrationCardProps> = ({
icon,
title,
description,
isEnabled,
configureActionText = 'Configure',
link,
addon,
}) => {
const isConfigured = addon !== undefined;
return (
<StyledLink to={link}>
<StyledHeader>
<StyledTitle variant="h3" data-loading>
<IntegrationIcon name={icon as string} /> {title}
</StyledTitle>
<ConditionallyRender
condition={isEnabled === true}
show={
<Badge color="success" data-loading>
Enabled
</Badge>
}
/>
<ConditionallyRender
condition={isEnabled === false}
show={<Badge data-loading>Disabled</Badge>}
/>
<ConditionallyRender
condition={isConfigured}
show={<IntegrationCardMenu addon={addon as AddonSchema} />}
/>
</StyledHeader>
<Typography variant="body1" data-loading>
{description}
</Typography>
<StyledAction data-loading>
{configureActionText} <ChevronRightIcon />
</StyledAction>
</StyledLink>
);
};

View File

@ -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<IIntegrationCardMenuProps> = ({
addon,
}) => {
const [open, setOpen] = useState(false);
const [anchorEl, setAnchorEl] = useState<Element | null>(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 (
<StyledMenu>
<Tooltip title="More actions" arrow>
<IconButton
onClick={handleMenuClick}
size="small"
aria-controls={open ? 'actions-menu' : undefined}
aria-haspopup="true"
aria-expanded={open ? 'true' : undefined}
data-loading
>
<MoreVertIcon sx={{ width: 32, height: 32 }} />
</IconButton>
</Tooltip>
<Menu
id="project-card-menu"
open={Boolean(anchorEl)}
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
onClick={event => {
event.preventDefault();
}}
onClose={handleMenuClick}
>
<MenuItem
onClick={e => {
e.preventDefault();
toggleIntegration();
}}
disabled={!updateAccess}
>
<ListItemIcon>
<PowerSettingsNew />
</ListItemIcon>
<ListItemText>
{addon.enabled ? 'Disable' : 'Enable'}
</ListItemText>
</MenuItem>{' '}
<MenuItem
disabled={!deleteAccess}
onClick={e => {
e.preventDefault();
deleteIntegration();
}}
>
<ListItemIcon>
<Delete />
</ListItemIcon>
<ListItemText>Delete</ListItemText>
</MenuItem>
</Menu>
</StyledMenu>
);
};

View File

@ -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',
}));

View File

@ -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 (
<>
<ConditionallyRender
condition={addons.length > 0}
show={<ConfiguredAddons />}
/>
<ConditionallyRender
condition={integrationsRework}
show={<AvailableIntegrations />}
elseShow={
<AvailableAddons loading={loading} providers={providers} />
show={
<ConfiguredIntegrations
addons={loading ? loadingPlaceholderAddons : addons}
providers={providers}
loading={loading}
/>
}
/>
<AvailableIntegrations providers={providers} loading={loading} />
</>
);
};

View File

@ -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',

View File

@ -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<AddonSchema, 'id'>) => {
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',

View File

@ -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<IAddonsResponse>(KEY, fetcher, options);
const { data, error } = useSWR<AddonsSchema>(KEY, fetcher, options);
const [loading, setLoading] = useState(!error && !data);
const refetchAddons = useCallback(() => {

View File

@ -1,58 +0,0 @@
import { ITagType } from './tags';
export interface IAddon {
provider: string;
parameters: Record<string, any>;
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<string, any>;
id: number;
events: string[];
projects?: string[];
environments?: string[];
enabled: boolean;
description: string;
}

View File

@ -39,6 +39,7 @@ process.nextTick(async () => {
responseTimeWithAppNameKillSwitch: false,
slackAppAddon: true,
lastSeenByEnvironment: true,
integrationsRework: true,
newApplicationList: true,
featureNamingPattern: true,
doraMetrics: true,