@equinor/fusion-framework-dev-portal 11.0.4 → 11.0.5

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 (40) hide show
  1. package/dist/main.js +17 -17
  2. package/package.json +43 -40
  3. package/CHANGELOG.md +0 -1117
  4. package/dev-server.ts +0 -109
  5. package/src/AppLoader.tsx +0 -135
  6. package/src/BookmarkSideSheet.tsx +0 -47
  7. package/src/ContextSelector/ContextSelector.tsx +0 -85
  8. package/src/ContextSelector/index.ts +0 -1
  9. package/src/ContextSelector/useContextResolver.ts +0 -337
  10. package/src/EquinorLoader.tsx +0 -33
  11. package/src/ErrorViewer.tsx +0 -26
  12. package/src/FusionLogo.tsx +0 -76
  13. package/src/Header.tsx +0 -94
  14. package/src/HeaderActions.tsx +0 -47
  15. package/src/PersonSideSheet/index.tsx +0 -65
  16. package/src/PersonSideSheet/sheets/FeatureSheetContent.tsx +0 -52
  17. package/src/PersonSideSheet/sheets/FeatureTogglerApp.tsx +0 -39
  18. package/src/PersonSideSheet/sheets/FeatureTogglerPortal.tsx +0 -33
  19. package/src/PersonSideSheet/sheets/LandingSheetContent.tsx +0 -64
  20. package/src/PersonSideSheet/sheets/index.ts +0 -3
  21. package/src/PersonSideSheet/sheets/roles/ClaimableRole.test.tsx +0 -81
  22. package/src/PersonSideSheet/sheets/roles/ClaimableRole.tsx +0 -229
  23. package/src/PersonSideSheet/sheets/roles/RolesApi.test.ts +0 -62
  24. package/src/PersonSideSheet/sheets/roles/RolesApi.ts +0 -112
  25. package/src/PersonSideSheet/sheets/roles/RolesSheetContent.test.tsx +0 -84
  26. package/src/PersonSideSheet/sheets/roles/RolesSheetContent.tsx +0 -207
  27. package/src/PersonSideSheet/sheets/roles/index.ts +0 -1
  28. package/src/PersonSideSheet/sheets/styled.tsx +0 -33
  29. package/src/PersonSideSheet/sheets/types.ts +0 -14
  30. package/src/Router.tsx +0 -83
  31. package/src/configure.ts +0 -186
  32. package/src/get-app-tag-from-url.ts +0 -17
  33. package/src/globals.d.ts +0 -32
  34. package/src/main.tsx +0 -43
  35. package/src/resources/svg.ts +0 -7
  36. package/src/version.ts +0 -2
  37. package/tsconfig.json +0 -79
  38. package/tsconfig.tsbuildinfo +0 -1
  39. package/vite.config.ts +0 -15
  40. package/vitest.config.ts +0 -30
