@evenicanpm/admin-integrate 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +29 -0
  2. package/documents/ProcessExecution/fragments.ts +16 -0
  3. package/documents/ProcessExecution/list.ts +31 -0
  4. package/package.json +19 -0
  5. package/src/api/dashboard/queries/get-process-executions.query.ts +48 -0
  6. package/src/api/dashboard/queries/get-process-executions.server.ts +11 -0
  7. package/src/api/dashboard/queries/index.ts +1 -0
  8. package/src/auth/.keep +0 -0
  9. package/src/auth/types/.keep +0 -0
  10. package/src/components/breadcrumbs/breadcrumbs.tsx +42 -0
  11. package/src/components/breadcrumbs/index.ts +2 -0
  12. package/src/components/button/index.ts +1 -0
  13. package/src/components/button/outlined-icon-button.tsx +47 -0
  14. package/src/components/core/index.ts +1 -0
  15. package/src/components/core/loading.tsx +15 -0
  16. package/src/components/dashboard-list/dashboard-list-row.tsx +145 -0
  17. package/src/components/dashboard-list/dashboard-list.tsx +157 -0
  18. package/src/components/dashboard-list/index.ts +1 -0
  19. package/src/components/data-table/index.ts +3 -0
  20. package/src/components/data-table/table-header.tsx +109 -0
  21. package/src/components/data-table/table-pagination.tsx +36 -0
  22. package/src/components/data-table/table-row.tsx +27 -0
  23. package/src/components/data-table/table-skeleton.tsx +45 -0
  24. package/src/components/execution/execution-filter.ts +30 -0
  25. package/src/components/execution/execution-status-icon.tsx +30 -0
  26. package/src/components/execution/execution-tag.tsx +100 -0
  27. package/src/components/execution/index.ts +2 -0
  28. package/src/components/footer/footer.tsx +55 -0
  29. package/src/components/footer/index.ts +1 -0
  30. package/src/components/header/dashboard-list-header.tsx +88 -0
  31. package/src/components/header/execution-details-header.tsx +154 -0
  32. package/src/components/header/header.tsx +42 -0
  33. package/src/components/header/index.ts +3 -0
  34. package/src/components/icons/up-down.tsx +21 -0
  35. package/src/components/integration-view/edges/default-edge.tsx +23 -0
  36. package/src/components/integration-view/elk-layout-options.ts +39 -0
  37. package/src/components/integration-view/elk-types.ts +19 -0
  38. package/src/components/integration-view/flow-types.tsx +16 -0
  39. package/src/components/integration-view/index.ts +1 -0
  40. package/src/components/integration-view/integration-view.tsx +159 -0
  41. package/src/components/integration-view/nodes/connection-node.tsx +10 -0
  42. package/src/components/integration-view/nodes/entry-node.tsx +10 -0
  43. package/src/components/integration-view/nodes/group-node.tsx +10 -0
  44. package/src/components/integration-view/nodes/task-node.tsx +17 -0
  45. package/src/components/integration-view/temp-data/initialElements.ts +238 -0
  46. package/src/components/integration-view/types.ts +12 -0
  47. package/src/components/layouts/index.ts +2 -0
  48. package/src/components/layouts/main-layout.tsx +28 -0
  49. package/src/components/layouts/root-container.tsx +43 -0
  50. package/src/components/link-cards/index.ts +1 -0
  51. package/src/components/link-cards/link-card.tsx +36 -0
  52. package/src/components/link-cards/styles.ts +29 -0
  53. package/src/components/list-filter/date-filter.tsx +55 -0
  54. package/src/components/list-filter/date-range-filter.tsx +101 -0
  55. package/src/components/list-filter/index.ts +6 -0
  56. package/src/components/list-filter/list-filter-types.ts +70 -0
  57. package/src/components/list-filter/list-filter.tsx +409 -0
  58. package/src/components/list-filter/multi-select-filter.tsx +117 -0
  59. package/src/components/list-filter/select-filter.tsx +82 -0
  60. package/src/hooks/use-breadcrumbs.ts +45 -0
  61. package/src/hooks/use-timestamp.tsx +25 -0
  62. package/src/lib/.keep +0 -0
  63. package/src/pages/dashboard/dashboard-link-cards.tsx +122 -0
  64. package/src/pages/dashboard/dashboard-list-sections.tsx +149 -0
  65. package/src/pages/dashboard/dashboard.tsx +14 -0
  66. package/src/pages/dashboard/index.ts +1 -0
  67. package/src/pages/execution-details/execution-details.tsx +43 -0
  68. package/src/pages/execution-details/index.ts +1 -0
  69. package/src/pages/executions/executions-list/executions-list-row.tsx +142 -0
  70. package/src/pages/executions/executions-list/executions-list-table.tsx +78 -0
  71. package/src/pages/executions/executions-list/executions-list.tsx +265 -0
  72. package/src/pages/executions/executions-list/index.ts +1 -0
  73. package/src/pages/executions/executions.tsx +98 -0
  74. package/src/pages/executions/index.ts +1 -0
  75. package/src/pages/integrations/index.ts +1 -0
  76. package/src/pages/integrations/integrations.tsx +39 -0
  77. package/src/pages/scheduler/index.ts +1 -0
  78. package/src/pages/scheduler/scheduler.tsx +39 -0
  79. package/tsconfig.json +28 -0
