@centreon/ui 24.4.68 → 24.4.70
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.
- package/package.json +1 -1
- package/src/Dashboard/Item.tsx +11 -2
- package/src/Dashboard/Layout.tsx +4 -2
- package/src/api/useGraphQuery/index.ts +7 -1
- package/src/components/Form/AccessRights/AccessRights.cypress.spec.tsx +27 -13
- package/src/components/Form/AccessRights/AccessRights.stories.tsx +0 -19
- package/src/components/Form/AccessRights/AccessRights.styles.ts +1 -1
- package/src/components/Form/AccessRights/AccessRights.tsx +6 -5
- package/src/components/Form/AccessRights/Actions/Actions.styles.ts +3 -7
- package/src/components/Form/AccessRights/Actions/Actions.tsx +15 -32
- package/src/components/Form/AccessRights/Actions/useActions.ts +4 -37
- package/src/components/Form/AccessRights/models.ts +0 -3
- package/src/components/Form/AccessRights/storiesData.ts +0 -3
- package/src/components/Form/AccessRights/useAccessRightsChange.ts +30 -0
- package/src/components/Form/AccessRights/utils.ts +18 -0
- package/src/components/List/Item/ListItem.styles.ts +2 -2
- package/src/components/Tabs/Tab.styles.ts +25 -0
- package/src/components/Tabs/TabPanel.tsx +22 -0
- package/src/components/Tabs/Tabs.cypress.spec.tsx +70 -0
- package/src/components/Tabs/Tabs.stories.tsx +55 -0
- package/src/components/Tabs/Tabs.tsx +55 -0
- package/src/components/Tabs/index.ts +6 -0
- package/src/components/index.ts +1 -0
package/package.json
CHANGED
package/src/Dashboard/Item.tsx
CHANGED
|
@@ -15,6 +15,7 @@ import { useMemoComponent } from '../utils';
|
|
|
15
15
|
import { useDashboardItemStyles } from './Dashboard.styles';
|
|
16
16
|
|
|
17
17
|
interface DashboardItemProps {
|
|
18
|
+
additionalMemoProps?: Array<unknown>;
|
|
18
19
|
canMove?: boolean;
|
|
19
20
|
children: ReactElement;
|
|
20
21
|
className?: string;
|
|
@@ -39,7 +40,8 @@ const Item = forwardRef<HTMLDivElement, DashboardItemProps>(
|
|
|
39
40
|
onTouchEnd,
|
|
40
41
|
id,
|
|
41
42
|
disablePadding = false,
|
|
42
|
-
canMove = false
|
|
43
|
+
canMove = false,
|
|
44
|
+
additionalMemoProps = []
|
|
43
45
|
}: DashboardItemProps,
|
|
44
46
|
ref: ForwardedRef<HTMLDivElement>
|
|
45
47
|
): ReactElement => {
|
|
@@ -92,7 +94,14 @@ const Item = forwardRef<HTMLDivElement, DashboardItemProps>(
|
|
|
92
94
|
</Card>
|
|
93
95
|
</div>
|
|
94
96
|
),
|
|
95
|
-
memoProps: [
|
|
97
|
+
memoProps: [
|
|
98
|
+
style,
|
|
99
|
+
className,
|
|
100
|
+
header,
|
|
101
|
+
theme.palette.mode,
|
|
102
|
+
canMove,
|
|
103
|
+
...additionalMemoProps
|
|
104
|
+
]
|
|
96
105
|
});
|
|
97
106
|
}
|
|
98
107
|
);
|
package/src/Dashboard/Layout.tsx
CHANGED
|
@@ -16,6 +16,7 @@ import Grid from './Grid';
|
|
|
16
16
|
const ReactGridLayout = WidthProvider(GridLayout);
|
|
17
17
|
|
|
18
18
|
interface DashboardLayoutProps<T> {
|
|
19
|
+
additionalMemoProps?: Array<unknown>;
|
|
19
20
|
changeLayout?: (newLayout: Array<Layout>) => void;
|
|
20
21
|
children: Array<JSX.Element>;
|
|
21
22
|
displayGrid?: boolean;
|
|
@@ -28,7 +29,8 @@ const DashboardLayout = <T extends Layout>({
|
|
|
28
29
|
changeLayout,
|
|
29
30
|
displayGrid,
|
|
30
31
|
layout,
|
|
31
|
-
isStatic = false
|
|
32
|
+
isStatic = false,
|
|
33
|
+
additionalMemoProps = []
|
|
32
34
|
}: DashboardLayoutProps<T>): JSX.Element => {
|
|
33
35
|
const { classes } = useDashboardLayoutStyles(isStatic);
|
|
34
36
|
|
|
@@ -72,7 +74,7 @@ const DashboardLayout = <T extends Layout>({
|
|
|
72
74
|
</ParentSize>
|
|
73
75
|
</ResponsiveHeight>
|
|
74
76
|
),
|
|
75
|
-
memoProps: [columns, layout, displayGrid, isStatic]
|
|
77
|
+
memoProps: [columns, layout, displayGrid, isStatic, ...additionalMemoProps]
|
|
76
78
|
});
|
|
77
79
|
};
|
|
78
80
|
|
|
@@ -24,6 +24,7 @@ interface CustomTimePeriod {
|
|
|
24
24
|
interface UseMetricsQueryProps {
|
|
25
25
|
baseEndpoint: string;
|
|
26
26
|
bypassMetricsExclusion?: boolean;
|
|
27
|
+
bypassQueryParams?: boolean;
|
|
27
28
|
includeAllResources?: boolean;
|
|
28
29
|
metrics: Array<Metric>;
|
|
29
30
|
refreshCount?: number;
|
|
@@ -90,7 +91,8 @@ const useGraphQuery = ({
|
|
|
90
91
|
timePeriodType: 1
|
|
91
92
|
},
|
|
92
93
|
refreshInterval = false,
|
|
93
|
-
refreshCount
|
|
94
|
+
refreshCount,
|
|
95
|
+
bypassQueryParams = false
|
|
94
96
|
}: UseMetricsQueryProps): UseMetricsQueryState => {
|
|
95
97
|
const timePeriodToUse = equals(timePeriod?.timePeriodType, -1)
|
|
96
98
|
? {
|
|
@@ -114,6 +116,10 @@ const useGraphQuery = ({
|
|
|
114
116
|
isLoading
|
|
115
117
|
} = useFetchQuery<PerformanceGraphData>({
|
|
116
118
|
getEndpoint: () => {
|
|
119
|
+
if (bypassQueryParams) {
|
|
120
|
+
return baseEndpoint;
|
|
121
|
+
}
|
|
122
|
+
|
|
117
123
|
const endpoint = buildListingEndpoint({
|
|
118
124
|
baseEndpoint,
|
|
119
125
|
parameters: {
|
|
@@ -15,11 +15,11 @@ import {
|
|
|
15
15
|
|
|
16
16
|
const initialize = ({
|
|
17
17
|
initialValues = simpleAccessRights,
|
|
18
|
-
loading = false
|
|
19
|
-
link = 'link'
|
|
18
|
+
loading = false
|
|
20
19
|
}): unknown => {
|
|
21
20
|
const cancel = cy.stub();
|
|
22
21
|
const save = cy.stub();
|
|
22
|
+
const change = cy.stub();
|
|
23
23
|
|
|
24
24
|
cy.interceptAPIRequest({
|
|
25
25
|
alias: 'getContacts',
|
|
@@ -47,10 +47,10 @@ const initialize = ({
|
|
|
47
47
|
}}
|
|
48
48
|
initialValues={initialValues}
|
|
49
49
|
labels={labels}
|
|
50
|
-
link={link}
|
|
51
50
|
loading={loading}
|
|
52
51
|
roles={roles}
|
|
53
52
|
submit={save}
|
|
53
|
+
onChange={change}
|
|
54
54
|
/>
|
|
55
55
|
</Provider>
|
|
56
56
|
</TestQueryProvider>
|
|
@@ -60,6 +60,7 @@ const initialize = ({
|
|
|
60
60
|
|
|
61
61
|
return {
|
|
62
62
|
cancel,
|
|
63
|
+
change,
|
|
63
64
|
save
|
|
64
65
|
};
|
|
65
66
|
};
|
|
@@ -74,21 +75,12 @@ describe('Access rights', () => {
|
|
|
74
75
|
cy.findByLabelText('Add a contact').should('be.visible');
|
|
75
76
|
cy.findByTestId('add_role').should('be.disabled');
|
|
76
77
|
cy.findByTestId('add').should('be.disabled');
|
|
77
|
-
cy.findByLabelText('Copy link').should('be.visible');
|
|
78
78
|
cy.findByLabelText('Cancel').should('be.visible');
|
|
79
79
|
cy.findByLabelText('Save').should('be.visible');
|
|
80
80
|
|
|
81
81
|
cy.makeSnapshot();
|
|
82
82
|
});
|
|
83
83
|
|
|
84
|
-
it('displays the access rights without link', () => {
|
|
85
|
-
initialize({ link: null });
|
|
86
|
-
|
|
87
|
-
cy.findByLabelText('Copy link').should('not.exist');
|
|
88
|
-
|
|
89
|
-
cy.makeSnapshot();
|
|
90
|
-
});
|
|
91
|
-
|
|
92
84
|
it('displays the access rights with an empty list', () => {
|
|
93
85
|
initialize({ initialValues: emptyAccessRights });
|
|
94
86
|
|
|
@@ -97,7 +89,7 @@ describe('Access rights', () => {
|
|
|
97
89
|
cy.makeSnapshot();
|
|
98
90
|
});
|
|
99
91
|
|
|
100
|
-
it('displays the access rights
|
|
92
|
+
it('displays the access rights list', () => {
|
|
101
93
|
initialize({});
|
|
102
94
|
|
|
103
95
|
simpleAccessRights.forEach(({ name, email, isContactGroup, role }) => {
|
|
@@ -370,4 +362,26 @@ describe('Access rights', () => {
|
|
|
370
362
|
|
|
371
363
|
cy.makeSnapshot();
|
|
372
364
|
});
|
|
365
|
+
|
|
366
|
+
it('calls the change function when the corresponding prop is set and the form is updated', () => {
|
|
367
|
+
const { change } = initialize({});
|
|
368
|
+
|
|
369
|
+
cy.contains(labels.add.contact).click();
|
|
370
|
+
cy.findByLabelText(labels.add.autocompleteContact).click();
|
|
371
|
+
|
|
372
|
+
cy.waitForRequest('@getContacts');
|
|
373
|
+
|
|
374
|
+
cy.contains('Entity 10').click();
|
|
375
|
+
|
|
376
|
+
cy.findByTestId('add').click();
|
|
377
|
+
|
|
378
|
+
cy.contains('Entity 10').should('be.visible');
|
|
379
|
+
|
|
380
|
+
cy.findByTestId('role-Entity 10').should('have.value', 'viewer');
|
|
381
|
+
cy.contains(labels.list.added)
|
|
382
|
+
.should('be.visible')
|
|
383
|
+
.then(() => {
|
|
384
|
+
expect(change).to.have.callCount(2);
|
|
385
|
+
});
|
|
386
|
+
});
|
|
373
387
|
});
|
|
@@ -47,7 +47,6 @@ export const Default: Story = {
|
|
|
47
47
|
},
|
|
48
48
|
initialValues: defaultAccessRights,
|
|
49
49
|
labels,
|
|
50
|
-
link: 'link',
|
|
51
50
|
roles,
|
|
52
51
|
submit: () => undefined
|
|
53
52
|
},
|
|
@@ -63,7 +62,6 @@ export const AccessRightsWithStates: Story = {
|
|
|
63
62
|
},
|
|
64
63
|
initialValues: accessRightsWithStates,
|
|
65
64
|
labels,
|
|
66
|
-
link: 'link',
|
|
67
65
|
roles,
|
|
68
66
|
submit: () => undefined
|
|
69
67
|
},
|
|
@@ -79,22 +77,6 @@ export const withEmptyState: Story = {
|
|
|
79
77
|
},
|
|
80
78
|
initialValues: emptyAccessRights,
|
|
81
79
|
labels,
|
|
82
|
-
link: 'link',
|
|
83
|
-
roles,
|
|
84
|
-
submit: () => undefined
|
|
85
|
-
},
|
|
86
|
-
render: Template
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
export const withoutLink: Story = {
|
|
90
|
-
args: {
|
|
91
|
-
cancel: () => undefined,
|
|
92
|
-
endpoints: {
|
|
93
|
-
contact: '/contact',
|
|
94
|
-
contactGroup: '/contactGroup'
|
|
95
|
-
},
|
|
96
|
-
initialValues: defaultAccessRights,
|
|
97
|
-
labels,
|
|
98
80
|
roles,
|
|
99
81
|
submit: () => undefined
|
|
100
82
|
},
|
|
@@ -110,7 +92,6 @@ export const loading: Story = {
|
|
|
110
92
|
},
|
|
111
93
|
initialValues: emptyAccessRights,
|
|
112
94
|
labels,
|
|
113
|
-
link: 'link',
|
|
114
95
|
loading: true,
|
|
115
96
|
roles,
|
|
116
97
|
submit: () => undefined
|
|
@@ -8,16 +8,17 @@ import Provider from './Provider';
|
|
|
8
8
|
import ShareInput from './ShareInput/ShareInput';
|
|
9
9
|
import Stats from './Stats/Stats';
|
|
10
10
|
import { AccessRightInitialValues, Endpoints, Labels } from './models';
|
|
11
|
+
import { useAccessRightsChange } from './useAccessRightsChange';
|
|
11
12
|
import { useAccessRightsInitValues } from './useAccessRightsInitValues';
|
|
12
13
|
|
|
13
14
|
interface Props {
|
|
14
|
-
cancel
|
|
15
|
+
cancel?: ({ dirty, values }) => void;
|
|
15
16
|
endpoints: Endpoints;
|
|
16
17
|
initialValues: Array<AccessRightInitialValues>;
|
|
17
18
|
isSubmitting?: boolean;
|
|
18
19
|
labels: Labels;
|
|
19
|
-
link?: string;
|
|
20
20
|
loading?: boolean;
|
|
21
|
+
onChange?: (values: Array<AccessRightInitialValues>) => void;
|
|
21
22
|
roles: Array<SelectEntry>;
|
|
22
23
|
submit: (values: Array<AccessRightInitialValues>) => Promise<void>;
|
|
23
24
|
}
|
|
@@ -28,13 +29,14 @@ export const AccessRights = ({
|
|
|
28
29
|
endpoints,
|
|
29
30
|
submit,
|
|
30
31
|
cancel,
|
|
31
|
-
link,
|
|
32
32
|
loading,
|
|
33
33
|
labels,
|
|
34
|
-
isSubmitting
|
|
34
|
+
isSubmitting,
|
|
35
|
+
onChange
|
|
35
36
|
}: Props): JSX.Element => {
|
|
36
37
|
const { classes } = useAccessRightsStyles();
|
|
37
38
|
const clear = useAccessRightsInitValues({ initialValues });
|
|
39
|
+
useAccessRightsChange(onChange);
|
|
38
40
|
|
|
39
41
|
return (
|
|
40
42
|
<div className={classes.container}>
|
|
@@ -46,7 +48,6 @@ export const AccessRights = ({
|
|
|
46
48
|
clear={clear}
|
|
47
49
|
isSubmitting={isSubmitting}
|
|
48
50
|
labels={labels.actions}
|
|
49
|
-
link={link}
|
|
50
51
|
submit={submit}
|
|
51
52
|
/>
|
|
52
53
|
</div>
|
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import { makeStyles } from 'tss-react/mui';
|
|
2
2
|
|
|
3
3
|
export const useActionsStyles = makeStyles()((theme) => ({
|
|
4
|
-
actions: {
|
|
5
|
-
backgroundColor: theme.palette.background.paper,
|
|
6
|
-
display: 'flex',
|
|
7
|
-
justifyContent: 'space-between'
|
|
8
|
-
},
|
|
9
4
|
cancelAndSave: {
|
|
10
5
|
display: 'flex',
|
|
11
|
-
|
|
12
|
-
gap: theme.spacing(2)
|
|
6
|
+
flexDirection: 'row',
|
|
7
|
+
gap: theme.spacing(2),
|
|
8
|
+
justifyContent: 'flex-end'
|
|
13
9
|
}
|
|
14
10
|
}));
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { useTranslation } from 'react-i18next';
|
|
2
2
|
|
|
3
|
-
import LinkIcon from '@mui/icons-material/Link';
|
|
4
3
|
import { CircularProgress } from '@mui/material';
|
|
5
4
|
|
|
6
5
|
import { Button } from '../../..';
|
|
@@ -10,11 +9,10 @@ import { useActions } from './useActions';
|
|
|
10
9
|
import { useActionsStyles } from './Actions.styles';
|
|
11
10
|
|
|
12
11
|
interface Props {
|
|
13
|
-
cancel
|
|
12
|
+
cancel?: ({ dirty, values }) => void;
|
|
14
13
|
clear: () => void;
|
|
15
14
|
isSubmitting?: boolean;
|
|
16
15
|
labels: Labels['actions'];
|
|
17
|
-
link?: string;
|
|
18
16
|
submit: (values: Array<AccessRightInitialValues>) => Promise<void>;
|
|
19
17
|
}
|
|
20
18
|
|
|
@@ -22,17 +20,15 @@ const Actions = ({
|
|
|
22
20
|
labels,
|
|
23
21
|
cancel,
|
|
24
22
|
submit,
|
|
25
|
-
link,
|
|
26
23
|
isSubmitting,
|
|
27
24
|
clear
|
|
28
25
|
}: Props): JSX.Element => {
|
|
29
26
|
const { t } = useTranslation();
|
|
30
27
|
const { classes } = useActionsStyles();
|
|
31
28
|
|
|
32
|
-
const { dirty,
|
|
29
|
+
const { dirty, save, formattedValues } = useActions({
|
|
33
30
|
clear,
|
|
34
31
|
labels,
|
|
35
|
-
link,
|
|
36
32
|
submit
|
|
37
33
|
});
|
|
38
34
|
|
|
@@ -41,21 +37,8 @@ const Actions = ({
|
|
|
41
37
|
};
|
|
42
38
|
|
|
43
39
|
return (
|
|
44
|
-
<div className={classes.
|
|
45
|
-
{
|
|
46
|
-
<Button
|
|
47
|
-
aria-label={t(labels.copyLink)}
|
|
48
|
-
icon={<LinkIcon />}
|
|
49
|
-
iconVariant="start"
|
|
50
|
-
variant="ghost"
|
|
51
|
-
onClick={copyLink}
|
|
52
|
-
>
|
|
53
|
-
{t(labels.copyLink)}
|
|
54
|
-
</Button>
|
|
55
|
-
) : (
|
|
56
|
-
<div />
|
|
57
|
-
)}
|
|
58
|
-
<div className={classes.cancelAndSave}>
|
|
40
|
+
<div className={classes.cancelAndSave}>
|
|
41
|
+
{cancel && (
|
|
59
42
|
<Button
|
|
60
43
|
aria-label={t(labels.cancel)}
|
|
61
44
|
variant="secondary"
|
|
@@ -63,17 +46,17 @@ const Actions = ({
|
|
|
63
46
|
>
|
|
64
47
|
{t(labels.cancel)}
|
|
65
48
|
</Button>
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
</
|
|
49
|
+
)}
|
|
50
|
+
<Button
|
|
51
|
+
aria-label={t(labels.save)}
|
|
52
|
+
disabled={isSubmitting || !dirty}
|
|
53
|
+
icon={isSubmitting ? <CircularProgress size={24} /> : null}
|
|
54
|
+
iconVariant={isSubmitting ? 'start' : 'none'}
|
|
55
|
+
variant="primary"
|
|
56
|
+
onClick={save}
|
|
57
|
+
>
|
|
58
|
+
{t(labels.save)}
|
|
59
|
+
</Button>
|
|
77
60
|
</div>
|
|
78
61
|
);
|
|
79
62
|
};
|
|
@@ -1,64 +1,32 @@
|
|
|
1
1
|
import { useAtomValue } from 'jotai';
|
|
2
|
-
import { equals
|
|
2
|
+
import { equals } from 'ramda';
|
|
3
3
|
|
|
4
4
|
import { initialValuesAtom, valuesAtom } from '../atoms';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
const formatValue = (accessRight: AccessRight): AccessRightInitialValues => {
|
|
9
|
-
return omit(['isAdded', 'isUpdated', 'isRemoved'], accessRight);
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
const formatValueForSubmition = (
|
|
13
|
-
accessRight: AccessRight
|
|
14
|
-
): AccessRightInitialValues => {
|
|
15
|
-
return {
|
|
16
|
-
...formatValue(accessRight),
|
|
17
|
-
id: Number((accessRight.id as string).split('_')[1])
|
|
18
|
-
};
|
|
19
|
-
};
|
|
5
|
+
import { AccessRightInitialValues, Labels } from '../models';
|
|
6
|
+
import { formatValue, formatValueForSubmition } from '../utils';
|
|
20
7
|
|
|
21
8
|
interface Props {
|
|
22
9
|
clear: () => void;
|
|
23
10
|
labels: Labels['actions'];
|
|
24
|
-
link?: string;
|
|
25
11
|
submit: (values: Array<AccessRightInitialValues>) => Promise<void>;
|
|
26
12
|
}
|
|
27
13
|
|
|
28
14
|
interface UseActionsState {
|
|
29
|
-
copyLink: () => void;
|
|
30
15
|
dirty: boolean;
|
|
31
16
|
formattedValues: Array<AccessRightInitialValues>;
|
|
32
17
|
save: () => void;
|
|
33
18
|
}
|
|
34
19
|
|
|
35
|
-
export const useActions = ({
|
|
36
|
-
link,
|
|
37
|
-
labels,
|
|
38
|
-
submit,
|
|
39
|
-
clear
|
|
40
|
-
}: Props): UseActionsState => {
|
|
20
|
+
export const useActions = ({ submit, clear }: Props): UseActionsState => {
|
|
41
21
|
const values = useAtomValue(valuesAtom);
|
|
42
22
|
const initialValues = useAtomValue(initialValuesAtom);
|
|
43
23
|
|
|
44
|
-
const { copy } = useCopyToClipboard({
|
|
45
|
-
errorMessage: labels.copyError,
|
|
46
|
-
successMessage: labels.copySuccess
|
|
47
|
-
});
|
|
48
|
-
|
|
49
24
|
const formattedValues = values
|
|
50
25
|
.filter(({ isRemoved }) => !isRemoved)
|
|
51
26
|
.map(formatValue);
|
|
52
27
|
|
|
53
28
|
const dirty = !equals(initialValues, formattedValues);
|
|
54
29
|
|
|
55
|
-
const copyLink = (): void => {
|
|
56
|
-
if (!link) {
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
copy(link);
|
|
60
|
-
};
|
|
61
|
-
|
|
62
30
|
const save = (): void => {
|
|
63
31
|
submit(
|
|
64
32
|
values.filter(({ isRemoved }) => !isRemoved).map(formatValueForSubmition)
|
|
@@ -71,7 +39,6 @@ export const useActions = ({
|
|
|
71
39
|
};
|
|
72
40
|
|
|
73
41
|
return {
|
|
74
|
-
copyLink,
|
|
75
42
|
dirty,
|
|
76
43
|
formattedValues,
|
|
77
44
|
save
|
|
@@ -105,9 +105,6 @@ export const buildResult = (isGroup): Listing<SelectEntry> => ({
|
|
|
105
105
|
export const labels: Labels = {
|
|
106
106
|
actions: {
|
|
107
107
|
cancel: 'Cancel',
|
|
108
|
-
copyError: 'Failed to copy',
|
|
109
|
-
copyLink: 'Copy link',
|
|
110
|
-
copySuccess: 'Copied',
|
|
111
108
|
save: 'Save'
|
|
112
109
|
},
|
|
113
110
|
add: {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
import { useAtomValue } from 'jotai';
|
|
4
|
+
|
|
5
|
+
import { useDeepCompare } from '../../../utils';
|
|
6
|
+
|
|
7
|
+
import { valuesAtom } from './atoms';
|
|
8
|
+
import { formatValueForSubmition } from './utils';
|
|
9
|
+
import { AccessRightInitialValues } from './models';
|
|
10
|
+
|
|
11
|
+
export const useAccessRightsChange = (
|
|
12
|
+
onChange?: (values: Array<AccessRightInitialValues>) => void
|
|
13
|
+
): void => {
|
|
14
|
+
const values = useAtomValue(valuesAtom);
|
|
15
|
+
|
|
16
|
+
useEffect(
|
|
17
|
+
() => {
|
|
18
|
+
if (!onChange) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
onChange(
|
|
23
|
+
values
|
|
24
|
+
.filter(({ isRemoved }) => !isRemoved)
|
|
25
|
+
.map(formatValueForSubmition)
|
|
26
|
+
);
|
|
27
|
+
},
|
|
28
|
+
useDeepCompare([values])
|
|
29
|
+
);
|
|
30
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { omit } from 'ramda';
|
|
2
|
+
|
|
3
|
+
import { AccessRight, AccessRightInitialValues } from './models';
|
|
4
|
+
|
|
5
|
+
export const formatValue = (
|
|
6
|
+
accessRight: AccessRight
|
|
7
|
+
): AccessRightInitialValues => {
|
|
8
|
+
return omit(['isAdded', 'isUpdated', 'isRemoved'], accessRight);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const formatValueForSubmition = (
|
|
12
|
+
accessRight: AccessRight
|
|
13
|
+
): AccessRightInitialValues => {
|
|
14
|
+
return {
|
|
15
|
+
...formatValue(accessRight),
|
|
16
|
+
id: Number((accessRight.id as string).split('_')[1])
|
|
17
|
+
};
|
|
18
|
+
};
|
|
@@ -18,10 +18,10 @@ export const useStyles = makeStyles()((theme) => ({
|
|
|
18
18
|
display: 'flex',
|
|
19
19
|
flexGrow: 1,
|
|
20
20
|
gap: theme.spacing(2),
|
|
21
|
-
maxWidth: '520px',
|
|
22
21
|
overflow: 'hidden',
|
|
23
22
|
paddingBottom: theme.spacing(1),
|
|
24
|
-
paddingTop: theme.spacing(1)
|
|
23
|
+
paddingTop: theme.spacing(1),
|
|
24
|
+
width: '100%'
|
|
25
25
|
},
|
|
26
26
|
secondary: {
|
|
27
27
|
alignItems: 'center',
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { makeStyles } from 'tss-react/mui';
|
|
2
|
+
|
|
3
|
+
export const useTabsStyles = makeStyles()((theme) => ({
|
|
4
|
+
indicator: {
|
|
5
|
+
bottom: 'unset'
|
|
6
|
+
},
|
|
7
|
+
tab: {
|
|
8
|
+
'&[aria-selected="true"]': {
|
|
9
|
+
color: theme.palette.text.primary,
|
|
10
|
+
fontWeight: theme.typography.fontWeightBold
|
|
11
|
+
},
|
|
12
|
+
color: theme.palette.text.primary,
|
|
13
|
+
fontWeight: theme.typography.fontWeightRegular,
|
|
14
|
+
marginRight: theme.spacing(2),
|
|
15
|
+
minHeight: 0,
|
|
16
|
+
minWidth: 0,
|
|
17
|
+
padding: theme.spacing(0.5, 0)
|
|
18
|
+
},
|
|
19
|
+
tabPanel: {
|
|
20
|
+
padding: theme.spacing(1, 0, 0)
|
|
21
|
+
},
|
|
22
|
+
tabs: {
|
|
23
|
+
minHeight: theme.spacing(4.5)
|
|
24
|
+
}
|
|
25
|
+
}));
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { TabPanel as MuiTabPanel } from '@mui/lab';
|
|
2
|
+
|
|
3
|
+
import { useTabsStyles } from './Tab.styles';
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
6
|
+
children: JSX.Element;
|
|
7
|
+
value: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const TabPanel = ({ children, value }: Props): JSX.Element => {
|
|
11
|
+
const { classes } = useTabsStyles();
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<MuiTabPanel
|
|
15
|
+
className={classes.tabPanel}
|
|
16
|
+
data-tabPanel={value}
|
|
17
|
+
value={value}
|
|
18
|
+
>
|
|
19
|
+
{children}
|
|
20
|
+
</MuiTabPanel>
|
|
21
|
+
);
|
|
22
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Typography } from '@mui/material';
|
|
2
|
+
|
|
3
|
+
import { TabPanel } from './TabPanel';
|
|
4
|
+
|
|
5
|
+
import { Tabs } from '.';
|
|
6
|
+
|
|
7
|
+
const initialize = (withTabListProps = false): void => {
|
|
8
|
+
cy.mount({
|
|
9
|
+
Component: (
|
|
10
|
+
<Tabs
|
|
11
|
+
defaultTab="tab 0"
|
|
12
|
+
tabList={
|
|
13
|
+
withTabListProps
|
|
14
|
+
? {
|
|
15
|
+
variant: 'fullWidth'
|
|
16
|
+
}
|
|
17
|
+
: undefined
|
|
18
|
+
}
|
|
19
|
+
tabs={[
|
|
20
|
+
{ label: 'Tab 0', value: 'tab 0' },
|
|
21
|
+
{ label: 'Tab 1', value: 'tab 1' }
|
|
22
|
+
]}
|
|
23
|
+
>
|
|
24
|
+
<TabPanel value="tab 0">
|
|
25
|
+
<Typography>Tab 0</Typography>
|
|
26
|
+
</TabPanel>
|
|
27
|
+
<TabPanel value="tab 1">
|
|
28
|
+
<Typography>Tab 1</Typography>
|
|
29
|
+
</TabPanel>
|
|
30
|
+
</Tabs>
|
|
31
|
+
)
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
describe('Tabs', () => {
|
|
36
|
+
it('displays tabs and their content when a tab is selected', () => {
|
|
37
|
+
initialize();
|
|
38
|
+
|
|
39
|
+
cy.get('[data-TabPanel="tab 0"]').should('not.have.attr', 'hidden');
|
|
40
|
+
cy.get('[data-TabPanel="tab 1"]').should('have.attr', 'hidden');
|
|
41
|
+
cy.findByLabelText('Tab 0')
|
|
42
|
+
.should('have.attr', 'aria-selected')
|
|
43
|
+
.and('equals', 'true');
|
|
44
|
+
cy.findByLabelText('Tab 1')
|
|
45
|
+
.should('have.attr', 'aria-selected')
|
|
46
|
+
.and('equals', 'false');
|
|
47
|
+
|
|
48
|
+
cy.contains('Tab 1').click();
|
|
49
|
+
|
|
50
|
+
cy.get('[data-TabPanel="tab 0"]').should('have.attr', 'hidden');
|
|
51
|
+
cy.get('[data-TabPanel="tab 1"]').should('not.have.attr', 'hidden');
|
|
52
|
+
cy.findByLabelText('Tab 0')
|
|
53
|
+
.should('have.attr', 'aria-selected')
|
|
54
|
+
.and('equals', 'false');
|
|
55
|
+
cy.findByLabelText('Tab 1')
|
|
56
|
+
.should('have.attr', 'aria-selected')
|
|
57
|
+
.and('equals', 'true');
|
|
58
|
+
|
|
59
|
+
cy.makeSnapshot();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('displays tabs when tabList props are set', () => {
|
|
63
|
+
initialize(true);
|
|
64
|
+
|
|
65
|
+
cy.get('[data-TabPanel="tab 0"]').should('not.have.attr', 'hidden');
|
|
66
|
+
cy.get('[data-TabPanel="tab 1"]').should('have.attr', 'hidden');
|
|
67
|
+
|
|
68
|
+
cy.makeSnapshot();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
|
|
3
|
+
import { Typography } from '@mui/material';
|
|
4
|
+
|
|
5
|
+
import { TabPanel } from './TabPanel';
|
|
6
|
+
|
|
7
|
+
import { Tabs } from '.';
|
|
8
|
+
|
|
9
|
+
const meta: Meta<typeof Tabs> = {
|
|
10
|
+
component: Tabs
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default meta;
|
|
14
|
+
type Story = StoryObj<typeof Tabs>;
|
|
15
|
+
|
|
16
|
+
const generateTabs = (size: number): Array<{ label: string; value: string }> =>
|
|
17
|
+
Array(size)
|
|
18
|
+
.fill(0)
|
|
19
|
+
.map((_, index) => ({ label: `Tab ${index}`, value: `tab ${index}` }));
|
|
20
|
+
|
|
21
|
+
export const Default: Story = {
|
|
22
|
+
args: {
|
|
23
|
+
defaultTab: 'tab 0',
|
|
24
|
+
tabs: generateTabs(2)
|
|
25
|
+
},
|
|
26
|
+
render: (args) => (
|
|
27
|
+
<Tabs {...args}>
|
|
28
|
+
{generateTabs(2).map(({ value, label }) => (
|
|
29
|
+
<TabPanel key={value} value={value}>
|
|
30
|
+
<Typography>{label}</Typography>
|
|
31
|
+
</TabPanel>
|
|
32
|
+
))}
|
|
33
|
+
</Tabs>
|
|
34
|
+
)
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const WithTabListProps: Story = {
|
|
38
|
+
args: {
|
|
39
|
+
defaultTab: 'tab 0',
|
|
40
|
+
tabList: {
|
|
41
|
+
textColor: 'inherit',
|
|
42
|
+
variant: 'fullWidth'
|
|
43
|
+
},
|
|
44
|
+
tabs: generateTabs(2)
|
|
45
|
+
},
|
|
46
|
+
render: (args) => (
|
|
47
|
+
<Tabs {...args}>
|
|
48
|
+
{generateTabs(2).map(({ value, label }) => (
|
|
49
|
+
<TabPanel key={value} value={value}>
|
|
50
|
+
<Typography>{label}</Typography>
|
|
51
|
+
</TabPanel>
|
|
52
|
+
))}
|
|
53
|
+
</Tabs>
|
|
54
|
+
)
|
|
55
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { TabContext, TabList, TabListProps } from '@mui/lab';
|
|
4
|
+
import { Tab } from '@mui/material';
|
|
5
|
+
|
|
6
|
+
import { useTabsStyles } from './Tab.styles';
|
|
7
|
+
|
|
8
|
+
type Props = {
|
|
9
|
+
children: Array<JSX.Element>;
|
|
10
|
+
defaultTab: string;
|
|
11
|
+
tabList?: TabListProps;
|
|
12
|
+
tabs: Array<{
|
|
13
|
+
label: string;
|
|
14
|
+
value: string;
|
|
15
|
+
}>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const Tabs = ({
|
|
19
|
+
children,
|
|
20
|
+
defaultTab,
|
|
21
|
+
tabs,
|
|
22
|
+
tabList
|
|
23
|
+
}: Props): JSX.Element => {
|
|
24
|
+
const { classes } = useTabsStyles();
|
|
25
|
+
|
|
26
|
+
const [selectedTab, setSelectedTab] = useState(defaultTab);
|
|
27
|
+
|
|
28
|
+
const changeTab = useCallback((_, newValue: string): void => {
|
|
29
|
+
setSelectedTab(newValue);
|
|
30
|
+
}, []);
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<TabContext value={selectedTab}>
|
|
34
|
+
<TabList
|
|
35
|
+
classes={{
|
|
36
|
+
indicator: classes.indicator,
|
|
37
|
+
root: classes.tabs
|
|
38
|
+
}}
|
|
39
|
+
onChange={changeTab}
|
|
40
|
+
{...tabList}
|
|
41
|
+
>
|
|
42
|
+
{tabs.map(({ value, label }) => (
|
|
43
|
+
<Tab
|
|
44
|
+
aria-label={label}
|
|
45
|
+
className={classes.tab}
|
|
46
|
+
key={value}
|
|
47
|
+
label={label}
|
|
48
|
+
value={value}
|
|
49
|
+
/>
|
|
50
|
+
))}
|
|
51
|
+
</TabList>
|
|
52
|
+
{children}
|
|
53
|
+
</TabContext>
|
|
54
|
+
);
|
|
55
|
+
};
|
package/src/components/index.ts
CHANGED