1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

chore: add integration events modal (#7648)

https://linear.app/unleash/issue/2-2441/create-integration-events-modal

Adds the integration events modal to the UI, allowing us to visualize
them. This is the core of the UI work for this feature.

<img width="587" alt="image"
src="https://github.com/user-attachments/assets/f64cbb8c-1c01-4638-a661-5943ad7a890c">

### Example: Success

<img width="1277" alt="image"
src="https://github.com/user-attachments/assets/578bc7dc-d37d-4c0a-b74a-4bd33e859b51">

### Example: Success with errors

<img width="1255" alt="image"
src="https://github.com/user-attachments/assets/f784104d-7f11-4146-829d-6b3a3808815b">

### Example: Failed

<img width="1254" alt="image"
src="https://github.com/user-attachments/assets/543f857d-3877-4c17-92eb-58e6f038b8ac">
This commit is contained in:
Nuno Góis 2024-07-24 08:14:16 +01:00 committed by GitHub
parent 9c2b906d79
commit e63503e832
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 404 additions and 8 deletions

View File

@ -0,0 +1,75 @@
import { Alert, styled } from '@mui/material';
import type { IntegrationEvent } from 'interfaces/integrationEvent';
import { IntegrationEventsDetailsAccordion } from './IntegrationEventsDetailsAccordion';
import CheckCircleOutline from '@mui/icons-material/CheckCircleOutline';
import { Link } from 'react-router-dom';
import { lazy, Suspense } from 'react';
const LazyReactJSONEditor = lazy(
() => import('component/common/ReactJSONEditor/ReactJSONEditor'),
);
const StyledDetails = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(2),
padding: theme.spacing(2),
}));
const StyledAlert = styled(Alert)({
fontSize: 'inherit',
lineBreak: 'anywhere',
});
const StyledLink = styled(Link)(({ theme }) => ({
marginLeft: theme.spacing(1),
}));
export const IntegrationEventsDetails = ({
state,
stateDetails,
event,
details,
}: IntegrationEvent) => {
const severity =
state === 'failed'
? 'error'
: state === 'success'
? 'success'
: 'warning';
const icon = state === 'success' ? <CheckCircleOutline /> : undefined;
return (
<StyledDetails>
<StyledAlert severity={severity} icon={icon}>
{stateDetails}
</StyledAlert>
<IntegrationEventsDetailsAccordion
header={
<>
Event:
<StyledLink to='/history'>{event.type}</StyledLink>
</>
}
>
<Suspense fallback={null}>
<LazyReactJSONEditor
content={{ json: event }}
readOnly
statusBar={false}
editorStyle='sidePanel'
/>
</Suspense>
</IntegrationEventsDetailsAccordion>
<Suspense fallback={null}>
<LazyReactJSONEditor
content={{ json: details }}
readOnly
statusBar={false}
editorStyle='sidePanel'
/>
</Suspense>
</StyledDetails>
);
};

View File

@ -0,0 +1,45 @@
import ExpandMore from '@mui/icons-material/ExpandMore';
import {
Accordion,
AccordionDetails,
AccordionSummary,
IconButton,
styled,
} from '@mui/material';
import type { ReactNode } from 'react';
const StyledAccordion = styled(Accordion)(({ theme }) => ({
boxShadow: 'none',
border: `1px solid ${theme.palette.divider}`,
borderRadius: theme.shape.borderRadiusMedium,
'&:before': {
display: 'none',
},
}));
const StyledAccordionSummary = styled(AccordionSummary)({
lineHeight: '1.5rem',
});
interface IIntegrationEventsDetailsEventProps {
header: ReactNode;
children: ReactNode;
}
export const IntegrationEventsDetailsAccordion = ({
header,
children,
}: IIntegrationEventsDetailsEventProps) => (
<StyledAccordion>
<StyledAccordionSummary
expandIcon={
<IconButton>
<ExpandMore titleAccess='Toggle' />
</IconButton>
}
>
{header}
</StyledAccordionSummary>
<AccordionDetails>{children}</AccordionDetails>
</StyledAccordion>
);

View File