@@ -0,0 +1,409 @@
1
+ "use client";
2
+ import { debounce } from "lodash";
3
+ import { useTranslations } from "next-intl";
4
+ import React, { useEffect, useState } from "react";
5
+
6
+ import {
7
+ Search,
8
+ FilterAltOutlined,
9
+ CloseOutlined,
10
+ ArrowForwardIos,
11
+ Delete,
12
+ } from "@mui/icons-material";
13
+ import {
14
+ Box,
15
+ Button,
16
+ IconButton,
17
+ TextField,
18
+ InputAdornment,
19
+ Select,
20
+ MenuItem,
21
+ Theme,
22
+ useTheme,
23
+ Paper,
24
+ Popper,
25
+ Badge,
26
+ ClickAwayListener,
27
+ } from "@mui/material";
28
+ import {
29
+ Filters,
30
+ FilterResult,
31
+ ListFilterOutput,
32
+ FilterConfig,
33
+ } from "./list-filter-types";
34
+
35
+ import { OutlinedIconButton } from "../button";
36
+ import SelectFilterComponent from "./select-filter";
37
+ import MultiSelectFilterComponent from "./multi-select-filter";
38
+ import DateFilterComponent from "./date-filter";
39
+ import DateRangeFilterComponent from "./date-range-filter";
40
+
41
+ interface FilterProps<FilterShape extends Filters> {
42
+ setFilters: (filterUpdate: ListFilterOutput<FilterShape>) => void;
43
+ clearFilters: () => void;
44
+ filters?: FilterShape;
45
+ config?: FilterConfig;
46
+ manualClose?: boolean;
47
+ }
48
+
49
+ type FilterErrors<FilterShape extends Filters> = {
50
+ [Property in keyof FilterShape]: boolean;
51
+ };
52
+
53
+ const ListFilter = function ListFilter<FilterShape extends Filters>(
54
+ props: FilterProps<FilterShape>,
55
+ ) {
56
+ const t = useTranslations("Integrate.Filter");
57
+ const theme = useTheme();
58
+ const styles = getStyles(theme);
59
+
60
+ const {
61
+ setFilters,
62
+ clearFilters,
63
+ filters = {} as FilterShape,
64
+ manualClose = false,
65
+ config: { pagination = {}, search = {} } = {},
66
+ } = props;
67
+
68
+ const { pageOptions = [10, 20, 30], pageDefault = 10 } = pagination || {};
69
+
70
+ const { label: searchLabel = t("defaultSearchLabel") } = search || {};
71
+
72
+ const defaultValues = Object.entries(filters).reduce(
73
+ (values, [key, filter]) => {
74
+ if (filter.type === "select" && filter.value)
75
+ return {
76
+ ...values,
77
+ [key]: filter.options.find((o) => o.id === filter.value[0].id),
78
+ };
79
+ if (filter.type === "multiSelect" && filter.value)
80
+ return {
81
+ ...values,
82
+ [key]: filter.options.filter((o) =>
83
+ filter.value.find((v) => v.id === o.id),
84
+ ),
85
+ };
86
+
87
+ return { ...values, [key]: filter.value };
88
+ },
89
+ {},
90
+ ) as FilterResult<FilterShape>;
91
+
92
+ const filterErrorsBase: FilterErrors<FilterShape> = Object.keys(
93
+ filters,
94
+ ).reduce(
95
+ (values, key) => ({
96
+ ...values,
97
+ [key]: false,
98
+ }),
99
+ {},
100
+ ) as FilterErrors<FilterShape>;
101
+
102
+ const [perPage, setPerPage] = useState(pageDefault);
103
+ const [searchTerm, setSearchTerm] = useState("");
104
+ const [open, setOpen] = useState(false);
105
+ const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
106
+ const [filterErrors, setFilterErrors] = useState(filterErrorsBase);
107
+ const [publishedFilterValues, setPublishedFilterValues] =
108
+ useState<FilterResult<FilterShape>>(defaultValues);
109
+ const [filterValues, setFilterValues] = useState<FilterResult<FilterShape>>(
110
+ publishedFilterValues,
111
+ );
112
+
113
+ const activeFilterCount = Object.entries(publishedFilterValues).reduce(
114
+ (sum, [key, value]) => {
115
+ if (!value) return sum;
116
+
117
+ const activeFilters =
118
+ filters[key].type === "multiSelect" ? value.length : 1;
119
+
120
+ return sum + activeFilters;
121
+ },
122
+ 0,
123
+ );
124
+
125
+ const updateFilterValue = (name, value) => {
126
+ if (Array.isArray(value) && !value.length) value = null;
127
+ setFilterValues({
128
+ ...filterValues,
129
+ [name]: value || null,
130
+ });
131
+ };
132
+
133
+ const updateFilterErrors = (name: keyof FilterShape, error: boolean) => {
134
+ setFilterErrors({
135
+ ...filterErrors,
136
+ [name]: error,
137
+ });
138
+ };
139
+
140
+ const sendFilterOutput = (
141
+ update: {
142
+ search?: string;
143
+ itemsPerPage?: number;
144
+ filters?: FilterResult<FilterShape>;
145
+ } = {},
146
+ ) => {
147
+ setFilters({
148
+ search: searchTerm,
149
+ itemsPerPage: perPage,
150
+ filters: publishedFilterValues,
151
+ ...update,
152
+ });
153
+ };
154
+
155
+ const handleFilterMenuClick = (event: React.MouseEvent<HTMLElement>) => {
156
+ if (!open) {
157
+ setFilterValues(publishedFilterValues);
158
+ setAnchorEl(event.currentTarget);
159
+ }
160
+ setOpen(!open);
161
+ };
162
+
163
+ const handleFilterSet = () => {
164
+ setPublishedFilterValues(filterValues);
165
+ sendFilterOutput({ filters: filterValues });
166
+ setOpen(false);
167
+ };
168
+
169
+ const handleFilterClear = () => {
170
+ const emptyFilterValues = Object.keys(filterValues).reduce(
171
+ (values, key) => ({ ...values, [key]: null }),
172
+ {},
173
+ ) as FilterResult<FilterShape>;
174
+ setPublishedFilterValues(emptyFilterValues);
175
+ setOpen(false);
176
+ clearFilters();
177
+ };
178
+
179
+ const debounceSendSearchOutput = debounce(
180
+ (s) => sendFilterOutput({ search: s }),
181
+ 400,
182
+ );
183
+
184
+ useEffect(() => {
185
+ debounceSendSearchOutput(searchTerm);
186
+ return debounceSendSearchOutput.cancel;
187
+ }, [searchTerm]);
188
+
189
+ const handlePerPageUpdate = (itemsPerPage: number) => {
190
+ setPerPage(itemsPerPage);
191
+ sendFilterOutput({ itemsPerPage });
192
+ };
193
+
194
+ return (
195
+ <Box sx={styles.filter}>
196
+ {Object.keys(filters).length ? (
197
+ <ClickAwayListener
198
+ onClickAway={() => !manualClose && setOpen(false)}
199
+ mouseEvent="onMouseUp"
200
+ >
201
+ <Box>
202
+ <Badge
203
+ badgeContent={activeFilterCount}
204
+ color="error"
205
+ invisible={!activeFilterCount}
206
+ >
207
+ <OutlinedIconButton
208
+ color="info"
209
+ onClick={(e) => handleFilterMenuClick(e)}
210
+ >
211
+ <FilterAltOutlined />
212
+ </OutlinedIconButton>
213
+ </Badge>
214
+ <Popper open={open} anchorEl={anchorEl} placement="bottom-start">
215
+ <Paper sx={styles.popOut} elevation={3}>
216
+ <Box sx={styles.closeRow}>
217
+ {manualClose ? (
218
+ <IconButton
219
+ centerRipple={false}
220
+ onClick={() => setOpen(false)}
221
+ >
222
+ <CloseOutlined />
223
+ </IconButton>
224
+ ) : (
225
+ <Box sx={styles.bufferRow} />
226
+ )}
227
+ </Box>
228
+
229
+ <Box sx={styles.filterRows}>
230
+ {Object.entries(filters)
231
+ .sort(([keyA, { sort: a }], [keyB, { sort: b }]) => a - b)
232
+ .map(([name, filter]) => {
233
+ switch (filter.type) {
234
+ case "select":
235
+ return (
236
+ <SelectFilterComponent
237
+ key={name}
238
+ filter={filter}
239
+ current={
240
+ filterValues[name] as (typeof filter)["value"]
241
+ }
242
+ onSet={(v) => updateFilterValue(name, v)}
243
+ onError={(e) => updateFilterErrors(name, e)}
244
+ />
245
+ );
246
+ case "multiSelect":
247
+ return (
248
+ <MultiSelectFilterComponent
249
+ key={name}
250
+ filter={filter}
251
+ current={
252
+ filterValues[name] as (typeof filter)["value"]
253
+ }
254
+ onSet={(v) => updateFilterValue(name, v)}
255
+ onError={(e) => updateFilterErrors(name, e)}
256
+ />
257
+ );
258
+ case "date":
259
+ return (
260
+ <DateFilterComponent
261
+ key={name}
262
+ filter={filter}
263
+ current={
264
+ filterValues[name] as (typeof filter)["value"]
265
+ }
266
+ onSet={(v) => updateFilterValue(name, v)}
267
+ onError={(e) => updateFilterErrors(name, e)}
268
+ />
269
+ );
270
+ case "dateRange":
271
+ return (
272
+ <DateRangeFilterComponent
273
+ key={name}
274
+ filter={filter}
275
+ current={
276
+ filterValues[name] as (typeof filter)["value"]
277
+ }
278
+ onSet={(v) => updateFilterValue(name, v)}
279
+ onError={(e) => updateFilterErrors(name, e)}
280
+ />
281
+ );
282
+ }
283
+ })}
284
+ <Box sx={styles.buttonRow}>
285
+ {activeFilterCount ? (
286
+ <Button
287
+ variant="contained"
288
+ color="error"
289
+ startIcon={<Delete />}
290
+ onClick={handleFilterClear}
291
+ >
292
+ Clear
293
+ </Button>
294
+ ) : (
295
+ ""
296
+ )}
297
+
298
+ <Button
299
+ variant="contained"
300
+ color="primary"
301
+ startIcon={<ArrowForwardIos />}
302
+ disabled={Object.values(filterErrors).some((v) => v)}
303
+ onClick={handleFilterSet}
304
+ >
305
+ Filter
306
+ </Button>
307
+ </Box>
308
+ </Box>
309
+ </Paper>
310
+ </Popper>
311
+ </Box>
312
+ </ClickAwayListener>
313
+ ) : (
314
+ ""
315
+ )}
316
+
317
+ {search ? (
318
+ <TextField
319
+ sx={styles.search}
320
+ variant="outlined"
321
+ label={searchLabel}
322
+ value={searchTerm}
323
+ onChange={(e) => setSearchTerm(e.target.value)}
324
+ slotProps={{
325
+ input: {
326
+ startAdornment: (
327
+ <InputAdornment position="start">
328
+ <Search />
329
+ </InputAdornment>
330
+ ),
331
+ },
332
+ }}
333
+ />
334
+ ) : (
335
+ ""
336
+ )}
337
+ {pagination ? (
338
+ <Select
339
+ sx={styles.select}
340
+ value={perPage}
341
+ onChange={(e) => handlePerPageUpdate(e.target.value as number)}
342
+ >
343
+ {pageOptions.map((option) => (
344
+ <MenuItem value={option}>
345
+ {option} {t("itemsPerPage")}
346
+ </MenuItem>
347
+ ))}
348
+ </Select>
349
+ ) : (
350
+ ""
351
+ )}
352
+ </Box>
353
+ );
354
+ };
355
+
356
+ const getStyles = (theme: Theme) => {
357
+ return {
358
+ filter: {
359
+ width: "100%",
360
+ maxWidth: "600px",
361
+ display: "flex",
362
+ justifyContent: "center",
363
+ alignItems: "center",
364
+ columnGap: theme.spacing(2),
365
+ },
366
+ popOut: {
367
+ width: "512px",
368
+ padding: theme.spacing(2),
369
+ paddingTop: theme.spacing(1),
370
+ margin: theme.spacing(1, 0),
371
+ },
372
+ filterRows: {
373
+ display: "flex",
374
+ flexDirection: "column",
375
+ rowGap: theme.spacing(2),
376
+ },
377
+ closeRow: {
378
+ display: "flex",
379
+ justifyContent: "end",
380
+ },
381
+ bufferRow: {
382
+ paddingTop: theme.spacing(1),
383
+ },
384
+ buttonRow: {
385
+ width: "100%",
386
+ display: "flex",
387
+ justifyContent: "end",
388
+ columnGap: theme.spacing(1),
389
+ },
390
+ search: {
391
+ "& .MuiOutlinedInput-root": {
392
+ "& input": {
393
+ padding: theme.spacing(1.5),
394
+ },
395
+ },
396
+ backgroundColor: theme.palette.common.white,
397
+ width: "100%",
398
+ flexGrow: "1",
399
+ },
400
+ select: {
401
+ "& .MuiSelect-select": {
402
+ padding: theme.spacing(1.5),
403
+ },
404
+ backgroundColor: theme.palette.common.white,
405
+ },
406
+ };
407
+ };
408
+
409
+ export default ListFilter;
@@ -0,0 +1,117 @@
1
+ import { useTranslations } from "next-intl";
2
+ import {
3
+ Box,
4
+ Typography,
5
+ IconButton,
6
+ InputAdornment,
7
+ Select,
8
+ MenuItem,
9
+ Theme,
10
+ useTheme,
11
+ FormControl,
12
+ Chip,
13
+ } from "@mui/material";
14
+ import { CloseOutlined } from "@mui/icons-material";
15
+ import React, { useState } from "react";
16
+ import {
17
+ FilterOption,
18
+ MultiSelectFilter,
19
+ FilterProps,
20
+ } from "./list-filter-types";
21
+
22
+ const MultiSelectFilterComponent: React.FC<FilterProps<MultiSelectFilter>> = ({
23
+ filter,
24
+ onSet,
25
+ onError,
26
+ current,
27
+ }) => {
28
+ const theme = useTheme();
29
+ const styles = getStyles(theme);
30
+
31
+ const t = useTranslations("Integrate");
32
+
33
+ const mappedOptionDictionary = filter.options.reduce(
34
+ (dictionary, option) => ({
35
+ ...dictionary,
36
+ [option.id]: {
37
+ id: option.id,
38
+ value: t(option.value),
39
+ },
40
+ }),
41
+ {},
42
+ ) as { [id: string | number]: FilterOption };
43
+
44
+ const [value, setValue] = useState(current);
45
+
46
+ const updateValue = (optionIds: (string | number)[] | null) => {
47
+ const newValue = optionIds?.map((id) => mappedOptionDictionary[id]) || null;
48
+
49
+ setValue(newValue);
50
+ onSet(newValue);
51
+ };
52
+
53
+ return (
54
+ <FormControl>
55
+ <Typography>{filter.label}</Typography>
56
+ <Select
57
+ sx={styles.select}
58
+ multiple
59
+ value={(value || []).map(({ id }) => id)}
60
+ onChange={(e) => updateValue(e.target.value as (string | number)[])}
61
+ renderValue={(selected) => (
62
+ <Box sx={styles.chipSelect}>
63
+ {selected.map((id) => (
64
+ <Chip
65
+ key={id}
66
+ label={mappedOptionDictionary[id].value}
67
+ size="small"
68
+ />
69
+ ))}
70
+ </Box>
71
+ )}
72
+ endAdornment={
73
+ value ? (
74
+ <InputAdornment sx={styles.inputClearButton} position="end">
75
+ <IconButton
76
+ onClick={() => {
77
+ updateValue(null);
78
+ }}
79
+ >
80
+ <CloseOutlined></CloseOutlined>
81
+ </IconButton>
82
+ </InputAdornment>
83
+ ) : (
84
+ ""
85
+ )
86
+ }
87
+ >
88
+ {filter.options.map((option) => (
89
+ <MenuItem key={option.id} value={option.id}>
90
+ {t(option.value)}
91
+ </MenuItem>
92
+ ))}
93
+ </Select>
94
+ </FormControl>
95
+ );
96
+ };
97
+
98
+ const getStyles = (theme: Theme) => ({
99
+ inputClearButton: {
100
+ position: "absolute",
101
+ right: theme.spacing(3),
102
+ },
103
+ chipSelect: {
104
+ display: "flex",
105
+ flexWrap: "wrap",
106
+ gap: theme.spacing(1),
107
+ marginRight: theme.spacing(1),
108
+ },
109
+ select: {
110
+ "& .MuiSelect-select": {
111
+ padding: theme.spacing(1),
112
+ minHeight: theme.spacing(3),
113
+ },
114
+ },
115
+ });
116
+
117
+ export default MultiSelectFilterComponent;
@@ -0,0 +1,82 @@
1
+ import {
2
+ Typography,
3
+ IconButton,
4
+ InputAdornment,
5
+ Select,
6
+ MenuItem,
7
+ Theme,
8
+ useTheme,
9
+ FormControl,
10
+ } from "@mui/material";
11
+ import { CloseOutlined } from "@mui/icons-material";
12
+ import React, { useState } from "react";
13
+ import { FilterOption, SelectFilter, FilterProps } from "./list-filter-types";
14
+
15
+ const SelectFilterComponent: React.FC<FilterProps<SelectFilter>> = ({
16
+ filter,
17
+ onSet,
18
+ onError,
19
+ current,
20
+ }) => {
21
+ const theme = useTheme();
22
+ const styles = getStyles(theme);
23
+
24
+ const [value, setValue] = useState(current);
25
+
26
+ const mappedOptionDictionary = filter.options.reduce(
27
+ (dictionary, option) => ({
28
+ ...dictionary,
29
+ [option.id]: option,
30
+ }),
31
+ {},
32
+ ) as { [id: string | number]: FilterOption };
33
+
34
+ const updateValue = (optionId: string | number) => {
35
+ const newValue = mappedOptionDictionary[optionId] || null;
36
+ setValue(newValue && [newValue]);
37
+ onSet(newValue && [newValue]);
38
+ };
39
+
40
+ return (
41
+ <FormControl>
42
+ <Typography>{filter.label}</Typography>
43
+ <Select
44
+ value={value?.[0]?.id || ""}
45
+ onChange={(e) => updateValue(e.target.value)}
46
+ endAdornment={
47
+ value ? (
48
+ <InputAdornment sx={styles.inputClearButton} position="end">
49
+ <IconButton
50
+ onClick={() => {
51
+ updateValue(null);
52
+ }}
53
+ >
54
+ <CloseOutlined></CloseOutlined>
55
+ </IconButton>
56
+ </InputAdornment>
57
+ ) : (
58
+ ""
59
+ )
60
+ }
61
+ renderValue={(selected) =>
62
+ selected && mappedOptionDictionary[selected]?.value
63
+ }
64
+ >
65
+ {filter.options.map((option) => (
66
+ <MenuItem key={option.id} value={option.id}>
67
+ {option.value}
68
+ </MenuItem>
69
+ ))}
70
+ </Select>
71
+ </FormControl>
72
+ );
73
+ };
74
+
75
+ const getStyles = (theme: Theme) => ({
76
+ inputClearButton: {
77
+ position: "absolute",
78
+ right: theme.spacing(3),
79
+ },
80
+ });
81
+
82
+ export default SelectFilterComponent;
@@ -0,0 +1,45 @@
1
+ import { Crumb } from "../components/breadcrumbs/breadcrumbs";
2
+
3
+ const baseUrl = "/e4integrate";
4
+
5
+ const lookup: { [key: string]: Crumb } = {
6
+ dashboard: {
7
+ name: "Dashboard",
8
+ url: `${baseUrl}/dashboard`,
9
+ },
10
+ executions: {
11
+ name: "Executions",
12
+ url: `${baseUrl}/executions`,
13
+ },
14
+ integrations: {
15
+ name: "Integrations",
16
+ url: `${baseUrl}/integrations`,
17
+ },
18
+ scheduler: {
19
+ name: "Schedules",
20
+ url: `${baseUrl}/scheduler`,
21
+ },
22
+ };
23
+
24
+ // hook that generates breadcrumbs based on url & name of integration if applicable
25
+ const useBreadcrumbs = (url: string, integrationName?: string) => {
26
+ // Dashboard crumb is always the root (breadcrumbs not shown on Dashboard page itself)
27
+ const breadcrumbs: Crumb[] = [lookup.dashboard];
28
+
29
+ const splitUrl = url.split("/").splice(4);
30
+ const path = splitUrl[0];
31
+ if (!lookup[path])
32
+ throw new Error(`Breadcrumb entry does not exist for path ${path}`);
33
+ if (path !== "dashboard") {
34
+ breadcrumbs.push(lookup[path]);
35
+ }
36
+
37
+ if (integrationName) {
38
+ breadcrumbs.push({ name: integrationName, url: "" });
39
+ }
40
+ delete breadcrumbs.at(-1).url;
41
+
42
+ return breadcrumbs;
43
+ };
44
+
45
+ export default useBreadcrumbs;
@@ -0,0 +1,25 @@
1
+ export default function useTimestamp(dateTime: string) {
2
+ const then = new Date(dateTime);
3
+ const diffTime = Date.now() - then.getTime();
4
+ const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
5
+
6
+ let result: string;
7
+ if (diffDays === 0) {
8
+ const diffMin = Math.floor(diffTime / (1000 * 60));
9
+ if (diffMin === 0) {
10
+ result = "Now";
11
+ } else if (diffMin >= 60) {
12
+ result = Math.floor(diffTime / (1000 * 60 * 60)) + "h ago";
13
+ } else {
14
+ result = diffMin + "m ago";
15
+ }
16
+ } else {
17
+ if (diffDays === 1) {
18
+ result = "Yesterday";
19
+ } else {
20
+ result = diffDays + " days ago";
21
+ }
22
+ }
23
+
24
+ return result;
25
+ }
package/src/lib/.keep ADDED
File without changes