mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-09 13:47:13 +02:00
integrations loading
This commit is contained in:
parent
67b04c9731
commit
50dc7ee57a
@ -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} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -3,34 +3,27 @@ import type { AddonTypeSchema } from 'openapi';
|
|||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
import { IntegrationCard } from '../IntegrationCard/IntegrationCard';
|
import { IntegrationCard } from '../IntegrationCard/IntegrationCard';
|
||||||
import { styled } from '@mui/material';
|
import { StyledCardsGrid } from '../IntegrationList.styles';
|
||||||
|
|
||||||
interface IAvailableIntegrationsProps {
|
interface IAvailableIntegrationsProps {
|
||||||
providers: AddonTypeSchema[];
|
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> = ({
|
export const AvailableIntegrations: VFC<IAvailableIntegrationsProps> = ({
|
||||||
providers,
|
providers,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<PageContent header={<PageHeader title="Available integrations" />}>
|
<PageContent header={<PageHeader title="Available integrations" />}>
|
||||||
<StyledGrid>
|
<StyledCardsGrid>
|
||||||
{providers?.map(({ name, displayName, description }) => (
|
{providers?.map(({ name, displayName, description }) => (
|
||||||
<IntegrationCard
|
<IntegrationCard
|
||||||
key={name}
|
key={name}
|
||||||
icon={name}
|
icon={name}
|
||||||
title={displayName || name}
|
title={displayName || name}
|
||||||
description={description}
|
description={description}
|
||||||
|
link={`/integrations/create/${name}`}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</StyledGrid>
|
</StyledCardsGrid>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -1,40 +1,93 @@
|
|||||||
import { VFC } from 'react';
|
import { useState, VFC } from 'react';
|
||||||
import { Box, styled, Typography } from '@mui/material';
|
import { Link } from 'react-router-dom';
|
||||||
|
import { styled, Typography } from '@mui/material';
|
||||||
import { IntegrationIcon } from '../IntegrationIcon/IntegrationIcon';
|
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 {
|
interface IIntegrationCardProps {
|
||||||
icon: string;
|
id?: string | number;
|
||||||
|
icon?: string;
|
||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
isConfigured?: boolean;
|
||||||
isEnabled?: 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),
|
padding: theme.spacing(3),
|
||||||
borderRadius: `${theme.shape.borderRadiusMedium}px`,
|
borderRadius: `${theme.shape.borderRadiusMedium}px`,
|
||||||
border: `1px solid ${theme.palette.divider}`,
|
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',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginBottom: theme.spacing(2),
|
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> = ({
|
export const IntegrationCard: VFC<IIntegrationCardProps> = ({
|
||||||
|
id,
|
||||||
icon,
|
icon,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
isDeprecated,
|
|
||||||
isEnabled,
|
isEnabled,
|
||||||
|
configureActionText = 'Configure',
|
||||||
|
link,
|
||||||
}) => {
|
}) => {
|
||||||
|
const isConfigured = id !== undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledBox>
|
<StyledLink to={link}>
|
||||||
<StyledHeader variant="h3">
|
<StyledHeader>
|
||||||
|
<StyledTitle variant="h3">
|
||||||
<IntegrationIcon name={icon as string} /> {title}
|
<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>
|
</StyledHeader>
|
||||||
<Typography variant="body1">{description}</Typography>
|
<Typography variant="body1">{description}</Typography>
|
||||||
</StyledBox>
|
<StyledAction>
|
||||||
|
{configureActionText} <ChevronRightIcon />
|
||||||
|
</StyledAction>
|
||||||
|
</StyledLink>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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',
|
||||||
|
}));
|
@ -1,28 +1,19 @@
|
|||||||
import { ConfiguredAddons } from './ConfiguredAddons/ConfiguredAddons';
|
import { VFC } from 'react';
|
||||||
import { AvailableAddons } from './AvailableAddons/AvailableAddons';
|
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import useAddons from 'hooks/api/getters/useAddons/useAddons';
|
import useAddons from 'hooks/api/getters/useAddons/useAddons';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
|
||||||
import { AvailableIntegrations } from './AvailableIntegrations/AvailableIntegrations';
|
import { AvailableIntegrations } from './AvailableIntegrations/AvailableIntegrations';
|
||||||
|
import { ConfiguredIntegrations } from './ConfiguredIntegrations/ConfiguredIntegrations';
|
||||||
|
|
||||||
export const IntegrationList = () => {
|
export const IntegrationList: VFC = () => {
|
||||||
const { providers, addons, loading } = useAddons();
|
const { providers, addons, loading } = useAddons();
|
||||||
const { uiConfig } = useUiConfig();
|
|
||||||
const integrationsRework = uiConfig?.flags?.integrationsRework || false;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={addons.length > 0}
|
condition={addons.length > 0}
|
||||||
show={<ConfiguredAddons />}
|
show={<ConfiguredIntegrations />}
|
||||||
/>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={integrationsRework}
|
|
||||||
show={<AvailableIntegrations providers={providers} />}
|
|
||||||
elseShow={
|
|
||||||
<AvailableAddons loading={loading} providers={providers} />
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
<AvailableIntegrations providers={providers} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -44,6 +44,7 @@ import { LazyAdmin } from 'component/admin/LazyAdmin';
|
|||||||
import { LazyProject } from 'component/project/Project/LazyProject';
|
import { LazyProject } from 'component/project/Project/LazyProject';
|
||||||
import { LoginHistory } from 'component/loginHistory/LoginHistory';
|
import { LoginHistory } from 'component/loginHistory/LoginHistory';
|
||||||
import { FeatureTypesList } from 'component/featureTypes/FeatureTypesList';
|
import { FeatureTypesList } from 'component/featureTypes/FeatureTypesList';
|
||||||
|
import { AddonsList } from 'component/integrations/IntegrationList/AddonsList';
|
||||||
|
|
||||||
export const routes: IRoute[] = [
|
export const routes: IRoute[] = [
|
||||||
// Splash
|
// Splash
|
||||||
@ -322,7 +323,7 @@ export const routes: IRoute[] = [
|
|||||||
{
|
{
|
||||||
path: '/addons',
|
path: '/addons',
|
||||||
title: 'Addons',
|
title: 'Addons',
|
||||||
component: IntegrationList,
|
component: AddonsList,
|
||||||
// TODO: use AddonRedirect after removing `integrationsRework` menu flag
|
// TODO: use AddonRedirect after removing `integrationsRework` menu flag
|
||||||
hidden: false,
|
hidden: false,
|
||||||
type: 'protected',
|
type: 'protected',
|
||||||
|
@ -45,6 +45,7 @@ process.nextTick(async () => {
|
|||||||
frontendNavigationUpdate: true,
|
frontendNavigationUpdate: true,
|
||||||
lastSeenByEnvironment: true,
|
lastSeenByEnvironment: true,
|
||||||
segmentChangeRequests: true,
|
segmentChangeRequests: true,
|
||||||
|
integrationsRework: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
authentication: {
|
authentication: {
|
||||||
|
Loading…
Reference in New Issue
Block a user