@@ -1,52 +0,0 @@
1
- import { useState } from 'react';
2
- import { FeatureTogglerApp } from './FeatureTogglerApp';
3
- import { FeatureTogglerPortal } from './FeatureTogglerPortal';
4
-
5
- import { Divider, Icon, Button, Tabs } from '@equinor/eds-core-react';
6
- import { arrow_back, category } from '@equinor/eds-icons';
7
- Icon.add({ arrow_back, category });
8
-
9
- import type { SheetContentProps } from './types';
10
-
11
- /**
12
- * Feature flags sub-sheet for the person settings side sheet.
13
- *
14
- * Contains tabbed panels for toggling application-level and portal-level
15
- * feature flags. Includes a back-navigation button to return to the landing sheet.
16
- *
17
- * @param props.navigate - Callback to navigate back to the landing sheet.
18
- */
19
- export const FeatureSheetContent = ({ navigate }: SheetContentProps) => {
20
- const [tab, setTab] = useState<number>(0);
21
-
22
- return (
23
- <section>
24
- <div>
25
- <div>
26
- <Button variant="ghost" onClick={() => navigate()}>
27
- <Icon name="arrow_back" />
28
- <Icon name="category" />
29
- My Features
30
- </Button>
31
- </div>
32
- </div>
33
- <Divider />
34
- <div>
35
- <Tabs activeTab={tab} onChange={(index) => setTab(Number(index))}>
36
- <Tabs.List>
37
- <Tabs.Tab>App features</Tabs.Tab>
38
- <Tabs.Tab>Portal features</Tabs.Tab>
39
- </Tabs.List>
40
- <Tabs.Panels>
41
- <Tabs.Panel>
42
- <FeatureTogglerApp />
43
- </Tabs.Panel>
44
- <Tabs.Panel>
45
- <FeatureTogglerPortal />
46
- </Tabs.Panel>
47
- </Tabs.Panels>
48
- </Tabs>
49
- </div>
50
- </section>
51
- );
52
- };
@@ -1,39 +0,0 @@
1
- import { useCurrentAppFeatures } from '@equinor/fusion-framework-react/feature-flag';
2
-
3
- import { Typography, Switch } from '@equinor/eds-core-react';
4
-
5
- import { Styled } from './styled';
6
-
7
- /**
8
- * Feature toggle list for application-level feature flags.
9
- *
10
- * Reads feature flags from the current app's feature-flag module and renders
11
- * each flag as a labeled switch. Clicking a row toggles the flag.
12
- */
13
- export const FeatureTogglerApp = () => {
14
- const { features, toggleFeature } = useCurrentAppFeatures();
15
- return (
16
- <Styled.SwitchList>
17
- {features?.map((feature) => {
18
- return (
19
- <Styled.SwitchListItem
20
- key={`feat-${feature.key}`}
21
- onClick={() => toggleFeature(feature.key)}
22
- >
23
- <Styled.SwitchLabel>
24
- <Typography variant="body_short_bold">{feature.title ?? feature.key}</Typography>
25
- {feature.description && (
26
- <Typography variant="body_short_italic">{feature.description}</Typography>
27
- )}
28
- </Styled.SwitchLabel>
29
- <Styled.Switch>
30
- <Switch checked={feature.enabled} disabled={feature.readonly} />
31
- </Styled.Switch>
32
- </Styled.SwitchListItem>
33
- );
34
- })}
35
- </Styled.SwitchList>
36
- );
37
- };
38
-
39
- export default FeatureTogglerApp;
@@ -1,33 +0,0 @@
1
- import { useFrameworkFeatures } from '@equinor/fusion-framework-react/feature-flag';
2
-
3
- import { Typography, Switch } from '@equinor/eds-core-react';
4
-
5
- import { Styled } from './styled';
6
-
7
- /**
8
- * Feature toggle list for portal-level feature flags.
9
- *
10
- * Reads feature flags from the framework's feature-flag module and renders
11
- * each flag as a labeled switch. Clicking a row toggles the flag.
12
- */
13
- export const FeatureTogglerPortal = () => {
14
- const { features, toggleFeature } = useFrameworkFeatures();
15
- return (
16
- <Styled.SwitchList>
17
- {features?.map((feature) => (
18
- <Styled.SwitchListItem
19
- key={`feat-${feature.key}`}
20
- onClick={() => toggleFeature(feature.key)}
21
- >
22
- <Styled.SwitchLabel>
23
- <Typography variant="body_short_bold">{feature.title ?? feature.key}</Typography>
24
- <Typography variant="body_short_italic">{feature.description ?? ''}</Typography>
25
- </Styled.SwitchLabel>
26
- <Switch checked={feature.enabled} disabled={feature.readonly} />
27
- </Styled.SwitchListItem>
28
- ))}
29
- </Styled.SwitchList>
30
- );
31
- };
32
-
33
- export default FeatureTogglerPortal;
@@ -1,64 +0,0 @@
1
- import { Divider, Icon, Button } from '@equinor/eds-core-react';
2
- import { bandage, category, work_outline, security, verified_user } from '@equinor/eds-icons';
3
- Icon.add({ bandage, category, work_outline, security, verified_user });
4
-
5
- import type { SheetContentProps } from './types';
6
-
7
- import styled from 'styled-components';
8
-
9
- const BtnList = styled.ul`
10
- list-style: none;
11
- padding-left: 0;
12
- `;
13
- const BtnListItem = styled.li`
14
- margin: 1em 0;
15
- `;
16
-
17
- /**
18
- * Landing page content for the person settings side sheet.
19
- *
20
- * Displays navigation buttons for sub-sheets (e.g., feature toggles) and
21
- * an external link to the user's Delve profile.
22
- *
23
- * @param props.azureId - Azure AD object ID used to build the Delve profile link.
24
- * @param props.navigate - Callback to navigate to a sub-sheet by key.
25
- */
26
- export const LandingSheetContent = ({ azureId, navigate }: SheetContentProps) => {
27
- return (
28
- <section>
29
- <BtnList>
30
- <BtnListItem>
31
- <Button variant="ghost" onClick={() => navigate('features')}>
32
- <Icon name="category" />
33
- My Features
34
- </Button>
35
- </BtnListItem>
36
- <BtnListItem>
37
- <Button variant="ghost" onClick={() => navigate('roles')}>
38
- <Icon name="verified_user" />
39
- My Roles
40
- </Button>
41
- </BtnListItem>
42
- </BtnList>
43
- <Divider />
44
- <BtnList>
45
- <BtnListItem>
46
- <Button
47
- variant="ghost"
48
- href={`https://eur.delve.office.com/?u=${azureId}&v=work`}
49
- target="_blank"
50
- >
51
- <svg height="24" viewBox="0 0 24 24" width="24">
52
- <title>Delve</title>
53
- <path
54
- d="M22.5 3C22.7031 3 22.8789 3.07422 23.0273 3.22266C23.1758 3.37109 23.25 3.54688 23.25 3.75V20.25C23.25 20.4531 23.1758 20.6289 23.0273 20.7773C22.8789 20.9258 22.7031 21 22.5 21H14.25V23.3438L0.75 20.9883V3.08203L14.25 0.65625V3H22.5ZM7.07812 16.6992C7.52344 16.6992 7.92188 16.6289 8.27344 16.4883C8.625 16.3477 8.9375 16.1602 9.21094 15.9258C9.48438 15.6914 9.71484 15.4102 9.90234 15.082C10.0898 14.7539 10.25 14.4062 10.3828 14.0391C10.5156 13.6719 10.6055 13.293 10.6523 12.9023C10.6992 12.5117 10.7266 12.1289 10.7344 11.7539C10.7344 11.1602 10.6836 10.5859 10.582 10.0312C10.4805 9.47656 10.3008 8.98047 10.043 8.54297C9.78516 8.10547 9.42969 7.75781 8.97656 7.5C8.52344 7.24219 7.95312 7.10938 7.26562 7.10156C6.78906 7.10156 6.3125 7.12891 5.83594 7.18359C5.35938 7.23828 4.88281 7.28906 4.40625 7.33594V16.5117L5.74219 16.6289C6.1875 16.668 6.63281 16.6914 7.07812 16.6992ZM17.25 16.5H14.25V20.25H17.25V16.5ZM17.25 3.75H14.25V15.75H17.25V3.75ZM22.5 9H18V20.25H22.5V9ZM22.5 3.75H18V8.25H22.5V3.75ZM5.89453 8.92969C6.04297 8.91406 6.1875 8.90234 6.32812 8.89453C6.46875 8.88672 6.62109 8.88281 6.78516 8.88281C7.19141 8.88281 7.53125 8.96875 7.80469 9.14062C8.07812 9.3125 8.29688 9.53906 8.46094 9.82031C8.625 10.1016 8.74219 10.4141 8.8125 10.7578C8.88281 11.1016 8.91797 11.4453 8.91797 11.7891C8.91797 12.125 8.88672 12.4766 8.82422 12.8438C8.76172 13.2109 8.64453 13.5508 8.47266 13.8633C8.30078 14.1758 8.08203 14.4297 7.81641 14.625C7.55078 14.8203 7.20312 14.9219 6.77344 14.9297H6.33984C6.19922 14.9297 6.05078 14.9219 5.89453 14.9062V8.92969Z"
55
- fill="currentColor"
56
- />
57
- </svg>
58
- Delve
59
- </Button>
60
- </BtnListItem>
61
- </BtnList>
62
- </section>
63
- );
64
- };
@@ -1,3 +0,0 @@
1
- export { FeatureSheetContent } from './FeatureSheetContent';
2
- export { LandingSheetContent } from './LandingSheetContent';
3
- export { RolesSheetContent } from './roles';
@@ -1,81 +0,0 @@
1
- import { cleanup, render } from 'vitest-browser-react';
2
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
-
4
- import { ClaimableRole } from './ClaimableRole';
5
- import type { ClaimableRoleAssignment } from './RolesApi';
6
-
7
- const mocks = vi.hoisted(() => ({
8
- createClient: vi.fn(),
9
- currentUser: { localAccountId: 'account-id' },
10
- }));
11
-
12
- vi.mock('@equinor/fusion-framework-react', () => ({
13
- useFramework: () => ({
14
- modules: { serviceDiscovery: { createClient: mocks.createClient } },
15
- }),
16
- }));
17
-
18
- vi.mock('@equinor/fusion-framework-react/hooks', () => ({
19
- useCurrentUser: () => mocks.currentUser,
20
- }));
21
-
22
- const assignment: ClaimableRoleAssignment = {
23
- id: 'assignment-id',
24
- claimableRole: {
25
- name: 'Developer',
26
- displayName: 'Developer role',
27
- description: 'Temporary development access',
28
- },
29
- isActive: false,
30
- activeTo: null,
31
- };
32
-
33
- describe('ClaimableRole', () => {
34
- beforeEach(() => {
35
- mocks.createClient.mockReset();
36
- });
37
-
38
- afterEach(() => {
39
- cleanup();
40
- });
41
-
42
- it('requires a reason before activating a role', async () => {
43
- const json = vi.fn();
44
- mocks.createClient.mockResolvedValue({ json });
45
-
46
- const screen = await render(<ClaimableRole assignment={assignment} onChange={vi.fn()} />);
47
-
48
- await screen.getByLabelText('Activate Developer role').click();
49
- await screen.getByRole('button', { name: 'Activate' }).click();
50
-
51
- await expect.element(screen.getByText('Reason is required.')).toBeVisible();
52
- expect(json).not.toHaveBeenCalled();
53
- });
54
-
55
- it('activates a role and reports its updated state', async () => {
56
- const json = vi.fn().mockResolvedValue({ activeToDate: '2026-08-07T16:00:00Z' });
57
- const onChange = vi.fn();
58
- mocks.createClient.mockResolvedValue({ json });
59
-
60
- const screen = await render(<ClaimableRole assignment={assignment} onChange={onChange} />);
61
-
62
- await screen.getByLabelText('Activate Developer role').click();
63
- await screen.getByLabelText('Reason for activation').fill('Testing role-dependent behavior');
64
- await screen.getByRole('button', { name: 'Activate' }).click();
65
-
66
- await vi.waitFor(() =>
67
- expect(onChange).toHaveBeenCalledWith({
68
- ...assignment,
69
- isActive: true,
70
- activeTo: '2026-08-07T16:00:00Z',
71
- }),
72
- );
73
- expect(json).toHaveBeenCalledWith(
74
- '/accounts/account-id/claimable-role-assignments/assignment-id/activate',
75
- {
76
- method: 'POST',
77
- body: JSON.stringify({ reason: 'Testing role-dependent behavior', hours: 2 }),
78
- },
79
- );
80
- });
81
- });
@@ -1,229 +0,0 @@
1
- import {
2
- Banner,
3
- Button,
4
- CircularProgress,
5
- InputWrapper,
6
- Slider,
7
- Switch,
8
- Textarea,
9
- Typography,
10
- } from '@equinor/eds-core-react';
11
- import { useFramework } from '@equinor/fusion-framework-react';
12
- import { useCurrentUser } from '@equinor/fusion-framework-react/hooks';
13
- import type { ChangeEvent, ReactElement } from 'react';
14
- import { useState } from 'react';
15
- import styled from 'styled-components';
16
-
17
- import { RolesApi, type ClaimableRoleAssignment } from './RolesApi';
18
-
19
- interface ClaimableRoleProps {
20
- readonly assignment: ClaimableRoleAssignment;
21
- onChange(assignment: ClaimableRoleAssignment): void;
22
- }
23
-
24
- const Styled = {
25
- Role: styled.div<{ $isClaiming: boolean }>`
26
- display: flex;
27
- flex-direction: column;
28
- gap: 0.5rem;
29
- padding: 0.5rem;
30
- border: 1px solid ${({ $isClaiming }) => ($isClaiming ? '#6f6f6f' : 'transparent')};
31
- border-radius: 4px;
32
- `,
33
- Summary: styled.div`
34
- display: flex;
35
- gap: 1rem;
36
- align-items: center;
37
- `,
38
- Name: styled.div`
39
- flex: 1;
40
- `,
41
- Indicator: styled.div<{ $active: boolean }>`
42
- width: 0.25rem;
43
- height: 2.5rem;
44
- background: ${({ $active }) => ($active ? '#007079' : '#dcdcdc')};
45
- `,
46
- Form: styled.div`
47
- display: flex;
48
- flex-direction: column;
49
- gap: 1rem;
50
- padding: 0.5rem;
51
- `,
52
- Actions: styled.div`
53
- display: flex;
54
- justify-content: flex-end;
55
- gap: 0.5rem;
56
- `,
57
- };
58
-
59
- /**
60
- * Renders one claimable role with the production activation and deactivation workflow.
61
- *
62
- * @param props.assignment - Consolidated role assignment to display and mutate.
63
- * @param props.onChange - Reports successful activation state changes to the parent tab.
64
- * @returns A role row with activation controls.
65
- */
66
- export const ClaimableRole = ({ assignment, onChange }: ClaimableRoleProps): ReactElement => {
67
- const framework = useFramework();
68
- const user = useCurrentUser();
69
- const [isClaiming, setIsClaiming] = useState(false);
70
- const [isPending, setIsPending] = useState(false);
71
- const [durationHours, setDurationHours] = useState(2);
72
- const [reason, setReason] = useState('');
73
- const [error, setError] = useState<string>();
74
- const now = Date.now();
75
- const isUpcoming = Boolean(
76
- assignment.validFrom && new Date(assignment.validFrom).getTime() > now,
77
- );
78
- const isExpired = Boolean(assignment.validTo && new Date(assignment.validTo).getTime() <= now);
79
- const isUnavailable = isUpcoming || isExpired;
80
- const availabilityLabel = isUpcoming ? 'Upcoming' : isExpired ? 'Expired' : undefined;
81
-
82
- /** Resets the activation form without changing the current assignment. */
83
- const resetForm = (): void => {
84
- setIsClaiming(false);
85
- setDurationHours(2);
86
- setReason('');
87
- setError(undefined);
88
- };
89
-
90
- /** Activates the role for the selected duration after validating the required reason. */
91
- const activateRole = async (): Promise<void> => {
92
- // The backend requires a meaningful reason for every temporary privilege elevation.
93
- if (!reason.trim()) {
94
- setError('Reason is required.');
95
- return;
96
- }
97
-
98
- // Role mutations require the Roles V2 account identifier from the authenticated session.
99
- if (!user?.localAccountId) {
100
- setError('Unable to resolve the signed-in Fusion account.');
101
- return;
102
- }
103
-
104
- setIsPending(true);
105
- setError(undefined);
106
-
107
- try {
108
- const client = await framework.modules.serviceDiscovery.createClient('rolesv2');
109
- const result = await new RolesApi(client, user.localAccountId).activateRole(
110
- assignment.id,
111
- reason.trim(),
112
- durationHours,
113
- );
114
- onChange({ ...assignment, isActive: true, activeTo: result.activeToDate });
115
- resetForm();
116
- } catch (cause) {
117
- setError(cause instanceof Error ? cause.message : 'Failed to activate role.');
118
- } finally {
119
- setIsPending(false);
120
- }
121
- };
122
-
123
- /** Deactivates an active claimable role and reports the updated assignment. */
124
- const deactivateRole = async (): Promise<void> => {
125
- // Role mutations require the Roles V2 account identifier from the authenticated session.
126
- if (!user?.localAccountId) {
127
- setError('Unable to resolve the signed-in Fusion account.');
128
- return;
129
- }
130
-
131
- setIsPending(true);
132
- setError(undefined);
133
-
134
- try {
135
- const client = await framework.modules.serviceDiscovery.createClient('rolesv2');
136
- await new RolesApi(client, user.localAccountId).deactivateRole(assignment.id);
137
- onChange({ ...assignment, isActive: false, activeTo: null });
138
- resetForm();
139
- } catch (cause) {
140
- setError(cause instanceof Error ? cause.message : 'Failed to deactivate role.');
141
- } finally {
142
- setIsPending(false);
143
- }
144
- };
145
-
146
- /**
147
- * Opens the activation form or deactivates the current assignment.
148
- * @param event - Switch state emitted by the role control.
149
- */
150
- const handleToggle = (event: ChangeEvent<HTMLInputElement>): void => {
151
- // Turning on an inactive role first opens the required activation form.
152
- if (event.target.checked && !assignment.isActive) {
153
- setIsClaiming(true);
154
- return;
155
- }
156
-
157
- // Turning off an active role immediately requests deactivation.
158
- if (!event.target.checked && assignment.isActive) {
159
- void deactivateRole();
160
- }
161
- };
162
-
163
- return (
164
- <Styled.Role $isClaiming={isClaiming}>
165
- <Styled.Summary>
166
- <Styled.Indicator $active={assignment.isActive} />
167
- <Styled.Name>
168
- <Typography>{assignment.claimableRole.displayName}</Typography>
169
- <Typography variant="overline">
170
- {assignment.claimableRole.name}
171
- {availabilityLabel ? ` (${availabilityLabel})` : ''}
172
- </Typography>
173
- </Styled.Name>
174
- <Switch
175
- aria-label={`Activate ${assignment.claimableRole.displayName}`}
176
- checked={assignment.isActive || isClaiming}
177
- disabled={isPending || isUnavailable}
178
- onChange={handleToggle}
179
- />
180
- </Styled.Summary>
181
-
182
- {isClaiming && !assignment.isActive && (
183
- <Styled.Form>
184
- <Typography variant="body_short">{assignment.claimableRole.description}</Typography>
185
- <InputWrapper
186
- labelProps={{ label: 'Duration (hours)' }}
187
- helperProps={{ text: 'Select how long this role should remain active' }}
188
- >
189
- <Slider
190
- value={durationHours}
191
- min={1}
192
- max={8}
193
- step={1}
194
- minMaxValues={false}
195
- onChangeCommitted={(_event, value) => setDurationHours(value[0])}
196
- />
197
- </InputWrapper>
198
- <Textarea
199
- label="Reason for activation"
200
- helperText="Enter a descriptive reason for activating this role"
201
- required
202
- rows={3}
203
- value={reason}
204
- onChange={(event: ChangeEvent<HTMLTextAreaElement>) => setReason(event.target.value)}
205
- />
206
- {error && (
207
- <Banner>
208
- <Banner.Message>{error}</Banner.Message>
209
- </Banner>
210
- )}
211
- <Styled.Actions>
212
- <Button variant="outlined" disabled={isPending} onClick={resetForm}>
213
- Cancel
214
- </Button>
215
- <Button variant="contained" disabled={isPending} onClick={() => void activateRole()}>
216
- {isPending ? <CircularProgress size={24} /> : 'Activate'}
217
- </Button>
218
- </Styled.Actions>
219
- </Styled.Form>
220
- )}
221
-
222
- {!isClaiming && error && (
223
- <Banner>
224
- <Banner.Message>{error}</Banner.Message>
225
- </Banner>
226
- )}
227
- </Styled.Role>
228
- );
229
- };
@@ -1,62 +0,0 @@
1
- import { describe, expect, it, vi } from 'vitest';
2
-
3
- import { RolesApi, type RolesClient } from './RolesApi';
4
-
5
- /** Creates a typed Roles V2 client mock around a Vitest function. */
6
- const createClient = (): { client: RolesClient; json: ReturnType<typeof vi.fn> } => {
7
- const json = vi.fn();
8
- return { client: { json }, json };
9
- };
10
-
11
- describe('RolesApi', () => {
12
- it('loads consolidated claimable and permanent assignments', async () => {
13
- const { client, json } = createClient();
14
- json.mockResolvedValueOnce([{ id: 'claimable' }]).mockResolvedValueOnce([{ id: 'permanent' }]);
15
- const rolesApi = new RolesApi(client, 'account-id');
16
-
17
- await expect(rolesApi.getClaimableRoles()).resolves.toEqual([{ id: 'claimable' }]);
18
- await expect(rolesApi.getPermanentRoles()).resolves.toEqual([{ id: 'permanent' }]);
19
- expect(json).toHaveBeenNthCalledWith(
20
- 1,
21
- '/accounts/account-id/consolidated-claimable-role-assignments',
22
- );
23
- expect(json).toHaveBeenNthCalledWith(2, '/accounts/account-id/consolidated-role-assignments');
24
- });
25
-
26
- it('sends the activation reason and duration to the claimable assignment', async () => {
27
- const { client, json } = createClient();
28
- json.mockResolvedValue({ activeToDate: '2026-08-07T12:00:00Z' });
29
- const rolesApi = new RolesApi(client, 'account-id');
30
-
31
- await expect(rolesApi.activateRole('role-id', 'Testing elevated access', 4)).resolves.toEqual({
32
- activeToDate: '2026-08-07T12:00:00Z',
33
- });
34
- expect(json).toHaveBeenCalledWith(
35
- '/accounts/account-id/claimable-role-assignments/role-id/activate',
36
- {
37
- method: 'POST',
38
- body: JSON.stringify({ reason: 'Testing elevated access', hours: 4 }),
39
- },
40
- );
41
- });
42
-
43
- it('posts deactivation to the active claimable assignment', async () => {
44
- const { client, json } = createClient();
45
- json.mockResolvedValue(undefined);
46
- const rolesApi = new RolesApi(client, 'account-id');
47
-
48
- await expect(rolesApi.deactivateRole('role-id')).resolves.toBeUndefined();
49
- expect(json).toHaveBeenCalledWith(
50
- '/accounts/account-id/claimable-role-assignments/role-id/deactivate',
51
- { method: 'POST' },
52
- );
53
- });
54
-
55
- it('propagates service failures to the role UI', async () => {
56
- const { client, json } = createClient();
57
- json.mockRejectedValue(new Error('Roles service unavailable'));
58
- const rolesApi = new RolesApi(client, 'account-id');
59
-
60
- await expect(rolesApi.getClaimableRoles()).rejects.toThrow('Roles service unavailable');
61
- });
62
- });
@@ -1,112 +0,0 @@
1
- /** Role metadata returned by the Roles V2 consolidated assignment endpoints. */
2
- export interface RoleDefinition {
3
- readonly name: string;
4
- readonly displayName: string;
5
- readonly description: string;
6
- }
7
-
8
- /** Scope metadata attached to a permanent role assignment. */
9
- export interface RoleScope {
10
- readonly isGlobal: boolean;
11
- readonly value: string | null;
12
- }
13
-
14
- /** Claimable role assignment state used by the activation controls. */
15
- export interface ClaimableRoleAssignment {
16
- readonly id: string;
17
- readonly claimableRole: RoleDefinition;
18
- readonly isActive: boolean;
19
- readonly activeTo: string | null;
20
- readonly validFrom?: string | null;
21
- readonly validTo?: string | null;
22
- }
23
-
24
- /** Permanent role assignment displayed as a read-only entitlement. */
25
- export interface PermanentRoleAssignment {
26
- readonly id: string;
27
- readonly role: RoleDefinition;
28
- readonly scope?: RoleScope | null;
29
- readonly validFrom?: string | null;
30
- readonly validTo?: string | null;
31
- }
32
-
33
- /** Minimal HTTP client contract required by the Roles V2 API. */
34
- export interface RolesClient {
35
- json(path: string, init?: RequestInit): Promise<unknown>;
36
- }
37
-
38
- interface ActivationResult {
39
- readonly activeToDate: string;
40
- }
41
-
42
- /**
43
- * Provides the Roles V2 requests used by the dev portal person side sheet.
44
- *
45
- * @example
46
- * ```ts
47
- * const api = new RolesApi(await serviceDiscovery.createClient('rolesv2'), accountId);
48
- * const roles = await api.getClaimableRoles();
49
- * ```
50
- */
51
- export class RolesApi {
52
- #client: RolesClient;
53
- #accountId: string;
54
-
55
- /**
56
- * Creates a Roles V2 API client scoped to one Fusion account.
57
- * @param client - Service discovery HTTP client for the `rolesv2` service.
58
- * @param accountId - Local Fusion account identifier for the signed-in user.
59
- */
60
- constructor(client: RolesClient, accountId: string) {
61
- this.#client = client;
62
- this.#accountId = accountId;
63
- }
64
-
65
- /**
66
- * Fetches consolidated claimable role assignments.
67
- * @returns Claimable assignments for the scoped Fusion account.
68
- */
69
- async getClaimableRoles(): Promise<ClaimableRoleAssignment[]> {
70
- return (await this.#client.json(
71
- `/accounts/${this.#accountId}/consolidated-claimable-role-assignments`,
72
- )) as ClaimableRoleAssignment[];
73
- }
74
-
75
- /**
76
- * Fetches consolidated permanent role assignments.
77
- * @returns Permanent assignments for the scoped Fusion account.
78
- */
79
- async getPermanentRoles(): Promise<PermanentRoleAssignment[]> {
80
- return (await this.#client.json(
81
- `/accounts/${this.#accountId}/consolidated-role-assignments`,
82
- )) as PermanentRoleAssignment[];
83
- }
84
-
85
- /**
86
- * Activates a claimable role for a bounded duration.
87
- * @param roleId - Claimable assignment identifier.
88
- * @param reason - User-provided reason for privilege elevation.
89
- * @param hours - Requested activation duration in hours.
90
- * @returns Activation metadata containing the server-calculated expiry.
91
- */
92
- async activateRole(roleId: string, reason: string, hours: number): Promise<ActivationResult> {
93
- return (await this.#client.json(
94
- `/accounts/${this.#accountId}/claimable-role-assignments/${roleId}/activate`,
95
- {
96
- method: 'POST',
97
- body: JSON.stringify({ reason, hours }),
98
- },
99
- )) as ActivationResult;
100
- }
101
-
102
- /**
103
- * Deactivates an active claimable role.
104
- * @param roleId - Claimable assignment identifier.
105
- */
106
- async deactivateRole(roleId: string): Promise<void> {
107
- await this.#client.json(
108
- `/accounts/${this.#accountId}/claimable-role-assignments/${roleId}/deactivate`,
109
- { method: 'POST' },
110
- );
111
- }
112
- }