@centreon/ui 24.11.2 → 24.11.4

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 (75) hide show
  1. package/package.json +2 -3
  2. package/src/Dashboard/Dashboard.styles.ts +4 -3
  3. package/src/Dashboard/DashboardLayout.stories.tsx +1 -1
  4. package/src/Dashboard/Grid.tsx +17 -11
  5. package/src/Dashboard/Layout.tsx +56 -27
  6. package/src/FileDropZone/index.tsx +21 -23
  7. package/src/Form/CollapsibleGroup.tsx +3 -2
  8. package/src/Form/Form.cypress.spec.tsx +39 -0
  9. package/src/Form/Form.tsx +1 -0
  10. package/src/Form/Inputs/Autocomplete.tsx +27 -4
  11. package/src/Form/Inputs/ConnectedAutocomplete.tsx +20 -10
  12. package/src/Form/Inputs/File.tsx +69 -0
  13. package/src/Form/Inputs/Grid.tsx +30 -2
  14. package/src/Form/Inputs/Radio.tsx +12 -4
  15. package/src/Form/Inputs/Switch.tsx +10 -2
  16. package/src/Form/Inputs/Text.tsx +13 -4
  17. package/src/Form/Inputs/index.tsx +5 -2
  18. package/src/Form/Inputs/models.ts +18 -2
  19. package/src/Form/storiesData.tsx +15 -3
  20. package/src/Form/translatedLabels.ts +1 -0
  21. package/src/Graph/BarChart/BarChart.tsx +4 -1
  22. package/src/Graph/BarChart/ResponsiveBarChart.tsx +3 -2
  23. package/src/Graph/Chart/Chart.tsx +9 -2
  24. package/src/Graph/Chart/InteractiveComponents/AnchorPoint/useTickGraph.ts +2 -2
  25. package/src/Graph/Chart/InteractiveComponents/index.tsx +10 -2
  26. package/src/Graph/Chart/helpers/index.ts +5 -5
  27. package/src/Graph/Chart/index.tsx +7 -0
  28. package/src/Graph/Chart/models.ts +1 -0
  29. package/src/Graph/common/timeSeries/index.ts +15 -8
  30. package/src/InputField/Text/index.tsx +1 -1
  31. package/src/Listing/index.tsx +39 -27
  32. package/src/Listing/models.ts +8 -0
  33. package/src/MultiSelectEntries/index.tsx +0 -2
  34. package/src/PopoverMenu/index.tsx +9 -2
  35. package/src/SortableItems/index.tsx +1 -0
  36. package/src/ThemeProvider/index.tsx +1 -1
  37. package/src/ThemeProvider/palettes.ts +4 -4
  38. package/src/api/customFetch.ts +4 -1
  39. package/src/components/CrudPage/Actions/Actions.styles.ts +16 -0
  40. package/src/components/CrudPage/Actions/Actions.tsx +24 -0
  41. package/src/components/CrudPage/Actions/AddButton.tsx +23 -0
  42. package/src/components/CrudPage/Actions/Filters.tsx +25 -0
  43. package/src/components/CrudPage/Actions/Search.tsx +31 -0
  44. package/src/components/CrudPage/Actions/useSearch.tsx +24 -0
  45. package/src/components/CrudPage/Columns/Actions.tsx +88 -0
  46. package/src/components/CrudPage/CrudPage.cypress.spec.tsx +559 -0
  47. package/src/components/CrudPage/CrudPage.stories.tsx +278 -0
  48. package/src/components/CrudPage/CrudPageRoot.tsx +142 -0
  49. package/src/components/CrudPage/DeleteModal.tsx +77 -0
  50. package/src/components/CrudPage/Form/AddModal.tsx +35 -0
  51. package/src/components/CrudPage/Form/Buttons.tsx +98 -0
  52. package/src/components/CrudPage/Form/UpdateModal.tsx +60 -0
  53. package/src/components/CrudPage/Listing.tsx +63 -0
  54. package/src/components/CrudPage/atoms.ts +30 -0
  55. package/src/components/CrudPage/hooks/useDeleteItem.ts +53 -0
  56. package/src/components/CrudPage/hooks/useGetItem.ts +36 -0
  57. package/src/components/CrudPage/hooks/useGetItems.ts +67 -0
  58. package/src/components/CrudPage/hooks/useListingQueryKey.ts +31 -0
  59. package/src/components/CrudPage/index.tsx +7 -0
  60. package/src/components/CrudPage/models.ts +118 -0
  61. package/src/components/CrudPage/utils.ts +4 -0
  62. package/src/components/DataTable/DataTable.cypress.spec.tsx +2 -1
  63. package/src/components/DataTable/DataTable.stories.tsx +17 -0
  64. package/src/components/DataTable/DataTable.styles.ts +1 -1
  65. package/src/components/DataTable/EmptyState/DataTableEmptyState.styles.ts +3 -1
  66. package/src/components/DataTable/EmptyState/DataTableEmptyState.tsx +6 -0
  67. package/src/components/DataTable/Item/DataTableItem.styles.ts +28 -2
  68. package/src/components/DataTable/Item/DataTableItem.tsx +19 -4
  69. package/src/components/Layout/AreaIndicator.tsx +1 -1
  70. package/src/components/Layout/PageLayout/PageLayout.styles.ts +7 -2
  71. package/src/components/Layout/PageLayout/PageLayoutBody.tsx +1 -0
  72. package/src/components/Modal/Modal.styles.ts +1 -1
  73. package/src/components/Zoom/Zoom.tsx +2 -2
  74. package/src/components/Zoom/ZoomContent.tsx +2 -2
  75. package/src/components/index.ts +1 -0
