1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-08-04 13:48:56 +02:00

integrations loading

This commit is contained in:
Tymoteusz Czech 2023-08-21 17:04:40 +02:00
parent 67b04c9731
commit 50dc7ee57a
No known key found for this signature in database
GPG Key ID: 133555230D88D75F
9 changed files with 294 additions and 37 deletions

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

@ -3,34 +3,27 @@ import type { AddonTypeSchema } from 'openapi';
import { PageContent } from 'component/common/PageContent/PageContent';
import { PageHeader } from 'component/common/PageHeader/PageHeader';
import { IntegrationCard } from '../IntegrationCard/IntegrationCard';
import { styled } from '@mui/material';
import { StyledCardsGrid } from '../IntegrationList.styles';
interface IAvailableIntegrationsProps {
providers: AddonTypeSchema[];
}
const StyledGrid = styled('div')(({ theme }) => ({
gridTemplateColumns: 'repeat(auto-fit, minmax(350px, 1fr))',
gridAutoRows: '1fr',
gap: theme.spacing(2),
display: 'grid',
}));
export const AvailableIntegrations: VFC<IAvailableIntegrationsProps> = ({
providers,
}) => {
return (
<PageContent header={<PageHeader title="Available integrations" />}>
<StyledGrid>
<StyledCardsGrid>
{providers?.map(({ name, displayName, description }) => (
<IntegrationCard
key={name}
icon={name}
title={displayName || name}
description={description}
link={`/integrations/create/${name}`}
/>
))}
</StyledGrid>
</StyledCardsGrid>
</PageContent>
);
};

View File

@ -0,0 +1,63 @@
import { Table, TableBody, TableCell, TableRow } from 'component/common/Table';
import { useMemo, useState, useCallback } from 'react';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
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 { Dialogue } from 'component/common/Dialogue/Dialogue';
import { formatUnknownError } from 'utils/formatUnknownError';
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 { IntegrationIcon } from '../IntegrationIcon/IntegrationIcon';
import { IntegrationNameCell } from '../IntegrationNameCell/IntegrationNameCell';
import { StyledCardsGrid } from '../IntegrationList.styles';
import { IntegrationCard } from '../IntegrationCard/IntegrationCard';
export const ConfiguredIntegrations = () => {
const { refetchAddons, addons, loading } = useAddons();
const { updateAddon, removeAddon } = useAddonsApi();
const { setToastData, setToastApiError } = useToast();
const [showDelete, setShowDelete] = useState(false);
const data = useMemo(() => {
if (loading) {
return Array(5).fill({
name: 'Addon name',
description: 'Addon description when loading',
});
}
return addons.map(addon => ({
...addon,
}));
}, [addons, loading]);
const counter = addons.length ? `(${addons.length})` : '';
return (
<PageContent
isLoading={loading}
header={<PageHeader title={`Configured integrations ${counter}`} />}
sx={theme => ({ marginBottom: theme.spacing(2) })}
>
<StyledCardsGrid>
{addons?.map(({ id, enabled, provider, description }) => (
<IntegrationCard
key={id}
id={id}
icon={provider}
title={provider}
isEnabled={enabled}
description={description || ''}
isConfigured
link={`/integrations/edit/${id}`}
/>
))}
</StyledCardsGrid>
</PageContent>
);
};

View File

@ -1,40 +1,93 @@
import { VFC } from 'react';
import { Box, styled, Typography } from '@mui/material';
import { useState, 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';
interface IIntegrationCardProps {
icon: string;
id?: string | number;
icon?: string;
title: string;
description?: string;
isConfigured?: boolean;
isEnabled?: boolean;
isDeprecated?: boolean;
configureActionText?: string;
link: string;
}
const StyledBox = styled(Box)(({ theme }) => ({
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(Typography)(({ theme }) => ({
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> = ({
id,
icon,
title,
description,
isDeprecated,
isEnabled,
configureActionText = 'Configure',
link,
}) => {
const isConfigured = id !== undefined;
return (
<StyledBox>
<StyledHeader variant="h3">
<StyledLink to={link}>
<StyledHeader>
<StyledTitle variant="h3">
<IntegrationIcon name={icon as string} /> {title}
</StyledTitle>
<ConditionallyRender
condition={isEnabled === true}
show={<Badge color="success">Enabled</Badge>}
/>
<ConditionallyRender
condition={isEnabled === false}
show={<Badge>Disabled</Badge>}
/>
<ConditionallyRender
condition={isConfigured}
show={<IntegrationCardMenu id={id as string} />}
/>
</StyledHeader>
<Typography variant="body1">{description}</Typography>
</StyledBox>
<StyledAction>
{configureActionText} <ChevronRightIcon />
</StyledAction>
</StyledLink>
);
};

View File

@ -0,0 +1,126 @@
import { 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';
interface IIntegrationCardMenuProps {
id: string;
}
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> = ({ id }) => {
const [open, setOpen] = useState(false);
const [anchorEl, setAnchorEl] = useState<Element | null>(null);
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);
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}
>
<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',
}}
// style={{ top: 0, left: -100 }}
onClick={event => {
event.preventDefault();
}}
onClose={handleMenuClick}
>
<MenuItem
// onClick={e => {
// e.preventDefault();
// navigate(
// getProjectEditPath(
// id,
// Boolean(
// uiConfig.flags
// .newProjectLayout
// )
// )
// );
// }}
disabled={!updateAccess}
>
<ListItemIcon>
<PowerSettingsNew />
</ListItemIcon>
<ListItemText>Disable</ListItemText>
</MenuItem>{' '}
<MenuItem
disabled={!deleteAccess}
// onClick={() => setRemoveDialogOpen(true)}
>
<ListItemIcon>
<Delete />
</ListItemIcon>
<ListItemText>Delete</ListItemText>
</MenuItem>
{/* <MenuItem
onClick={e => {
e.preventDefault();
setShowDelDialog(true);
}}
disabled={!canDeleteProject}
>
<StyledDeleteIcon />
{id === DEFAULT_PROJECT_ID &&
!canDeleteProject
? "You can't delete the default project"
: 'Delete project'}
</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,19 @@
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';
export const IntegrationList = () => {
export const IntegrationList: VFC = () => {
const { providers, addons, loading } = useAddons();
const { uiConfig } = useUiConfig();
const integrationsRework = uiConfig?.flags?.integrationsRework || false;
return (
<>
<ConditionallyRender
condition={addons.length > 0}
show={<ConfiguredAddons />}
/>
<ConditionallyRender
condition={integrationsRework}
show={<AvailableIntegrations providers={providers} />}
elseShow={
<AvailableAddons loading={loading} providers={providers} />
}
show={<ConfiguredIntegrations />}
/>
<AvailableIntegrations providers={providers} />
</>
);
};

View File

@ -44,6 +44,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';
export const routes: IRoute[] = [
// Splash
@ -322,7 +323,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

@ -45,6 +45,7 @@ process.nextTick(async () => {
frontendNavigationUpdate: true,
lastSeenByEnvironment: true,
segmentChangeRequests: true,
integrationsRework: true,
},
},
authentication: {