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:
parent
9c2b906d79
commit
e63503e832
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
@ -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>
|
||||
);
|
||||
};
|
@ -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 />;
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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}
|
||||
|
Loading…
Reference in New Issue
Block a user