@@ -0,0 +1,63 @@
1
+ import { useAtom, useAtomValue, useSetAtom } from 'jotai';
2
+ import { ColumnType } from '../../../';
3
+ import { MemoizedListing } from '../../Listing';
4
+ import Actions from './Actions/Actions';
5
+ import ColumnActions from './Columns/Actions';
6
+ import {
7
+ changeSortAtom,
8
+ limitAtom,
9
+ pageAtom,
10
+ sortFieldAtom,
11
+ sortOrderAtom
12
+ } from './atoms';
13
+ import { ListingProps } from './models';
14
+
15
+ const Listing = <TData extends { id: number; name: string }>({
16
+ rows,
17
+ total,
18
+ isLoading,
19
+ columns,
20
+ subItems,
21
+ labels,
22
+ filters
23
+ }: ListingProps<TData> & {
24
+ labels: {
25
+ search: string;
26
+ add: string;
27
+ };
28
+ }): JSX.Element => {
29
+ const [page, setPage] = useAtom(pageAtom);
30
+ const [limit, setLimit] = useAtom(limitAtom);
31
+ const sortOrder = useAtomValue(sortOrderAtom);
32
+ const sortField = useAtomValue(sortFieldAtom);
33
+ const changeSort = useSetAtom(changeSortAtom);
34
+
35
+ const listingColumns = columns.concat({
36
+ type: ColumnType.component,
37
+ id: 'actions',
38
+ label: '',
39
+ Component: ColumnActions,
40
+ width: 'min-content',
41
+ clickable: true
42
+ });
43
+
44
+ return (
45
+ <MemoizedListing
46
+ actions={<Actions labels={labels} filters={filters} />}
47
+ columns={listingColumns}
48
+ subItems={subItems}
49
+ loading={isLoading}
50
+ rows={rows}
51
+ currentPage={page}
52
+ onPaginate={setPage}
53
+ limit={limit}
54
+ onLimitChange={setLimit}
55
+ totalRows={total}
56
+ sortField={sortField}
57
+ sortOrder={sortOrder}
58
+ onSort={changeSort}
59
+ />
60
+ );
61
+ };
62
+
63
+ export default Listing;
@@ -0,0 +1,30 @@
1
+ import { atom } from 'jotai';
2
+ import { ItemToDelete } from './models';
3
+
4
+ export const pageAtom = atom(0);
5
+ export const limitAtom = atom(10);
6
+ export const searchAtom = atom('');
7
+ export const sortOrderAtom = atom('asc');
8
+ export const sortFieldAtom = atom('name');
9
+ export const openFormModalAtom = atom<number | 'add' | null>(null);
10
+ export const itemToDeleteAtom = atom<ItemToDelete | null>(null);
11
+ export const canDeleteSubItemsAtom = atom<boolean | undefined>(true);
12
+ export const formLabelButtonsAtom = atom({
13
+ add: {
14
+ cancel: 'Cancel',
15
+ confirm: 'Save'
16
+ },
17
+ update: {
18
+ cancel: 'Cancel',
19
+ confirm: 'Save'
20
+ }
21
+ });
22
+ export const askBeforeCloseFormModalAtom = atom(false);
23
+
24
+ export const changeSortAtom = atom(
25
+ null,
26
+ (_get, set, { sortOrder, sortField }) => {
27
+ set(sortOrderAtom, sortOrder);
28
+ set(sortFieldAtom, sortField);
29
+ }
30
+ );
@@ -0,0 +1,53 @@
1
+ import {
2
+ Method,
3
+ ResponseError,
4
+ useMutationQuery,
5
+ useSnackbar
6
+ } from '@centreon/ui';
7
+ import { useQueryClient } from '@tanstack/react-query';
8
+ import { ReactElement } from 'react';
9
+ import { ItemToDelete } from '../models';
10
+ import { isAFunction } from '../utils';
11
+
12
+ interface UseDeleteItem {
13
+ isMutating: boolean;
14
+ deleteItem: (item: ItemToDelete) => Promise<object | ResponseError>;
15
+ }
16
+
17
+ interface UseDeleteItemProps {
18
+ deleteEndpoint: (item: ItemToDelete) => string;
19
+ listingQueryKey: string;
20
+ successMessage:
21
+ | ((item: ItemToDelete) => string | ReactElement)
22
+ | string
23
+ | ReactElement;
24
+ }
25
+
26
+ export const useDeleteItem = ({
27
+ deleteEndpoint,
28
+ listingQueryKey,
29
+ successMessage
30
+ }: UseDeleteItemProps): UseDeleteItem => {
31
+ const queryClient = useQueryClient();
32
+
33
+ const { showSuccessMessage } = useSnackbar();
34
+
35
+ const { mutateAsync, isMutating } = useMutationQuery<object, ItemToDelete>({
36
+ getEndpoint: (_meta) => deleteEndpoint(_meta),
37
+ method: Method.DELETE,
38
+ onSuccess: (_data, { _meta }) => {
39
+ queryClient.invalidateQueries({ queryKey: [listingQueryKey] });
40
+ showSuccessMessage(
41
+ isAFunction(successMessage) ? successMessage(_meta) : successMessage
42
+ );
43
+ }
44
+ });
45
+
46
+ const deleteItem = (item: ItemToDelete): Promise<object | ResponseError> =>
47
+ mutateAsync({ _meta: item });
48
+
49
+ return {
50
+ isMutating,
51
+ deleteItem
52
+ };
53
+ };
@@ -0,0 +1,36 @@
1
+ import { equals, isNotNil } from 'ramda';
2
+ import { useFetchQuery } from '../../..';
3
+ import { GetItem } from '../models';
4
+
5
+ interface UseGetItem<TItemForm> {
6
+ initialValues?: TItemForm;
7
+ isLoading: boolean;
8
+ }
9
+
10
+ export const useGetItem = <
11
+ TItem extends { id: number; name: string },
12
+ TItemForm
13
+ >({
14
+ id,
15
+ decoder,
16
+ baseEndpoint,
17
+ itemQueryKey,
18
+ adapter
19
+ }: GetItem<TItem, TItemForm> & {
20
+ id: number | 'add' | null;
21
+ }): UseGetItem<TItemForm> => {
22
+ const { data, isLoading } = useFetchQuery<TItem>({
23
+ getEndpoint: () => baseEndpoint(id),
24
+ getQueryKey: () => [itemQueryKey, id],
25
+ decoder,
26
+ queryOptions: {
27
+ enabled: isNotNil(id) && !equals('add', id),
28
+ suspense: false
29
+ }
30
+ });
31
+
32
+ return {
33
+ initialValues: data ? adapter(data) : undefined,
34
+ isLoading
35
+ };
36
+ };
@@ -0,0 +1,67 @@
1
+ import { useAtomValue } from 'jotai';
2
+ import { isEmpty } from 'ramda';
3
+ import { ListingModel, buildListingEndpoint } from '../../..';
4
+ import useFetchQuery from '../../../api/useFetchQuery';
5
+ import {
6
+ limitAtom,
7
+ pageAtom,
8
+ searchAtom,
9
+ sortFieldAtom,
10
+ sortOrderAtom
11
+ } from '../atoms';
12
+ import { UseGetItemsProps, UseGetItemsState } from '../models';
13
+ import { useListingQueryKey } from './useListingQueryKey';
14
+
15
+ export const useGetItems = <TData, TFilters>({
16
+ queryKeyName,
17
+ filtersAtom,
18
+ decoder,
19
+ getSearchParameters,
20
+ baseEndpoint
21
+ }: UseGetItemsProps<TData, TFilters>): UseGetItemsState<TData> => {
22
+ const queryKey = useListingQueryKey({ queryKeyName, filtersAtom });
23
+
24
+ const page = useAtomValue(pageAtom);
25
+ const limit = useAtomValue(limitAtom);
26
+ const search = useAtomValue(searchAtom);
27
+ const sortOrder = useAtomValue(sortOrderAtom);
28
+ const sortField = useAtomValue(sortFieldAtom);
29
+ const filters = useAtomValue(filtersAtom);
30
+
31
+ const { data, isLoading } = useFetchQuery<ListingModel<TData>>({
32
+ decoder,
33
+ getQueryKey: () => queryKey,
34
+ getEndpoint: () =>
35
+ buildListingEndpoint({
36
+ baseEndpoint,
37
+ parameters: {
38
+ page: page + 1,
39
+ limit,
40
+ sort: {
41
+ [sortField]: sortOrder
42
+ },
43
+ search: {
44
+ regex: {
45
+ fields: ['name'],
46
+ value: search
47
+ },
48
+ ...getSearchParameters({ filters, search })
49
+ }
50
+ }
51
+ }),
52
+ queryOptions: {
53
+ suspense: false
54
+ }
55
+ });
56
+
57
+ const items = data?.result || [];
58
+ const hasItems = !!data;
59
+
60
+ return {
61
+ items,
62
+ isDataEmpty: isEmpty(items),
63
+ hasItems,
64
+ isLoading,
65
+ total: data?.meta.total || 0
66
+ };
67
+ };
@@ -0,0 +1,31 @@
1
+ import { useAtomValue } from 'jotai';
2
+ import {
3
+ limitAtom,
4
+ pageAtom,
5
+ searchAtom,
6
+ sortFieldAtom,
7
+ sortOrderAtom
8
+ } from '../atoms';
9
+ import { UseListingQueryKeyProps } from '../models';
10
+
11
+ export const useListingQueryKey = <TFilter>({
12
+ filtersAtom,
13
+ queryKeyName
14
+ }: UseListingQueryKeyProps<TFilter>): Array<string | number> => {
15
+ const page = useAtomValue(pageAtom);
16
+ const limit = useAtomValue(limitAtom);
17
+ const search = useAtomValue(searchAtom);
18
+ const sortOrder = useAtomValue(sortOrderAtom);
19
+ const sortField = useAtomValue(sortFieldAtom);
20
+ const filters = useAtomValue(filtersAtom);
21
+
22
+ return [
23
+ queryKeyName,
24
+ limit,
25
+ page,
26
+ search,
27
+ sortField,
28
+ sortOrder,
29
+ JSON.stringify(filters)
30
+ ];
31
+ };
@@ -0,0 +1,7 @@
1
+ import { CrudPageRoot } from './CrudPageRoot';
2
+ import { askBeforeCloseFormModalAtom, openFormModalAtom } from './atoms';
3
+
4
+ export const CrudPage = Object.assign(CrudPageRoot, {
5
+ openFormModalAtom,
6
+ askBeforeCloseFormModalAtom
7
+ });
@@ -0,0 +1,118 @@
1
+ import { PrimitiveAtom } from 'jotai';
2
+ import { ReactElement } from 'react';
3
+ import { JsonDecoder } from 'ts.data.json';
4
+ import { Column, ListingModel, SearchParameter } from '../../..';
5
+ import { ListingSubItems } from '../../Listing/models';
6
+
7
+ interface CrudPageRootLabels {
8
+ title: string;
9
+ welcome: {
10
+ title: string;
11
+ description?: string;
12
+ };
13
+ actions: {
14
+ create: string;
15
+ };
16
+ listing: {
17
+ search: string;
18
+ };
19
+ }
20
+
21
+ export interface UseListingQueryKeyProps<TFilters> {
22
+ queryKeyName: string;
23
+ filtersAtom: PrimitiveAtom<TFilters>;
24
+ }
25
+
26
+ export interface UseGetItemsProps<TData, TFilters> {
27
+ queryKeyName: string;
28
+ filtersAtom: PrimitiveAtom<TFilters>;
29
+ decoder?: JsonDecoder.Decoder<ListingModel<TData>>;
30
+ baseEndpoint: string;
31
+ getSearchParameters: ({
32
+ search,
33
+ filters
34
+ }: { search: string; filters: TFilters }) => SearchParameter;
35
+ }
36
+
37
+ export interface UseGetItemsState<TData> {
38
+ items: Array<TData>;
39
+ hasItems: boolean;
40
+ isDataEmpty: boolean;
41
+ isLoading: boolean;
42
+ total: number;
43
+ }
44
+
45
+ export interface DeleteItem {
46
+ deleteEndpoint: (item: ItemToDelete) => string;
47
+ modalSize?: 'small' | 'medium' | 'large' | 'xlarge';
48
+ labels: {
49
+ successMessage:
50
+ | ((item: ItemToDelete) => string | ReactElement)
51
+ | string
52
+ | ReactElement;
53
+ title:
54
+ | ((item: ItemToDelete) => string | ReactElement)
55
+ | string
56
+ | ReactElement;
57
+ description:
58
+ | ((item: ItemToDelete) => string | ReactElement)
59
+ | string
60
+ | ReactElement;
61
+ cancel: string;
62
+ confirm: string;
63
+ };
64
+ }
65
+
66
+ export interface GetItem<TItem, TItemForm> {
67
+ baseEndpoint: (id: number) => string;
68
+ decoder?: JsonDecoder.Decoder<TItem>;
69
+ adapter: (item: TItem) => TItemForm;
70
+ itemQueryKey: string;
71
+ }
72
+
73
+ export interface Form<TItem, TItemForm> {
74
+ Form: (props: {
75
+ Buttons: () => JSX.Element;
76
+ initialValues?: TItemForm;
77
+ isLoading?: boolean;
78
+ }) => JSX.Element;
79
+ getItem: GetItem<TItem, TItemForm>;
80
+ modalSize?: 'small' | 'medium' | 'large' | 'xlarge';
81
+ labels: {
82
+ add: {
83
+ title: string;
84
+ cancel: string;
85
+ confirm: string;
86
+ };
87
+ update: {
88
+ title: string;
89
+ cancel: string;
90
+ confirm: string;
91
+ };
92
+ };
93
+ }
94
+
95
+ export interface ListingProps<TData> {
96
+ rows: Array<TData>;
97
+ total: number;
98
+ isLoading: boolean;
99
+ columns: Array<Column>;
100
+ subItems?: ListingSubItems & {
101
+ canDeleteSubItems?: boolean;
102
+ };
103
+ filters: JSX.Element;
104
+ }
105
+
106
+ export interface ItemToDelete {
107
+ id: number;
108
+ name: string;
109
+ parent?: { id: number; name: string };
110
+ }
111
+
112
+ export interface CrudPageRootProps<TData, TFilters, TItem, TItemForm>
113
+ extends UseGetItemsProps<TData, TFilters>,
114
+ Omit<ListingProps<TData>, 'rows' | 'total' | 'isLoading'> {
115
+ form: Form<TItem, TItemForm>;
116
+ labels: CrudPageRootLabels;
117
+ deleteItem: DeleteItem;
118
+ }
@@ -0,0 +1,4 @@
1
+ import { equals, type } from 'ramda';
2
+
3
+ export const isAFunction = (property): boolean =>
4
+ equals('Function', type(property));
@@ -53,7 +53,8 @@ const initializeDataTableEmpty = (canCreate = false): void => {
53
53
  actions: {
54
54
  create: 'Create'
55
55
  },
56
- title: 'Welcome'
56
+ title: 'Welcome',
57
+ description: 'Description'
57
58
  }}