@ -0,0 +1,174 @@
import { Button, Link, styled } from '@mui/material';
import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
import type { AddonSchema } from 'openapi';
import { useIntegrationEvents } from 'hooks/api/getters/useIntegrationEvents/useIntegrationEvents';
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
import { SidePanelList } from 'component/common/SidePanelList/SidePanelList';
import { formatDateYMDHMS } from 'utils/formatDate';
import { useLocationSettings } from 'hooks/useLocationSettings';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { IntegrationEventsStateIcon } from './IntegrationEventsStateIcon';
import { IntegrationEventsDetails } from './IntegrationEventsDetails/IntegrationEventsDetails';
import { useNavigate } from 'react-router-dom';
const StyledHeader = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
marginBottom: theme.fontSizes.mainHeader,
}));
const StyledHeaderRow = styled('div')({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
width: '100%',
});
const StyledTitle = styled('h1')({
fontWeight: 'normal',
});
const StyledSubtitle = styled('h2')(({ theme }) => ({
marginTop: theme.spacing(1.5),
fontSize: theme.fontSizes.smallBody,
fontWeight: 'normal',
}));
const StyledForm = styled('form')({
display: 'flex',
flexDirection: 'column',
height: '100%',
});
const StyledFailedItemWrapper = styled('div')(({ theme }) => ({
backgroundColor: theme.palette.error.light,
}));
const StyledButtonContainer = styled('div')(({ theme }) => ({
marginTop: 'auto',
display: 'flex',
justifyContent: 'flex-end',
paddingTop: theme.spacing(4),
}));
interface IIntegrationEventsModalProps {
addon?: AddonSchema;
open: boolean;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
}
export const IntegrationEventsModal = ({
addon,
open,
setOpen,
}: IIntegrationEventsModalProps) => {
const navigate = useNavigate();
const { locationSettings } = useLocationSettings();
const { integrationEvents, hasMore, loadMore, loading } =
useIntegrationEvents(addon?.id, 20, {
refreshInterval: 5000,
});
if (!addon) {
return null;
}
const title = `Events: ${addon.provider}${addon.description ? ` - ${addon.description}` : ''} (id: ${addon.id})`;
return (
<SidebarModal
open={open}
onClose={() => {
setOpen(false);
}}
label={title}
>
<FormTemplate
loading={loading && integrationEvents.length === 0}
modal
description=''
documentationLink=''
documentationLinkLabel=''
showGuidance={false}
>
<StyledHeader>
<StyledHeaderRow>
<StyledTitle>{title}</StyledTitle>
<Link
onClick={() => {
setOpen(false);
navigate(`/integrations/edit/${addon.id}`);
}}
>
View configuration
</Link>
</StyledHeaderRow>
<StyledHeaderRow>
<StyledSubtitle>
All events older than the last 100 or older than the
past 2 hours will be automatically deleted.
</StyledSubtitle>
</StyledHeaderRow>
</StyledHeader>
<StyledForm>
<SidePanelList
height={960}
items={integrationEvents}
columns={[
{
header: 'Status',
align: 'center',
maxWidth: 100,
cell: IntegrationEventsStateIcon,
},
{
header: 'Date',
maxWidth: 240,
cell: ({ createdAt }) =>
formatDateYMDHMS(
createdAt,
locationSettings?.locale,
),
},
]}
sidePanelHeader='Details'
renderContent={IntegrationEventsDetails}
renderItem={({ id, state }, children) => {
if (state === 'failed') {
return (
<StyledFailedItemWrapper key={id}>
{children}
</StyledFailedItemWrapper>
);
}
return children;
}}
listEnd={
<ConditionallyRender
condition={hasMore}
show={
<Button onClick={loadMore}>
Load more
</Button>
}
/>
}
/>
<ConditionallyRender
condition={integrationEvents.length === 0}
show={
<p>
No events have been registered for this
integration configuration.
</p>
}
/>
<StyledButtonContainer>
<Button onClick={() => setOpen(false)}>Close</Button>
</StyledButtonContainer>
</StyledForm>
</FormTemplate>
</SidebarModal>
);
};

View File

@ -0,0 +1,31 @@
import { styled } from '@mui/material';
import CheckCircleOutline from '@mui/icons-material/CheckCircleOutline';
import ErrorOutline from '@mui/icons-material/ErrorOutline';
import WarningAmber from '@mui/icons-material/WarningAmber';
import type { IntegrationEvent } from 'interfaces/integrationEvent';
export const StyledSuccessIcon = styled(CheckCircleOutline)(({ theme }) => ({
color: theme.palette.success.main,
}));
export const StyledFailedIcon = styled(ErrorOutline)(({ theme }) => ({
color: theme.palette.error.main,
}));
export const StyledSuccessWithErrorsIcon = styled(WarningAmber)(
({ theme }) => ({
color: theme.palette.warning.main,
}),
);
export const IntegrationEventsStateIcon = ({ state }: IntegrationEvent) => {
if (state === 'success') {
return <StyledSuccessIcon />;
}
if (state === 'failed') {
return <StyledFailedIcon />;
}
return <StyledSuccessWithErrorsIcon />;
};

View File

