@vuu-ui/vuu-shell 0.5.13 → 0.5.15

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 CHANGED
@@ -1,26 +1,22 @@
1
1
  {
2
2
  "name": "@vuu-ui/vuu-shell",
3
- "version": "0.5.13",
3
+ "version": "0.5.15",
4
4
  "description": "VUU UI Shell",
5
+ "main": "src/index.ts",
5
6
  "author": "heswell",
6
7
  "license": "Apache-2.0",
8
+ "scripts": {
9
+ "build": "node ../../scripts/run-build.mjs"
10
+ },
7
11
  "peerDependencies": {
8
12
  "@salt-ds/core": "1.0.0",
9
13
  "@salt-ds/icons": "1.0.0",
10
14
  "@heswell/salt-lab": "1.0.0-alpha.0",
11
- "@vuu-ui/vuu-data": "0.5.13",
12
- "@vuu-ui/vuu-layout": "0.5.13",
13
- "@vuu-ui/vuu-utils": "0.5.13",
15
+ "@vuu-ui/vuu-data": "0.5.15",
16
+ "@vuu-ui/vuu-layout": "0.5.15",
17
+ "@vuu-ui/vuu-utils": "0.5.15",
14
18
  "classnames": "^2.2.6",
15
19
  "react": "^17.0.2",
16
20
  "react-dom": "^17.0.2"
17
- },
18
- "files": [
19
- "cjs",
20
- "esm",
21
- "index.css",
22
- "index.css.map"
23
- ],
24
- "module": "esm/index.js",
25
- "main": "cjs/index.js"
26
- }
21
+ }
22
+ }
@@ -0,0 +1,60 @@
1
+ import { MenuRpcResponse } from "@vuu-ui/vuu-data";
2
+ import { ColumnDescriptor } from "@vuu-ui/vuu-datagrid-types";
3
+ import { createContext, ReactElement, ReactNode, useContext } from "react";
4
+
5
+ export interface ShellContextProps {
6
+ getDefaultColumnConfig?: (
7
+ tableName: string,
8
+ columnName: string
9
+ ) => Partial<ColumnDescriptor>;
10
+ handleRpcResponse?: (response: MenuRpcResponse) => void;
11
+ }
12
+
13
+ const defaultConfig = {};
14
+
15
+ const ShellContext = createContext<ShellContextProps>(defaultConfig);
16
+
17
+ export interface ShellProviderProps {
18
+ children: ReactNode;
19
+ value: ShellContextProps;
20
+ }
21
+
22
+ const Provider = ({
23
+ children,
24
+ context,
25
+ inheritedContext,
26
+ }: {
27
+ children: ReactNode;
28
+ context?: ShellContextProps;
29
+ inheritedContext?: ShellContextProps;
30
+ }) => {
31
+ // TODO functions provided at multiple levels must be merged
32
+ const mergedContext = {
33
+ ...inheritedContext,
34
+ ...context,
35
+ };
36
+ return (
37
+ <ShellContext.Provider value={mergedContext}>
38
+ {children}
39
+ </ShellContext.Provider>
40
+ );
41
+ };
42
+
43
+ export const ShellContextProvider = ({
44
+ children,
45
+ value,
46
+ }: ShellProviderProps): ReactElement => {
47
+ return (
48
+ <ShellContext.Consumer>
49
+ {(context) => (
50
+ <Provider context={value} inheritedContext={context}>
51
+ {children}
52
+ </Provider>
53
+ )}
54
+ </ShellContext.Consumer>
55
+ );
56
+ };
57
+
58
+ export const useShellContext = () => {
59
+ return useContext(ShellContext);
60
+ };
@@ -0,0 +1,6 @@
1
+ .hwAppHeader {
2
+ justify-content: flex-end;
3
+ display: flex;
4
+ height: 40px;
5
+ border-bottom: solid 1px #ccc;
6
+ }
@@ -0,0 +1,33 @@
1
+ import { HTMLAttributes } from "react";
2
+ import { VuuUser } from "../shell";
3
+ import { UserProfile } from "../user-profile";
4
+ import "./AppHeader.css";
5
+
6
+ export interface AppHeaderProps extends HTMLAttributes<HTMLDivElement> {
7
+ layoutId: string;
8
+ loginUrl?: string;
9
+ onNavigate: (id: string) => void;
10
+ user: VuuUser;
11
+ }
12
+
13
+ export const AppHeader = ({
14
+ layoutId,
15
+ loginUrl,
16
+ onNavigate,
17
+ user,
18
+ ...htmlAttributes
19
+ }: AppHeaderProps) => {
20
+ return (
21
+ <header className="hwAppHeader" {...htmlAttributes}>
22
+ {/* <ToggleButton onChange={toggleColorScheme}>
23
+ theme
24
+ </ToggleButton> */}
25
+ <UserProfile
26
+ layoutId={layoutId}
27
+ loginUrl={loginUrl}
28
+ onNavigate={onNavigate}
29
+ user={user}
30
+ />
31
+ </header>
32
+ );
33
+ };
@@ -0,0 +1 @@
1
+ export * from './AppHeader';
@@ -0,0 +1,56 @@
1
+ import React from "react";
2
+ import cx from "classnames";
3
+ import {
4
+ ComponentRegistry,
5
+ isRegistered,
6
+ Palette,
7
+ PaletteItem,
8
+ } from "@vuu-ui/vuu-layout";
9
+
10
+ const getPaletteItems = (config) => {
11
+ const paletteItems = [];
12
+
13
+ config.forEach((configItem) => {
14
+ const { label, items = [] } = configItem;
15
+ paletteItems.push(
16
+ <div key={label} data-header>
17
+ {label}
18
+ </div>
19
+ );
20
+ items.forEach((paletteItem, i) => {
21
+ const { component, type, props, ...args } = paletteItem;
22
+ if (component) {
23
+ paletteItems.push(
24
+ <PaletteItem {...args} key={i}>
25
+ {component}
26
+ </PaletteItem>
27
+ );
28
+ } else if (type && isRegistered(type)) {
29
+ const Component = ComponentRegistry[type];
30
+ paletteItems.push(
31
+ <PaletteItem {...args} key={i}>
32
+ {React.createElement(Component, {
33
+ ...props,
34
+ key: i,
35
+ })}
36
+ </PaletteItem>
37
+ );
38
+ }
39
+ });
40
+ });
41
+
42
+ return paletteItems;
43
+ };
44
+
45
+ export const AppPalette = ({ className, config, ...props }) => {
46
+ return (
47
+ <Palette
48
+ className={cx("TableList", className)}
49
+ orientation="vertical"
50
+ collapsibleHeaders
51
+ {...props}
52
+ >
53
+ {getPaletteItems(config)}
54
+ </Palette>
55
+ );
56
+ };
@@ -0,0 +1 @@
1
+ export * from './AppPalette';
@@ -0,0 +1,31 @@
1
+ import React from 'react';
2
+ // TODO
3
+ export class ErrorBoundary extends React.Component {
4
+ constructor(props) {
5
+ super(props);
6
+ this.state = { errorMessage: null };
7
+ }
8
+
9
+ static getDerivedStateFromError(error) {
10
+ // Update state so the next render will show the fallback UI.
11
+ return { errorMessage: error.message };
12
+ }
13
+
14
+ componentDidCatch(error, errorInfo) {
15
+ // You can also log the error to an error reporting service
16
+ console.log(error, errorInfo);
17
+ }
18
+
19
+ render() {
20
+ if (this.state.errorMessage) {
21
+ return (
22
+ <>
23
+ <h1>Something went wrong.</h1>
24
+ <p>{this.state.errorMessage}</p>
25
+ </>
26
+ );
27
+ }
28
+
29
+ return this.props.children;
30
+ }
31
+ }
@@ -0,0 +1,52 @@
1
+ import React, { Suspense } from "react";
2
+ import { registerComponent } from "@vuu-ui/vuu-layout";
3
+ import { ErrorBoundary } from "./ErrorBoundary";
4
+ import { Loader } from "./Loader";
5
+ import { importCSS } from "./css-module-loader";
6
+
7
+ export interface FeatureProps<Params extends object | undefined = undefined> {
8
+ height?: number;
9
+ url: string;
10
+ css?: string;
11
+ width?: number;
12
+ params: Params;
13
+ }
14
+
15
+ // const RawFeature = <Params extends object | undefined>({
16
+ function RawFeature<Params extends object | undefined>({
17
+ url,
18
+ css,
19
+ params,
20
+ ...props
21
+ }: FeatureProps<Params>) {
22
+ if (css) {
23
+ // import(/* @vite-ignore */ css, { assert: { type: "css" } }).then(
24
+ // (cssModule) => {
25
+ // document.adoptedStyleSheets = [
26
+ // ...document.adoptedStyleSheets,
27
+ // cssModule.default,
28
+ // ];
29
+ // }
30
+ // );
31
+ // Polyfill until vite build supports import assertions
32
+ // Note: already fully supported in esbuild, so vite dev
33
+ importCSS(css).then((styleSheet) => {
34
+ document.adoptedStyleSheets = [
35
+ ...document.adoptedStyleSheets,
36
+ styleSheet,
37
+ ];
38
+ });
39
+ }
40
+ const LazyFeature = React.lazy(() => import(/* @vite-ignore */ url));
41
+ return (
42
+ <ErrorBoundary>
43
+ <Suspense fallback={<Loader />}>
44
+ <LazyFeature {...props} {...params} />
45
+ </Suspense>
46
+ </ErrorBoundary>
47
+ );
48
+ }
49
+
50
+ export const Feature = React.memo(RawFeature);
51
+ Feature.displayName = "Feature";
52
+ registerComponent("Feature", Feature, "view");
@@ -0,0 +1,2 @@
1
+ // TODO
2
+ export const Loader = () => <div className="hwLoader">loading</div>;
@@ -0,0 +1,6 @@
1
+ export const importCSS = async (path: string) => {
2
+ const container = new CSSStyleSheet();
3
+ return fetch(path)
4
+ .then((x) => x.text())
5
+ .then((x) => container.replace(x));
6
+ };
@@ -0,0 +1 @@
1
+ export * from './Feature';
@@ -0,0 +1,12 @@
1
+ export const getLayoutHistory = async (user) => {
2
+ const history = await fetch(`api/vui/${user.username}`, {})
3
+ .then((response) => {
4
+ return response.ok ? response.json() : null;
5
+ })
6
+ .catch(() => {
7
+ // TODO we should set a layout with a warning here
8
+ console.log(`error getting history`);
9
+ });
10
+
11
+ return history;
12
+ };
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * from "./feature";
2
+ export * from "./login";
3
+ export * from "./shell";
4
+ export * from "./shellTypes";
5
+ export * from "./ShellContextProvider";
@@ -0,0 +1,24 @@
1
+ .vuuLoginPanel {
2
+ --hwTextInput-border: solid 1px #ccc;
3
+ --hwTextInput-height: 28px;
4
+ --hwTextInput-padding: 0 12px;
5
+ --hwTextInput-width: 100%;
6
+ --login-row-height: 60px;
7
+ align-content: center;
8
+ align-items: center;
9
+ border: solid 1px lightgray;
10
+ display: flex;
11
+ flex-direction: column;
12
+ gap: 24px;
13
+ justify-content: center;
14
+ justify-items: center;
15
+ margin: 0 auto;
16
+ padding: 48px 48px 24px 48px;
17
+ width: fit-content;
18
+ }
19
+
20
+ .vuuLoginPanel-login {
21
+ grid-column: 2/3;
22
+ align-self: end;
23
+ justify-self: end;
24
+ }
@@ -0,0 +1,63 @@
1
+ import { ChangeEvent, HTMLAttributes, useState } from "react";
2
+ import { Button } from "@salt-ds/core";
3
+ import { FormField, Input } from "@heswell/salt-lab";
4
+
5
+ import "./LoginPanel.css";
6
+
7
+ const classBase = "vuuLoginPanel";
8
+
9
+ export interface LoginPanelProps
10
+ extends Omit<HTMLAttributes<HTMLDivElement>, "onSubmit"> {
11
+ onSubmit: (username: string, password: string) => void;
12
+ }
13
+
14
+ export const LoginPanel = ({ onSubmit }: LoginPanelProps) => {
15
+ const [username, setUserName] = useState("");
16
+ const [password, setPassword] = useState("");
17
+
18
+ const login = () => {
19
+ onSubmit(username, password);
20
+ };
21
+
22
+ const handleUsername = (
23
+ _event: ChangeEvent<HTMLInputElement>,
24
+ value: string
25
+ ) => {
26
+ setUserName(value);
27
+ };
28
+
29
+ const handlePassword = (
30
+ _event: ChangeEvent<HTMLInputElement>,
31
+ value: string
32
+ ) => {
33
+ setPassword(value);
34
+ };
35
+
36
+ const dataIsValid = username.trim() !== "" && password.trim() !== "";
37
+
38
+ return (
39
+ <div className={classBase}>
40
+ <FormField label="Username" style={{ width: 200 }}>
41
+ <Input value={username} id="text-username" onChange={handleUsername} />
42
+ </FormField>
43
+
44
+ <FormField label="Password" style={{ width: 200 }}>
45
+ <Input
46
+ type="password"
47
+ value={password}
48
+ id="text-password"
49
+ onChange={handlePassword}
50
+ />
51
+ </FormField>
52
+
53
+ <Button
54
+ className={`${classBase}-login`}
55
+ disabled={!dataIsValid}
56
+ onClick={login}
57
+ variant="cta"
58
+ >
59
+ Login
60
+ </Button>
61
+ </div>
62
+ );
63
+ };
@@ -0,0 +1,2 @@
1
+ export * from './LoginPanel';
2
+ export * from './login-utils';
@@ -0,0 +1,21 @@
1
+ const getCookieValue = (name: string) =>
2
+ document.cookie
3
+ .split("; ")
4
+ .find((row) => row.startsWith(`${name}=`))
5
+ ?.split("=")[1];
6
+
7
+ export const getAuthDetailsFromCookies = () => {
8
+ const username = getCookieValue("vuu-username");
9
+ const token = getCookieValue("vuu-auth-token");
10
+ return [username, token];
11
+ };
12
+
13
+ export const redirectToLogin = (loginUrl = "/login.html") => {
14
+ window.location.href = loginUrl;
15
+ };
16
+
17
+ export const logout = (loginUrl?: string) => {
18
+ document.cookie = "vuu-username= ; expires = Thu, 01 Jan 1970 00:00:00 GMT";
19
+ document.cookie = "vuu-auth-token= ; expires = Thu, 01 Jan 1970 00:00:00 GMT";
20
+ redirectToLogin(loginUrl);
21
+ };
package/src/shell.css ADDED
@@ -0,0 +1,15 @@
1
+ .vuuShell {
2
+ background-color: var(--salt-container-primary-background, ivory);
3
+ height: var(--vuuShell-height, 100vh);
4
+ width: var(--vuuShell-width, 100vw);
5
+ }
6
+
7
+ .vuuShell-palette {
8
+ --vuuView-border: none;
9
+ --vuuView-margin: 0;
10
+ }
11
+
12
+ .vuuShell-warningPlaceholder {
13
+ background-color: var(--salt-container-background-high);
14
+ height: 100%;
15
+ }
package/src/shell.tsx ADDED
@@ -0,0 +1,171 @@
1
+ import { connectToServer } from "@vuu-ui/vuu-data";
2
+ import {
3
+ HTMLAttributes,
4
+ MouseEvent,
5
+ ReactElement,
6
+ ReactNode,
7
+ useCallback,
8
+ useEffect,
9
+ useRef,
10
+ useState,
11
+ } from "react";
12
+ import useLayoutConfig from "./use-layout-config";
13
+ import { ShellContextProvider } from "./ShellContextProvider";
14
+ import cx from "classnames";
15
+
16
+ import {
17
+ Chest,
18
+ DraggableLayout,
19
+ Drawer,
20
+ FlexboxLayout as Flexbox,
21
+ LayoutProvider,
22
+ View,
23
+ } from "@vuu-ui/vuu-layout";
24
+
25
+ import { AppHeader } from "./app-header";
26
+ // import { AppPalette } from "./app-palette";
27
+
28
+ import { LayoutJSON } from "@vuu-ui/vuu-layout/src/layout-reducer";
29
+ import "./shell.css";
30
+
31
+ export type VuuUser = {
32
+ username: string;
33
+ token: string;
34
+ };
35
+
36
+ const warningLayout = {
37
+ type: "View",
38
+ props: {
39
+ style: { height: "calc(100% - 6px)" },
40
+ },
41
+ children: [
42
+ {
43
+ props: {
44
+ className: "vuuShell-warningPlaceholder",
45
+ },
46
+ type: "Placeholder",
47
+ },
48
+ ],
49
+ };
50
+
51
+ export interface ShellProps extends HTMLAttributes<HTMLDivElement> {
52
+ children?: ReactNode;
53
+ defaultLayout?: LayoutJSON;
54
+ leftSidePanel?: ReactElement;
55
+ loginUrl?: string;
56
+ // paletteConfig: any;
57
+ serverUrl?: string;
58
+ user: VuuUser;
59
+ }
60
+
61
+ export const Shell = ({
62
+ children,
63
+ className,
64
+ defaultLayout = warningLayout,
65
+ leftSidePanel,
66
+ loginUrl,
67
+ serverUrl,
68
+ user,
69
+ ...htmlAttributes
70
+ }: ShellProps) => {
71
+ const paletteView = useRef<HTMLDivElement>(null);
72
+ const [open, setOpen] = useState(false);
73
+ const layoutId = useRef("latest");
74
+
75
+ const [layout, setLayoutConfig, loadLayoutById] = useLayoutConfig(
76
+ user,
77
+ defaultLayout
78
+ );
79
+
80
+ const handleLayoutChange = useCallback(
81
+ (layout) => {
82
+ setLayoutConfig(layout);
83
+ },
84
+ [setLayoutConfig]
85
+ );
86
+
87
+ const handleDrawerClick = (e: MouseEvent<HTMLElement>) => {
88
+ const target = e.target as HTMLElement;
89
+ if (!paletteView.current?.contains(target)) {
90
+ setOpen(!open);
91
+ }
92
+ };
93
+
94
+ const handleNavigate = useCallback(
95
+ (id) => {
96
+ layoutId.current = id;
97
+ loadLayoutById(id);
98
+ },
99
+ [loadLayoutById]
100
+ );
101
+
102
+ useEffect(() => {
103
+ if (serverUrl && user.token) {
104
+ connectToServer(serverUrl, user.token);
105
+ }
106
+ }, [serverUrl, user.token]);
107
+
108
+ const getDrawers = () => {
109
+ const drawers: ReactElement[] = [];
110
+ if (leftSidePanel) {
111
+ drawers.push(
112
+ <Drawer
113
+ key="left-panel"
114
+ onClick={handleDrawerClick}
115
+ open={open}
116
+ position="left"
117
+ inline
118
+ peekaboo
119
+ sizeOpen={200}
120
+ toggleButton="end"
121
+ >
122
+ <View
123
+ className="vuuShell-palette"
124
+ id="vw-app-palette"
125
+ key="app-palette"
126
+ ref={paletteView}
127
+ style={{ height: "100%" }}
128
+ >
129
+ {leftSidePanel}
130
+ </View>
131
+ </Drawer>
132
+ );
133
+ }
134
+
135
+ return drawers;
136
+ };
137
+
138
+ return (
139
+ // ShellContext TBD
140
+ <ShellContextProvider value={undefined}>
141
+ <LayoutProvider layout={layout} onLayoutChange={handleLayoutChange}>
142
+ <DraggableLayout
143
+ className={cx("vuuShell", className)}
144
+ {...htmlAttributes}
145
+ >
146
+ <Flexbox
147
+ className="App"
148
+ style={{ flexDirection: "column", height: "100%", width: "100%" }}
149
+ >
150
+ <AppHeader
151
+ layoutId={layoutId.current}
152
+ loginUrl={loginUrl}
153
+ user={user}
154
+ onNavigate={handleNavigate}
155
+ />
156
+ <Chest style={{ flex: 1 }}>
157
+ {getDrawers().concat(
158
+ <DraggableLayout
159
+ dropTarget
160
+ key="main-content"
161
+ style={{ width: "100%", height: "100%" }}
162
+ />
163
+ )}
164
+ </Chest>
165
+ </Flexbox>
166
+ </DraggableLayout>
167
+ </LayoutProvider>
168
+ {children}
169
+ </ShellContextProvider>
170
+ );
171
+ };
@@ -0,0 +1,18 @@
1
+ declare global {
2
+ const vuuConfig: Promise<VuuConfig>;
3
+ }
4
+
5
+ export interface FeatureConfig {
6
+ name: string;
7
+ title: string;
8
+ url: string;
9
+ css?: string;
10
+ }
11
+
12
+ export type Features = {
13
+ [key: string]: FeatureConfig;
14
+ };
15
+ export interface VuuConfig {
16
+ features?: Features;
17
+ websocketUrl: string;
18
+ }
@@ -0,0 +1,6 @@
1
+ import { useState } from 'react';
2
+
3
+ export const useForceRender = () => {
4
+ const [, forceUpdate] = useState({});
5
+ return forceUpdate;
6
+ };