Skip to content

Commit

Permalink
chore: incoming webhook events UI (#6317)
Browse files Browse the repository at this point in the history
https://linear.app/unleash/issue/2-1937/incoming-webhook-events-ui

This PR implements the UI for incoming webhook events.

We're also introducing a new `SidePanelList` component that we'll be
able to reuse when we tackle action set events. This PR also promotes
`ReactJSONEditor` to a common component and adapts it slightly for this
use case.


![image](https://github.com/Unleash/unleash/assets/14320932/b1abc2e0-3971-4882-b6f6-0ae48d1523d5)


![image](https://github.com/Unleash/unleash/assets/14320932/ce5c31e4-650a-4df5-a966-2ce06fd6baa8)

We're refreshing the events view every 5s, so if you're monitoring
events for a specific incoming webhook you can see the latest ones
coming in.
We load 20 (configurable through the hook) events by default. Everytime
you reach the end of the list you can load 20 more events until you
reach the end of the event list.


![image](https://github.com/Unleash/unleash/assets/14320932/94f187a1-8b0f-4138-8dbc-d3ebc9914bfd)
  • Loading branch information
nunogois authored Feb 23, 2024
1 parent a54ef27 commit 12ff4ab
Show file tree
Hide file tree
Showing 12 changed files with 603 additions and 9 deletions.
6 changes: 4 additions & 2 deletions frontend/src/component/common/FormTemplate/FormTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ interface ICreateProps {
formatApiCode?: () => string;
footer?: ReactNode;
compact?: boolean;
showGuidance?: boolean;
}

const StyledContainer = styled('section', {
Expand Down Expand Up @@ -202,6 +203,7 @@ const FormTemplate: React.FC<ICreateProps> = ({
showLink = true,
footer,
compact,
showGuidance = true,
}) => {
const { setToastData } = useToast();
const smallScreen = useMediaQuery(`(max-width:${1099}px)`);
Expand Down Expand Up @@ -252,7 +254,7 @@ const FormTemplate: React.FC<ICreateProps> = ({
return (
<StyledContainer modal={modal} compact={compact}>
<ConditionallyRender
condition={smallScreen}
condition={showGuidance && smallScreen}
show={
<StyledRelativeDiv>
<MobileGuidance
Expand Down Expand Up @@ -293,7 +295,7 @@ const FormTemplate: React.FC<ICreateProps> = ({
/>
</StyledMain>
<ConditionallyRender
condition={!smallScreen}
condition={showGuidance && !smallScreen}
show={
<Guidance
description={description}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import 'vanilla-jsoneditor/themes/jse-theme-dark.css';
import { styled } from '@mui/material';
import UIContext from 'contexts/UIContext';

const JSONEditorThemeWrapper = styled('div')(({ theme }) => ({
type EditorStyle = 'default' | 'sidePanel';

const JSONEditorThemeWrapper = styled('div', {
shouldForwardProp: (prop) => prop !== 'editorStyle',
})<{ editorStyle?: EditorStyle }>(({ theme, editorStyle = 'default' }) => ({
'&.jse-theme-dark': {
'--jse-background-color': theme.palette.background.default,
'--jse-panel-background': theme.palette.background.default,
Expand All @@ -24,9 +28,40 @@ const JSONEditorThemeWrapper = styled('div')(({ theme }) => ({
borderBottomLeftRadius: theme.shape.borderRadius,
borderBottomRightRadius: theme.shape.borderRadius,
},
...(editorStyle === 'sidePanel' && {
'&&&': {
'& .jse-main': {
minHeight: 0,
},
'--jse-main-border': 0,
'& > div': {
height: '100%',
},
'& .jse-focus': {
'--jse-main-border': 0,
},
'& .cm-gutters': {
'--jse-panel-background': 'transparent',
'--jse-panel-border': 'transparent',
},
'& .cm-gutter-lint': {
width: 0,
},
'& .jse-text-mode': {
borderBottomRightRadius: theme.shape.borderRadiusMedium,
},
'& .cm-scroller': {
'--jse-delimiter-color': theme.palette.text.primary,
},
},
}),
}));

const VanillaJSONEditor: React.FC<JSONEditorPropsOptional> = (props) => {
interface IReactJSONEditorProps extends JSONEditorPropsOptional {
editorStyle?: EditorStyle;
}

const VanillaJSONEditor: React.FC<IReactJSONEditorProps> = (props) => {
const refContainer = useRef<HTMLDivElement | null>(null);
const refEditor = useRef<JSONEditor | null>(null);

Expand Down Expand Up @@ -58,11 +93,12 @@ const VanillaJSONEditor: React.FC<JSONEditorPropsOptional> = (props) => {
return <div ref={refContainer} />;
};

const ReactJSONEditor: React.FC<JSONEditorPropsOptional> = (props) => {
const ReactJSONEditor: React.FC<IReactJSONEditorProps> = (props) => {
const { themeMode } = useContext(UIContext);
return (
<JSONEditorThemeWrapper
className={themeMode === 'dark' ? 'jse-theme-dark' : ''}
editorStyle={props.editorStyle}
>
<VanillaJSONEditor
mainMenuBar={false}
Expand Down
122 changes: 122 additions & 0 deletions frontend/src/component/common/SidePanelList/SidePanelList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { styled } from '@mui/material';
import { ReactNode, useState } from 'react';
import { SidePanelListHeader } from './SidePanelListHeader';
import { SidePanelListItem } from './SidePanelListItem';

const StyledSidePanelListWrapper = styled('div')({
display: 'flex',
flexDirection: 'column',
width: '100%',
});

const StyledSidePanelListBody = styled('div')({
display: 'flex',
flexDirection: 'row',
});

const StyledSidePanelHalf = styled('div')({
display: 'flex',
flexDirection: 'column',
flex: 1,
});

const StyledSidePanelHalfLeft = styled(StyledSidePanelHalf, {
shouldForwardProp: (prop) => prop !== 'height',
})<{ height?: number }>(({ theme, height }) => ({
border: `1px solid ${theme.palette.divider}`,
borderTop: 0,
borderBottomLeftRadius: theme.shape.borderRadiusMedium,
overflow: 'auto',
...(height && { height }),
}));

const StyledSidePanelHalfRight = styled(StyledSidePanelHalf)(({ theme }) => ({
border: `1px solid ${theme.palette.divider}`,
borderTop: 0,
borderLeft: 0,
borderBottomRightRadius: theme.shape.borderRadiusMedium,
}));

type ColumnAlignment = 'start' | 'end' | 'center';

export const StyledSidePanelListColumn = styled('div', {
shouldForwardProp: (prop) => prop !== 'maxWidth' && prop !== 'align',
})<{ maxWidth?: number; align?: ColumnAlignment }>(
({ theme, maxWidth, align = 'start' }) => ({
flex: 1,
padding: theme.spacing(2),
fontSize: theme.fontSizes.smallBody,
justifyContent: align,
...(maxWidth && { maxWidth }),
textAlign: align,
}),
);

export type SidePanelListColumn<T> = {
header: string;
maxWidth?: number;
align?: ColumnAlignment;
cell: (item: T) => ReactNode;
};

interface ISidePanelListProps<T> {
items: T[];
columns: SidePanelListColumn<T>[];
sidePanelHeader: string;
renderContent: (item: T) => ReactNode;
height?: number;
listEnd?: ReactNode;
}

export const SidePanelList = <T extends { id: string | number }>({
items,
columns,
sidePanelHeader,
renderContent,
height,
listEnd,
}: ISidePanelListProps<T>) => {
const [selectedItem, setSelectedItem] = useState<T>(items[0]);

if (items.length === 0) {
return null;
}

const activeItem = selectedItem || items[0];

return (
<StyledSidePanelListWrapper>
<SidePanelListHeader
columns={columns}
sidePanelHeader={sidePanelHeader}
/>
<StyledSidePanelListBody>
<StyledSidePanelHalfLeft height={height}>
{items.map((item) => (
<SidePanelListItem
key={item.id}
selected={activeItem.id === item.id}
onClick={() => setSelectedItem(item)}
>
{columns.map(
({ header, maxWidth, align, cell }) => (
<StyledSidePanelListColumn
key={header}
maxWidth={maxWidth}
align={align}
>
{cell(item)}
</StyledSidePanelListColumn>
),
)}
</SidePanelListItem>
))}
{listEnd}
</StyledSidePanelHalfLeft>
<StyledSidePanelHalfRight>
{renderContent(activeItem)}
</StyledSidePanelHalfRight>
</StyledSidePanelListBody>
</StyledSidePanelListWrapper>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { styled } from '@mui/material';
import {
SidePanelListColumn,
StyledSidePanelListColumn,
} from './SidePanelList';

const StyledHeader = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
borderTopLeftRadius: theme.shape.borderRadiusMedium,
borderTopRightRadius: theme.shape.borderRadiusMedium,
backgroundColor: theme.palette.table.headerBackground,
}));

const StyledHeaderHalf = styled('div')({
display: 'flex',
flex: 1,
});

interface ISidePanelListHeaderProps<T> {
columns: SidePanelListColumn<T>[];
sidePanelHeader: string;
}

export const SidePanelListHeader = <T,>({
columns,
sidePanelHeader,
}: ISidePanelListHeaderProps<T>) => (
<StyledHeader>
<StyledHeaderHalf>
{columns.map(({ header, maxWidth, align }) => (
<StyledSidePanelListColumn
key={header}
maxWidth={maxWidth}
align={align}
>
{header}
</StyledSidePanelListColumn>
))}
</StyledHeaderHalf>
<StyledHeaderHalf>
<StyledSidePanelListColumn>
{sidePanelHeader}
</StyledSidePanelListColumn>
</StyledHeaderHalf>
</StyledHeader>
);
58 changes: 58 additions & 0 deletions frontend/src/component/common/SidePanelList/SidePanelListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Button, styled } from '@mui/material';
import { ReactNode } from 'react';

const StyledItemRow = styled('div')(({ theme }) => ({
borderBottom: `1px solid ${theme.palette.divider}`,
}));

const StyledItem = styled(Button, {
shouldForwardProp: (prop) => prop !== 'selected',
})<{ selected: boolean }>(({ theme, selected }) => ({
'&.MuiButton-root': {
width: '100%',
backgroundColor: selected
? theme.palette.secondary.light
: 'transparent',
borderRight: `${theme.spacing(0.5)} solid ${
selected ? theme.palette.background.alternative : 'transparent'
}`,
padding: 0,
borderRadius: 0,
justifyContent: 'start',
transition: 'background-color 0.2s ease',
color: theme.palette.text.primary,
textAlign: 'left',
fontWeight: selected ? theme.fontWeight.bold : theme.fontWeight.medium,
fontSize: theme.fontSizes.smallBody,
overflow: 'auto',
},
'&:hover': {
backgroundColor: selected
? theme.palette.secondary.light
: theme.palette.neutral.light,
},
'&.Mui-disabled': {
pointerEvents: 'auto',
},
'&:focus-visible': {
outline: `2px solid ${theme.palette.primary.main}`,
},
}));

interface ISidePanelListItemProps<T> {
selected: boolean;
onClick: () => void;
children: ReactNode;
}

export const SidePanelListItem = <T,>({
selected,
onClick,
children,
}: ISidePanelListItemProps<T>) => (
<StyledItemRow>
<StyledItem selected={selected} onClick={onClick}>
{children}
</StyledItem>
</StyledItemRow>
);
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ import { WeightType } from 'constants/variantTypes';
import { IFeatureVariantEdit } from '../EnvironmentVariantsModal';
import { Delete } from '@mui/icons-material';

const LazyReactJSONEditor = React.lazy(() => import('./ReactJSONEditor'));
const LazyReactJSONEditor = React.lazy(
() => import('component/common/ReactJSONEditor/ReactJSONEditor'),
);

const StyledVariantForm = styled('div')(({ theme }) => ({
position: 'relative',
Expand Down
Loading

0 comments on commit 12ff4ab

Please sign in to comment.