mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-18 01:18:23 +02:00
Feat/new addons table (#1021)
* feat: initial list of available addons * feat: add columns * fix: update referential equality * fix: remove search * fix: remove unused imports * fix: padding * fix: imports * refactor: based on comments
This commit is contained in:
parent
a791ea59c1
commit
ded3c22bb1
@ -0,0 +1,71 @@
|
|||||||
|
import { Avatar } from '@mui/material';
|
||||||
|
import { DeviceHub } from '@mui/icons-material';
|
||||||
|
import { formatAssetPath } from 'utils/formatPath';
|
||||||
|
|
||||||
|
import slackIcon from 'assets/icons/slack.svg';
|
||||||
|
import jiraIcon from 'assets/icons/jira.svg';
|
||||||
|
import webhooksIcon from 'assets/icons/webhooks.svg';
|
||||||
|
import teamsIcon from 'assets/icons/teams.svg';
|
||||||
|
import dataDogIcon from 'assets/icons/datadog.svg';
|
||||||
|
|
||||||
|
const style: React.CSSProperties = {
|
||||||
|
width: '32.5px',
|
||||||
|
height: '32.5px',
|
||||||
|
marginRight: '16px',
|
||||||
|
borderRadius: '50%',
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IAddonIconProps {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddonIcon = ({ name }: IAddonIconProps) => {
|
||||||
|
switch (name) {
|
||||||
|
case 'slack':
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
style={style}
|
||||||
|
alt="Slack logo"
|
||||||
|
src={formatAssetPath(slackIcon)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'jira-comment':
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
style={style}
|
||||||
|
alt="JIRA logo"
|
||||||
|
src={formatAssetPath(jiraIcon)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'webhook':
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
style={style}
|
||||||
|
alt="Generic Webhook logo"
|
||||||
|
src={formatAssetPath(webhooksIcon)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'teams':
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
style={style}
|
||||||
|
alt="Microsoft Teams logo"
|
||||||
|
src={formatAssetPath(teamsIcon)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'datadog':
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
style={style}
|
||||||
|
alt="Datadog logo"
|
||||||
|
src={formatAssetPath(dataDogIcon)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<Avatar>
|
||||||
|
<DeviceHub />
|
||||||
|
</Avatar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
@ -13,13 +13,13 @@ import { formatAssetPath } from 'utils/formatPath';
|
|||||||
import useAddons from 'hooks/api/getters/useAddons/useAddons';
|
import useAddons from 'hooks/api/getters/useAddons/useAddons';
|
||||||
|
|
||||||
const style: React.CSSProperties = {
|
const style: React.CSSProperties = {
|
||||||
width: '40px',
|
width: '32.5px',
|
||||||
height: '40px',
|
height: '32.5px',
|
||||||
marginRight: '16px',
|
marginRight: '16px',
|
||||||
float: 'left',
|
borderRadius: '50%',
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAddonIcon = (name: string): ReactElement => {
|
export const getAddonIcon = (name: string): ReactElement => {
|
||||||
switch (name) {
|
switch (name) {
|
||||||
case 'slack':
|
case 'slack':
|
||||||
return (
|
return (
|
||||||
@ -71,20 +71,17 @@ const getAddonIcon = (name: string): ReactElement => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const AddonList = () => {
|
export const AddonList = () => {
|
||||||
const { providers, addons } = useAddons();
|
const { providers, addons, loading } = useAddons();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={addons.length > 0}
|
condition={addons.length > 0}
|
||||||
show={<ConfiguredAddons getAddonIcon={getAddonIcon} />}
|
show={<ConfiguredAddons />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
<AvailableAddons
|
<AvailableAddons loading={loading} providers={providers} />
|
||||||
providers={providers}
|
|
||||||
getAddonIcon={getAddonIcon}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,15 +1,24 @@
|
|||||||
import { ReactElement } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
List,
|
Table,
|
||||||
ListItem,
|
SortableTableHeader,
|
||||||
ListItemAvatar,
|
TableBody,
|
||||||
ListItemSecondaryAction,
|
TableCell,
|
||||||
ListItemText,
|
TableRow,
|
||||||
} from '@mui/material';
|
TablePlaceholder,
|
||||||
import { CREATE_ADDON } from 'component/providers/AccessProvider/permissions';
|
} from 'component/common/Table';
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
|
import { useTable, useSortBy } from 'react-table';
|
||||||
|
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
|
import { sortTypes } from 'utils/sortTypes';
|
||||||
|
import { IconCell } from 'component/common/Table/cells/IconCell/IconCell';
|
||||||
|
import { ActionCell } from 'component/common/Table/cells/ActionCell/ActionCell';
|
||||||
|
import { ConfigureAddonButton } from './ConfigureAddonButton/ConfigureAddonButton';
|
||||||
|
import { AddonIcon } from '../AddonIcon/AddonIcon';
|
||||||
|
|
||||||
interface IProvider {
|
interface IProvider {
|
||||||
name: string;
|
name: string;
|
||||||
@ -21,40 +30,153 @@ interface IProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface IAvailableAddonsProps {
|
interface IAvailableAddonsProps {
|
||||||
getAddonIcon: (name: string) => ReactElement;
|
|
||||||
providers: IProvider[];
|
providers: IProvider[];
|
||||||
|
loading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AvailableAddons = ({
|
export const AvailableAddons = ({
|
||||||
providers,
|
providers,
|
||||||
getAddonIcon,
|
loading,
|
||||||
}: IAvailableAddonsProps) => {
|
}: IAvailableAddonsProps) => {
|
||||||
const navigate = useNavigate();
|
const data = useMemo(() => {
|
||||||
|
if (loading) {
|
||||||
|
return Array(5).fill({
|
||||||
|
name: 'Provider name',
|
||||||
|
description: 'Provider description when loading',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const renderProvider = (provider: IProvider) => (
|
return providers.map(({ name, displayName, description }) => ({
|
||||||
<ListItem key={provider.name}>
|
name,
|
||||||
<ListItemAvatar>{getAddonIcon(provider.name)}</ListItemAvatar>
|
displayName,
|
||||||
<ListItemText
|
description,
|
||||||
primary={provider.displayName}
|
}));
|
||||||
secondary={provider.description}
|
}, [providers, loading]);
|
||||||
/>
|
|
||||||
<ListItemSecondaryAction>
|
const columns = useMemo(
|
||||||
<PermissionButton
|
() => [
|
||||||
permission={CREATE_ADDON}
|
{
|
||||||
onClick={() => navigate(`/addons/create/${provider.name}`)}
|
id: 'Icon',
|
||||||
>
|
Cell: ({
|
||||||
Configure
|
row: {
|
||||||
</PermissionButton>
|
original: { name },
|
||||||
</ListItemSecondaryAction>
|
},
|
||||||
</ListItem>
|
}: any) => {
|
||||||
|
return (
|
||||||
|
<IconCell icon={<AddonIcon name={name as string} />} />
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Name',
|
||||||
|
accessor: 'name',
|
||||||
|
width: '90%',
|
||||||
|
Cell: ({
|
||||||
|
row: {
|
||||||
|
original: { name, description },
|
||||||
|
},
|
||||||
|
}: any) => {
|
||||||
|
return (
|
||||||
|
<LinkCell
|
||||||
|
data-loading
|
||||||
|
title={name}
|
||||||
|
subtitle={description}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
sortType: 'alphanumeric',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Actions',
|
||||||
|
id: 'Actions',
|
||||||
|
align: 'center',
|
||||||
|
Cell: ({ row: { original } }: any) => (
|
||||||
|
<ActionCell>
|
||||||
|
<ConfigureAddonButton name={original.name} />
|
||||||
|
</ActionCell>
|
||||||
|
),
|
||||||
|
width: 150,
|
||||||
|
disableSortBy: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'description',
|
||||||
|
disableSortBy: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const initialState = useMemo(
|
||||||
|
() => ({
|
||||||
|
sortBy: [{ id: 'name', desc: false }],
|
||||||
|
hiddenColumns: ['description'],
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
getTableProps,
|
||||||
|
getTableBodyProps,
|
||||||
|
headerGroups,
|
||||||
|
rows,
|
||||||
|
prepareRow,
|
||||||
|
state: { globalFilter },
|
||||||
|
} = useTable(
|
||||||
|
{
|
||||||
|
columns: columns as any[], // TODO: fix after `react-table` v8 update
|
||||||
|
data,
|
||||||
|
initialState,
|
||||||
|
sortTypes,
|
||||||
|
autoResetGlobalFilter: false,
|
||||||
|
autoResetSortBy: false,
|
||||||
|
disableSortRemove: true,
|
||||||
|
},
|
||||||
|
useSortBy
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent header="Available addons">
|
<PageContent
|
||||||
<List>
|
isLoading={loading}
|
||||||
{providers.map((provider: IProvider) =>
|
header={<PageHeader title="Available addons" />}
|
||||||
renderProvider(provider)
|
>
|
||||||
)}
|
<Table {...getTableProps()}>
|
||||||
</List>
|
<SortableTableHeader headerGroups={headerGroups} />
|
||||||
|
<TableBody {...getTableBodyProps()}>
|
||||||
|
{rows.map(row => {
|
||||||
|
prepareRow(row);
|
||||||
|
return (
|
||||||
|
<TableRow hover {...row.getRowProps()}>
|
||||||
|
{row.cells.map(cell => (
|
||||||
|
<TableCell {...cell.getCellProps()}>
|
||||||
|
{cell.render('Cell')}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={rows.length === 0}
|
||||||
|
show={
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={globalFilter?.length > 0}
|
||||||
|
show={
|
||||||
|
<TablePlaceholder>
|
||||||
|
No providers found matching “
|
||||||
|
{globalFilter}
|
||||||
|
”
|
||||||
|
</TablePlaceholder>
|
||||||
|
}
|
||||||
|
elseShow={
|
||||||
|
<TablePlaceholder>
|
||||||
|
No providers available.
|
||||||
|
</TablePlaceholder>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
|
||||||
|
import { CREATE_ADDON } from 'component/providers/AccessProvider/permissions';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
interface IConfigureAddonButtonProps {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ConfigureAddonButton = ({ name }: IConfigureAddonButtonProps) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PermissionButton
|
||||||
|
permission={CREATE_ADDON}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => navigate(`/addons/create/${name}`)}
|
||||||
|
>
|
||||||
|
Configure
|
||||||
|
</PermissionButton>
|
||||||
|
);
|
||||||
|
};
|
@ -1,38 +1,34 @@
|
|||||||
import {
|
import { useMemo } from 'react';
|
||||||
List,
|
import { Box, Table, TableBody, TableCell, TableRow } from '@mui/material';
|
||||||
ListItem,
|
|
||||||
ListItemAvatar,
|
|
||||||
ListItemSecondaryAction,
|
|
||||||
ListItemText,
|
|
||||||
} from '@mui/material';
|
|
||||||
import { Delete, Edit, Visibility, VisibilityOff } from '@mui/icons-material';
|
import { Delete, Edit, Visibility, VisibilityOff } from '@mui/icons-material';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import {
|
import {
|
||||||
DELETE_ADDON,
|
DELETE_ADDON,
|
||||||
UPDATE_ADDON,
|
UPDATE_ADDON,
|
||||||
} from 'component/providers/AccessProvider/permissions';
|
} from 'component/providers/AccessProvider/permissions';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
import useAddons from 'hooks/api/getters/useAddons/useAddons';
|
import useAddons from 'hooks/api/getters/useAddons/useAddons';
|
||||||
import useToast from 'hooks/useToast';
|
import useToast from 'hooks/useToast';
|
||||||
import useAddonsApi from 'hooks/api/actions/useAddonsApi/useAddonsApi';
|
import useAddonsApi from 'hooks/api/actions/useAddonsApi/useAddonsApi';
|
||||||
import { ReactElement, useContext, useState } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import AccessContext from 'contexts/AccessContext';
|
|
||||||
import { IAddon } from 'interfaces/addons';
|
import { IAddon } from 'interfaces/addons';
|
||||||
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
|
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
|
||||||
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
|
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
|
||||||
|
import { sortTypes } from 'utils/sortTypes';
|
||||||
|
import { useTable, useSortBy } from 'react-table';
|
||||||
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
|
import { SortableTableHeader, TablePlaceholder } from 'component/common/Table';
|
||||||
|
import { IconCell } from 'component/common/Table/cells/IconCell/IconCell';
|
||||||
|
import { AddonIcon } from '../AddonIcon/AddonIcon';
|
||||||
|
import { ConfiguredAddonsActionsCell } from './ConfiguredAddonsActionCell/ConfiguredAddonsActionsCell';
|
||||||
|
|
||||||
interface IConfigureAddonsProps {
|
export const ConfiguredAddons = () => {
|
||||||
getAddonIcon: (name: string) => ReactElement;
|
const { refetchAddons, addons, loading } = useAddons();
|
||||||
}
|
|
||||||
|
|
||||||
export const ConfiguredAddons = ({ getAddonIcon }: IConfigureAddonsProps) => {
|
|
||||||
const { refetchAddons, addons } = useAddons();
|
|
||||||
const { updateAddon, removeAddon } = useAddonsApi();
|
const { updateAddon, removeAddon } = useAddonsApi();
|
||||||
const { setToastData, setToastApiError } = useToast();
|
const { setToastData, setToastApiError } = useToast();
|
||||||
const { hasAccess } = useContext(AccessContext);
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const [showDelete, setShowDelete] = useState(false);
|
const [showDelete, setShowDelete] = useState(false);
|
||||||
const [deletedAddon, setDeletedAddon] = useState<IAddon>({
|
const [deletedAddon, setDeletedAddon] = useState<IAddon>({
|
||||||
id: 0,
|
id: 0,
|
||||||
@ -43,27 +39,117 @@ export const ConfiguredAddons = ({ getAddonIcon }: IConfigureAddonsProps) => {
|
|||||||
parameters: {},
|
parameters: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
const sortAddons = (addons: IAddon[]) => {
|
const data = useMemo(() => {
|
||||||
if (!addons) return [];
|
if (loading) {
|
||||||
|
return Array(5).fill({
|
||||||
return addons.sort((addonA: IAddon, addonB: IAddon) => {
|
name: 'Addon name',
|
||||||
return addonA.id - addonB.id;
|
description: 'Addon description when loading',
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleAddon = async (addon: IAddon) => {
|
|
||||||
try {
|
|
||||||
await updateAddon({ ...addon, enabled: !addon.enabled });
|
|
||||||
refetchAddons();
|
|
||||||
setToastData({
|
|
||||||
type: 'success',
|
|
||||||
title: 'Success',
|
|
||||||
text: 'Addon state switched successfully',
|
|
||||||
});
|
});
|
||||||
} catch (error: unknown) {
|
|
||||||
setToastApiError(formatUnknownError(error));
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
return addons.map(addon => ({
|
||||||
|
...addon,
|
||||||
|
}));
|
||||||
|
}, [addons, loading]);
|
||||||
|
|
||||||
|
const toggleAddon = useCallback(
|
||||||
|
async (addon: IAddon) => {
|
||||||
|
try {
|
||||||
|
await updateAddon({ ...addon, enabled: !addon.enabled });
|
||||||
|
refetchAddons();
|
||||||
|
setToastData({
|
||||||
|
type: 'success',
|
||||||
|
title: 'Success',
|
||||||
|
text: 'Addon state switched successfully',
|
||||||
|
});
|
||||||
|
} catch (error: unknown) {
|
||||||
|
setToastApiError(formatUnknownError(error));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setToastApiError, refetchAddons, setToastData, updateAddon]
|
||||||
|
);
|
||||||
|
|
||||||
|
const columns = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
id: 'Icon',
|
||||||
|
Cell: ({
|
||||||
|
row: {
|
||||||
|
original: { provider },
|
||||||
|
},
|
||||||
|
}: any) => (
|
||||||
|
<IconCell icon={<AddonIcon name={provider as string} />} />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Name',
|
||||||
|
accessor: 'provider',
|
||||||
|
width: '90%',
|
||||||
|
Cell: ({
|
||||||
|
row: {
|
||||||
|
original: { provider, description },
|
||||||
|
},
|
||||||
|
}: any) => {
|
||||||
|
return (
|
||||||
|
<LinkCell
|
||||||
|
data-loading
|
||||||
|
title={provider}
|
||||||
|
subtitle={description}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
sortType: 'alphanumeric',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Actions',
|
||||||
|
id: 'Actions',
|
||||||
|
align: 'center',
|
||||||
|
Cell: ({ row: { original } }: any) => (
|
||||||
|
<ConfiguredAddonsActionsCell
|
||||||
|
setShowDelete={setShowDelete}
|
||||||
|
toggleAddon={toggleAddon}
|
||||||
|
setDeletedAddon={setDeletedAddon}
|
||||||
|
original={original as IAddon}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
width: 150,
|
||||||
|
disableSortBy: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'description',
|
||||||
|
disableSortBy: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[toggleAddon]
|
||||||
|
);
|
||||||
|
|
||||||
|
const initialState = useMemo(
|
||||||
|
() => ({
|
||||||
|
sortBy: [{ id: 'provider', desc: false }],
|
||||||
|
hiddenColumns: ['description'],
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
getTableProps,
|
||||||
|
getTableBodyProps,
|
||||||
|
headerGroups,
|
||||||
|
rows,
|
||||||
|
prepareRow,
|
||||||
|
state: { globalFilter },
|
||||||
|
} = useTable(
|
||||||
|
{
|
||||||
|
columns: columns as any[], // TODO: fix after `react-table` v8 update
|
||||||
|
data,
|
||||||
|
initialState,
|
||||||
|
sortTypes,
|
||||||
|
autoResetGlobalFilter: false,
|
||||||
|
autoResetSortBy: false,
|
||||||
|
disableSortRemove: true,
|
||||||
|
},
|
||||||
|
useSortBy
|
||||||
|
);
|
||||||
|
|
||||||
const onRemoveAddon = async (addon: IAddon) => {
|
const onRemoveAddon = async (addon: IAddon) => {
|
||||||
try {
|
try {
|
||||||
@ -74,78 +160,56 @@ export const ConfiguredAddons = ({ getAddonIcon }: IConfigureAddonsProps) => {
|
|||||||
title: 'Success',
|
title: 'Success',
|
||||||
text: 'Deleted addon successfully',
|
text: 'Deleted addon successfully',
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (error: unknown) {
|
||||||
setToastData({
|
setToastApiError(formatUnknownError(error));
|
||||||
type: 'error',
|
|
||||||
title: 'Error',
|
|
||||||
text: 'Can not delete addon',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderAddon = (addon: IAddon) => (
|
|
||||||
<ListItem key={addon.id}>
|
|
||||||
<ListItemAvatar>{getAddonIcon(addon.provider)}</ListItemAvatar>
|
|
||||||
<ListItemText
|
|
||||||
primary={
|
|
||||||
<span>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={hasAccess(UPDATE_ADDON)}
|
|
||||||
show={
|
|
||||||
<Link
|
|
||||||
style={{
|
|
||||||
textDecoration: 'none',
|
|
||||||
color: 'inherit',
|
|
||||||
}}
|
|
||||||
to={`/addons/edit/${addon.id}`}
|
|
||||||
>
|
|
||||||
<strong>{addon.provider}</strong>
|
|
||||||
</Link>
|
|
||||||
}
|
|
||||||
elseShow={<strong>{addon.provider}</strong>}
|
|
||||||
/>
|
|
||||||
{addon.enabled ? null : <small> (Disabled)</small>}
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
secondary={addon.description}
|
|
||||||
/>
|
|
||||||
<ListItemSecondaryAction>
|
|
||||||
<PermissionIconButton
|
|
||||||
permission={UPDATE_ADDON}
|
|
||||||
onClick={() => toggleAddon(addon)}
|
|
||||||
tooltipProps={{ title: 'Toggle addon' }}
|
|
||||||
>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={addon.enabled}
|
|
||||||
show={<Visibility />}
|
|
||||||
elseShow={<VisibilityOff />}
|
|
||||||
/>
|
|
||||||
</PermissionIconButton>
|
|
||||||
<PermissionIconButton
|
|
||||||
permission={UPDATE_ADDON}
|
|
||||||
tooltipProps={{ title: 'Edit Addon' }}
|
|
||||||
onClick={() => navigate(`/addons/edit/${addon.id}`)}
|
|
||||||
>
|
|
||||||
<Edit />
|
|
||||||
</PermissionIconButton>
|
|
||||||
<PermissionIconButton
|
|
||||||
permission={DELETE_ADDON}
|
|
||||||
tooltipProps={{ title: 'Remove Addon' }}
|
|
||||||
onClick={() => {
|
|
||||||
setDeletedAddon(addon);
|
|
||||||
setShowDelete(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Delete />
|
|
||||||
</PermissionIconButton>
|
|
||||||
</ListItemSecondaryAction>
|
|
||||||
</ListItem>
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<PageContent header="Configured addons">
|
<PageContent
|
||||||
<List>
|
isLoading={loading}
|
||||||
{sortAddons(addons).map((addon: IAddon) => renderAddon(addon))}
|
header={<PageHeader title="Configured addons" />}
|
||||||
</List>
|
>
|
||||||
|
<Table {...getTableProps()}>
|
||||||
|
<SortableTableHeader headerGroups={headerGroups} />
|
||||||
|
<TableBody {...getTableBodyProps()}>
|
||||||
|
{rows.map(row => {
|
||||||
|
prepareRow(row);
|
||||||
|
return (
|
||||||
|
<TableRow hover {...row.getRowProps()}>
|
||||||
|
{row.cells.map(cell => (
|
||||||
|
<TableCell
|
||||||
|
{...cell.getCellProps()}
|
||||||
|
padding="none"
|
||||||
|
>
|
||||||
|
{cell.render('Cell')}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={rows.length === 0}
|
||||||
|
show={
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={globalFilter?.length > 0}
|
||||||
|
show={
|
||||||
|
<TablePlaceholder>
|
||||||
|
No addons found matching “
|
||||||
|
{globalFilter}
|
||||||
|
”
|
||||||
|
</TablePlaceholder>
|
||||||
|
}
|
||||||
|
elseShow={
|
||||||
|
<TablePlaceholder>
|
||||||
|
No addons configured
|
||||||
|
</TablePlaceholder>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Dialogue
|
<Dialogue
|
||||||
open={showDelete}
|
open={showDelete}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -0,0 +1,58 @@
|
|||||||
|
import { Visibility, VisibilityOff, Edit, Delete } from '@mui/icons-material';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
|
||||||
|
import { ActionCell } from 'component/common/Table/cells/ActionCell/ActionCell';
|
||||||
|
import {
|
||||||
|
UPDATE_ADDON,
|
||||||
|
DELETE_ADDON,
|
||||||
|
} from 'component/providers/AccessProvider/permissions';
|
||||||
|
import { IAddon } from 'interfaces/addons';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
interface IConfiguredAddonsActionsCellProps {
|
||||||
|
toggleAddon: (addon: IAddon) => Promise<void>;
|
||||||
|
original: IAddon;
|
||||||
|
setShowDelete: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
setDeletedAddon: React.Dispatch<React.SetStateAction<IAddon>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ConfiguredAddonsActionsCell = ({
|
||||||
|
toggleAddon,
|
||||||
|
setShowDelete,
|
||||||
|
setDeletedAddon,
|
||||||
|
original,
|
||||||
|
}: IConfiguredAddonsActionsCellProps) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
return (
|
||||||
|
<ActionCell>
|
||||||
|
<PermissionIconButton
|
||||||
|
permission={UPDATE_ADDON}
|
||||||
|
onClick={() => toggleAddon(original)}
|
||||||
|
tooltipProps={{ title: 'Toggle addon' }}
|
||||||
|
>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={original.enabled}
|
||||||
|
show={<Visibility />}
|
||||||
|
elseShow={<VisibilityOff />}
|
||||||
|
/>
|
||||||
|
</PermissionIconButton>
|
||||||
|
<PermissionIconButton
|
||||||
|
permission={UPDATE_ADDON}
|
||||||
|
tooltipProps={{ title: 'Edit Addon' }}
|
||||||
|
onClick={() => navigate(`/addons/edit/${original.id}`)}
|
||||||
|
>
|
||||||
|
<Edit />
|
||||||
|
</PermissionIconButton>
|
||||||
|
<PermissionIconButton
|
||||||
|
permission={DELETE_ADDON}
|
||||||
|
tooltipProps={{ title: 'Remove Addon' }}
|
||||||
|
onClick={() => {
|
||||||
|
setDeletedAddon(original);
|
||||||
|
setShowDelete(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Delete />
|
||||||
|
</PermissionIconButton>
|
||||||
|
</ActionCell>
|
||||||
|
);
|
||||||
|
};
|
@ -1,4 +1,5 @@
|
|||||||
import { IAddon } from 'interfaces/addons';
|
import { IAddon } from 'interfaces/addons';
|
||||||
|
import { useCallback } from 'react';
|
||||||
import useAPI from '../useApi/useApi';
|
import useAPI from '../useApi/useApi';
|
||||||
|
|
||||||
const useAddonsApi = () => {
|
const useAddonsApi = () => {
|
||||||
@ -15,13 +16,7 @@ const useAddonsApi = () => {
|
|||||||
body: JSON.stringify(addonConfig),
|
body: JSON.stringify(addonConfig),
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
return makeRequest(req.caller, req.id);
|
||||||
const res = await makeRequest(req.caller, req.id);
|
|
||||||
|
|
||||||
return res;
|
|
||||||
} catch (e) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeAddon = async (id: number) => {
|
const removeAddon = async (id: number) => {
|
||||||
@ -29,29 +24,22 @@ const useAddonsApi = () => {
|
|||||||
const req = createRequest(path, {
|
const req = createRequest(path, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
});
|
});
|
||||||
try {
|
|
||||||
const res = await makeRequest(req.caller, req.id);
|
|
||||||
|
|
||||||
return res;
|
return await makeRequest(req.caller, req.id);
|
||||||
} catch (e) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateAddon = async (addonConfig: IAddon) => {
|
const updateAddon = useCallback(
|
||||||
const path = `${URI}/${addonConfig.id}`;
|
async (addonConfig: IAddon) => {
|
||||||
const req = createRequest(path, {
|
const path = `${URI}/${addonConfig.id}`;
|
||||||
method: 'PUT',
|
const req = createRequest(path, {
|
||||||
body: JSON.stringify(addonConfig),
|
method: 'PUT',
|
||||||
});
|
body: JSON.stringify(addonConfig),
|
||||||
try {
|
});
|
||||||
const res = await makeRequest(req.caller, req.id);
|
|
||||||
|
|
||||||
return res;
|
return makeRequest(req.caller, req.id);
|
||||||
} catch (e) {
|
},
|
||||||
throw e;
|
[createRequest, makeRequest]
|
||||||
}
|
);
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createAddon,
|
createAddon,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import useSWR, { mutate, SWRConfiguration } from 'swr';
|
import useSWR, { mutate, SWRConfiguration } from 'swr';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { formatApiPath } from 'utils/formatPath';
|
import { formatApiPath } from 'utils/formatPath';
|
||||||
import handleErrorResponses from '../httpErrorResponseHandler';
|
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||||
import { IAddon, IAddonProvider } from 'interfaces/addons';
|
import { IAddon, IAddonProvider } from 'interfaces/addons';
|
||||||
@ -23,9 +23,9 @@ const useAddons = (options: SWRConfiguration = {}) => {
|
|||||||
const { data, error } = useSWR<IAddonsResponse>(KEY, fetcher, options);
|
const { data, error } = useSWR<IAddonsResponse>(KEY, fetcher, options);
|
||||||
const [loading, setLoading] = useState(!error && !data);
|
const [loading, setLoading] = useState(!error && !data);
|
||||||
|
|
||||||
const refetchAddons = () => {
|
const refetchAddons = useCallback(() => {
|
||||||
mutate(KEY);
|
mutate(KEY);
|
||||||
};
|
}, [KEY]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(!error && !data);
|
setLoading(!error && !data);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useContext } from 'react';
|
import { useCallback, useContext } from 'react';
|
||||||
import UIContext from '../contexts/UIContext';
|
import UIContext from '../contexts/UIContext';
|
||||||
import { IToast } from '../interfaces/toast';
|
import { IToast } from '../interfaces/toast';
|
||||||
|
|
||||||
@ -11,24 +11,30 @@ const useToast = () => {
|
|||||||
show: false,
|
show: false,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const setToastApiError = (errorText: string, overrides?: IToast) => {
|
const setToastApiError = useCallback(
|
||||||
setToast({
|
(errorText: string, overrides?: IToast) => {
|
||||||
title: 'Something went wrong',
|
setToast({
|
||||||
text: `We had trouble talking to our API. Here's why: ${errorText}`,
|
title: 'Something went wrong',
|
||||||
type: 'error',
|
text: `We had trouble talking to our API. Here's why: ${errorText}`,
|
||||||
show: true,
|
type: 'error',
|
||||||
autoHideDuration: 12000,
|
show: true,
|
||||||
...overrides,
|
autoHideDuration: 12000,
|
||||||
});
|
...overrides,
|
||||||
};
|
});
|
||||||
|
},
|
||||||
|
[setToast]
|
||||||
|
);
|
||||||
|
|
||||||
const setToastData = (toast: IToast) => {
|
const setToastData = useCallback(
|
||||||
if (toast.persist) {
|
(toast: IToast) => {
|
||||||
setToast({ ...toast, show: true });
|
if (toast.persist) {
|
||||||
} else {
|
setToast({ ...toast, show: true });
|
||||||
setToast({ ...toast, show: true, autoHideDuration: 6000 });
|
} else {
|
||||||
}
|
setToast({ ...toast, show: true, autoHideDuration: 6000 });
|
||||||
};
|
}
|
||||||
|
},
|
||||||
|
[setToast]
|
||||||
|
);
|
||||||
|
|
||||||
return { setToastData, setToastApiError, hideToast };
|
return { setToastData, setToastApiError, hideToast };
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user