add dynamic dates for webhookGraphDataUsage (#7720)
**Before:** Only last 5 days where displayed on Developers Settings Webhook Usage Graph.  **Now** Added component where you can select the time range where you want to view the webhook usage. To do better the styling and content depassing . <img width="652" alt="Screenshot 2024-10-15 at 16 56 45" src="https://github.com/user-attachments/assets/d06e7f4c-a689-49a0-8839-f015ce36bab9"> **In order to test** 1. Set ANALYTICS_ENABLED to true 2. Set TINYBIRD_TOKEN to your token from the workspace twenty_analytics_playground 3. Write your client tinybird token in SettingsDeveloppersWebhookDetail.tsx in line 93 4. Create a Webhook in twenty and set wich events it needs to track 5. Run twenty-worker in order to make the webhooks work. 6. Do your tasks in order to populate the data 7. Enter to settings> webhook>your webhook and the statistics section should be displayed. 8. Select the desired time range in the dropdown **To do list** - Tooltip is truncated when accessing values at the right end of the graph - DateTicks needs to follow a more clear standard - Update this PR with more representative images
This commit is contained in:
committed by
GitHub
parent
0c24001e23
commit
8cadcdf577
@ -0,0 +1,89 @@
|
||||
import { formatDateISOStringToDateTimeSimplified } from '@/localization/utils/formatDateISOStringToDateTimeSimplified';
|
||||
import { UserContext } from '@/users/contexts/UserContext';
|
||||
import styled from '@emotion/styled';
|
||||
import { Point } from '@nivo/line';
|
||||
import { ReactElement, useContext } from 'react';
|
||||
|
||||
const StyledTooltipContainer = styled.div`
|
||||
align-items: center;
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
display: flex;
|
||||
width: 128px;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
background: ${({ theme }) => theme.background.transparent.secondary};
|
||||
box-shadow: ${({ theme }) => theme.boxShadow.light};
|
||||
backdrop-filter: ${({ theme }) => theme.blur.medium};
|
||||
`;
|
||||
|
||||
const StyledTooltipDateContainer = styled.div`
|
||||
align-items: flex-start;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
font-family: ${({ theme }) => theme.font.family};
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledTooltipDataRow = styled.div`
|
||||
align-items: flex-start;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledLine = styled.div`
|
||||
background-color: ${({ theme }) => theme.border.color.medium};
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
`;
|
||||
const StyledColorPoint = styled.div<{ color: string }>`
|
||||
background-color: ${({ color }) => color};
|
||||
border-radius: 50%;
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
display: inline-block;
|
||||
`;
|
||||
const StyledDataDefinition = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
const StyledSpan = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
`;
|
||||
type SettingsDevelopersWebhookTooltipProps = {
|
||||
point: Point;
|
||||
};
|
||||
export const SettingsDevelopersWebhookTooltip = ({
|
||||
point,
|
||||
}: SettingsDevelopersWebhookTooltipProps): ReactElement => {
|
||||
const { timeFormat, timeZone } = useContext(UserContext);
|
||||
const windowInterval = new Date(point.data.x);
|
||||
const windowIntervalDate = formatDateISOStringToDateTimeSimplified(
|
||||
windowInterval,
|
||||
timeZone,
|
||||
timeFormat,
|
||||
);
|
||||
return (
|
||||
<StyledTooltipContainer>
|
||||
<StyledTooltipDateContainer>
|
||||
{windowIntervalDate}
|
||||
</StyledTooltipDateContainer>
|
||||
<StyledLine />
|
||||
<StyledTooltipDataRow>
|
||||
<StyledDataDefinition>
|
||||
<StyledColorPoint color={point.serieColor} />
|
||||
{String(point.serieId)}
|
||||
</StyledDataDefinition>
|
||||
<StyledSpan>{String(point.data.y)}</StyledSpan>
|
||||
</StyledTooltipDataRow>
|
||||
</StyledTooltipContainer>
|
||||
);
|
||||
};
|
||||
@ -1,8 +1,13 @@
|
||||
import { SettingsDevelopersWebhookTooltip } from '@/settings/developers/webhook/components/SettingsDevelopersWebhookTooltip';
|
||||
import { useGraphData } from '@/settings/developers/webhook/hooks/useGraphData';
|
||||
import { webhookGraphDataState } from '@/settings/developers/webhook/states/webhookGraphDataState';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { ResponsiveLine } from '@nivo/line';
|
||||
import { Section } from '@react-email/components';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { useState } from 'react';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { H2Title } from 'twenty-ui';
|
||||
|
||||
export type NivoLineInput = {
|
||||
@ -14,22 +19,102 @@ export type NivoLineInput = {
|
||||
}>;
|
||||
};
|
||||
const StyledGraphContainer = styled.div`
|
||||
height: 200px;
|
||||
width: 100%;
|
||||
background-color: ${({ theme }) => theme.background.secondary};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
height: 199px;
|
||||
|
||||
padding: ${({ theme }) => theme.spacing(4, 2, 2, 2)};
|
||||
width: 496px;
|
||||
`;
|
||||
export const SettingsDeveloppersWebhookUsageGraph = () => {
|
||||
const StyledTitleContainer = styled.div`
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
type SettingsDevelopersWebhookUsageGraphProps = {
|
||||
webhookId: string;
|
||||
};
|
||||
|
||||
export const SettingsDevelopersWebhookUsageGraph = ({
|
||||
webhookId,
|
||||
}: SettingsDevelopersWebhookUsageGraphProps) => {
|
||||
const webhookGraphData = useRecoilValue(webhookGraphDataState);
|
||||
const setWebhookGraphData = useSetRecoilState(webhookGraphDataState);
|
||||
const theme = useTheme();
|
||||
|
||||
const [windowLengthGraphOption, setWindowLengthGraphOption] = useState<
|
||||
'7D' | '1D' | '12H' | '4H'
|
||||
>('7D');
|
||||
|
||||
const { fetchGraphData } = useGraphData(webhookId);
|
||||
|
||||
return (
|
||||
<>
|
||||
{webhookGraphData.length ? (
|
||||
<Section>
|
||||
<H2Title title="Statistics" />
|
||||
<StyledTitleContainer>
|
||||
<H2Title
|
||||
title="Activity"
|
||||
description="See your webhook activity over time"
|
||||
/>
|
||||
<Select
|
||||
dropdownId="test-id-webhook-graph"
|
||||
value={windowLengthGraphOption}
|
||||
options={[
|
||||
{ value: '7D', label: 'This week' },
|
||||
{ value: '1D', label: 'Today' },
|
||||
{ value: '12H', label: 'Last 12 hours' },
|
||||
{ value: '4H', label: 'Last 4 hours' },
|
||||
]}
|
||||
onChange={(windowLengthGraphOption) => {
|
||||
setWindowLengthGraphOption(windowLengthGraphOption);
|
||||
fetchGraphData(windowLengthGraphOption).then((graphInput) => {
|
||||
setWebhookGraphData(graphInput);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</StyledTitleContainer>
|
||||
|
||||
<StyledGraphContainer>
|
||||
<ResponsiveLine
|
||||
data={webhookGraphData}
|
||||
curve={'monotoneX'}
|
||||
enableArea={true}
|
||||
colors={(d) => d.color}
|
||||
margin={{ top: 0, right: 0, bottom: 50, left: 60 }}
|
||||
theme={{
|
||||
text: {
|
||||
fill: theme.font.color.light,
|
||||
fontSize: theme.font.size.sm,
|
||||
fontFamily: theme.font.family,
|
||||
},
|
||||
axis: {
|
||||
domain: {
|
||||
line: {
|
||||
stroke: theme.border.color.light,
|
||||
},
|
||||
},
|
||||
ticks: {
|
||||
line: {
|
||||
stroke: theme.border.color.light,
|
||||
},
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
line: {
|
||||
stroke: theme.border.color.light,
|
||||
},
|
||||
},
|
||||
|
||||
crosshair: {
|
||||
line: {
|
||||
stroke: theme.font.color.light,
|
||||
strokeDasharray: '2 2',
|
||||
},
|
||||
},
|
||||
}}
|
||||
margin={{ top: 20, right: 0, bottom: 30, left: 30 }}
|
||||
xFormat="time:%Y-%m-%d %H:%M%"
|
||||
xScale={{
|
||||
type: 'time',
|
||||
@ -37,17 +122,54 @@ export const SettingsDeveloppersWebhookUsageGraph = () => {
|
||||
format: '%Y-%m-%d %H:%M:%S',
|
||||
precision: 'hour',
|
||||
}}
|
||||
defs={[
|
||||
{
|
||||
colors: [
|
||||
{
|
||||
color: 'inherit',
|
||||
offset: 0,
|
||||
},
|
||||
{
|
||||
color: 'inherit',
|
||||
offset: 100,
|
||||
opacity: 0,
|
||||
},
|
||||
],
|
||||
id: 'gradientGraph',
|
||||
type: 'linearGradient',
|
||||
},
|
||||
]}
|
||||
fill={[
|
||||
{
|
||||
id: 'gradientGraph',
|
||||
match: '*',
|
||||
},
|
||||
]}
|
||||
yScale={{
|
||||
type: 'linear',
|
||||
}}
|
||||
axisBottom={{
|
||||
tickValues: 'every day',
|
||||
format: '%b %d',
|
||||
format: '%b %d, %I:%M %p',
|
||||
tickValues: 2,
|
||||
tickPadding: 5,
|
||||
tickSize: 6,
|
||||
}}
|
||||
axisLeft={{
|
||||
tickPadding: 5,
|
||||
tickSize: 6,
|
||||
tickValues: 4,
|
||||
}}
|
||||
enableTouchCrosshair={true}
|
||||
enableGridY={false}
|
||||
enableGridX={false}
|
||||
lineWidth={1}
|
||||
gridYValues={4}
|
||||
enablePoints={false}
|
||||
isInteractive={true}
|
||||
useMesh={true}
|
||||
enableSlices={false}
|
||||
enableCrosshair={false}
|
||||
tooltip={({ point }) => (
|
||||
<SettingsDevelopersWebhookTooltip point={point} />
|
||||
)}
|
||||
/>
|
||||
</StyledGraphContainer>
|
||||
</Section>
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
import { NivoLineInput } from '@/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraph';
|
||||
import { useGraphData } from '@/settings/developers/webhook/hooks/useGraphData';
|
||||
import { webhookGraphDataState } from '@/settings/developers/webhook/states/webhookGraphDataState';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useEffect } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
@ -14,88 +12,13 @@ export const SettingsDevelopersWebhookUsageGraphEffect = ({
|
||||
}: SettingsDevelopersWebhookUsageGraphEffectProps) => {
|
||||
const setWebhookGraphData = useSetRecoilState(webhookGraphDataState);
|
||||
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
const { fetchGraphData } = useGraphData(webhookId);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const queryString = new URLSearchParams({
|
||||
webhookIdRequest: webhookId,
|
||||
}).toString();
|
||||
const token = 'REPLACE_ME';
|
||||
const response = await fetch(
|
||||
`https://api.eu-central-1.aws.tinybird.co/v0/pipes/getWebhooksAnalytics.json?${queryString}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + token,
|
||||
},
|
||||
},
|
||||
);
|
||||
const result = await response.json();
|
||||
fetchGraphData('7D').then((graphInput) => {
|
||||
setWebhookGraphData(graphInput);
|
||||
});
|
||||
}, [fetchGraphData, setWebhookGraphData, webhookId]);
|
||||
|
||||
if (!response.ok) {
|
||||
enqueueSnackBar('Something went wrong while fetching webhook usage', {
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const graphInput = result.data
|
||||
.flatMap(
|
||||
(dataRow: {
|
||||
start_interval: string;
|
||||
failure_count: number;
|
||||
success_count: number;
|
||||
}) => [
|
||||
{
|
||||
x: dataRow.start_interval,
|
||||
y: dataRow.failure_count,
|
||||
id: 'failure_count',
|
||||
color: 'red',
|
||||
},
|
||||
{
|
||||
x: dataRow.start_interval,
|
||||
y: dataRow.success_count,
|
||||
id: 'success_count',
|
||||
color: 'green',
|
||||
},
|
||||
],
|
||||
)
|
||||
.reduce(
|
||||
(
|
||||
acc: NivoLineInput[],
|
||||
{
|
||||
id,
|
||||
x,
|
||||
y,
|
||||
color,
|
||||
}: { id: string; x: string; y: number; color: string },
|
||||
) => {
|
||||
const existingGroupIndex = acc.findIndex(
|
||||
(group) => group.id === id,
|
||||
);
|
||||
const isExistingGroup = existingGroupIndex !== -1;
|
||||
|
||||
if (isExistingGroup) {
|
||||
return acc.map((group, index) =>
|
||||
index === existingGroupIndex
|
||||
? { ...group, data: [...group.data, { x, y }] }
|
||||
: group,
|
||||
);
|
||||
} else {
|
||||
return [...acc, { id, color, data: [{ x, y }] }];
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
setWebhookGraphData(graphInput);
|
||||
} catch (error) {
|
||||
enqueueSnackBar('Something went wrong while fetching webhook usage', {
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, [enqueueSnackBar, setWebhookGraphData, webhookId]);
|
||||
return <></>;
|
||||
};
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
export const WEBHOOK_GRAPH_API_OPTIONS_MAP = {
|
||||
'7D': { windowInHours: '168', tickIntervalInMinutes: '420' },
|
||||
'1D': { windowInHours: '24', tickIntervalInMinutes: '60' },
|
||||
'12H': { windowInHours: '12', tickIntervalInMinutes: '30' },
|
||||
'4H': { windowInHours: '4', tickIntervalInMinutes: '10' },
|
||||
};
|
||||
@ -0,0 +1,23 @@
|
||||
import { fetchGraphDataOrThrow } from '@/settings/developers/webhook/utils/fetchGraphDataOrThrow';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
|
||||
export const useGraphData = (webhookId: string) => {
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
const fetchGraphData = async (
|
||||
windowLengthGraphOption: '7D' | '1D' | '12H' | '4H',
|
||||
) => {
|
||||
try {
|
||||
return await fetchGraphDataOrThrow({
|
||||
webhookId,
|
||||
windowLength: windowLengthGraphOption,
|
||||
});
|
||||
} catch (error) {
|
||||
enqueueSnackBar('Something went wrong while fetching webhook usage', {
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
return [];
|
||||
}
|
||||
};
|
||||
return { fetchGraphData };
|
||||
};
|
||||
@ -0,0 +1,115 @@
|
||||
import { WEBHOOK_GRAPH_API_OPTIONS_MAP } from '@/settings/developers/webhook/constants/WebhookGraphApiOptionsMap';
|
||||
import { fetchGraphDataOrThrow } from '@/settings/developers/webhook/utils/fetchGraphDataOrThrow';
|
||||
|
||||
// Mock the global fetch function
|
||||
global.fetch = jest.fn();
|
||||
|
||||
describe('fetchGraphDataOrThrow', () => {
|
||||
const mockWebhookId = 'test-webhook-id';
|
||||
const mockWindowLength = '7D';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should fetch and transform data successfully', async () => {
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
json: jest.fn().mockResolvedValue({
|
||||
data: [
|
||||
{ start_interval: '2023-05-01', failure_count: 2, success_count: 8 },
|
||||
{ start_interval: '2023-05-02', failure_count: 1, success_count: 9 },
|
||||
],
|
||||
}),
|
||||
};
|
||||
global.fetch.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await fetchGraphDataOrThrow({
|
||||
webhookId: mockWebhookId,
|
||||
windowLength: mockWindowLength,
|
||||
});
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
`https://api.eu-central-1.aws.tinybird.co/v0/pipes/getWebhooksAnalyticsV2.json?`,
|
||||
),
|
||||
expect.objectContaining({
|
||||
headers: {
|
||||
Authorization: expect.stringContaining('Bearer '),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
id: 'Failed',
|
||||
color: 'red',
|
||||
data: [
|
||||
{ x: '2023-05-01', y: 2 },
|
||||
{ x: '2023-05-02', y: 1 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'Succeeded',
|
||||
color: 'blue',
|
||||
data: [
|
||||
{ x: '2023-05-01', y: 8 },
|
||||
{ x: '2023-05-02', y: 9 },
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should throw an error when the response is not ok', async () => {
|
||||
const mockResponse = {
|
||||
ok: false,
|
||||
json: jest.fn().mockResolvedValue({ error: 'Some error' }),
|
||||
};
|
||||
global.fetch.mockResolvedValue(mockResponse);
|
||||
|
||||
await expect(
|
||||
fetchGraphDataOrThrow({
|
||||
webhookId: mockWebhookId,
|
||||
windowLength: mockWindowLength,
|
||||
}),
|
||||
).rejects.toThrow('Something went wrong while fetching webhook usage');
|
||||
});
|
||||
|
||||
it('should use correct query parameters based on window length', async () => {
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
json: jest.fn().mockResolvedValue({ data: [] }),
|
||||
};
|
||||
global.fetch.mockResolvedValue(mockResponse);
|
||||
|
||||
await fetchGraphDataOrThrow({
|
||||
webhookId: mockWebhookId,
|
||||
windowLength: '1D',
|
||||
});
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
new URLSearchParams({
|
||||
...WEBHOOK_GRAPH_API_OPTIONS_MAP['1D'],
|
||||
webhookIdRequest: mockWebhookId,
|
||||
}).toString(),
|
||||
),
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle empty response data', async () => {
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
json: jest.fn().mockResolvedValue({ data: [] }),
|
||||
};
|
||||
global.fetch.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await fetchGraphDataOrThrow({
|
||||
webhookId: mockWebhookId,
|
||||
windowLength: mockWindowLength,
|
||||
});
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,80 @@
|
||||
import { NivoLineInput } from '@/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraph';
|
||||
import { WEBHOOK_GRAPH_API_OPTIONS_MAP } from '@/settings/developers/webhook/constants/WebhookGraphApiOptionsMap';
|
||||
|
||||
type fetchGraphDataOrThrowProps = {
|
||||
webhookId: string;
|
||||
windowLength: '7D' | '1D' | '12H' | '4H';
|
||||
};
|
||||
|
||||
export const fetchGraphDataOrThrow = async ({
|
||||
webhookId,
|
||||
windowLength,
|
||||
}: fetchGraphDataOrThrowProps) => {
|
||||
const queryString = new URLSearchParams({
|
||||
...WEBHOOK_GRAPH_API_OPTIONS_MAP[windowLength],
|
||||
webhookIdRequest: webhookId,
|
||||
}).toString();
|
||||
const token = 'REPLACE_ME';
|
||||
const response = await fetch(
|
||||
`https://api.eu-central-1.aws.tinybird.co/v0/pipes/getWebhooksAnalyticsV2.json?${queryString}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + token,
|
||||
},
|
||||
},
|
||||
);
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Something went wrong while fetching webhook usage');
|
||||
}
|
||||
// Next steps: separate the map logic to a different component (response.data, {id:str, color:str}[])=>NivoLineInput[]
|
||||
|
||||
const graphInput = result.data
|
||||
.flatMap(
|
||||
(dataRow: {
|
||||
start_interval: string;
|
||||
failure_count: number;
|
||||
success_count: number;
|
||||
}) => [
|
||||
{
|
||||
x: dataRow.start_interval,
|
||||
y: dataRow.failure_count,
|
||||
id: 'Failed',
|
||||
color: 'red', // need to refacto this
|
||||
},
|
||||
{
|
||||
x: dataRow.start_interval,
|
||||
y: dataRow.success_count,
|
||||
id: 'Succeeded',
|
||||
color: 'blue',
|
||||
},
|
||||
],
|
||||
)
|
||||
.reduce(
|
||||
(
|
||||
acc: NivoLineInput[],
|
||||
{
|
||||
id,
|
||||
x,
|
||||
y,
|
||||
color,
|
||||
}: { id: string; x: string; y: number; color: string },
|
||||
) => {
|
||||
const existingGroupIndex = acc.findIndex((group) => group.id === id);
|
||||
const isExistingGroup = existingGroupIndex !== -1;
|
||||
|
||||
if (isExistingGroup) {
|
||||
return acc.map((group, index) =>
|
||||
index === existingGroupIndex
|
||||
? { ...group, data: [...group.data, { x, y }] }
|
||||
: group,
|
||||
);
|
||||
} else {
|
||||
return [...acc, { id, color, data: [{ x, y }] }];
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
return graphInput;
|
||||
};
|
||||
Reference in New Issue
Block a user