58
59
  onCreate={cy.stub()}
59
60
  />
@@ -48,6 +48,23 @@ export const AsEmptyState: Story = {
48
48
  }
49
49
  };
50
50
 
51
+ export const AsEmptyStateWithDescription: Story = {
52
+ args: {
53
+ children: (
54
+ <DataTable.EmptyState
55
+ labels={{
56
+ actions: {
57
+ create: 'Create item'
58
+ },
59
+ title: 'No items found',
60
+ description: 'Description'
61
+ }}
62
+ />
63
+ ),
64
+ isEmpty: true
65
+ }
66
+ };
67
+
51
68
  export const withFixedHeightContainer: Story = {
52
69
  args: { ...Default.args },
53
70
  render: (args) => (
@@ -8,7 +8,7 @@ const useStyles = makeStyles()((theme) => ({
8
8
  },
9
9
  display: 'grid',
10
10
  gridGap: theme.spacing(2.5),
11
- gridTemplateColumns: `repeat(auto-fill, ${theme.spacing(45)})`
11
+ gridTemplateColumns: `repeat(auto-fill, ${theme.spacing(53)})`
12
12
  },
13
13
  '&[data-variant="listing"]': {
14
14
  height: '100%'
@@ -10,7 +10,6 @@ const useStyles = makeStyles()((theme) => ({
10
10
  display: 'flex',
11
11
  flexDirection: 'column',
12
12
  gap: theme.spacing(4),
13
-
14
13
  h2: {
15
14
  color: theme.palette.text.primary,
16
15
  font: 'normal normal 600 34px/36px Roboto',
@@ -20,6 +19,9 @@ const useStyles = makeStyles()((theme) => ({
20
19
  justifyContent: 'center',
21
20
  minHeight: '30vh',
22
21
  width: '100%'
22
+ },
23
+ description: {
24
+ maxWidth: '65%'
23
25
  }
24
26
  }));
25
27
 
@@ -16,6 +16,7 @@ type ListEmptyStateProps = {
16
16
  create: string;
17
17
  };
18
18
  title: string;
19
+ description?: string;
19
20
  };
20
21
  onCreate?: () => void;
21
22
  };
@@ -34,6 +35,11 @@ const DataTableEmptyState = ({
34
35
  data-testid="data-table-empty-state"
35
36
  >
36
37
  <MuiTypography variant="h2">{t(labels.title)}</MuiTypography>
38
+ {labels.description && (
39
+ <MuiTypography className={classes.description}>
40
+ {t(labels.description)}
41
+ </MuiTypography>
42
+ )}
37
43
  <div className={classes.actions}>
38
44
  {canCreate && (
39
45
  <Button
@@ -6,6 +6,16 @@ const useStyles = makeStyles()((theme) => ({
6
6
  flexDirection: 'row',
7
7
  justifyContent: 'space-between'
8
8
  },
9
+ cardActions: {
10
+ backgroundColor: theme.palette.background.paper,
11
+ bottom: 0,
12
+ position: 'absolute',
13
+ width: '100%'
14
+ },
15
+ cardContent: {
16
+ padding: theme.spacing(2),
17
+ zIndex: 1
18
+ },
9
19
  dataTableItem: {
10
20
  '& .MuiCardActionArea-root': {
11
21
  alignItems: 'flex-start',
@@ -22,16 +32,32 @@ const useStyles = makeStyles()((theme) => ({
22
32
  display: 'flex',
23
33
  justifyContent: 'space-between'
24
34
  },
35
+ '&:hover img[alt*="thumbnail"]': {
36
+ transform: 'scale(1.1)',
37
+ transformOrigin: 'center'
38
+ },
25
39
  borderRadius: theme.shape.borderRadius,
26
40
  display: 'flex',
27
41
  flexDirection: 'column',
28
- height: '186px',
42
+ height: '250px',
29
43
  justifyContent: 'space-between',
30
44
  p: {
31
45
  color: theme.palette.text.secondary,
32
46
  letterSpacing: '0',
33
47
  margin: '0'
34
- }
48
+ },
49
+ position: 'relative'
50
+ },
51
+ description: {
52
+ maxHeight: '42px',
53
+ overflow: 'hidden'
54
+ },
55
+ thumbnail: {
56
+ height: theme.spacing(10.25),
57
+ objectFit: 'cover',
58
+ objectPosition: 'top',
59
+ transition: 'transform 150ms ease-out',
60
+ width: '100%'
35
61
  }
36
62
  }));
37
63
 
@@ -16,6 +16,7 @@ export interface DataTableItemProps {
16
16
  hasActions?: boolean;
17
17
  hasCardAction?: boolean;
18
18
  onClick?: () => void;
19
+ thumbnail?: string | null;
19
20
  title: string;
20
21
  }
21
22
 
@@ -27,7 +28,8 @@ const DataTableItem = forwardRef(
27
28
  hasCardAction = false,
28
29
  hasActions = false,
29
30
  onClick,
30
- Actions
31
+ Actions,
32
+ thumbnail
31
33
  }: DataTableItemProps,
32
34
  ref
33
35
  ): ReactElement => {
@@ -46,15 +48,28 @@ const DataTableItem = forwardRef(
46
48
  variant="outlined"
47
49
  >
48
50
  <ActionArea aria-label="view" onClick={() => onClick?.()}>
49
- <MuiCardContent>
51
+ {thumbnail && (
52
+ <img
53
+ alt={`thumbnail-${title}-${description}`}
54
+ className={classes.thumbnail}
55
+ data-testId={`thumbnail-${title}-${description}`}
56
+ loading="lazy"
57
+ src={thumbnail}
58
+ />
59
+ )}
60
+ <MuiCardContent className={classes.cardContent}>
50
61
  <MuiTypography fontWeight={500} variant="h5">
51
62
  {title}
52
63
  </MuiTypography>
53
- {description && <MuiTypography>{description}</MuiTypography>}
64
+ {description && (
65
+ <MuiTypography className={classes.description}>
66
+ {description}
67
+ </MuiTypography>
68
+ )}
54
69
  </MuiCardContent>
55
70
  </ActionArea>
56
71
  {hasActions && (
57
- <MuiCardActions>
72
+ <MuiCardActions className={classes.cardActions}>
58
73
  <span />
59
74
  <span>{Actions}</span>
60
75
  </MuiCardActions>
@@ -25,7 +25,7 @@ const AreaIndicator = ({
25
25
  data-depth={depth}
26
26
  style={{ height, width }}
27
27
  >
28
- {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
28
+ {/* biome-ignore lint/a11y: */}
29
29
  <label>{name}</label>
30
30
  {children}
31
31
  </div>
@@ -2,8 +2,10 @@ import { makeStyles } from 'tss-react/mui';
2
2
 
3
3
  export const useStyles = makeStyles()((theme) => ({
4
4
  pageLayout: {
5
+ height: '100%',
5
6
  display: 'grid',
6
- gridTemplateRows: 'min-content',
7
+ gridTemplateRows: 'auto 1fr',
8
+ height: '100%',
7
9
  overflow: 'hidden'
8
10
  },
9
11
  pageLayoutActions: {
@@ -22,8 +24,11 @@ export const useStyles = makeStyles()((theme) => ({
22
24
  '&[data-has-background="true"]': {
23
25
  backgroundColor: theme.palette.layout.body.background
24
26
  },
27
+ '&[data-has-actions="true"]': {
28
+ gridTemplateRows: 'min-content auto',
29
+ },
25
30
  display: 'grid',
26
- gridTemplateRows: 'min-content',
31
+ gridTemplateRows: 'auto',
27
32
  overflow: 'hidden',
28
33
  padding: theme.spacing(1.5, 3, 5)
29
34
  },
@@ -17,6 +17,7 @@ export const PageLayoutBody = ({
17
17
  <section
18
18
  className={classes.pageLayoutBody}
19
19
  data-has-background={hasBackground}
20
+ data-has-actions={!!children?.length}
20
21
  id="page-body"
21
22
  >
22
23
  {children}
@@ -45,7 +45,7 @@ const useStyles = makeStyles<{
45
45
  width: '400px'
46
46
  },
47
47
  '&[data-size="xlarge"] .MuiDialog-paper': {
48
- maxWidth: '1400px',
48
+ maxWidth: '900px',
49
49
  width: 'calc(100% - 64px)'
50
50
  }
51
51
  },
@@ -3,10 +3,10 @@ import { Zoom as VisxZoom } from '@visx/zoom';
3
3
  import { ParentSize } from '../..';
4
4
 
5
5
  import ZoomContent from './ZoomContent';
6
- import { MinimapPosition } from './models';
6
+ import type { MinimapPosition } from './models';
7
7
 
8
8
  export interface ZoomProps {
9
- children: JSX.Element | (({ width, height }) => JSX.Element);
9
+ children: JSX.Element | (({ width, height, transformMatrix }) => JSX.Element);
10
10
  id?: number | string;
11
11
  minimapPosition?: MinimapPosition;
12
12
  scaleMax?: number;