@ -2,11 +2,19 @@ import {
type ChangeEventHandler,
type FormEventHandler,
type MouseEventHandler,
useContext,
useEffect,
useState,
type VFC,
} from 'react';
import { Alert, Button, Divider, Typography } from '@mui/material';
import {
Alert,
Button,
Divider,
Link,
styled,
Typography,
} from '@mui/material';
import produce from 'immer';
import { trim } from 'component/common/util';
import type { AddonSchema, AddonTypeSchema } from 'openapi';
@ -44,6 +52,21 @@ import { IntegrationDelete } from './IntegrationDelete/IntegrationDelete';
import { IntegrationStateSwitch } from './IntegrationStateSwitch/IntegrationStateSwitch';
import { capitalizeFirst } from 'utils/capitalizeFirst';
import { IntegrationHowToSection } from '../IntegrationHowToSection/IntegrationHowToSection';
import { useUiFlag } from 'hooks/useUiFlag';
import { IntegrationEventsModal } from '../IntegrationEventsModal/IntegrationEventsModal';
import AccessContext from 'contexts/AccessContext';
const StyledHeader = styled('div')(({ theme }) => ({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
width: '100%',
marginBottom: theme.fontSizes.mainHeader,
}));
const StyledHeaderTitle = styled('h1')({
fontWeight: 'normal',
});
type IntegrationFormProps = {
provider?: AddonTypeSchema;
@ -91,6 +114,10 @@ export const IntegrationForm: VFC<IntegrationFormProps> = ({
containsErrors: false,
parameters: {},
});
const [eventsModalOpen, setEventsModalOpen] = useState(false);
const { isAdmin } = useContext(AccessContext);
const integrationEventsEnabled = useUiFlag('integrationEvents');
const submitText = editMode ? 'Update' : 'Create';
const url = `${uiConfig.unleashUrl}/api/admin/addons${
editMode ? `/${(formValues as AddonSchema).id}` : ``
@ -259,13 +286,6 @@ export const IntegrationForm: VFC<IntegrationFormProps> = ({
return (
<FormTemplate
title={
<>
{submitText}{' '}
{displayName || (name ? capitalizeFirst(name) : '')}{' '}
integration
</>
}
description={description || ''}
documentationLink={documentationUrl}
documentationLinkLabel={`${
@ -291,6 +311,21 @@ export const IntegrationForm: VFC<IntegrationFormProps> = ({
</StyledButtonContainer>
}
>
<StyledHeader>
<StyledHeaderTitle>
{submitText}{' '}
{displayName || (name ? capitalizeFirst(name) : '')}{' '}
integration
</StyledHeaderTitle>
<ConditionallyRender
condition={editMode && isAdmin && integrationEventsEnabled}
show={
<Link onClick={() => setEventsModalOpen(true)}>
View events
</Link>
}
/>
</StyledHeader>
<StyledForm onSubmit={onSubmit}>
<StyledContainer>
<ConditionallyRender
@ -403,6 +438,11 @@ export const IntegrationForm: VFC<IntegrationFormProps> = ({
</section>
</StyledContainer>
</StyledForm>
<IntegrationEventsModal
addon={initialValues as AddonSchema}
open={eventsModalOpen}
setOpen={setEventsModalOpen}
/>
</FormTemplate>
);
};

View File

@ -14,6 +14,7 @@ import MoreVertIcon from '@mui/icons-material/MoreVert';
import Delete from '@mui/icons-material/Delete';
import PowerSettingsNew from '@mui/icons-material/PowerSettingsNew';
import {
ADMIN,
DELETE_ADDON,
UPDATE_ADDON,
} from 'component/providers/AccessProvider/permissions';
@ -24,6 +25,11 @@ import useAddons from 'hooks/api/getters/useAddons/useAddons';
import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError';
import { Dialogue } from 'component/common/Dialogue/Dialogue';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useUiFlag } from 'hooks/useUiFlag';
import Visibility from '@mui/icons-material/Visibility';
import { PermissionHOC } from 'component/common/PermissionHOC/PermissionHOC';
import { IntegrationEventsModal } from 'component/integrations/IntegrationEventsModal/IntegrationEventsModal';
interface IIntegrationCardMenuProps {
addon: AddonSchema;
@ -48,6 +54,8 @@ export const IntegrationCardMenu: VFC<IIntegrationCardMenuProps> = ({
const { updateAddon, removeAddon } = useAddonsApi();
const { refetchAddons } = useAddons();
const { setToastData, setToastApiError } = useToast();
const [eventsModalOpen, setEventsModalOpen] = useState(false);
const integrationEventsEnabled = useUiFlag('integrationEvents');
const closeMenu = () => {
setIsMenuOpen(false);
@ -123,6 +131,24 @@ export const IntegrationCardMenu: VFC<IIntegrationCardMenuProps> = ({
}}
onClose={handleMenuClick}
>
<ConditionallyRender
condition={integrationEventsEnabled}
show={
<PermissionHOC permission={ADMIN}>
{({ hasAccess }) => (
<MenuItem
onClick={() => setEventsModalOpen(true)}
disabled={!hasAccess}
>
<ListItemIcon>
<Visibility />
</ListItemIcon>
<ListItemText>View events</ListItemText>
</MenuItem>
)}
</PermissionHOC>
}
/>
<MenuItem
onClick={() => {
setIsToggleOpen(true);
@ -151,6 +177,11 @@ export const IntegrationCardMenu: VFC<IIntegrationCardMenuProps> = ({
</MenuItem>
</Menu>
<IntegrationEventsModal
addon={addon}
open={eventsModalOpen}
setOpen={setEventsModalOpen}
/>
<Dialogue
open={isToggleOpen}
onClick={toggleIntegration}