@equinor/fusion-framework-dev-portal 1.0.0-next.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 +19 -0
- package/LICENSE +21 -0
- package/README.md +86 -0
- package/dev-server.ts +96 -0
- package/package.json +67 -0
- package/rollup.config.js +16 -0
- package/src/AppLoader.tsx +121 -0
- package/src/BookMarkSideSheet.tsx +36 -0
- package/src/ContextSelector/ContextSelector.tsx +71 -0
- package/src/ContextSelector/index.ts +1 -0
- package/src/ContextSelector/useContextResolver.ts +287 -0
- package/src/EquinorLoader.tsx +24 -0
- package/src/ErrorViewer.tsx +17 -0
- package/src/FusionLogo.tsx +58 -0
- package/src/Header.Actions.tsx +35 -0
- package/src/Header.tsx +89 -0
- package/src/PersonSideSheet/index.tsx +54 -0
- package/src/PersonSideSheet/sheets/FeatureSheetContent.tsx +48 -0
- package/src/PersonSideSheet/sheets/FeatureTogglerApp.tsx +36 -0
- package/src/PersonSideSheet/sheets/FeatureTogglerPortal.tsx +30 -0
- package/src/PersonSideSheet/sheets/LandingSheetContent.tsx +52 -0
- package/src/PersonSideSheet/sheets/Styled.tsx +29 -0
- package/src/PersonSideSheet/sheets/index.ts +2 -0
- package/src/PersonSideSheet/sheets/types.ts +5 -0
- package/src/Router.tsx +79 -0
- package/src/config.ts +60 -0
- package/src/main.tsx +32 -0
- package/src/resources/fallback-photo.svg.ts +2 -0
- package/src/useAppContextNavigation.ts +104 -0
- package/src/version.ts +2 -0
- package/tsconfig.json +53 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { useFramework } from '@equinor/fusion-framework-react';
|
|
3
|
+
import { useCurrentApp } from '@equinor/fusion-framework-react/app';
|
|
4
|
+
import type { AppModule } from '@equinor/fusion-framework-module-app';
|
|
5
|
+
import type { NavigationModule } from '@equinor/fusion-framework-module-navigation';
|
|
6
|
+
import type {
|
|
7
|
+
ContextItem,
|
|
8
|
+
ContextModule,
|
|
9
|
+
IContextProvider,
|
|
10
|
+
} from '@equinor/fusion-framework-module-context';
|
|
11
|
+
import { useObservableState, useObservableSubscription } from '@equinor/fusion-observable/react';
|
|
12
|
+
import '@equinor/fusion-framework-app';
|
|
13
|
+
import { ChipElement } from '@equinor/fusion-wc-chip';
|
|
14
|
+
ChipElement;
|
|
15
|
+
|
|
16
|
+
import { EMPTY, catchError, lastValueFrom, map, of } from 'rxjs';
|
|
17
|
+
|
|
18
|
+
import type {
|
|
19
|
+
ContextResult,
|
|
20
|
+
ContextResultItem,
|
|
21
|
+
ContextResolver,
|
|
22
|
+
} from '@equinor/fusion-react-context-selector';
|
|
23
|
+
import type { AppModulesInstance } from '@equinor/fusion-framework-app';
|
|
24
|
+
import type { QueryClientError } from '@equinor/fusion-query/client';
|
|
25
|
+
import type { FusionContextSearchError } from '@equinor/fusion-framework-module-context/errors.js';
|
|
26
|
+
|
|
27
|
+
function capitalizeFirstLetter(string: string): string {
|
|
28
|
+
return string.charAt(0).toUpperCase() + string.slice(1).toLowerCase();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function convertGraphic(
|
|
32
|
+
graphic: ContextItem['graphic'],
|
|
33
|
+
): Pick<ContextResultItem, 'graphic' | 'graphicType'> {
|
|
34
|
+
if (graphic === undefined) {
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (typeof graphic === 'string') {
|
|
39
|
+
return {
|
|
40
|
+
graphicType: graphic.startsWith('<')
|
|
41
|
+
? 'inline-html'
|
|
42
|
+
: ('eds' as unknown as ContextResultItem['graphicType']),
|
|
43
|
+
graphic: graphic,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (graphic.type === 'svg') {
|
|
48
|
+
return {
|
|
49
|
+
graphicType: 'inline-svg',
|
|
50
|
+
graphic: graphic.content,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
graphicType: 'inline-html',
|
|
56
|
+
graphic: graphic.content,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function convertMeta(meta: ContextItem['meta']): Pick<ContextResultItem, 'metaType' | 'meta'> {
|
|
61
|
+
if (meta === undefined) {
|
|
62
|
+
return {};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (typeof meta === 'string') {
|
|
66
|
+
return {
|
|
67
|
+
metaType: meta.startsWith('<')
|
|
68
|
+
? 'inline-html'
|
|
69
|
+
: ('eds' as unknown as ContextResultItem['metaType']),
|
|
70
|
+
meta: meta,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (meta.type === 'svg') {
|
|
75
|
+
return {
|
|
76
|
+
metaType: 'inline-svg',
|
|
77
|
+
meta: meta.content,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
metaType: 'inline-html',
|
|
83
|
+
meta: meta.content,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Map context query result to ContextSelectorResult.
|
|
89
|
+
* Add any icons to selected types by using the 'graphic' property
|
|
90
|
+
* @param src context query result
|
|
91
|
+
* @returns src mapped to ContextResult type
|
|
92
|
+
*/
|
|
93
|
+
const mapper = (src: ContextItem<{ taskState?: string; state?: string }>[]): ContextResult => {
|
|
94
|
+
return src.map((i) => {
|
|
95
|
+
const baseResult = {
|
|
96
|
+
id: i.id,
|
|
97
|
+
title: i.title,
|
|
98
|
+
subTitle: i.subTitle ?? i.type.id,
|
|
99
|
+
...convertGraphic(i.graphic),
|
|
100
|
+
...convertMeta(i.meta),
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Displays the status of the EquinorTask if it is not 'active'
|
|
104
|
+
const isEquinorTaskInactive = !!(
|
|
105
|
+
i.value.taskState && i.value.taskState.toLowerCase() !== 'active'
|
|
106
|
+
);
|
|
107
|
+
if (i.type.id === 'EquinorTask' && isEquinorTaskInactive) {
|
|
108
|
+
baseResult.meta = `<fwc-chip disabled variant="outlined" value="${i.value.taskState}" />`;
|
|
109
|
+
baseResult.metaType = 'inline-html';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (i.type.id === 'OrgChart') {
|
|
113
|
+
// Org charts should always have 'list' icon
|
|
114
|
+
baseResult.graphic = 'list';
|
|
115
|
+
baseResult.graphicType = 'eds' as unknown as ContextResultItem['graphicType'];
|
|
116
|
+
|
|
117
|
+
// Displays the org chart status if it is not 'active'
|
|
118
|
+
const isOrgChartInactive = !!(i.value.state && i.value.state.toLowerCase() !== 'active');
|
|
119
|
+
if (isOrgChartInactive) {
|
|
120
|
+
baseResult.meta = `<fwc-chip disabled variant="outlined" value="${capitalizeFirstLetter(i.value.state ?? '')}" />`;
|
|
121
|
+
baseResult.metaType = 'inline-html';
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return baseResult;
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Create a single ContextResultItem
|
|
131
|
+
* @param props pops for the item to merge with defaults
|
|
132
|
+
* @returns ContextResultItem
|
|
133
|
+
*/
|
|
134
|
+
const singleItem = (props: Partial<ContextResultItem>): ContextResultItem => {
|
|
135
|
+
return Object.assign({ id: 'no-such-item', title: 'Change me' }, props);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Hook for querying context and setting resolver for ContextSelector component
|
|
140
|
+
* See React Components storybook for info about ContextSelector component and its resolver
|
|
141
|
+
* @link https://equinor.github.io/fusion-react-components/?path=/docs/data-contextselector--component
|
|
142
|
+
* @return Array<ContextResolver, SetContextCallback>
|
|
143
|
+
*/
|
|
144
|
+
export const useContextResolver = (): {
|
|
145
|
+
resolver: ContextResolver | null;
|
|
146
|
+
provider: IContextProvider | null;
|
|
147
|
+
currentContext: ContextResult;
|
|
148
|
+
} => {
|
|
149
|
+
/* Framework modules */
|
|
150
|
+
const framework = useFramework<[AppModule, NavigationModule]>();
|
|
151
|
+
|
|
152
|
+
const { currentApp } = useCurrentApp();
|
|
153
|
+
|
|
154
|
+
/** App module collection instance */
|
|
155
|
+
const instance$ = useMemo(() => currentApp?.instance$ || EMPTY, [currentApp]);
|
|
156
|
+
|
|
157
|
+
/* context provider state */
|
|
158
|
+
const [provider, setProvider] = useState<IContextProvider | null>(null);
|
|
159
|
+
|
|
160
|
+
/* Current context observable */
|
|
161
|
+
const { value: currentContext } = useObservableState(
|
|
162
|
+
useMemo(() => provider?.currentContext$ || EMPTY, [provider]),
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const preselected: ContextResult = useMemo(() => {
|
|
166
|
+
return currentContext ? mapper([currentContext]) : [];
|
|
167
|
+
}, [currentContext]);
|
|
168
|
+
|
|
169
|
+
/** callback function when current app instance changes */
|
|
170
|
+
const onContextProviderChange = useCallback((modules: AppModulesInstance) => {
|
|
171
|
+
/** try to get the context module from the app module instance */
|
|
172
|
+
const contextProvider = (modules as AppModulesInstance<[ContextModule]>).context;
|
|
173
|
+
if (contextProvider) {
|
|
174
|
+
setProvider(contextProvider);
|
|
175
|
+
} else {
|
|
176
|
+
setProvider(null);
|
|
177
|
+
}
|
|
178
|
+
}, []);
|
|
179
|
+
|
|
180
|
+
/** clear the app provider */
|
|
181
|
+
const clearContextProvider = useCallback(() => {
|
|
182
|
+
setProvider(null);
|
|
183
|
+
}, []);
|
|
184
|
+
|
|
185
|
+
/** observe changes to app modules and clear / set the context provider on change */
|
|
186
|
+
useObservableSubscription(instance$, onContextProviderChange, clearContextProvider);
|
|
187
|
+
useEffect(
|
|
188
|
+
() =>
|
|
189
|
+
framework.modules.event.addEventListener('onReactAppLoaded', (e) => {
|
|
190
|
+
console.debug('useContextResolver::onReactAppLoaded', 'using legacy register hack method');
|
|
191
|
+
return onContextProviderChange(e.detail.modules);
|
|
192
|
+
}),
|
|
193
|
+
[framework, onContextProviderChange],
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
const processError = useCallback((err: Error): ContextResult => {
|
|
197
|
+
if (err.name === 'QueryClientError') {
|
|
198
|
+
return processError((err as QueryClientError).cause as Error);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (err.name === 'FusionContextSearchError') {
|
|
202
|
+
const error = err as FusionContextSearchError;
|
|
203
|
+
return [
|
|
204
|
+
singleItem({
|
|
205
|
+
id: error.name,
|
|
206
|
+
title: error.title,
|
|
207
|
+
subTitle: error.description,
|
|
208
|
+
graphic: 'error_outlined',
|
|
209
|
+
isDisabled: true,
|
|
210
|
+
}),
|
|
211
|
+
];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return [
|
|
215
|
+
singleItem({
|
|
216
|
+
title: 'Unexpected error occurred',
|
|
217
|
+
subTitle: 'Please try again or report the issue in Services@Equinor',
|
|
218
|
+
graphic: 'error_outlined',
|
|
219
|
+
isDisabled: true,
|
|
220
|
+
}),
|
|
221
|
+
];
|
|
222
|
+
}, []);
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* set resolver for ContextSelector
|
|
226
|
+
* @return ContextResolver
|
|
227
|
+
*/
|
|
228
|
+
const minLength = 2;
|
|
229
|
+
const resolver = useMemo(
|
|
230
|
+
(): ContextResolver | null =>
|
|
231
|
+
provider && {
|
|
232
|
+
searchQuery: async (search: string): Promise<ContextResult> => {
|
|
233
|
+
if (search.length < minLength) {
|
|
234
|
+
return [
|
|
235
|
+
singleItem({
|
|
236
|
+
// TODO - make as enum if used for checks, or type
|
|
237
|
+
id: 'min-length',
|
|
238
|
+
title: `Type ${minLength - search.length} more chars to search`,
|
|
239
|
+
isDisabled: true,
|
|
240
|
+
}),
|
|
241
|
+
];
|
|
242
|
+
}
|
|
243
|
+
try {
|
|
244
|
+
return lastValueFrom(
|
|
245
|
+
provider.queryContext(search).pipe(
|
|
246
|
+
map(mapper),
|
|
247
|
+
map((x) =>
|
|
248
|
+
x.length
|
|
249
|
+
? x
|
|
250
|
+
: [
|
|
251
|
+
singleItem({
|
|
252
|
+
// TODO - make as enum if used for checks, or type
|
|
253
|
+
id: 'no-results',
|
|
254
|
+
title: 'No results found',
|
|
255
|
+
graphic: 'info_circle',
|
|
256
|
+
isDisabled: true,
|
|
257
|
+
}),
|
|
258
|
+
],
|
|
259
|
+
),
|
|
260
|
+
/** handle failures */
|
|
261
|
+
catchError((err) => {
|
|
262
|
+
console.error(
|
|
263
|
+
'PORTAL::ContextResolver',
|
|
264
|
+
`failed to resolve context for query ${search}`,
|
|
265
|
+
err,
|
|
266
|
+
err.cause,
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
return of(processError(err));
|
|
270
|
+
}),
|
|
271
|
+
),
|
|
272
|
+
);
|
|
273
|
+
/** this should NEVER happen! */
|
|
274
|
+
} catch (e) {
|
|
275
|
+
const err = e as Error;
|
|
276
|
+
console.error('PORTAL::ContextResolver', `unhandled error for [${search}]`, e);
|
|
277
|
+
return processError(err);
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
initialResult: preselected,
|
|
281
|
+
},
|
|
282
|
+
[provider, preselected, processError],
|
|
283
|
+
);
|
|
284
|
+
return { resolver, provider, currentContext: preselected };
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
export default useContextResolver;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import { StarProgress } from '@equinor/fusion-react-progress-indicator';
|
|
3
|
+
|
|
4
|
+
export const EquinorLoader = ({
|
|
5
|
+
children,
|
|
6
|
+
text,
|
|
7
|
+
}: React.PropsWithChildren<{ readonly text: string }>): JSX.Element => {
|
|
8
|
+
return (
|
|
9
|
+
<div
|
|
10
|
+
style={{
|
|
11
|
+
display: 'flex',
|
|
12
|
+
justifyContent: 'center',
|
|
13
|
+
alignItems: 'center',
|
|
14
|
+
width: '100vw',
|
|
15
|
+
height: '100vh',
|
|
16
|
+
overflow: 'hidden',
|
|
17
|
+
}}
|
|
18
|
+
>
|
|
19
|
+
<StarProgress text={text}>{children}</StarProgress>
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default EquinorLoader;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Typography } from '@equinor/eds-core-react';
|
|
2
|
+
|
|
3
|
+
export const ErrorViewer = ({ error }: { readonly error: Error }) => {
|
|
4
|
+
return (
|
|
5
|
+
<>
|
|
6
|
+
<div style={{ marginTop: 20, border: '1px solid' }}>
|
|
7
|
+
<Typography variant="h4" color="warning">
|
|
8
|
+
{error.message}
|
|
9
|
+
</Typography>
|
|
10
|
+
<section style={{ padding: 10 }}>{error.stack && <pre>{error.stack}</pre>}</section>
|
|
11
|
+
</div>
|
|
12
|
+
{error.cause && <ErrorViewer error={error.cause as Error} />}
|
|
13
|
+
</>
|
|
14
|
+
);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default ErrorViewer;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { SVGProps } from 'react';
|
|
2
|
+
|
|
3
|
+
type FusionLogoProps = Omit<SVGProps<SVGSVGElement>, 'viewBox'> & {
|
|
4
|
+
readonly scale?: number;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const FusionLogo = ({ scale = 1, style }: FusionLogoProps) => (
|
|
8
|
+
<svg viewBox="0 0 50 35" style={{ height: '1em', ...style, transform: `scale(${scale})` }}>
|
|
9
|
+
<title>Fusion Logo</title>
|
|
10
|
+
<path
|
|
11
|
+
d="M0 2V23.1776L7.05405 16.1235V7.05405H16.1235L23.1776 0H2C0.895431 0 0 0.89543 0 2Z"
|
|
12
|
+
transform="translate(50 17.5) scale(0.92727 1.06779) rotate(135)"
|
|
13
|
+
fill="url(#paint0_linear)"
|
|
14
|
+
/>
|
|
15
|
+
<path
|
|
16
|
+
d="M0 2V23.1776L7.05405 16.1235V7.05405H16.1235L23.1776 0H2C0.895431 0 0 0.89543 0 2Z"
|
|
17
|
+
transform="translate(0 17.5) scale(0.92727 1.06779) rotate(-45)"
|
|
18
|
+
fill="url(#paint1_linear)"
|
|
19
|
+
/>
|
|
20
|
+
<path
|
|
21
|
+
d="M9.61965 36.6972L2.60087 29.6784L1.96135 22.3809L8.42623 22.9069L9.61965 36.6972Z"
|
|
22
|
+
transform="translate(33.8887 34.9863) scale(0.92727 -1.06779) rotate(45)"
|
|
23
|
+
fill="#990025"
|
|
24
|
+
/>
|
|
25
|
+
<path
|
|
26
|
+
d="M7.05434 7.05434L0 0L1.21096 13.8183L7.68846 14.3818L7.05434 7.05434Z"
|
|
27
|
+
transform="translate(33.8887 34.9863) scale(0.92727 -1.06779) rotate(45)"
|
|
28
|
+
fill="#990025"
|
|
29
|
+
/>
|
|
30
|
+
<path
|
|
31
|
+
d="M0 0L2.49398 29.5715L9.61965 36.6972L7.01878 7.01878L0 0Z"
|
|
32
|
+
transform="translate(33.8887 0.015625) scale(0.92727 1.06779) rotate(45)"
|
|
33
|
+
fill="#FF1243"
|
|
34
|
+
/>
|
|
35
|
+
<defs>
|
|
36
|
+
<linearGradient
|
|
37
|
+
id="paint0_linear"
|
|
38
|
+
x2="1"
|
|
39
|
+
gradientUnits="userSpaceOnUse"
|
|
40
|
+
gradientTransform="matrix(-13.5478 9.01983 -12.9578 -13.5478 18.0677 6.77391)"
|
|
41
|
+
>
|
|
42
|
+
<stop offset="0.508287" stopColor="#DC002E" />
|
|
43
|
+
<stop offset="0.508387" stopColor="#FF1243" />
|
|
44
|
+
</linearGradient>
|
|
45
|
+
<linearGradient
|
|
46
|
+
id="paint1_linear"
|
|
47
|
+
x2="1"
|
|
48
|
+
gradientUnits="userSpaceOnUse"
|
|
49
|
+
gradientTransform="matrix(-13.5478 9.01983 -12.9578 -13.5478 18.0677 6.77391)"
|
|
50
|
+
>
|
|
51
|
+
<stop offset="0.508287" stopColor="#DC002E" />
|
|
52
|
+
<stop offset="0.508387" stopColor="#FF1243" />
|
|
53
|
+
</linearGradient>
|
|
54
|
+
</defs>
|
|
55
|
+
</svg>
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
export default FusionLogo;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { tag } from '@equinor/eds-icons';
|
|
2
|
+
import { Button, Icon, TopBar } from '@equinor/eds-core-react';
|
|
3
|
+
|
|
4
|
+
import PersonAvatarElement from '@equinor/fusion-wc-person/avatar';
|
|
5
|
+
PersonAvatarElement;
|
|
6
|
+
|
|
7
|
+
import { useBookmarkComponentContext } from '@equinor/fusion-framework-react-components-bookmark';
|
|
8
|
+
|
|
9
|
+
interface HeaderActionProps {
|
|
10
|
+
readonly userAzureId?: string;
|
|
11
|
+
readonly toggleBookmark: (open: (status: boolean) => boolean) => void;
|
|
12
|
+
readonly togglePerson: (open: (status: boolean) => boolean) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const HeaderActions = (props: HeaderActionProps) => {
|
|
16
|
+
const { toggleBookmark, togglePerson, userAzureId } = props;
|
|
17
|
+
|
|
18
|
+
const bookmarkContext = useBookmarkComponentContext();
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<TopBar.Actions style={{ minWidth: 48, minHeight: 48 }}>
|
|
22
|
+
<Button
|
|
23
|
+
onClick={() => toggleBookmark((x) => !x)}
|
|
24
|
+
variant="ghost_icon"
|
|
25
|
+
disabled={!bookmarkContext.provider}
|
|
26
|
+
title={bookmarkContext.provider ? 'Bookmarks' : 'Bookmarks not available, enable in app'}
|
|
27
|
+
>
|
|
28
|
+
<Icon data={tag} />
|
|
29
|
+
</Button>
|
|
30
|
+
<Button onClick={() => togglePerson((x) => !x)} variant="ghost_icon">
|
|
31
|
+
<fwc-person-avatar size="small" azureId={userAzureId} clickable={false} />
|
|
32
|
+
</Button>
|
|
33
|
+
</TopBar.Actions>
|
|
34
|
+
);
|
|
35
|
+
};
|
package/src/Header.tsx
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
|
+
import { ContextSelector } from './ContextSelector';
|
|
3
|
+
import { FusionLogo } from './FusionLogo';
|
|
4
|
+
|
|
5
|
+
/* typescript reference for makeStyles */
|
|
6
|
+
// import '@material-ui/styles';
|
|
7
|
+
|
|
8
|
+
import styled from 'styled-components';
|
|
9
|
+
import { add, menu, tag } from '@equinor/eds-icons';
|
|
10
|
+
import { Icon, TopBar } from '@equinor/eds-core-react';
|
|
11
|
+
Icon.add({ menu, add, tag });
|
|
12
|
+
|
|
13
|
+
import { useCurrentUser } from '@equinor/fusion-framework-react/hooks';
|
|
14
|
+
import { useCurrentApp, useCurrentAppModule } from '@equinor/fusion-framework-react/app';
|
|
15
|
+
|
|
16
|
+
import type { BookmarkModule } from '@equinor/fusion-framework-react-module-bookmark';
|
|
17
|
+
|
|
18
|
+
import { BookmarkProvider } from '@equinor/fusion-framework-react-components-bookmark';
|
|
19
|
+
|
|
20
|
+
import PersonAvatarElement from '@equinor/fusion-wc-person/avatar';
|
|
21
|
+
PersonAvatarElement;
|
|
22
|
+
|
|
23
|
+
import { PersonSideSheet } from './PersonSideSheet';
|
|
24
|
+
|
|
25
|
+
import { BookmarkSideSheet } from './BookMarkSideSheet';
|
|
26
|
+
|
|
27
|
+
import { HeaderActions } from './Header.Actions';
|
|
28
|
+
|
|
29
|
+
const Styled = {
|
|
30
|
+
Title: styled.div`
|
|
31
|
+
display: flex;
|
|
32
|
+
align-items: center;
|
|
33
|
+
gap: 0.75rem;
|
|
34
|
+
font-size: 1rem;
|
|
35
|
+
font-weight: 500;
|
|
36
|
+
`,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const Header = () => {
|
|
40
|
+
const currentUser = useCurrentUser();
|
|
41
|
+
const [isPersonSheetOpen, setIsPersonSheetOpen] = useState(false);
|
|
42
|
+
|
|
43
|
+
const [isBookmarkOpen, setIsBookmarkOpen] = useState(false);
|
|
44
|
+
const onBookmarkClose = useCallback(() => {
|
|
45
|
+
setIsBookmarkOpen(false);
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
const { currentApp } = useCurrentApp();
|
|
49
|
+
|
|
50
|
+
const { module: bookmarkProvider } = useCurrentAppModule<BookmarkModule>('bookmark');
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<BookmarkProvider
|
|
54
|
+
provider={bookmarkProvider ?? undefined}
|
|
55
|
+
currentApp={
|
|
56
|
+
currentApp
|
|
57
|
+
? { appKey: currentApp.appKey, name: currentApp.manifest?.displayName }
|
|
58
|
+
: undefined
|
|
59
|
+
}
|
|
60
|
+
currentUser={
|
|
61
|
+
currentUser ? { id: currentUser.localAccountId, name: currentUser.name } : undefined
|
|
62
|
+
}
|
|
63
|
+
>
|
|
64
|
+
<TopBar id="cli-top-bar" sticky={false} style={{ padding: '0 1em', height: 48 }}>
|
|
65
|
+
<TopBar.Header>
|
|
66
|
+
<Styled.Title>
|
|
67
|
+
<FusionLogo />
|
|
68
|
+
<span>Fusion Framework CLI</span>
|
|
69
|
+
</Styled.Title>
|
|
70
|
+
</TopBar.Header>
|
|
71
|
+
<HeaderActions
|
|
72
|
+
userAzureId={currentUser?.localAccountId}
|
|
73
|
+
toggleBookmark={setIsBookmarkOpen}
|
|
74
|
+
togglePerson={setIsPersonSheetOpen}
|
|
75
|
+
/>
|
|
76
|
+
<TopBar.CustomContent>
|
|
77
|
+
<ContextSelector />
|
|
78
|
+
</TopBar.CustomContent>
|
|
79
|
+
{/* since buttons are 40px but have 48px click bounds */}
|
|
80
|
+
</TopBar>
|
|
81
|
+
<BookmarkSideSheet isOpen={isBookmarkOpen} onClose={onBookmarkClose} />
|
|
82
|
+
<PersonSideSheet
|
|
83
|
+
azureId={currentUser?.localAccountId}
|
|
84
|
+
isOpen={isPersonSheetOpen}
|
|
85
|
+
onClose={() => setIsPersonSheetOpen(!isPersonSheetOpen)}
|
|
86
|
+
/>
|
|
87
|
+
</BookmarkProvider>
|
|
88
|
+
);
|
|
89
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
2
|
+
import { SideSheet } from '@equinor/fusion-react-side-sheet';
|
|
3
|
+
import PersonListItem from '@equinor/fusion-wc-person/list-item';
|
|
4
|
+
PersonListItem;
|
|
5
|
+
|
|
6
|
+
import { Divider } from '@equinor/eds-core-react';
|
|
7
|
+
|
|
8
|
+
import { LandingSheetContent, FeatureSheetContent } from './sheets';
|
|
9
|
+
|
|
10
|
+
type PersonSideSheetProps = {
|
|
11
|
+
readonly azureId?: string;
|
|
12
|
+
readonly isOpen: boolean;
|
|
13
|
+
onClose(): void;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Add Sidesheet with settings for the current user.
|
|
18
|
+
* @param PersonSideSheetProps
|
|
19
|
+
*/
|
|
20
|
+
export const PersonSideSheet = ({ azureId, isOpen, onClose }: PersonSideSheetProps) => {
|
|
21
|
+
const [currentSheet, setCurrentSheet] = useState<string>('landing');
|
|
22
|
+
|
|
23
|
+
const Component = useMemo(() => {
|
|
24
|
+
switch (currentSheet) {
|
|
25
|
+
case 'features':
|
|
26
|
+
return FeatureSheetContent;
|
|
27
|
+
default:
|
|
28
|
+
return LandingSheetContent;
|
|
29
|
+
}
|
|
30
|
+
}, [currentSheet]);
|
|
31
|
+
|
|
32
|
+
const navigateCallback = useCallback((sheet: string) => {
|
|
33
|
+
setCurrentSheet(sheet ?? 'landing');
|
|
34
|
+
}, []);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<SideSheet isOpen={isOpen} onClose={onClose} isDismissable={true}>
|
|
38
|
+
<SideSheet.Title title="User settings" />
|
|
39
|
+
<SideSheet.SubTitle subTitle={'Settings for your user in Fusion portal'} />
|
|
40
|
+
<SideSheet.Actions />
|
|
41
|
+
<SideSheet.Content>
|
|
42
|
+
<section style={{ paddingLeft: '0.5em' }}>
|
|
43
|
+
<div>
|
|
44
|
+
<fwc-person-list-item azureId={azureId} />
|
|
45
|
+
</div>
|
|
46
|
+
<Divider />
|
|
47
|
+
<Component azureId={azureId} sheet={currentSheet} navigate={navigateCallback} />
|
|
48
|
+
</section>
|
|
49
|
+
</SideSheet.Content>
|
|
50
|
+
</SideSheet>
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export default PersonSideSheet;
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
* JSX structure for the content of the PersonSidesheet's Features page.
|
|
13
|
+
* @param SheetContentProps
|
|
14
|
+
*/
|
|
15
|
+
export const FeatureSheetContent = ({ navigate }: SheetContentProps) => {
|
|
16
|
+
const [tab, setTab] = useState<number>(0);
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<section>
|
|
20
|
+
<div>
|
|
21
|
+
<div>
|
|
22
|
+
<Button variant="ghost" onClick={() => navigate()}>
|
|
23
|
+
<Icon name="arrow_back" />
|
|
24
|
+
<Icon name="category" />
|
|
25
|
+
My Features
|
|
26
|
+
</Button>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
<Divider />
|
|
30
|
+
<div>
|
|
31
|
+
<Tabs activeTab={tab} onChange={(index) => setTab(index)}>
|
|
32
|
+
<Tabs.List>
|
|
33
|
+
<Tabs.Tab>App features</Tabs.Tab>
|
|
34
|
+
<Tabs.Tab>Portal features</Tabs.Tab>
|
|
35
|
+
</Tabs.List>
|
|
36
|
+
<Tabs.Panels>
|
|
37
|
+
<Tabs.Panel>
|
|
38
|
+
<FeatureTogglerApp />
|
|
39
|
+
</Tabs.Panel>
|
|
40
|
+
<Tabs.Panel>
|
|
41
|
+
<FeatureTogglerPortal />
|
|
42
|
+
</Tabs.Panel>
|
|
43
|
+
</Tabs.Panels>
|
|
44
|
+
</Tabs>
|
|
45
|
+
</div>
|
|
46
|
+
</section>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
* JSX structure for Feature toggler tab for app features in the PersonSidesheet's Feature page.
|
|
9
|
+
*/
|
|
10
|
+
export const FeatureTogglerApp = () => {
|
|
11
|
+
const { features, toggleFeature } = useCurrentAppFeatures();
|
|
12
|
+
return (
|
|
13
|
+
<Styled.SwitchList>
|
|
14
|
+
{features?.map((feature) => {
|
|
15
|
+
return (
|
|
16
|
+
<Styled.SwitchListItem
|
|
17
|
+
key={`feat-${feature.key}`}
|
|
18
|
+
onClick={() => toggleFeature(feature.key)}
|
|
19
|
+
>
|
|
20
|
+
<Styled.SwitchLabel>
|
|
21
|
+
<Typography variant="body_short_bold">{feature.title ?? feature.key}</Typography>
|
|
22
|
+
{feature.description && (
|
|
23
|
+
<Typography variant="body_short_italic">{feature.description}</Typography>
|
|
24
|
+
)}
|
|
25
|
+
</Styled.SwitchLabel>
|
|
26
|
+
<Styled.Switch>
|
|
27
|
+
<Switch checked={feature.enabled} disabled={feature.readonly} />
|
|
28
|
+
</Styled.Switch>
|
|
29
|
+
</Styled.SwitchListItem>
|
|
30
|
+
);
|
|
31
|
+
})}
|
|
32
|
+
</Styled.SwitchList>
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default FeatureTogglerApp;
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
* Content for Feature toggler tab for portal features in the PersonSidesheet's Feature page.
|
|
9
|
+
*/
|
|
10
|
+
export const FeatureTogglerPortal = () => {
|
|
11
|
+
const { features, toggleFeature } = useFrameworkFeatures();
|
|
12
|
+
return (
|
|
13
|
+
<Styled.SwitchList>
|
|
14
|
+
{features?.map((feature) => (
|
|
15
|
+
<Styled.SwitchListItem
|
|
16
|
+
key={`feat-${feature.key}`}
|
|
17
|
+
onClick={() => toggleFeature(feature.key)}
|
|
18
|
+
>
|
|
19
|
+
<Styled.SwitchLabel>
|
|
20
|
+
<Typography variant="body_short_bold">{feature.title ?? feature.key}</Typography>
|
|
21
|
+
<Typography variant="body_short_italic">{feature.description ?? ''}</Typography>
|
|
22
|
+
</Styled.SwitchLabel>
|
|
23
|
+
<Switch checked={feature.enabled} disabled={feature.readonly} />
|
|
24
|
+
</Styled.SwitchListItem>
|
|
25
|
+
))}
|
|
26
|
+
</Styled.SwitchList>
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export default FeatureTogglerPortal;
|