Skip to content
This repository was archived by the owner on May 13, 2025. It is now read-only.

Commit c23947f

Browse files
authored
feat: implement shareable URL (#350)
1 parent a704184 commit c23947f

File tree

9 files changed

+194
-17
lines changed

9 files changed

+194
-17
lines changed

src/components/Header/TimeRange.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,18 @@ import classes from './styles/LogQuery.module.css';
1010
import { useOuterClick } from '@/hooks/useOuterClick';
1111
import { logsStoreReducers, useLogsStore } from '@/pages/Stream/providers/LogsProvider';
1212
import _ from 'lodash';
13+
import timeRangeUtils from '@/utils/timeRangeUtils';
1314

15+
const {getRelativeStartAndEndDate} = timeRangeUtils
1416
const { setTimeRange, setshiftInterval } = logsStoreReducers;
15-
type FixedDurations = (typeof FIXED_DURATIONS)[number];
17+
export type FixedDuration = (typeof FIXED_DURATIONS)[number];
1618

1719
const { timeRangeContainer, fixedRangeBtn, fixedRangeBtnSelected, customRangeContainer, shiftIntervalContainer } =
1820
classes;
1921

2022
const RelativeTimeIntervals = (props: {
2123
interval: number;
22-
onDurationSelect: (fixedDuration: FixedDurations) => void;
24+
onDurationSelect: (fixedDuration: FixedDuration) => void;
2325
}) => {
2426
const { interval, onDurationSelect } = props;
2527
return (
@@ -67,10 +69,8 @@ const TimeRange: FC = () => {
6769
setOpened((prev) => !prev);
6870
}, []);
6971

70-
const onDurationSelect = (duration: FixedDurations) => {
71-
const now = dayjs().startOf('minute');
72-
const startTime = now.subtract(duration.milliseconds, 'milliseconds');
73-
const endTime = now;
72+
const onDurationSelect = (duration: FixedDuration) => {
73+
const {startTime, endTime} = getRelativeStartAndEndDate(duration);
7474
setLogsStore((store) => setTimeRange(store, { startTime, endTime, type: 'fixed' }));
7575
setOpened(false);
7676
};

src/constants/timeConstants.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export type FixedDuration = {
44
name: string;
55
milliseconds: number;
66
label: string;
7+
paramValue: string;
78
};
89

910
export const REFRESH_INTERVALS: number[] = [10000, 30000, 60000, 300000, 600000, 1200000];
@@ -13,26 +14,31 @@ export const FIXED_DURATIONS: ReadonlyArray<FixedDuration> = [
1314
name: 'last 10 minutes',
1415
milliseconds: dayjs.duration({ minutes: 10 }).asMilliseconds(),
1516
label: '10m',
17+
paramValue: '10m',
1618
},
1719
{
1820
name: 'last 1 hour',
1921
milliseconds: dayjs.duration({ hours: 1 }).asMilliseconds(),
2022
label: '1h',
23+
paramValue: '1h',
2124
},
2225
{
2326
name: 'last 5 hours',
2427
milliseconds: dayjs.duration({ hours: 5 }).asMilliseconds(),
2528
label: '5h',
29+
paramValue: '5h',
2630
},
2731
{
2832
name: 'last 24 hours',
2933
milliseconds: dayjs.duration({ days: 1 }).asMilliseconds(),
3034
label: '1d',
35+
paramValue: '1d',
3136
},
3237
{
3338
name: 'last 3 days',
3439
milliseconds: dayjs.duration({ days: 3 }).asMilliseconds(),
3540
label: '3d',
41+
paramValue: '3d',
3642
},
3743
] as const;
3844

src/hooks/useDashboards.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ import {
1111
TileQueryResponse,
1212
UpdateDashboardType,
1313
} from '@/@types/parseable/api/dashboards';
14+
import { useSearchParams } from 'react-router-dom';
1415

1516
const { setDashboards, setTileData, selectDashboard } = dashboardsStoreReducers;
1617

1718
export const useDashboardsQuery = (opts: { updateTimeRange?: (dashboard: Dashboard) => void }) => {
1819
const [activeDashboard, setDashboardsStore] = useDashboardsStore((store) => store.activeDashboard);
20+
const [searchParams] = useSearchParams()
1921

2022
const {
2123
isError: fetchDashaboardsError,
@@ -31,7 +33,7 @@ export const useDashboardsQuery = (opts: { updateTimeRange?: (dashboard: Dashboa
3133
if (!activeDashboard && firstDashboard && opts.updateTimeRange) {
3234
opts.updateTimeRange(firstDashboard);
3335
}
34-
setDashboardsStore((store) => setDashboards(store, data.data || []));
36+
setDashboardsStore((store) => setDashboards(store, data.data || [], searchParams.get('id') || ''));
3537
},
3638
onError: () => {
3739
setDashboardsStore((store) => setDashboards(store, []));

src/pages/Dashboards/Dashboard.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,20 @@ const NoTilesView = () => {
321321
);
322322
};
323323

324+
const InvalidDashboardView = () => {
325+
return (
326+
<Stack className={classes.noDashboardsContainer} gap={4}>
327+
<Stack className={classes.dashboardIconContainer}>
328+
<IconLayoutDashboard className={classes.dashboardIcon} stroke={1.2} />
329+
</Stack>
330+
<Text className={classes.noDashboardsViewTitle}>Oops! Dashboard Not Found</Text>
331+
<Text className={classes.noDashboardsViewDescription}>
332+
It looks like the dashboard you’re looking for doesn’t exist. Please check the link or try a different one!
333+
</Text>
334+
</Stack>
335+
);
336+
};
337+
324338
const findTileByTileId = (tiles: TileType[], tileId: string | null) => {
325339
return _.find(tiles, tile => tile.tile_id === tileId)
326340
}
@@ -395,6 +409,7 @@ const DuplicateTileModal = () => {
395409

396410
const Dashboard = () => {
397411
const [dashboards] = useDashboardsStore((store) => store.dashboards);
412+
const [activeDashboard] = useDashboardsStore((store) => store.activeDashboard);
398413
const layoutRef = useRef<Layout[]>([]);
399414
const onLayoutChange = useCallback(
400415
(layout: Layout[]) => {
@@ -410,7 +425,7 @@ const Dashboard = () => {
410425
<DuplicateTileModal/>
411426
<Toolbar layoutRef={layoutRef} />
412427
<ImportDashboardModal/>
413-
<TilesView onLayoutChange={onLayoutChange} />
428+
{activeDashboard ? <TilesView onLayoutChange={onLayoutChange} /> : <InvalidDashboardView />}
414429
</Stack>
415430
);
416431
};
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { useCallback, useEffect, useState } from 'react';
2+
import { useDashboardsStore, dashboardsStoreReducers } from '../providers/DashboardsProvider';
3+
import { TimeRange, useLogsStore, logsStoreReducers } from '@/pages/Stream/providers/LogsProvider';
4+
import { useSearchParams } from 'react-router-dom';
5+
import _ from 'lodash';
6+
import { FIXED_DURATIONS } from '@/constants/timeConstants';
7+
import dayjs from 'dayjs';
8+
import timeRangeUtils from '@/utils/timeRangeUtils';
9+
import moment from 'moment-timezone';
10+
11+
const { getRelativeStartAndEndDate, formatDateWithTimezone, getLocalTimezone } = timeRangeUtils;
12+
const { selectDashboard } = dashboardsStoreReducers;
13+
const { setTimeRange } = logsStoreReducers;
14+
const timeRangeFormat = 'DD-MMM-YYYY_HH-mmz';
15+
const keys = ['id', 'interval', 'from', 'to'];
16+
17+
const dateToParamString = (date: Date) => {
18+
return formatDateWithTimezone(date, timeRangeFormat);
19+
};
20+
21+
const dateParamStrToDateObj = (str: string) => {
22+
const timeZoneMatch = str.match(/[A-Za-z]+$/);
23+
const timeZone = timeZoneMatch ? timeZoneMatch[0] : '';
24+
const localTimeZone = getLocalTimezone();
25+
const date = (() => {
26+
if (localTimeZone === timeZone) {
27+
return moment(str, timeRangeFormat).toDate();
28+
} else {
29+
return moment.tz(str, timeRangeFormat, timeZone).toDate();
30+
}
31+
})();
32+
return isNaN(new Date(date).getTime()) ? '' : date;
33+
};
34+
35+
const deriveTimeRangeParams = (timerange: TimeRange): { interval: string } | { from: string; to: string } => {
36+
const { startTime, endTime, type, interval } = timerange;
37+
38+
if (type === 'fixed') {
39+
const selectedDuration = _.find(FIXED_DURATIONS, (d) => d.milliseconds === interval);
40+
const defaultDuration = FIXED_DURATIONS[0];
41+
const paramValue = selectedDuration ? selectedDuration.paramValue : defaultDuration.paramValue;
42+
return { interval: paramValue };
43+
} else {
44+
return {
45+
from: isNaN(new Date(startTime).getTime()) ? '' : dateToParamString(startTime),
46+
to: isNaN(new Date(endTime).getTime()) ? '' : dateToParamString(endTime),
47+
};
48+
}
49+
};
50+
51+
const storeToParamsObj = (opts: { dashboardId: string; timeRange: TimeRange }): Record<string, string> => {
52+
const { dashboardId, timeRange } = opts;
53+
const params: Record<string, string> = {
54+
id: dashboardId,
55+
...deriveTimeRangeParams(timeRange),
56+
};
57+
return _.pickBy(params, (val, key) => !_.isEmpty(val) && _.includes(keys, key));
58+
};
59+
60+
const paramsStringToParamsObj = (searchParams: URLSearchParams): Record<string, string> => {
61+
return _.reduce(
62+
keys,
63+
(acc: Record<string, string>, key) => {
64+
const value = searchParams.get(key) || '';
65+
return _.isEmpty(value) ? acc : { ...acc, [key]: value };
66+
},
67+
{},
68+
);
69+
};
70+
71+
const useParamsController = () => {
72+
const [isStoreSyncd, setStoreSyncd] = useState(false);
73+
const [activeDashboard, setDashboardsStore] = useDashboardsStore((store) => store.activeDashboard);
74+
const [timeRange, setLogsStore] = useLogsStore((store) => store.timeRange);
75+
const [searchParams, setSearchParams] = useSearchParams();
76+
const dashboardId = activeDashboard?.dashboard_id || '';
77+
78+
useEffect(() => {
79+
const storeAsParams = storeToParamsObj({ dashboardId, timeRange });
80+
const presentParams = paramsStringToParamsObj(searchParams);
81+
syncTimeRangeToStore(storeAsParams, presentParams)
82+
setStoreSyncd(true);
83+
}, []);
84+
85+
useEffect(() => {
86+
if (isStoreSyncd) {
87+
const storeAsParams = storeToParamsObj({ dashboardId, timeRange });
88+
const presentParams = paramsStringToParamsObj(searchParams);
89+
if (_.isEqual(storeAsParams, presentParams)) return;
90+
91+
setSearchParams(storeAsParams);
92+
}
93+
}, [dashboardId, timeRange.startTime.toISOString(), timeRange.endTime.toISOString()]);
94+
95+
useEffect(() => {
96+
if (!isStoreSyncd) return;
97+
98+
const storeAsParams = storeToParamsObj({ dashboardId, timeRange });
99+
const presentParams = paramsStringToParamsObj(searchParams);
100+
if (_.isEqual(storeAsParams, presentParams)) return;
101+
102+
if (storeAsParams.id !== presentParams.id) {
103+
setDashboardsStore((store) => selectDashboard(store, presentParams.id));
104+
}
105+
106+
syncTimeRangeToStore(storeAsParams, presentParams)
107+
}, [searchParams]);
108+
109+
const syncTimeRangeToStore = useCallback(
110+
(storeAsParams: Record<string, string>, presentParams: Record<string, string>) => {
111+
if (_.has(presentParams, 'interval')) {
112+
if (storeAsParams.interval !== presentParams.interval) {
113+
const duration = _.find(FIXED_DURATIONS, (d) => d.paramValue === presentParams.interval);
114+
if (!duration) return;
115+
116+
const { startTime, endTime } = getRelativeStartAndEndDate(duration);
117+
return setLogsStore((store) => setTimeRange(store, { startTime, endTime, type: 'fixed' }));
118+
}
119+
} else if (_.has(presentParams, 'from') && _.has(presentParams, 'to')) {
120+
if (storeAsParams.from !== presentParams.from && storeAsParams.to !== presentParams.to) {
121+
const startTime = dateParamStrToDateObj(presentParams.from);
122+
const endTime = dateParamStrToDateObj(presentParams.to);
123+
if (_.isDate(startTime) && _.isDate(endTime)) {
124+
return setLogsStore((store) =>
125+
setTimeRange(store, { startTime: dayjs(startTime), endTime: dayjs(endTime), type: 'custom' }),
126+
);
127+
}
128+
}
129+
}
130+
},
131+
[],
132+
);
133+
134+
return { isStoreSyncd };
135+
};
136+
137+
export default useParamsController;

src/pages/Dashboards/index.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { useEffect } from 'react';
99
import { useDashboardsQuery } from '@/hooks/useDashboards';
1010
import CreateTileForm from './CreateTileForm';
1111
import { useSyncTimeRange } from './hooks';
12+
import _ from 'lodash';
13+
import useParamsController from './hooks/useParamsController';
1214

1315
const LoadingView = () => {
1416
return (
@@ -21,11 +23,14 @@ const LoadingView = () => {
2123
const Dashboards = () => {
2224
const [dashboards] = useDashboardsStore((store) => store.dashboards);
2325
const [createTileFormOpen] = useDashboardsStore((store) => store.createTileFormOpen);
26+
const { isStoreSyncd } = useParamsController();
2427
const { updateTimeRange } = useSyncTimeRange();
2528
const { fetchDashboards } = useDashboardsQuery({ updateTimeRange });
2629
useEffect(() => {
27-
fetchDashboards();
28-
}, []);
30+
if (isStoreSyncd) {
31+
fetchDashboards();
32+
}
33+
}, [isStoreSyncd]);
2934

3035
return (
3136
<Box
@@ -37,13 +42,13 @@ const Dashboards = () => {
3742
width: '100%',
3843
overflow: 'hidden',
3944
}}>
40-
{dashboards === null ? (
45+
{dashboards === null || !isStoreSyncd ? (
4146
<LoadingView />
4247
) : createTileFormOpen ? (
4348
<CreateTileForm />
4449
) : (
4550
<>
46-
<SideBar updateTimeRange={updateTimeRange}/>
51+
<SideBar updateTimeRange={updateTimeRange} />
4752
<CreateDashboardModal />
4853
<Dashboard />
4954
</>

src/pages/Dashboards/providers/DashboardsProvider.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ const initialState: DashboardsStore = {
115115
type ReducerOutput = Partial<DashboardsStore>;
116116

117117
type DashboardsStoreReducers = {
118-
setDashboards: (store: DashboardsStore, dashboards: Dashboard[]) => ReducerOutput;
118+
setDashboards: (store: DashboardsStore, dashboards: Dashboard[], dashboardId?: string) => ReducerOutput;
119119
toggleCreateDashboardModal: (store: DashboardsStore, val: boolean) => ReducerOutput;
120120
toggleEditDashboardModal: (store: DashboardsStore, val: boolean) => ReducerOutput;
121121
selectDashboard: (store: DashboardsStore, dashboardId?: string | null, dashboard?: Dashboard) => ReducerOutput;
@@ -188,11 +188,14 @@ const toggleAllowDrag = (store: DashboardsStore) => {
188188
};
189189
};
190190

191-
const setDashboards = (store: DashboardsStore, dashboards: Dashboard[]) => {
191+
const setDashboards = (store: DashboardsStore, dashboards: Dashboard[], dashboardId?: string) => {
192192
const { activeDashboard: activeDashboardFromStore, currentPage } = store;
193193
const defaultActiveDashboard = _.isArray(dashboards) && !_.isEmpty(dashboards) ? dashboards[0] : null;
194194
const activeDashboard = (() => {
195-
if (activeDashboardFromStore) {
195+
if (_.isString(dashboardId) && !_.isEmpty(dashboardId)) {
196+
return _.find(dashboards, (dashboard) => dashboard.dashboard_id === dashboardId);
197+
}
198+
else if (activeDashboardFromStore) {
196199
const id = activeDashboardFromStore.dashboard_id;
197200
const dashboard = _.find(dashboards, (dashboard) => dashboard.dashboard_id === id);
198201
return dashboard || defaultActiveDashboard;

src/pages/Stream/providers/LogsProvider.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export type TransformedAlerts = {
8383
alerts: TransformedAlert[];
8484
};
8585

86-
type TimeRange = {
86+
export type TimeRange = {
8787
startTime: Date;
8888
endTime: Date;
8989
type: 'fixed' | 'custom';

src/utils/timeRangeUtils.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import dayjs from 'dayjs';
22
import moment from 'moment-timezone';
33
import _ from 'lodash';
4+
import { FixedDuration } from '@/constants/timeConstants';
45

56
const defaultTimeRangeOption = {
67
value: 'none',
@@ -86,6 +87,13 @@ const formatDateAsCastType = (date: Date): string => {
8687
return dayjs(date).utc().format('YYYY-MM-DD HH:mm:ss[Z]');
8788
};
8889

90+
const getRelativeStartAndEndDate = (duration: FixedDuration) => {
91+
const now = dayjs().startOf('minute');
92+
const startTime = now.subtract(duration.milliseconds, 'milliseconds');
93+
const endTime = now;
94+
return { startTime, endTime };
95+
};
96+
8997
const timeRangeUtils = {
9098
defaultTimeRangeOption,
9199
formatDateWithTimezone,
@@ -96,7 +104,8 @@ const timeRangeUtils = {
96104
formatTime,
97105
formatDay,
98106
formatDateAsCastType,
99-
getLocalTimezone,
107+
getRelativeStartAndEndDate,
108+
getLocalTimezone
100109
};
101110

102111
export default timeRangeUtils;

0 commit comments

Comments
 (0)