@equinor/fusion-framework-dev-portal 9.0.0 → 10.0.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.
- package/CHANGELOG.md +15 -0
- package/dist/main.js +10331 -9330
- package/package.json +26 -22
- package/src/PersonSideSheet/index.tsx +3 -1
- package/src/PersonSideSheet/sheets/LandingSheetContent.tsx +8 -2
- package/src/PersonSideSheet/sheets/index.ts +1 -0
- package/src/PersonSideSheet/sheets/roles/ClaimableRole.test.tsx +83 -0
- package/src/PersonSideSheet/sheets/roles/ClaimableRole.tsx +229 -0
- package/src/PersonSideSheet/sheets/roles/RolesApi.test.ts +62 -0
- package/src/PersonSideSheet/sheets/roles/RolesApi.ts +112 -0
- package/src/PersonSideSheet/sheets/roles/RolesSheetContent.test.tsx +86 -0
- package/src/PersonSideSheet/sheets/roles/RolesSheetContent.tsx +207 -0
- package/src/PersonSideSheet/sheets/roles/index.ts +1 -0
- package/src/version.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/vitest.config.ts +12 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { RolesSheetContent } from './RolesSheetContent';
|
|
5
|
+
|
|
6
|
+
const mocks = vi.hoisted(() => ({
|
|
7
|
+
createClient: vi.fn(),
|
|
8
|
+
currentUser: { localAccountId: 'account-id' },
|
|
9
|
+
framework: {
|
|
10
|
+
modules: { serviceDiscovery: { createClient: vi.fn() } },
|
|
11
|
+
},
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
mocks.framework.modules.serviceDiscovery.createClient = mocks.createClient;
|
|
15
|
+
|
|
16
|
+
vi.mock('@equinor/fusion-framework-react', () => ({
|
|
17
|
+
useFramework: () => mocks.framework,
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
vi.mock('@equinor/fusion-framework-react/hooks', () => ({
|
|
21
|
+
useCurrentUser: () => mocks.currentUser,
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
vi.mock('./ClaimableRole', () => ({
|
|
25
|
+
ClaimableRole: () => <div>Claimable role</div>,
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
describe('RolesSheetContent', () => {
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
mocks.createClient.mockReset();
|
|
31
|
+
mocks.currentUser.localAccountId = 'account-id';
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
cleanup();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('renders loading and empty states', async () => {
|
|
39
|
+
const json = vi.fn().mockResolvedValue([]);
|
|
40
|
+
mocks.createClient.mockResolvedValue({ json });
|
|
41
|
+
|
|
42
|
+
render(<RolesSheetContent navigate={vi.fn()} />);
|
|
43
|
+
|
|
44
|
+
expect(screen.getByLabelText('Loading roles')).toBeTruthy();
|
|
45
|
+
expect(await screen.findByText('You have no available roles')).toBeTruthy();
|
|
46
|
+
expect(json).toHaveBeenCalledTimes(2);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('retries role retrieval after a failure', async () => {
|
|
50
|
+
const json = vi
|
|
51
|
+
.fn()
|
|
52
|
+
.mockRejectedValueOnce(new Error('Roles service unavailable'))
|
|
53
|
+
.mockResolvedValue([]);
|
|
54
|
+
mocks.createClient.mockResolvedValue({ json });
|
|
55
|
+
|
|
56
|
+
render(<RolesSheetContent navigate={vi.fn()} />);
|
|
57
|
+
|
|
58
|
+
expect(await screen.findByText('Roles service unavailable')).toBeTruthy();
|
|
59
|
+
fireEvent.click(screen.getByRole('button', { name: 'Retry' }));
|
|
60
|
+
|
|
61
|
+
expect(screen.getByLabelText('Loading roles')).toBeTruthy();
|
|
62
|
+
await waitFor(() => expect(mocks.createClient).toHaveBeenCalledTimes(2));
|
|
63
|
+
expect(await screen.findByText('You have no available roles')).toBeTruthy();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('hides stale assignments while loading roles for a changed account', async () => {
|
|
67
|
+
const initialJson = vi
|
|
68
|
+
.fn()
|
|
69
|
+
.mockResolvedValueOnce([{ id: 'old-assignment' }])
|
|
70
|
+
.mockResolvedValueOnce([]);
|
|
71
|
+
const pendingJson = vi.fn().mockReturnValue(new Promise(() => undefined));
|
|
72
|
+
mocks.createClient
|
|
73
|
+
.mockResolvedValueOnce({ json: initialJson })
|
|
74
|
+
.mockResolvedValueOnce({ json: pendingJson });
|
|
75
|
+
|
|
76
|
+
const { rerender } = render(<RolesSheetContent navigate={vi.fn()} />);
|
|
77
|
+
|
|
78
|
+
expect(await screen.findByText('Claimable role')).toBeTruthy();
|
|
79
|
+
|
|
80
|
+
mocks.currentUser.localAccountId = 'next-account-id';
|
|
81
|
+
rerender(<RolesSheetContent navigate={vi.fn()} />);
|
|
82
|
+
|
|
83
|
+
expect(screen.getByLabelText('Loading roles')).toBeTruthy();
|
|
84
|
+
expect(screen.queryByText('Claimable role')).toBeNull();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Banner,
|
|
3
|
+
Button,
|
|
4
|
+
CircularProgress,
|
|
5
|
+
Divider,
|
|
6
|
+
Icon,
|
|
7
|
+
Tabs,
|
|
8
|
+
Typography,
|
|
9
|
+
} from '@equinor/eds-core-react';
|
|
10
|
+
import { arrow_back, verified_user } from '@equinor/eds-icons';
|
|
11
|
+
import { useFramework } from '@equinor/fusion-framework-react';
|
|
12
|
+
import { useCurrentUser } from '@equinor/fusion-framework-react/hooks';
|
|
13
|
+
import { type ReactElement, useEffect, useRef, useState } from 'react';
|
|
14
|
+
import styled from 'styled-components';
|
|
15
|
+
|
|
16
|
+
import type { SheetContentProps } from '../types';
|
|
17
|
+
import { ClaimableRole } from './ClaimableRole';
|
|
18
|
+
import { RolesApi, type ClaimableRoleAssignment, type PermanentRoleAssignment } from './RolesApi';
|
|
19
|
+
|
|
20
|
+
Icon.add({ arrow_back, verified_user });
|
|
21
|
+
|
|
22
|
+
const Styled = {
|
|
23
|
+
Content: styled.div`
|
|
24
|
+
display: flex;
|
|
25
|
+
flex-direction: column;
|
|
26
|
+
gap: 1rem;
|
|
27
|
+
padding: 1rem 0.5rem;
|
|
28
|
+
`,
|
|
29
|
+
Role: styled.div`
|
|
30
|
+
display: flex;
|
|
31
|
+
gap: 1rem;
|
|
32
|
+
align-items: center;
|
|
33
|
+
padding: 0.5rem;
|
|
34
|
+
`,
|
|
35
|
+
Indicator: styled.div<{ $active: boolean }>`
|
|
36
|
+
width: 0.25rem;
|
|
37
|
+
height: 2.5rem;
|
|
38
|
+
background: ${({ $active }) => ($active ? '#007079' : '#dcdcdc')};
|
|
39
|
+
`,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Displays the signed-in user's consolidated claimable and permanent Fusion roles.
|
|
44
|
+
*
|
|
45
|
+
* The role collections use the same Roles V2 endpoints as the production portal.
|
|
46
|
+
*
|
|
47
|
+
* @param props.navigate - Navigates back to the person side sheet landing page.
|
|
48
|
+
* @returns A tabbed role overview with loading, error, and empty states.
|
|
49
|
+
*/
|
|
50
|
+
export const RolesSheetContent = ({ navigate }: SheetContentProps): ReactElement => {
|
|
51
|
+
const framework = useFramework();
|
|
52
|
+
const user = useCurrentUser();
|
|
53
|
+
const [tab, setTab] = useState(0);
|
|
54
|
+
const [claimableRoles, setClaimableRoles] = useState<ClaimableRoleAssignment[]>([]);
|
|
55
|
+
const [permanentRoles, setPermanentRoles] = useState<PermanentRoleAssignment[]>([]);
|
|
56
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
57
|
+
const [error, setError] = useState<string>();
|
|
58
|
+
const [loadAttempt, setLoadAttempt] = useState(0);
|
|
59
|
+
const latestLoadAttempt = useRef(loadAttempt);
|
|
60
|
+
latestLoadAttempt.current = loadAttempt;
|
|
61
|
+
|
|
62
|
+
/** Starts a fresh role request after a retrieval failure. */
|
|
63
|
+
const handleRetry = (): void => {
|
|
64
|
+
setError(undefined);
|
|
65
|
+
setIsLoading(true);
|
|
66
|
+
setLoadAttempt((attempt) => attempt + 1);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Replaces a changed assignment after activation or deactivation succeeds.
|
|
71
|
+
* @param changedAssignment - Updated claimable assignment returned by the role row.
|
|
72
|
+
*/
|
|
73
|
+
const handleClaimableRoleChange = (changedAssignment: ClaimableRoleAssignment): void => {
|
|
74
|
+
setClaimableRoles((currentRoles) => {
|
|
75
|
+
// Preserve the endpoint ordering while updating only the role that changed.
|
|
76
|
+
return currentRoles.map((assignment) =>
|
|
77
|
+
assignment.id === changedAssignment.id ? changedAssignment : assignment,
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
let isActive = true;
|
|
84
|
+
const currentLoadAttempt = loadAttempt;
|
|
85
|
+
|
|
86
|
+
// Hide assignments from the previous account before resolving the next role snapshot.
|
|
87
|
+
setIsLoading(true);
|
|
88
|
+
setError(undefined);
|
|
89
|
+
|
|
90
|
+
/** Loads both role collections together so the tabs represent one consistent snapshot. */
|
|
91
|
+
const loadRoles = async (): Promise<void> => {
|
|
92
|
+
// A Roles V2 account identifier is required before either endpoint can be queried.
|
|
93
|
+
if (!user?.localAccountId) {
|
|
94
|
+
setError('Unable to resolve the signed-in Fusion account.');
|
|
95
|
+
setIsLoading(false);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const client = await framework.modules.serviceDiscovery.createClient('rolesv2');
|
|
101
|
+
const rolesApi = new RolesApi(client, user.localAccountId);
|
|
102
|
+
const [claimable, permanent] = await Promise.all([
|
|
103
|
+
rolesApi.getClaimableRoles(),
|
|
104
|
+
rolesApi.getPermanentRoles(),
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
// Ignore a completed request after the side sheet content has unmounted.
|
|
108
|
+
if (isActive && latestLoadAttempt.current === currentLoadAttempt) {
|
|
109
|
+
setClaimableRoles(claimable);
|
|
110
|
+
setPermanentRoles(permanent);
|
|
111
|
+
setError(undefined);
|
|
112
|
+
}
|
|
113
|
+
} catch (cause) {
|
|
114
|
+
// Keep transport details out of the side sheet while preserving a useful retry direction.
|
|
115
|
+
if (isActive && latestLoadAttempt.current === currentLoadAttempt) {
|
|
116
|
+
setError(cause instanceof Error ? cause.message : 'Failed to load roles.');
|
|
117
|
+
}
|
|
118
|
+
} finally {
|
|
119
|
+
// Avoid updating state when navigation unmounts this sheet during a request.
|
|
120
|
+
if (isActive && latestLoadAttempt.current === currentLoadAttempt) {
|
|
121
|
+
setIsLoading(false);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
void loadRoles();
|
|
127
|
+
|
|
128
|
+
return () => {
|
|
129
|
+
isActive = false;
|
|
130
|
+
};
|
|
131
|
+
}, [framework, user?.localAccountId, loadAttempt]);
|
|
132
|
+
|
|
133
|
+
// Prepare role rows before markup so the tab panels only render presentation state.
|
|
134
|
+
const claimableItems = claimableRoles.map((assignment) => (
|
|
135
|
+
<ClaimableRole
|
|
136
|
+
key={assignment.id}
|
|
137
|
+
assignment={assignment}
|
|
138
|
+
onChange={handleClaimableRoleChange}
|
|
139
|
+
/>
|
|
140
|
+
));
|
|
141
|
+
|
|
142
|
+
// Permanent assignments include scope context but do not expose activation controls.
|
|
143
|
+
const permanentItems = permanentRoles.map((assignment) => {
|
|
144
|
+
const scope =
|
|
145
|
+
!assignment.scope || assignment.scope.isGlobal ? 'Global' : assignment.scope.value;
|
|
146
|
+
return (
|
|
147
|
+
<Styled.Role key={assignment.id}>
|
|
148
|
+
<Styled.Indicator $active={true} />
|
|
149
|
+
<div>
|
|
150
|
+
<Typography>{assignment.role.displayName}</Typography>
|
|
151
|
+
<Typography variant="overline">
|
|
152
|
+
{assignment.role.name}
|
|
153
|
+
{scope ? ` (${scope})` : ''}
|
|
154
|
+
</Typography>
|
|
155
|
+
</div>
|
|
156
|
+
</Styled.Role>
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<section>
|
|
162
|
+
<Button variant="ghost" onClick={() => navigate()}>
|
|
163
|
+
<Icon name="arrow_back" />
|
|
164
|
+
<Icon name="verified_user" />
|
|
165
|
+
My Roles
|
|
166
|
+
</Button>
|
|
167
|
+
<Divider />
|
|
168
|
+
<Styled.Content>
|
|
169
|
+
{isLoading ? (
|
|
170
|
+
<CircularProgress aria-label="Loading roles" />
|
|
171
|
+
) : error ? (
|
|
172
|
+
<>
|
|
173
|
+
<Banner>
|
|
174
|
+
<Banner.Message>{error}</Banner.Message>
|
|
175
|
+
</Banner>
|
|
176
|
+
<Button variant="outlined" onClick={handleRetry}>
|
|
177
|
+
Retry
|
|
178
|
+
</Button>
|
|
179
|
+
</>
|
|
180
|
+
) : (
|
|
181
|
+
<Tabs activeTab={tab} onChange={(index) => setTab(Number(index))}>
|
|
182
|
+
<Tabs.List>
|
|
183
|
+
<Tabs.Tab>Claimable</Tabs.Tab>
|
|
184
|
+
<Tabs.Tab>Permanent</Tabs.Tab>
|
|
185
|
+
</Tabs.List>
|
|
186
|
+
<Tabs.Panels>
|
|
187
|
+
<Tabs.Panel>
|
|
188
|
+
{claimableItems.length > 0 ? (
|
|
189
|
+
claimableItems
|
|
190
|
+
) : (
|
|
191
|
+
<Typography>You have no available roles</Typography>
|
|
192
|
+
)}
|
|
193
|
+
</Tabs.Panel>
|
|
194
|
+
<Tabs.Panel>
|
|
195
|
+
{permanentItems.length > 0 ? (
|
|
196
|
+
permanentItems
|
|
197
|
+
) : (
|
|
198
|
+
<Typography>You have no permanent roles assigned</Typography>
|
|
199
|
+
)}
|
|
200
|
+
</Tabs.Panel>
|
|
201
|
+
</Tabs.Panels>
|
|
202
|
+
</Tabs>
|
|
203
|
+
)}
|
|
204
|
+
</Styled.Content>
|
|
205
|
+
</section>
|
|
206
|
+
);
|
|
207
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { RolesSheetContent } from './RolesSheetContent';
|
package/src/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by genversion.
|
|
2
|
-
export const version = '
|
|
2
|
+
export const version = '10.0.0';
|