From 00376c1c24b63ed3c4a4ec50bab0256e5850cf7c Mon Sep 17 00:00:00 2001 From: jlandowner Date: Thu, 13 Jun 2024 17:55:40 +0900 Subject: [PATCH 1/2] Refactoring: apply prettier lint for all ts and tsx files --- web/dashboard-ui/src/__tests__/App.test.tsx | 6 +- .../__tests__/components/AuthRoute.spec.tsx | 95 +- .../src/__tests__/components/Base64.spec.ts | 35 +- .../components/ContextProvider.spec.tsx | 162 ++-- .../components/LoginProvider.spec.tsx | 377 ++++---- .../components/MyThemeProvider.spec.tsx | 30 +- .../components/ProgressProvider.spec.tsx | 42 +- .../views/atoms/AlertTooltip.spec.tsx | 30 +- .../__tests__/views/atoms/NameAvatar.spec.tsx | 27 +- .../views/atoms/PasswordTextField.spec.tsx | 32 +- .../views/atoms/TextFieldLabel.spec.tsx | 28 +- .../organisms/PasswordChangeDialog.spec.tsx | 283 ++++-- .../views/organisms/UserModule.spec.tsx | 389 ++++++--- .../organisms/UserNameChangeDialog.spec.tsx | 183 ++-- .../views/organisms/WorkspaceModule.spec.tsx | 816 ++++++++++++------ .../src/__tests__/views/pages/SignIn.spec.tsx | 101 ++- web/dashboard-ui/src/components/AuthRoute.tsx | 26 +- web/dashboard-ui/src/components/Base64.ts | 85 +- .../src/components/ContextProvider.tsx | 34 +- .../src/components/LoginProvider.tsx | 32 +- .../src/components/MyThemeProvider.tsx | 24 +- .../src/components/PageSettingsProvider.tsx | 61 +- .../src/components/ProgressProvider.tsx | 51 +- web/dashboard-ui/src/index.tsx | 12 +- web/dashboard-ui/src/reportWebVitals.ts | 4 +- .../src/services/DashboardServices.ts | 41 +- web/dashboard-ui/src/setupTests.ts | 8 +- .../src/views/atoms/AlertTooltip.tsx | 20 +- .../src/views/atoms/EditableTypography.tsx | 146 ++-- .../src/views/atoms/EllipsisTypography.tsx | 88 +- .../src/views/atoms/EventsDataGrid.tsx | 22 +- .../src/views/atoms/NameAvatar.tsx | 43 +- .../src/views/atoms/PasswordTextField.tsx | 52 +- .../src/views/atoms/SelectableChips.tsx | 82 +- .../src/views/atoms/TextFieldLabel.tsx | 31 +- .../organisms/AuthenticatorManageDialog.tsx | 262 ++++-- .../src/views/organisms/EventDetailDialog.tsx | 49 +- .../src/views/organisms/EventModule.tsx | 6 +- .../organisms/NetworkRuleActionDialog.tsx | 331 ++++--- .../views/organisms/PasswordChangeDialog.tsx | 143 ++- .../src/views/organisms/PasswordDialog.tsx | 78 +- .../src/views/organisms/RoleChangeDialog.tsx | 178 +++- .../src/views/organisms/UserActionDialog.tsx | 768 ++++++++++++----- .../organisms/UserAddonsChangeDialog.tsx | 291 ++++--- .../src/views/organisms/UserModule.tsx | 267 +++--- .../views/organisms/UserNameChangeDialog.tsx | 72 +- .../views/organisms/WorkspaceActionDialog.tsx | 335 +++++-- .../src/views/organisms/WorkspaceModule.tsx | 66 +- .../src/views/pages/EventPage.tsx | 2 +- web/dashboard-ui/src/views/pages/SignIn.tsx | 195 +++-- web/dashboard-ui/src/views/pages/UserPage.tsx | 518 +++++++---- .../src/views/pages/WorkspacePage.tsx | 311 +++---- .../src/views/templates/PageTemplate.tsx | 175 ++-- web/dashboard-ui/vite.config.ts | 28 +- 54 files changed, 4946 insertions(+), 2627 deletions(-) diff --git a/web/dashboard-ui/src/__tests__/App.test.tsx b/web/dashboard-ui/src/__tests__/App.test.tsx index 65bc11a6..657c902d 100644 --- a/web/dashboard-ui/src/__tests__/App.test.tsx +++ b/web/dashboard-ui/src/__tests__/App.test.tsx @@ -1,8 +1,6 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import App from '../App'; +import { it } from "vitest"; -it('renders learn react link', () => { +it("renders learn react link", () => { //render(); // const linkElement = screen.getByText(/learn react/i); // expect(linkElement).toBeInTheDocument(); diff --git a/web/dashboard-ui/src/__tests__/components/AuthRoute.spec.tsx b/web/dashboard-ui/src/__tests__/components/AuthRoute.spec.tsx index dc6680b4..0e111767 100644 --- a/web/dashboard-ui/src/__tests__/components/AuthRoute.spec.tsx +++ b/web/dashboard-ui/src/__tests__/components/AuthRoute.spec.tsx @@ -1,15 +1,23 @@ -import { Box } from '@mui/system'; -import { cleanup, render } from '@testing-library/react'; -import React from 'react'; -import { MemoryRouter, Route, Routes } from 'react-router-dom'; -import { afterEach, beforeEach, describe, expect, it, MockedFunction, vi } from "vitest"; -import { AuthRoute } from '../../components/AuthRoute'; -import { useLogin } from '../../components/LoginProvider'; +import { Box } from "@mui/system"; +import { cleanup, render } from "@testing-library/react"; +import React from "react"; +import { MemoryRouter, Route, Routes } from "react-router-dom"; +import { + MockedFunction, + afterEach, + beforeEach, + describe, + expect, + it, + vi, +} from "vitest"; +import { AuthRoute } from "../../components/AuthRoute"; +import { useLogin } from "../../components/LoginProvider"; //-------------------------------------------------- // mock definition //-------------------------------------------------- -vi.mock('../../components/LoginProvider'); +vi.mock("../../components/LoginProvider"); type MockedMemberFunction any> = { [P in keyof ReturnType]: MockedFunction[P]>; @@ -19,8 +27,7 @@ type MockedMemberFunction any> = { // test //----------------------------------------------- -describe('AuthRoute', () => { - +describe("AuthRoute", () => { const useLoginMock = useLogin as MockedFunction; const loginMock: MockedMemberFunction = { loginUser: undefined as any, @@ -30,7 +37,7 @@ describe('AuthRoute', () => { updataPassword: vi.fn(), refreshUserInfo: vi.fn(), clearLoginUser: vi.fn(), - } + }; beforeEach(async () => { useLoginMock.mockReturnValue(loginMock); @@ -46,45 +53,67 @@ describe('AuthRoute', () => { signin} /> -
workspace
} /> -
user
} /> - 404} /> + +
workspace
+ + } + /> + +
user
+ + } + /> + 404} />
-
+ ); - } + }; - it('normal not login =>/signin', async () => { - const { asFragment } = routerTester('/workspace'); + it("normal not login =>/signin", async () => { + const { asFragment } = routerTester("/workspace"); expect(asFragment()).toMatchSnapshot(); }); - it('normal not login admin =>/signin', async () => { - const { asFragment } = routerTester('/user'); + it("normal not login admin =>/signin", async () => { + const { asFragment } = routerTester("/user"); expect(asFragment()).toMatchSnapshot(); }); - it('normal login =>/workspace', async () => { - useLoginMock.mockReturnValue({ loginUser: { userName: 'user1' } } as ReturnType); - const { asFragment } = routerTester('/workspace'); + it("normal login =>/workspace", async () => { + useLoginMock.mockReturnValue({ + loginUser: { userName: "user1" }, + } as ReturnType); + const { asFragment } = routerTester("/workspace"); expect(asFragment()).toMatchSnapshot(); }); - it('normal login admin => /user', async () => { - useLoginMock.mockReturnValue({ loginUser: { userName: 'user1', roles: ["CosmoAdmin"] } } as ReturnType); - const { asFragment } = routerTester('/user'); + it("normal login admin => /user", async () => { + useLoginMock.mockReturnValue({ + loginUser: { userName: "user1", roles: ["CosmoAdmin"] }, + } as ReturnType); + const { asFragment } = routerTester("/user"); expect(asFragment()).toMatchSnapshot(); }); - it('normal login admin page not admin user page => 404', async () => { - useLoginMock.mockReturnValue({ loginUser: { userName: 'user1' } } as ReturnType); - const { asFragment } = routerTester('/user'); + it("normal login admin page not admin user page => 404", async () => { + useLoginMock.mockReturnValue({ + loginUser: { userName: "user1" }, + } as ReturnType); + const { asFragment } = routerTester("/user"); expect(asFragment()).toMatchSnapshot(); }); - it('normal login another page => 404', async () => { - useLoginMock.mockReturnValue({ loginUser: { userName: 'user1' } } as ReturnType); - const { asFragment } = routerTester('/xxx'); + it("normal login another page => 404", async () => { + useLoginMock.mockReturnValue({ + loginUser: { userName: "user1" }, + } as ReturnType); + const { asFragment } = routerTester("/xxx"); expect(asFragment()).toMatchSnapshot(); }); -}); \ No newline at end of file +}); diff --git a/web/dashboard-ui/src/__tests__/components/Base64.spec.ts b/web/dashboard-ui/src/__tests__/components/Base64.spec.ts index 625c09ab..301c271a 100644 --- a/web/dashboard-ui/src/__tests__/components/Base64.spec.ts +++ b/web/dashboard-ui/src/__tests__/components/Base64.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { base64url } from '../../components/Base64'; +import { base64url } from "../../components/Base64"; //----------------------------------------------- // test @@ -23,13 +23,14 @@ function toBuffer(arrayBuffer) { return buffer; } -describe('base64url', () => { - describe('encode', () => { - it('✅ ok', async () => { +describe("base64url", () => { + describe("encode", () => { + it("✅ ok", async () => { const tests = [ { name: "1", - input: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", + input: + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", }, { name: "1", @@ -53,31 +54,33 @@ describe('base64url', () => { }, ]; for (const t of tests) { - const raw = Buffer.from(t.input, 'utf8'); + const raw = Buffer.from(t.input, "utf8"); const got = base64url.encode(raw); - const want = raw.toString('base64url'); + const want = raw.toString("base64url"); expect(got).toEqual(want); } }); }); - describe('decode', () => { - it('✅ ok', async () => { - const base64str = 'QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ejAxMjM0NTY3ODkrLw=='; + describe("decode", () => { + it("✅ ok", async () => { + const base64str = + "QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ejAxMjM0NTY3ODkrLw=="; const got = base64url.decode(base64str); - const want = Buffer.from(base64str, 'base64url'); + const want = Buffer.from(base64str, "base64url"); expect(got).toEqual(toArrayBuffer(want)); }); - it('✅ ok: no padding', async () => { - const base64strWithPadding = 'QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ejAxMjM0NTY3ODkrLw=='; - const want = Buffer.from(base64strWithPadding, 'base64url'); + it("✅ ok: no padding", async () => { + const base64strWithPadding = + "QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ejAxMjM0NTY3ODkrLw=="; + const want = Buffer.from(base64strWithPadding, "base64url"); - const base64strNoPadding = 'QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ejAxMjM0NTY3ODkrLw'; + const base64strNoPadding = + "QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ejAxMjM0NTY3ODkrLw"; const got = base64url.decode(base64strNoPadding); expect(got).toEqual(toArrayBuffer(want)); }); }); - }); diff --git a/web/dashboard-ui/src/__tests__/components/ContextProvider.spec.tsx b/web/dashboard-ui/src/__tests__/components/ContextProvider.spec.tsx index 0537a4f5..70ee937d 100644 --- a/web/dashboard-ui/src/__tests__/components/ContextProvider.spec.tsx +++ b/web/dashboard-ui/src/__tests__/components/ContextProvider.spec.tsx @@ -1,6 +1,6 @@ import { Button, Dialog } from "@mui/material"; import { act, cleanup, render, screen } from "@testing-library/react"; -import userEvent from '@testing-library/user-event'; +import userEvent from "@testing-library/user-event"; import React, { useState } from "react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { DialogContext, ModuleContext } from "../../components/ContextProvider"; @@ -9,88 +9,129 @@ import { DialogContext, ModuleContext } from "../../components/ContextProvider"; // test //-------------------------------------------------- describe("DialogContext", () => { - - beforeEach(async () => { - }); + beforeEach(async () => {}); afterEach(() => { vi.restoreAllMocks(); cleanup(); }); - const TestDialog: React.VFC<{ onClose: () => void, hoge: string }> = ({ onClose, hoge }) => { - return ( - onClose()}> - - ) - } + const TestDialog: React.VFC<{ onClose: () => void; hoge: string }> = ({ + onClose, + hoge, + }) => { + return onClose()}>; + }; - const testDialogContext = DialogContext<{ hoge: string }>( - props => ()); + const testDialogContext = DialogContext<{ hoge: string }>((props) => ( + + )); it("open/close", async () => { - const Stub = () => { const dispatch = testDialogContext.useDispatch(); - return (<> - - - ); - } + return ( + <> + + + + ); + }; const user = userEvent.setup(); - render(); - await act(async () => { expect(document.body).toMatchSnapshot('1.initial render'); }); - await act(async () => { await user.click(screen.getByText('open')); }); - await act(async () => { expect(document.body).toMatchSnapshot('2.opend'); }); - await act(async () => { await user.click(screen.getByText('close')); }); - await act(async () => { expect(document.body).toMatchSnapshot('3.closed'); }); + render( + + + + ); + await act(async () => { + expect(document.body).toMatchSnapshot("1.initial render"); + }); + await act(async () => { + await user.click(screen.getByText("open")); + }); + await act(async () => { + expect(document.body).toMatchSnapshot("2.opend"); + }); + await act(async () => { + await user.click(screen.getByText("close")); + }); + await act(async () => { + expect(document.body).toMatchSnapshot("3.closed"); + }); }); it("close esc", async () => { - const Stub = () => { const dispatch = testDialogContext.useDispatch(); - return (<> - - - ); - } + return ( + <> + + + + ); + }; const user = userEvent.setup(); - render(); - await act(async () => { await user.click(screen.getByText('open')); }); - await act(async () => { await user.keyboard('{Esc}'); }); - await act(async () => { expect(document.body).toMatchSnapshot('closed'); }); + render( + + + + ); + await act(async () => { + await user.click(screen.getByText("open")); + }); + await act(async () => { + await user.keyboard("{Esc}"); + }); + await act(async () => { + expect(document.body).toMatchSnapshot("closed"); + }); }); - }); - -describe('ModuleContext', () => { - - it('normal', async () => { - +describe("ModuleContext", () => { + it("normal", async () => { const useTestHook = () => { const [state, setState] = useState("aaaa"); - return { state, setState } - } + return { state, setState }; + }; const TestContext = ModuleContext(useTestHook); const useTest = TestContext.useContext; - const Component1: React.FC> = ({ children }) => { + const Component1: React.FC> = ({ + children, + }) => { const { state, setState } = useTest(); - return (
- -
{state}
-
); - } - const Component2: React.FC> = ({ children }) => { + return ( +
+ +
{state}
+
+ ); + }; + const Component2: React.FC> = ({ + children, + }) => { const { state, setState } = useTest(); - return (
- -
{state}
-
); - } + return ( +
+ +
{state}
+
+ ); + }; const user = userEvent.setup(); render( @@ -99,12 +140,11 @@ describe('ModuleContext', () => { ); - await user.click(screen.getByText('button1')); - expect(screen.getByTestId('textbox1')).toHaveTextContent('11111'); - expect(screen.getByTestId('textbox2')).toHaveTextContent('11111'); - await user.click(screen.getByText('button2')); - expect(screen.getByTestId('textbox1')).toHaveTextContent('22222'); - expect(screen.getByTestId('textbox2')).toHaveTextContent('22222'); + await user.click(screen.getByText("button1")); + expect(screen.getByTestId("textbox1")).toHaveTextContent("11111"); + expect(screen.getByTestId("textbox2")).toHaveTextContent("11111"); + await user.click(screen.getByText("button2")); + expect(screen.getByTestId("textbox1")).toHaveTextContent("22222"); + expect(screen.getByTestId("textbox2")).toHaveTextContent("22222"); }); - }); diff --git a/web/dashboard-ui/src/__tests__/components/LoginProvider.spec.tsx b/web/dashboard-ui/src/__tests__/components/LoginProvider.spec.tsx index 2a7ce80f..3a0782e4 100644 --- a/web/dashboard-ui/src/__tests__/components/LoginProvider.spec.tsx +++ b/web/dashboard-ui/src/__tests__/components/LoginProvider.spec.tsx @@ -1,28 +1,44 @@ import { Timestamp } from "@bufbuild/protobuf"; import { cleanup, renderHook, waitFor } from "@testing-library/react"; import { useSnackbar } from "notistack"; -import React from 'react'; +import React from "react"; import { act } from "react-dom/test-utils"; -import { afterEach, beforeEach, describe, expect, it, MockedFunction, vi } from "vitest"; +import { + MockedFunction, + afterEach, + beforeEach, + describe, + expect, + it, + vi, +} from "vitest"; import { LoginProvider, useLogin } from "../../components/LoginProvider"; import { useProgress } from "../../components/ProgressProvider"; -import { LoginResponse, VerifyResponse } from "../../proto/gen/dashboard/v1alpha1/auth_service_pb"; +import { + LoginResponse, + VerifyResponse, +} from "../../proto/gen/dashboard/v1alpha1/auth_service_pb"; import { User } from "../../proto/gen/dashboard/v1alpha1/user_pb"; -import { GetUserResponse, UpdateUserPasswordResponse } from "../../proto/gen/dashboard/v1alpha1/user_service_pb"; -import { useAuthService, useUserService } from "../../services/DashboardServices"; +import { + GetUserResponse, + UpdateUserPasswordResponse, +} from "../../proto/gen/dashboard/v1alpha1/user_service_pb"; +import { + useAuthService, + useUserService, +} from "../../services/DashboardServices"; //-------------------------------------------------- // mock definition //-------------------------------------------------- -vi.mock('notistack'); -vi.mock('../../services/DashboardServices'); -vi.mock('../../components/ProgressProvider'); -vi.mock('react-router-dom', () => ({ +vi.mock("notistack"); +vi.mock("../../services/DashboardServices"); +vi.mock("../../components/ProgressProvider"); +vi.mock("react-router-dom", () => ({ //useHistory: vi.fn(), })); - type MockedMemberFunction any> = { [P in keyof ReturnType]: MockedFunction[P]>; }; @@ -31,20 +47,20 @@ const useSnackbarMock = useSnackbar as MockedFunction; const snackbarMock: MockedMemberFunction = { enqueueSnackbar: vi.fn(), closeSnackbar: vi.fn(), -} +}; const useProgressMock = useProgress as MockedFunction; const progressMock: MockedMemberFunction = { setMask: vi.fn(), releaseMask: vi.fn(), -} +}; const authService = useAuthService as MockedFunction; const authMock: MockedMemberFunction = { verify: vi.fn(), login: vi.fn(), logout: vi.fn(), -} +}; const userService = useUserService as MockedFunction; const userMock: MockedMemberFunction = { @@ -54,15 +70,14 @@ const userMock: MockedMemberFunction = { createUser: vi.fn(), updateUserDisplayName: vi.fn(), updateUserPassword: vi.fn(), - updateUserRole: vi.fn() -} + updateUserRole: vi.fn(), +}; //----------------------------------------------- // test //----------------------------------------------- -describe('LoginProvider', () => { - +describe("LoginProvider", () => { beforeEach(async () => { useSnackbarMock.mockReturnValue(snackbarMock); useProgressMock.mockReturnValue(progressMock); @@ -79,225 +94,287 @@ describe('LoginProvider', () => { async function renderUseLogin() { const utils = renderHook(() => useLogin(), { - wrapper: ({ children }) => ({children}), + wrapper: ({ children }) => {children}, + }); + await waitFor(async () => { + expect(utils.result.current).not.toBeNull(); }); - await waitFor(async () => { expect(utils.result.current).not.toBeNull(); }); return utils; } - describe('verify', () => { - - it('✅ ok', async () => { - authMock.verify.mockResolvedValueOnce(new VerifyResponse({ - userName: 'user1', - expireAt: Timestamp.fromDate(new Date("2022/11/4")), - requirePasswordUpdate: false, - })); - userMock.getUser.mockResolvedValue(new GetUserResponse({ - user: new User({ name: 'user1', roles: ["CosmoAdmin"], displayName: 'user1 name' }), - })); + describe("verify", () => { + it("✅ ok", async () => { + authMock.verify.mockResolvedValueOnce( + new VerifyResponse({ + userName: "user1", + expireAt: Timestamp.fromDate(new Date("2022/11/4")), + requirePasswordUpdate: false, + }) + ); + userMock.getUser.mockResolvedValue( + new GetUserResponse({ + user: new User({ + name: "user1", + roles: ["CosmoAdmin"], + displayName: "user1 name", + }), + }) + ); const { result } = await renderUseLogin(); expect(result.current.loginUser).toMatchSnapshot(); }); - - it('❌ ng / id is undefined', async () => { - authMock.verify.mockResolvedValueOnce(new VerifyResponse({ - userName: undefined as any, - expireAt: Timestamp.fromDate(new Date("2022/11/4")), - requirePasswordUpdate: false, - })); + it("❌ ng / id is undefined", async () => { + authMock.verify.mockResolvedValueOnce( + new VerifyResponse({ + userName: undefined as any, + expireAt: Timestamp.fromDate(new Date("2022/11/4")), + requirePasswordUpdate: false, + }) + ); const { result } = await renderUseLogin(); expect(result.current.loginUser).toMatchSnapshot(); }); - - it('❌ ng', async () => { - - authMock.verify.mockRejectedValue(new Error('[mock] verify error')); + it("❌ ng", async () => { + authMock.verify.mockRejectedValue(new Error("[mock] verify error")); const { result } = await renderUseLogin(); expect(result.current.loginUser).toMatchSnapshot(); }); - it('❌ getUser error', async () => { - - authMock.verify.mockResolvedValueOnce(new VerifyResponse({ - userName: 'user1', - expireAt: Timestamp.fromDate(new Date("2022/11/4")), - requirePasswordUpdate: false, - })); + it("❌ getUser error", async () => { + authMock.verify.mockResolvedValueOnce( + new VerifyResponse({ + userName: "user1", + expireAt: Timestamp.fromDate(new Date("2022/11/4")), + requirePasswordUpdate: false, + }) + ); - userMock.getUser.mockRejectedValue(new Error('[mock] getUser error')); + userMock.getUser.mockRejectedValue(new Error("[mock] getUser error")); const { result } = await renderUseLogin(); expect(result.current.loginUser).toMatchSnapshot(); }); - }); - - describe('login', () => { - - it('✅ ok', async () => { - authMock.login.mockResolvedValue(new LoginResponse({ - userName: 'user1', - expireAt: Timestamp.fromDate(new Date("2022/11/4")), - requirePasswordUpdate: true, - })); - userMock.getUser.mockResolvedValue(new GetUserResponse({ - user: new User({ name: 'user1', roles: ["CosmoAdmin"], displayName: 'user1 name' }), - })); + describe("login", () => { + it("✅ ok", async () => { + authMock.login.mockResolvedValue( + new LoginResponse({ + userName: "user1", + expireAt: Timestamp.fromDate(new Date("2022/11/4")), + requirePasswordUpdate: true, + }) + ); + userMock.getUser.mockResolvedValue( + new GetUserResponse({ + user: new User({ + name: "user1", + roles: ["CosmoAdmin"], + displayName: "user1 name", + }), + }) + ); const { result } = await renderUseLogin(); // await act(async () => { - await expect(result.current.login('user1', 'password1')).resolves.toMatchSnapshot(); + await expect( + result.current.login("user1", "password1") + ).resolves.toMatchSnapshot(); // }); - await waitFor(async () => { expect(result.current.loginUser).not.toBeUndefined(); }); + await waitFor(async () => { + expect(result.current.loginUser).not.toBeUndefined(); + }); expect(result.current.loginUser).toMatchSnapshot(); }); - - it('❌ ng', async () => { - authMock.login.mockResolvedValue(new LoginResponse({ - userName: 'user1', - expireAt: Timestamp.fromDate(new Date("2022/11/4")), - requirePasswordUpdate: true, - })); - userMock.getUser.mockResolvedValue(new GetUserResponse({ - user: { name: 'user1', roles: ["CosmoAdmin"], displayName: 'user1 name' }, - })); - authMock.login.mockRejectedValue(new Error('[mock] login error')); + it("❌ ng", async () => { + authMock.login.mockResolvedValue( + new LoginResponse({ + userName: "user1", + expireAt: Timestamp.fromDate(new Date("2022/11/4")), + requirePasswordUpdate: true, + }) + ); + userMock.getUser.mockResolvedValue( + new GetUserResponse({ + user: { + name: "user1", + roles: ["CosmoAdmin"], + displayName: "user1 name", + }, + }) + ); + authMock.login.mockRejectedValue(new Error("[mock] login error")); const { result } = await renderUseLogin(); await act(async () => { - await expect(result.current.login('user1', 'password1')).rejects.toMatchSnapshot(); + await expect( + result.current.login("user1", "password1") + ).rejects.toMatchSnapshot(); + }); + await waitFor(async () => { + expect(result.current).not.toBeNull(); }); - await waitFor(async () => { expect(result.current).not.toBeNull(); }); expect(result.current.loginUser).toMatchSnapshot(); }); - - it('❌ getUser error', async () => { + it("❌ getUser error", async () => { const { result } = await renderUseLogin(); - authMock.login.mockResolvedValue(new LoginResponse({ - userName: 'user1', - expireAt: Timestamp.fromDate(new Date("2022/11/4")), - requirePasswordUpdate: true, - })); + authMock.login.mockResolvedValue( + new LoginResponse({ + userName: "user1", + expireAt: Timestamp.fromDate(new Date("2022/11/4")), + requirePasswordUpdate: true, + }) + ); - userMock.getUser.mockRejectedValue(new Error('[mock] getUser error')); + userMock.getUser.mockRejectedValue(new Error("[mock] getUser error")); await act(async () => { - await expect(result.current.login('user1', 'password1')).rejects.toMatchSnapshot(); + await expect( + result.current.login("user1", "password1") + ).rejects.toMatchSnapshot(); }); expect(result.current.loginUser).toMatchSnapshot(); }); - }); - - describe('refreshUserInfo', () => { - - describe('not login', () => { - - it('✅ ok', async () => { + describe("refreshUserInfo", () => { + describe("not login", () => { + it("✅ ok", async () => { const { result } = await renderUseLogin(); - await expect(result.current.refreshUserInfo()).resolves.toMatchSnapshot(); + await expect( + result.current.refreshUserInfo() + ).resolves.toMatchSnapshot(); expect(result.current.loginUser).toMatchSnapshot(); }); - }); - describe('logined', () => { - + describe("logined", () => { beforeEach(async () => { - authMock.verify.mockResolvedValueOnce(new VerifyResponse({ - userName: 'user1', - expireAt: Timestamp.fromDate(new Date("2022/11/4")), - requirePasswordUpdate: false, - })); - userMock.getUser.mockResolvedValue(new GetUserResponse({ - user: { name: 'user1', roles: ["CosmoAdmin"], displayName: 'user1 name' }, - })); + authMock.verify.mockResolvedValueOnce( + new VerifyResponse({ + userName: "user1", + expireAt: Timestamp.fromDate(new Date("2022/11/4")), + requirePasswordUpdate: false, + }) + ); + userMock.getUser.mockResolvedValue( + new GetUserResponse({ + user: { + name: "user1", + roles: ["CosmoAdmin"], + displayName: "user1 name", + }, + }) + ); }); - it('✅ ok', async () => { + it("✅ ok", async () => { const { result } = await renderUseLogin(); await act(async () => { - await expect(result.current.refreshUserInfo()).resolves.toMatchSnapshot(); + await expect( + result.current.refreshUserInfo() + ).resolves.toMatchSnapshot(); }); expect(result.current.loginUser).toMatchSnapshot(); }); - }); }); - - describe('logout', () => { - + describe("logout", () => { beforeEach(async () => { - authMock.verify.mockResolvedValueOnce(new VerifyResponse({ - userName: 'user1', - expireAt: Timestamp.fromDate(new Date("2022/11/4")), - requirePasswordUpdate: false, - })); - userMock.getUser.mockResolvedValue(new GetUserResponse({ - user: { name: 'user1', roles: ["CosmoAdmin"], displayName: 'user1 name' }, - })); + authMock.verify.mockResolvedValueOnce( + new VerifyResponse({ + userName: "user1", + expireAt: Timestamp.fromDate(new Date("2022/11/4")), + requirePasswordUpdate: false, + }) + ); + userMock.getUser.mockResolvedValue( + new GetUserResponse({ + user: { + name: "user1", + roles: ["CosmoAdmin"], + displayName: "user1 name", + }, + }) + ); }); - it('✅ ok', async () => { + it("✅ ok", async () => { const { result } = await renderUseLogin(); await act(async () => { await expect(result.current.logout()).resolves.toMatchSnapshot(); }); - await waitFor(async () => { expect(result.current.loginUser).toBeUndefined(); }); - expect(authMock.logout.mock.calls).toMatchSnapshot('logout calls'); + await waitFor(async () => { + expect(result.current.loginUser).toBeUndefined(); + }); + expect(authMock.logout.mock.calls).toMatchSnapshot("logout calls"); }); - it('❌ error', async () => { + it("❌ error", async () => { const { result } = await renderUseLogin(); - authMock.logout.mockRejectedValue(new Error('[mock] logout error')); + authMock.logout.mockRejectedValue(new Error("[mock] logout error")); await act(async () => { await expect(result.current.logout()).rejects.toMatchSnapshot(); }); - await waitFor(async () => { expect(result.current.loginUser).toBeUndefined(); }); - expect(authMock.logout.mock.calls).toMatchSnapshot('logout calls'); + await waitFor(async () => { + expect(result.current.loginUser).toBeUndefined(); + }); + expect(authMock.logout.mock.calls).toMatchSnapshot("logout calls"); }); - }); - - describe('updatePassword', () => { - + describe("updatePassword", () => { beforeEach(async () => { - authMock.verify.mockResolvedValueOnce(new VerifyResponse({ - userName: 'user1', - expireAt: Timestamp.fromDate(new Date("2022/11/4")), - requirePasswordUpdate: false, - })); - userMock.getUser.mockResolvedValue(new GetUserResponse({ - user: { name: 'user1', roles: ["CosmoAdmin"], displayName: 'user1 name' }, - })); + authMock.verify.mockResolvedValueOnce( + new VerifyResponse({ + userName: "user1", + expireAt: Timestamp.fromDate(new Date("2022/11/4")), + requirePasswordUpdate: false, + }) + ); + userMock.getUser.mockResolvedValue( + new GetUserResponse({ + user: { + name: "user1", + roles: ["CosmoAdmin"], + displayName: "user1 name", + }, + }) + ); }); - it('✅ ok', async () => { + it("✅ ok", async () => { const { result } = await renderUseLogin(); - userMock.updateUserPassword.mockResolvedValue(new UpdateUserPasswordResponse({ - message: "ok", - })); - await expect(result.current.updataPassword('oldpw', 'newpw')).resolves.toMatchSnapshot(); - expect(userMock.updateUserPassword.mock.calls).toMatchSnapshot('putUserPassword calls'); + userMock.updateUserPassword.mockResolvedValue( + new UpdateUserPasswordResponse({ + message: "ok", + }) + ); + await expect( + result.current.updataPassword("oldpw", "newpw") + ).resolves.toMatchSnapshot(); + expect(userMock.updateUserPassword.mock.calls).toMatchSnapshot( + "putUserPassword calls" + ); }); - it('❌ error', async () => { + it("❌ error", async () => { const { result } = await renderUseLogin(); - userMock.updateUserPassword.mockRejectedValue(new Error('[mock] updateUserPassword error')); - await expect(result.current.updataPassword('oldpw', 'newpw')).rejects.toMatchSnapshot(); - expect(userMock.updateUserPassword.mock.calls).toMatchSnapshot('putUserPassword calls'); + userMock.updateUserPassword.mockRejectedValue( + new Error("[mock] updateUserPassword error") + ); + await expect( + result.current.updataPassword("oldpw", "newpw") + ).rejects.toMatchSnapshot(); + expect(userMock.updateUserPassword.mock.calls).toMatchSnapshot( + "putUserPassword calls" + ); }); - }); - }); diff --git a/web/dashboard-ui/src/__tests__/components/MyThemeProvider.spec.tsx b/web/dashboard-ui/src/__tests__/components/MyThemeProvider.spec.tsx index 8bd5e5a2..0107e80e 100644 --- a/web/dashboard-ui/src/__tests__/components/MyThemeProvider.spec.tsx +++ b/web/dashboard-ui/src/__tests__/components/MyThemeProvider.spec.tsx @@ -1,49 +1,43 @@ import { useTheme } from "@emotion/react"; import { useMediaQuery } from "@mui/material"; import { cleanup, renderHook } from "@testing-library/react"; -import React from 'react'; -import { afterEach, describe, expect, it, MockedFunction, vi } from "vitest"; -import { MyThemeProvider } from '../../components/MyThemeProvider'; +import React from "react"; +import { MockedFunction, afterEach, describe, expect, it, vi } from "vitest"; +import { MyThemeProvider } from "../../components/MyThemeProvider"; //-------------------------------------------------- // mock definition //-------------------------------------------------- -vi.mock('@mui/material'); +vi.mock("@mui/material"); //----------------------------------------------- // test //----------------------------------------------- -describe('AuthRoute', () => { - - const useMediaQueryMock = useMediaQuery as MockedFunction; +describe("AuthRoute", () => { + const useMediaQueryMock = useMediaQuery as MockedFunction< + typeof useMediaQuery + >; afterEach(() => { vi.restoreAllMocks(); cleanup(); }); - - it('normal light', async () => { - + it("normal light", async () => { useMediaQueryMock.mockReturnValue(false); const { result } = renderHook(() => useTheme(), { - wrapper: ({ children }) => ({children}), + wrapper: ({ children }) => {children}, }); expect(result.current).toMatchSnapshot(); - }); - - it('normal dark', async () => { - + it("normal dark", async () => { useMediaQueryMock.mockReturnValue(true); const { result } = renderHook(() => useTheme(), { - wrapper: ({ children }) => ({children}), + wrapper: ({ children }) => {children}, }); expect(result.current).toMatchSnapshot(); - }); - }); diff --git a/web/dashboard-ui/src/__tests__/components/ProgressProvider.spec.tsx b/web/dashboard-ui/src/__tests__/components/ProgressProvider.spec.tsx index 9edff37e..c82348ea 100644 --- a/web/dashboard-ui/src/__tests__/components/ProgressProvider.spec.tsx +++ b/web/dashboard-ui/src/__tests__/components/ProgressProvider.spec.tsx @@ -1,29 +1,42 @@ import { Button } from "@mui/material"; import { cleanup, fireEvent, render, screen } from "@testing-library/react"; -import React from 'react'; +import React from "react"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { ProgressProvider, useProgress } from "../../components/ProgressProvider"; +import { + ProgressProvider, + useProgress, +} from "../../components/ProgressProvider"; //----------------------------------------------- // test //----------------------------------------------- -describe('ProgressProvider', () => { - +describe("ProgressProvider", () => { afterEach(() => { vi.restoreAllMocks(); cleanup(); }); - it('normal', async () => { - + it("normal", async () => { const MockView = () => { const { setMask, releaseMask } = useProgress(); - return (<> - - - ); - } + return ( + <> + + + + ); + }; const user = userEvent.setup(); - render(); - await act(async () => { expect(document.body).toMatchSnapshot('1.initial render'); }); - await act(async () => { await user.click(screen.getByText('open')); }); - await act(async () => { expect(document.body).toMatchSnapshot('2.opend'); }); - await act(async () => { await user.click(screen.getByText('close')); }); - await act(async () => { expect(document.body).toMatchSnapshot('3.closed'); }); + render( + + + + ); + await act(async () => { + expect(document.body).toMatchSnapshot("1.initial render"); + }); + await act(async () => { + await user.click(screen.getByText("open")); + }); + await act(async () => { + expect(document.body).toMatchSnapshot("2.opend"); + }); + await act(async () => { + await user.click(screen.getByText("close")); + }); + await act(async () => { + expect(document.body).toMatchSnapshot("3.closed"); + }); }); }); - -}); \ No newline at end of file +}); diff --git a/web/dashboard-ui/src/__tests__/views/organisms/UserModule.spec.tsx b/web/dashboard-ui/src/__tests__/views/organisms/UserModule.spec.tsx index 6e8db4d0..9c67589f 100644 --- a/web/dashboard-ui/src/__tests__/views/organisms/UserModule.spec.tsx +++ b/web/dashboard-ui/src/__tests__/views/organisms/UserModule.spec.tsx @@ -1,26 +1,47 @@ -import { Code, ConnectError } from '@bufbuild/connect'; -import '@testing-library/jest-dom'; -import { act, cleanup, renderHook } from '@testing-library/react'; +import { Code, ConnectError } from "@bufbuild/connect"; +import "@testing-library/jest-dom"; +import { act, cleanup, renderHook } from "@testing-library/react"; import { useSnackbar } from "notistack"; -import React from 'react'; -import { afterEach, beforeEach, describe, expect, it, MockedFunction, vi } from "vitest"; -import { useLogin } from '../../../components/LoginProvider'; -import { useProgress } from '../../../components/ProgressProvider'; -import { Template } from '../../../proto/gen/dashboard/v1alpha1/template_pb'; -import { GetUserAddonTemplatesResponse } from '../../../proto/gen/dashboard/v1alpha1/template_service_pb'; -import { User } from '../../../proto/gen/dashboard/v1alpha1/user_pb'; -import { CreateUserResponse, DeleteUserResponse, GetUsersResponse, UpdateUserDisplayNameResponse, UpdateUserRoleResponse } from '../../../proto/gen/dashboard/v1alpha1/user_service_pb'; -import { useTemplateService, useUserService } from "../../../services/DashboardServices"; -import { UserContext, useTemplates, useUserModule } from '../../../views/organisms/UserModule'; +import React from "react"; +import { + MockedFunction, + afterEach, + beforeEach, + describe, + expect, + it, + vi, +} from "vitest"; +import { useLogin } from "../../../components/LoginProvider"; +import { useProgress } from "../../../components/ProgressProvider"; +import { Template } from "../../../proto/gen/dashboard/v1alpha1/template_pb"; +import { GetUserAddonTemplatesResponse } from "../../../proto/gen/dashboard/v1alpha1/template_service_pb"; +import { User } from "../../../proto/gen/dashboard/v1alpha1/user_pb"; +import { + CreateUserResponse, + DeleteUserResponse, + GetUsersResponse, + UpdateUserDisplayNameResponse, + UpdateUserRoleResponse, +} from "../../../proto/gen/dashboard/v1alpha1/user_service_pb"; +import { + useTemplateService, + useUserService, +} from "../../../services/DashboardServices"; +import { + UserContext, + useTemplates, + useUserModule, +} from "../../../views/organisms/UserModule"; //-------------------------------------------------- // mock definition //-------------------------------------------------- -vi.mock('notistack'); -vi.mock('../../../components/LoginProvider'); -vi.mock('../../../services/DashboardServices'); -vi.mock('../../../components/ProgressProvider'); -vi.mock('react-router-dom', () => ({ +vi.mock("notistack"); +vi.mock("../../../components/LoginProvider"); +vi.mock("../../../services/DashboardServices"); +vi.mock("../../../components/ProgressProvider"); +vi.mock("react-router-dom", () => ({ useNavigate: () => vi.fn(), })); @@ -28,7 +49,9 @@ type MockedMemberFunction any> = { [P in keyof ReturnType]: MockedFunction[P]>; }; -const useUserServiceMock = useUserService as MockedFunction; +const useUserServiceMock = useUserService as MockedFunction< + typeof useUserService +>; const userMock: MockedMemberFunction = { getUser: vi.fn(), getUsers: vi.fn(), @@ -37,7 +60,7 @@ const userMock: MockedMemberFunction = { updateUserDisplayName: vi.fn(), updateUserPassword: vi.fn(), updateUserRole: vi.fn(), -} +}; const useLoginMock = useLogin as MockedFunction; const loginMock: MockedMemberFunction = { loginUser: {} as any, @@ -48,35 +71,40 @@ const loginMock: MockedMemberFunction = { refreshUserInfo: vi.fn(), clearLoginUser: vi.fn(), }; -const useTemplateServiceMock = useTemplateService as MockedFunction; +const useTemplateServiceMock = useTemplateService as MockedFunction< + typeof useTemplateService +>; const templateMock: MockedMemberFunction = { getUserAddonTemplates: vi.fn(), getWorkspaceTemplates: vi.fn(), -} +}; const useProgressMock = useProgress as MockedFunction; const progressMock: MockedMemberFunction = { setMask: vi.fn(), releaseMask: vi.fn(), -} +}; const useSnackbarMock = useSnackbar as MockedFunction; const snackbarMock: MockedMemberFunction = { enqueueSnackbar: vi.fn(), closeSnackbar: vi.fn(), -} +}; //-------------------------------------------------- // mock data definition //-------------------------------------------------- -const user1 = new User({ name: 'user1', roles: ['cosmoAdmin'], displayName: 'user1 name' }); -const user2 = new User({ name: 'user2', displayName: 'user2 name' }); -const user3 = new User({ name: 'user3', displayName: 'user3 name' }); +const user1 = new User({ + name: "user1", + roles: ["cosmoAdmin"], + displayName: "user1 name", +}); +const user2 = new User({ name: "user2", displayName: "user2 name" }); +const user3 = new User({ name: "user3", displayName: "user3 name" }); //----------------------------------------------- // test //----------------------------------------------- -describe('useUserModule', () => { - +describe("useUserModule", () => { beforeEach(async () => { useSnackbarMock.mockReturnValue(snackbarMock); useProgressMock.mockReturnValue(progressMock); @@ -92,164 +120,250 @@ describe('useUserModule', () => { async function renderUseUserModule() { return renderHook(() => useUserModule(), { - wrapper: ({ children }) => ({children}), + wrapper: ({ children }) => ( + {children} + ), }); } - describe('useUserModule getUsers', () => { - - it('normal', async () => { + describe("useUserModule getUsers", () => { + it("normal", async () => { const { result } = await renderUseUserModule(); - userMock.getUsers.mockResolvedValue(new GetUsersResponse({ items: [user2, user1, user3] })); - await act(async () => { result.current.getUsers() }); + userMock.getUsers.mockResolvedValue( + new GetUsersResponse({ items: [user2, user1, user3] }) + ); + await act(async () => { + result.current.getUsers(); + }); expect(result.current.users).toMatchSnapshot(); }); - it('normal2', async () => { + it("normal2", async () => { const { result } = await renderUseUserModule(); - userMock.getUsers.mockResolvedValue(new GetUsersResponse({ items: undefined as any })); - await act(async () => { result.current.getUsers() }); + userMock.getUsers.mockResolvedValue( + new GetUsersResponse({ items: undefined as any }) + ); + await act(async () => { + result.current.getUsers(); + }); expect(result.current.users).toMatchSnapshot(); }); - it('normal3', async () => { + it("normal3", async () => { const { result } = await renderUseUserModule(); userMock.getUsers.mockResolvedValue(new GetUsersResponse({ items: [] })); - await act(async () => { result.current.getUsers() }); + await act(async () => { + result.current.getUsers(); + }); expect(result.current.users).toMatchSnapshot(); }); - it('error', async () => { + it("error", async () => { const { result } = await renderUseUserModule(); //userMock.getUsers.mockRejectedValue(new Error('[mock] getUsers error')); - userMock.getUsers.mockRejectedValue(new ConnectError('[mock] getUsers error', Code.Unauthenticated)); + userMock.getUsers.mockRejectedValue( + new ConnectError("[mock] getUsers error", Code.Unauthenticated) + ); await expect(result.current.getUsers()).rejects.toMatchSnapshot(); }); - }); - - describe('useUserModule createUser', () => { + describe("useUserModule createUser", () => { beforeEach(async () => { - userMock.getUsers.mockResolvedValue(new GetUsersResponse({ items: [user1, user2, user3] })); + userMock.getUsers.mockResolvedValue( + new GetUsersResponse({ items: [user1, user2, user3] }) + ); }); - it('nomal', async () => { + it("nomal", async () => { const { result } = await renderUseUserModule(); - await act(async () => { result.current.getUsers() }); - userMock.createUser.mockResolvedValue(new CreateUserResponse({ message: "ok", user: user2 })); - await act(async () => { result.current.createUser('user2', 'user2 name') }); + await act(async () => { + result.current.getUsers(); + }); + userMock.createUser.mockResolvedValue( + new CreateUserResponse({ message: "ok", user: user2 }) + ); + await act(async () => { + result.current.createUser("user2", "user2 name"); + }); expect(result.current.users).toMatchSnapshot(); }); - it('error', async () => { + it("error", async () => { const { result } = await renderUseUserModule(); - await act(async () => { result.current.getUsers() }); - userMock.createUser.mockRejectedValue(new Error('[mock] createUser error')); - await expect(result.current.createUser('user2', 'user2 name')).rejects.toMatchSnapshot(); + await act(async () => { + result.current.getUsers(); + }); + userMock.createUser.mockRejectedValue( + new Error("[mock] createUser error") + ); + await expect( + result.current.createUser("user2", "user2 name") + ).rejects.toMatchSnapshot(); }); }); - describe('useUserModule updateUserName', () => { - - it('nomal before getUsers', async () => { + describe("useUserModule updateUserName", () => { + it("nomal before getUsers", async () => { const { result } = await renderUseUserModule(); - const user2x = { ...user2, displayName: 'displayNameChange' } - userMock.updateUserDisplayName.mockResolvedValue(new UpdateUserDisplayNameResponse({ message: "ok", user: user2x })); - await act(async () => { result.current.updateName('user2', 'displayNameChange') }); + const user2x = { ...user2, displayName: "displayNameChange" }; + userMock.updateUserDisplayName.mockResolvedValue( + new UpdateUserDisplayNameResponse({ message: "ok", user: user2x }) + ); + await act(async () => { + result.current.updateName("user2", "displayNameChange"); + }); expect(result.current.users).toMatchSnapshot(); }); - it('nomal after getUsers', async () => { + it("nomal after getUsers", async () => { const { result } = await renderUseUserModule(); - userMock.getUsers.mockResolvedValue(new GetUsersResponse({ message: "ok", items: [user1, user2, user3] })); - await act(async () => { result.current.getUsers() }); - const user2x = { ...user2, displayName: 'displayNameChange' } - userMock.updateUserDisplayName.mockResolvedValue(new UpdateUserDisplayNameResponse({ message: "ok", user: user2x })); - await act(async () => { result.current.updateName('user2', 'displayNameChange') }); + userMock.getUsers.mockResolvedValue( + new GetUsersResponse({ message: "ok", items: [user1, user2, user3] }) + ); + await act(async () => { + result.current.getUsers(); + }); + const user2x = { ...user2, displayName: "displayNameChange" }; + userMock.updateUserDisplayName.mockResolvedValue( + new UpdateUserDisplayNameResponse({ message: "ok", user: user2x }) + ); + await act(async () => { + result.current.updateName("user2", "displayNameChange"); + }); expect(result.current.users).toMatchSnapshot(); }); - it('nomal after getUsers. return value nothing', async () => { + it("nomal after getUsers. return value nothing", async () => { const { result } = await renderUseUserModule(); - userMock.getUsers.mockResolvedValue(new GetUsersResponse({ message: "ok", items: [user1, user2, user3] })); - await act(async () => { result.current.getUsers() }); - userMock.updateUserDisplayName.mockResolvedValue(new UpdateUserDisplayNameResponse({ message: "ok", user: undefined as any })); - await act(async () => { result.current.updateName('user2', 'displayNameChange') }); + userMock.getUsers.mockResolvedValue( + new GetUsersResponse({ message: "ok", items: [user1, user2, user3] }) + ); + await act(async () => { + result.current.getUsers(); + }); + userMock.updateUserDisplayName.mockResolvedValue( + new UpdateUserDisplayNameResponse({ + message: "ok", + user: undefined as any, + }) + ); + await act(async () => { + result.current.updateName("user2", "displayNameChange"); + }); expect(result.current.users).toMatchSnapshot(); }); - it('error', async () => { + it("error", async () => { const { result } = await renderUseUserModule(); - userMock.updateUserDisplayName.mockRejectedValue(new Error('[mock] putUserName error')); - await expect(result.current.updateName('user2', 'displayNameChange')).rejects.toMatchSnapshot(); + userMock.updateUserDisplayName.mockRejectedValue( + new Error("[mock] putUserName error") + ); + await expect( + result.current.updateName("user2", "displayNameChange") + ).rejects.toMatchSnapshot(); }); }); - describe('useUserModule updateRole', () => { - - it('nomal before getUsers', async () => { + describe("useUserModule updateRole", () => { + it("nomal before getUsers", async () => { const { result } = await renderUseUserModule(); - userMock.updateUserRole.mockResolvedValue(new UpdateUserRoleResponse({ message: "ok", user: user2 })); - await act(async () => { result.current.updateRole('user2', ['user2 name']) }); + userMock.updateUserRole.mockResolvedValue( + new UpdateUserRoleResponse({ message: "ok", user: user2 }) + ); + await act(async () => { + result.current.updateRole("user2", ["user2 name"]); + }); expect(result.current.users).toMatchSnapshot(); }); - it('nomal after getUsers', async () => { + it("nomal after getUsers", async () => { const { result } = await renderUseUserModule(); - userMock.getUsers.mockResolvedValue(new GetUsersResponse({ message: "ok", items: [user1, user2, user3] })); - await act(async () => { result.current.getUsers() }); - - const user2x = { ...user2, roles: ['Role2'] } - userMock.updateUserRole.mockResolvedValue(new UpdateUserRoleResponse({ message: "ok", user: user2x })); - - await act(async () => { result.current.updateRole('user2', ['Role2']) }); + userMock.getUsers.mockResolvedValue( + new GetUsersResponse({ message: "ok", items: [user1, user2, user3] }) + ); + await act(async () => { + result.current.getUsers(); + }); + + const user2x = { ...user2, roles: ["Role2"] }; + userMock.updateUserRole.mockResolvedValue( + new UpdateUserRoleResponse({ message: "ok", user: user2x }) + ); + + await act(async () => { + result.current.updateRole("user2", ["Role2"]); + }); expect(result.current.users).toMatchSnapshot(); }); - it('nomal after getUsers. return value nothing', async () => { + it("nomal after getUsers. return value nothing", async () => { const { result } = await renderUseUserModule(); - userMock.getUsers.mockResolvedValue(new GetUsersResponse({ message: "ok", items: [user1, user2, user3] })); - await act(async () => { result.current.getUsers() }); - - userMock.updateUserRole.mockResolvedValue(new UpdateUserRoleResponse({ message: "ok", user: undefined as any })); - - await act(async () => { result.current.updateRole('user2', ['Role2']) }); + userMock.getUsers.mockResolvedValue( + new GetUsersResponse({ message: "ok", items: [user1, user2, user3] }) + ); + await act(async () => { + result.current.getUsers(); + }); + + userMock.updateUserRole.mockResolvedValue( + new UpdateUserRoleResponse({ message: "ok", user: undefined as any }) + ); + + await act(async () => { + result.current.updateRole("user2", ["Role2"]); + }); expect(result.current.users).toMatchSnapshot(); }); - it('error', async () => { + it("error", async () => { const { result } = await renderUseUserModule(); - userMock.updateUserRole.mockRejectedValue(new Error('[mock] updateUserRole error')); - await expect(result.current.updateRole('user2', ['user2 name'])).rejects.toMatchSnapshot(); + userMock.updateUserRole.mockRejectedValue( + new Error("[mock] updateUserRole error") + ); + await expect( + result.current.updateRole("user2", ["user2 name"]) + ).rejects.toMatchSnapshot(); }); }); - describe('useUserModule deleteUser', () => { + describe("useUserModule deleteUser", () => { beforeEach(async () => { - userMock.getUsers.mockResolvedValue(new GetUsersResponse({ message: "ok", items: [user1, user2, user3] })); + userMock.getUsers.mockResolvedValue( + new GetUsersResponse({ message: "ok", items: [user1, user2, user3] }) + ); }); - it('nomal', async () => { + it("nomal", async () => { const { result } = await renderUseUserModule(); - await act(async () => { result.current.getUsers() }); - userMock.deleteUser.mockResolvedValue(new DeleteUserResponse({ message: "ok", user: user2 })); - await act(async () => { result.current.deleteUser('user2') }); + await act(async () => { + result.current.getUsers(); + }); + userMock.deleteUser.mockResolvedValue( + new DeleteUserResponse({ message: "ok", user: user2 }) + ); + await act(async () => { + result.current.deleteUser("user2"); + }); expect(result.current.users).toMatchSnapshot(); }); - it('error', async () => { + it("error", async () => { const { result } = await renderUseUserModule(); - await act(async () => { result.current.getUsers() }); - userMock.deleteUser.mockRejectedValue(new Error('[mock] deleteUser error')); - await expect(result.current.deleteUser('user2')).rejects.toMatchSnapshot(); + await act(async () => { + result.current.getUsers(); + }); + userMock.deleteUser.mockRejectedValue( + new Error("[mock] deleteUser error") + ); + await expect( + result.current.deleteUser("user2") + ).rejects.toMatchSnapshot(); }); }); - }); -describe('useTemplates', () => { - +describe("useTemplates", () => { beforeEach(async () => { useSnackbarMock.mockReturnValue(snackbarMock); useProgressMock.mockReturnValue(progressMock); @@ -264,45 +378,60 @@ describe('useTemplates', () => { async function renderUseTemplates() { const utils = renderHook(() => useTemplates(), { - wrapper: ({ children }) => ({children}), + wrapper: ({ children }) => ( + {children} + ), }); return utils; } - describe('useTemplates getUserAddonTemplates', () => { - - const tmpl1 = new Template({ name: 'tmpl1' }); + describe("useTemplates getUserAddonTemplates", () => { + const tmpl1 = new Template({ name: "tmpl1" }); const tmpl2 = new Template({ - name: 'tmpl2', + name: "tmpl2", description: "hoge", - requiredVars: [{ varName: 'var1', defaultValue: 'var1Value' }, { varName: 'var2' }], + requiredVars: [ + { varName: "var1", defaultValue: "var1Value" }, + { varName: "var2" }, + ], isDefaultUserAddon: true, }); - const tmpl3 = new Template({ name: 'tmpl3' }); + const tmpl3 = new Template({ name: "tmpl3" }); - it('normal', async () => { + it("normal", async () => { const { result } = await renderUseTemplates(); - templateMock.getUserAddonTemplates.mockResolvedValue(new GetUserAddonTemplatesResponse({ - message: "ok", items: [tmpl1, tmpl3, tmpl2] - })); - await act(async () => { result.current.getUserAddonTemplates() }); + templateMock.getUserAddonTemplates.mockResolvedValue( + new GetUserAddonTemplatesResponse({ + message: "ok", + items: [tmpl1, tmpl3, tmpl2], + }) + ); + await act(async () => { + result.current.getUserAddonTemplates(); + }); expect(result.current.templates).toMatchSnapshot(); }); - it('normal template empty', async () => { + it("normal template empty", async () => { const { result } = await renderUseTemplates(); - templateMock.getUserAddonTemplates.mockResolvedValue(new GetUserAddonTemplatesResponse({ items: [] })); - await act(async () => { result.current.getUserAddonTemplates() }); + templateMock.getUserAddonTemplates.mockResolvedValue( + new GetUserAddonTemplatesResponse({ items: [] }) + ); + await act(async () => { + result.current.getUserAddonTemplates(); + }); expect(result.current.templates).toMatchSnapshot(); }); - it('error', async () => { + it("error", async () => { const { result } = await renderUseTemplates(); - templateMock.getUserAddonTemplates.mockRejectedValue(new Error('[mock] getUser error')); - await expect(result.current.getUserAddonTemplates()).rejects.toMatchSnapshot(); + templateMock.getUserAddonTemplates.mockRejectedValue( + new Error("[mock] getUser error") + ); + await expect( + result.current.getUserAddonTemplates() + ).rejects.toMatchSnapshot(); expect(result.current.templates).toMatchSnapshot(); }); - }); - }); diff --git a/web/dashboard-ui/src/__tests__/views/organisms/UserNameChangeDialog.spec.tsx b/web/dashboard-ui/src/__tests__/views/organisms/UserNameChangeDialog.spec.tsx index c3e54025..724047c3 100644 --- a/web/dashboard-ui/src/__tests__/views/organisms/UserNameChangeDialog.spec.tsx +++ b/web/dashboard-ui/src/__tests__/views/organisms/UserNameChangeDialog.spec.tsx @@ -1,14 +1,25 @@ import { Button } from "@mui/material"; -import '@testing-library/jest-dom'; +import "@testing-library/jest-dom"; import { act, cleanup, render, screen } from "@testing-library/react"; -import userEvent from '@testing-library/user-event'; +import userEvent from "@testing-library/user-event"; import { useSnackbar } from "notistack"; import React from "react"; -import { MockedFunction, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { + MockedFunction, + afterEach, + beforeEach, + describe, + expect, + it, + vi, +} from "vitest"; import { useLogin } from "../../../components/LoginProvider"; import { User } from "../../../proto/gen/dashboard/v1alpha1/user_pb"; import { useUserModule } from "../../../views/organisms/UserModule"; -import { UserNameChangeDialog, UserNameChangeDialogContext } from "../../../views/organisms/UserNameChangeDialog"; +import { + UserNameChangeDialog, + UserNameChangeDialogContext, +} from "../../../views/organisms/UserNameChangeDialog"; //-------------------------------------------------- // mock definition @@ -30,7 +41,7 @@ const userModuleMock: MockedMemberFunction = { updateName: vi.fn(), updateRole: vi.fn(), deleteUser: vi.fn(), -} +}; const useLoginMock = useLogin as MockedFunction; const loginMock: MockedMemberFunction = { loginUser: {} as any, @@ -54,7 +65,6 @@ const closeHandlerMock = vi.fn(); // test //-------------------------------------------------- describe("UserNameChangeDialog", () => { - beforeEach(async () => { useSnackbarMock.mockReturnValue(snackbarMock); useUserModuleMock.mockReturnValue(userModuleMock); @@ -66,53 +76,74 @@ describe("UserNameChangeDialog", () => { cleanup(); }); - describe("render", () => { - it("render", async () => { - const user1: User = new User({ name: 'user1', roles: ["CosmoAdmin"], displayName: 'user1 name' }); + const user1: User = new User({ + name: "user1", + roles: ["CosmoAdmin"], + displayName: "user1 name", + }); render( closeHandlerMock()} user={user1} /> ); expect(document.body).toMatchSnapshot(); }); - }); - describe("behavior", () => { - - const user1: User = new User({ name: 'user1', roles: ["CosmoAdmin"], displayName: 'user1 name' }); + const user1: User = new User({ + name: "user1", + roles: ["CosmoAdmin"], + displayName: "user1 name", + }); it("ok", async () => { const user = userEvent.setup(); - const { baseElement } = render( closeHandlerMock()} user={user1} />); + const { baseElement } = render( + closeHandlerMock()} user={user1} /> + ); const nameElement = baseElement.querySelector('[name="name"]')!; - await user.type(nameElement, 'New Name', { initialSelectionStart: 0, initialSelectionEnd: 99 }); + await user.type(nameElement, "New Name", { + initialSelectionStart: 0, + initialSelectionEnd: 99, + }); userModuleMock.updateName.mockResolvedValue({} as any); loginMock.refreshUserInfo.mockResolvedValue({} as any); - await user.click(screen.getByText('Update')); - expect(userModuleMock.updateName.mock.calls).toMatchObject([["user1", "New Name"]]); + await user.click(screen.getByText("Update")); + expect(userModuleMock.updateName.mock.calls).toMatchObject([ + ["user1", "New Name"], + ]); expect(loginMock.refreshUserInfo.mock.calls).toMatchObject([[]]); expect(closeHandlerMock.mock.calls.length).toEqual(1); }); it("ng required", async () => { const user = userEvent.setup(); - const { baseElement } = render( closeHandlerMock()} user={user1} />); + const { baseElement } = render( + closeHandlerMock()} user={user1} /> + ); - expect(screen.getAllByText('User Name')[0].parentElement! - .getElementsByClassName('MuiFormHelperText-root')[0]).toBeUndefined(); + expect( + screen + .getAllByText("User Name")[0] + .parentElement!.getElementsByClassName("MuiFormHelperText-root")[0] + ).toBeUndefined(); const nameElement = baseElement.querySelector('[name="name"]')!; - await user.type(nameElement, '{backspace}', { initialSelectionStart: 0, initialSelectionEnd: 99 }); + await user.type(nameElement, "{backspace}", { + initialSelectionStart: 0, + initialSelectionEnd: 99, + }); await act(async () => { - await user.click(screen.getByText('Update')); + await user.click(screen.getByText("Update")); }); - expect(screen.getAllByText('User Name')[0].parentElement! - .getElementsByClassName('MuiFormHelperText-root')[0]).toHaveTextContent('Required'); + expect( + screen + .getAllByText("User Name")[0] + .parentElement!.getElementsByClassName("MuiFormHelperText-root")[0] + ).toHaveTextContent("Required"); expect(userModuleMock.updateName.mock.calls.length).toEqual(0); expect(loginMock.refreshUserInfo.mock.calls.length).toEqual(0); expect(closeHandlerMock.mock.calls.length).toEqual(0); @@ -120,34 +151,53 @@ describe("UserNameChangeDialog", () => { it("ng over 32", async () => { const user = userEvent.setup(); - const { baseElement } = render( closeHandlerMock()} user={user1} />); + const { baseElement } = render( + closeHandlerMock()} user={user1} /> + ); - expect(screen.getAllByText('User Name')[0].parentElement! - .getElementsByClassName('MuiFormHelperText-root')[0]).toBeUndefined(); + expect( + screen + .getAllByText("User Name")[0] + .parentElement!.getElementsByClassName("MuiFormHelperText-root")[0] + ).toBeUndefined(); const nameElement = baseElement.querySelector('[name="name"]')!; - await user.type(nameElement, '----+----1----+----2----+----3--x', { initialSelectionStart: 0, initialSelectionEnd: 99 }); + await user.type(nameElement, "----+----1----+----2----+----3--x", { + initialSelectionStart: 0, + initialSelectionEnd: 99, + }); await act(async () => { - await user.click(screen.getByText('Update')); + await user.click(screen.getByText("Update")); }); - expect(screen.getAllByText('User Name')[0].parentElement! - .getElementsByClassName('MuiFormHelperText-root')[0]).toHaveTextContent('Max 32 characters'); + expect( + screen + .getAllByText("User Name")[0] + .parentElement!.getElementsByClassName("MuiFormHelperText-root")[0] + ).toHaveTextContent("Max 32 characters"); expect(userModuleMock.updateName.mock.calls.length).toEqual(0); expect(loginMock.refreshUserInfo.mock.calls.length).toEqual(0); expect(closeHandlerMock.mock.calls.length).toEqual(0); await act(async () => { - await user.type(nameElement, '----+----1----+----2----+----3--', { initialSelectionStart: 0, initialSelectionEnd: 99 }); + await user.type(nameElement, "----+----1----+----2----+----3--", { + initialSelectionStart: 0, + initialSelectionEnd: 99, + }); }); - expect(screen.getAllByText('User Name')[0].parentElement! - .getElementsByClassName('MuiFormHelperText-root')[0]).toBeUndefined(); + expect( + screen + .getAllByText("User Name")[0] + .parentElement!.getElementsByClassName("MuiFormHelperText-root")[0] + ).toBeUndefined(); }); it("cancel", async () => { const user = userEvent.setup(); - render( closeHandlerMock()} user={user1} />); - await user.click(screen.getByText('Cancel')); + render( + closeHandlerMock()} user={user1} /> + ); + await user.click(screen.getByText("Cancel")); expect(userModuleMock.updateName.mock.calls.length).toEqual(0); expect(loginMock.refreshUserInfo.mock.calls.length).toEqual(0); expect(closeHandlerMock.mock.calls.length).toEqual(1); @@ -155,37 +205,56 @@ describe("UserNameChangeDialog", () => { it("not close <- click outside", async () => { const user = userEvent.setup(); - const { baseElement } = render( closeHandlerMock()} user={user1} />); - await user.click(baseElement.getElementsByClassName('MuiDialog-container')[0]); + const { baseElement } = render( + closeHandlerMock()} user={user1} /> + ); + await user.click( + baseElement.getElementsByClassName("MuiDialog-container")[0] + ); expect(userModuleMock.updateName.mock.calls.length).toEqual(0); expect(loginMock.refreshUserInfo.mock.calls.length).toEqual(0); expect(closeHandlerMock.mock.calls.length).toEqual(0); }); - }); - describe("UserNameChangeDialogContext", () => { - it("open/close", async () => { - const Stub = () => { const dispatch = UserNameChangeDialogContext.useDispatch(); - const user1: User = new User({ name: 'user1', displayName: 'user1name' }); - return (<> - - - ); - } + const user1: User = new User({ + name: "user1", + displayName: "user1name", + }); + return ( + <> + + + + ); + }; const user = userEvent.setup(); - render(); - await act(async () => { expect(document.body).toMatchSnapshot('1.initial render'); }); - await act(async () => { await user.click(screen.getByText('open')); }); - await act(async () => { expect(document.body).toMatchSnapshot('2.opend'); }); - await act(async () => { await user.click(screen.getByText('close')); }); - await act(async () => { expect(document.body).toMatchSnapshot('3.closed'); }); + render( + + + + ); + await act(async () => { + expect(document.body).toMatchSnapshot("1.initial render"); + }); + await act(async () => { + await user.click(screen.getByText("open")); + }); + await act(async () => { + expect(document.body).toMatchSnapshot("2.opend"); + }); + await act(async () => { + await user.click(screen.getByText("close")); + }); + await act(async () => { + expect(document.body).toMatchSnapshot("3.closed"); + }); }); - }); - -}); \ No newline at end of file +}); diff --git a/web/dashboard-ui/src/__tests__/views/organisms/WorkspaceModule.spec.tsx b/web/dashboard-ui/src/__tests__/views/organisms/WorkspaceModule.spec.tsx index 3caa346d..dede940e 100644 --- a/web/dashboard-ui/src/__tests__/views/organisms/WorkspaceModule.spec.tsx +++ b/web/dashboard-ui/src/__tests__/views/organisms/WorkspaceModule.spec.tsx @@ -1,28 +1,59 @@ -import { Code, ConnectError } from '@bufbuild/connect'; -import { protoInt64 } from '@bufbuild/protobuf'; -import { act, cleanup, renderHook } from '@testing-library/react'; +import { Code, ConnectError } from "@bufbuild/connect"; +import { protoInt64 } from "@bufbuild/protobuf"; +import { act, cleanup, renderHook } from "@testing-library/react"; import { useSnackbar } from "notistack"; -import React from 'react'; -import { afterEach, beforeEach, describe, expect, it, MockedFunction, vi } from "vitest"; +import React from "react"; +import { + MockedFunction, + afterEach, + beforeEach, + describe, + expect, + it, + vi, +} from "vitest"; import { useLogin } from "../../../components/LoginProvider"; -import { useProgress } from '../../../components/ProgressProvider'; -import { Template } from '../../../proto/gen/dashboard/v1alpha1/template_pb'; -import { GetWorkspaceTemplatesResponse } from '../../../proto/gen/dashboard/v1alpha1/template_service_pb'; -import { User } from '../../../proto/gen/dashboard/v1alpha1/user_pb'; -import { GetUsersResponse } from '../../../proto/gen/dashboard/v1alpha1/user_service_pb'; -import { NetworkRule, Workspace } from '../../../proto/gen/dashboard/v1alpha1/workspace_pb'; -import { CreateWorkspaceResponse, DeleteNetworkRuleResponse, DeleteWorkspaceResponse, GetWorkspaceResponse, GetWorkspacesResponse, UpdateWorkspaceResponse, UpsertNetworkRuleResponse } from '../../../proto/gen/dashboard/v1alpha1/workspace_service_pb'; -import { useTemplateService, useUserService, useWorkspaceService } from '../../../services/DashboardServices'; -import { computeStatus, useNetworkRule, useTemplates, useWorkspaceModule, useWorkspaceUsersModule, WorkspaceContext, WorkspaceUsersContext } from '../../../views/organisms/WorkspaceModule'; +import { useProgress } from "../../../components/ProgressProvider"; +import { Template } from "../../../proto/gen/dashboard/v1alpha1/template_pb"; +import { GetWorkspaceTemplatesResponse } from "../../../proto/gen/dashboard/v1alpha1/template_service_pb"; +import { User } from "../../../proto/gen/dashboard/v1alpha1/user_pb"; +import { GetUsersResponse } from "../../../proto/gen/dashboard/v1alpha1/user_service_pb"; +import { + NetworkRule, + Workspace, +} from "../../../proto/gen/dashboard/v1alpha1/workspace_pb"; +import { + CreateWorkspaceResponse, + DeleteNetworkRuleResponse, + DeleteWorkspaceResponse, + GetWorkspaceResponse, + GetWorkspacesResponse, + UpdateWorkspaceResponse, + UpsertNetworkRuleResponse, +} from "../../../proto/gen/dashboard/v1alpha1/workspace_service_pb"; +import { + useTemplateService, + useUserService, + useWorkspaceService, +} from "../../../services/DashboardServices"; +import { + WorkspaceContext, + WorkspaceUsersContext, + computeStatus, + useNetworkRule, + useTemplates, + useWorkspaceModule, + useWorkspaceUsersModule, +} from "../../../views/organisms/WorkspaceModule"; //-------------------------------------------------- // mock definition //-------------------------------------------------- -vi.mock('notistack'); -vi.mock('../../../components/LoginProvider'); -vi.mock('../../../services/DashboardServices'); -vi.mock('../../../components/ProgressProvider'); -vi.mock('react-router-dom', () => ({ +vi.mock("notistack"); +vi.mock("../../../components/LoginProvider"); +vi.mock("../../../services/DashboardServices"); +vi.mock("../../../components/ProgressProvider"); +vi.mock("react-router-dom", () => ({ useNavigate: () => vi.fn(), })); @@ -32,7 +63,9 @@ type MockedMemberFunction any> = { [P in keyof ReturnType]: MockedFunction[P]>; }; -const useWorkspaceServiceMock = useWorkspaceService as MockedFunction; +const useWorkspaceServiceMock = useWorkspaceService as MockedFunction< + typeof useWorkspaceService +>; const wsMock: MockedMemberFunction = { getWorkspace: vi.fn(), getWorkspaces: vi.fn(), @@ -40,13 +73,15 @@ const wsMock: MockedMemberFunction = { deleteNetworkRule: vi.fn(), createWorkspace: vi.fn(), updateWorkspace: vi.fn(), - upsertNetworkRule: vi.fn() -} -const useTemplateServiceMock = useTemplateService as MockedFunction; + upsertNetworkRule: vi.fn(), +}; +const useTemplateServiceMock = useTemplateService as MockedFunction< + typeof useTemplateService +>; const templateMock: MockedMemberFunction = { getWorkspaceTemplates: vi.fn(), getUserAddonTemplates: vi.fn(), -} +}; const useLoginMock = useLogin as MockedFunction; const loginMock: MockedMemberFunction = { loginUser: {} as any, @@ -61,66 +96,123 @@ const useProgressMock = useProgress as MockedFunction; const progressMock: MockedMemberFunction = { setMask: vi.fn(), releaseMask: vi.fn(), -} +}; const useSnackbarMock = useSnackbar as MockedFunction; const snackbarMock: MockedMemberFunction = { enqueueSnackbar: vi.fn(), closeSnackbar: vi.fn(), -} +}; //-------------------------------------------------- // mock data definition //-------------------------------------------------- -function newWorkspace(name: string, user: User, tmpl: Template, replicas = 1, phase = 'Running'): Workspace { - return (new Workspace({ +function newWorkspace( + name: string, + user: User, + tmpl: Template, + replicas = 1, + phase = "Running" +): Workspace { + return new Workspace({ name: name, ownerName: user.name, - spec: { template: tmpl.name, replicas: protoInt64.parse(1), vars: { xxx: 'XXXX', yyy: 'YYYY' }, additionalNetwork: [] }, - status: { phase: phase, mainUrl: "", urlBase: 'urlbasexxxx' } - })); + spec: { + template: tmpl.name, + replicas: protoInt64.parse(1), + vars: { xxx: "XXXX", yyy: "YYYY" }, + additionalNetwork: [], + }, + status: { phase: phase, mainUrl: "", urlBase: "urlbasexxxx" }, + }); } function wsStat(ws: Workspace, replicas: number, phase: string): Workspace { - return new Workspace({ ...ws, spec: { ...ws.spec!, replicas: protoInt64.parse(replicas) }, status: { ...ws.status, phase } }); + return new Workspace({ + ...ws, + spec: { ...ws.spec!, replicas: protoInt64.parse(replicas) }, + status: { ...ws.status, phase }, + }); } -const user1: User = new User({ name: 'user1', roles: ["cosmo-admin"], displayName: 'user1 name' }); -const user2: User = new User({ name: 'user2', displayName: 'user2 name' }); -const user3: User = new User({ name: 'user3', displayName: 'user2 name' }); -const tmpl1: Template = new Template({ name: 'tmpl1' }); -const tmpl2: Template = new Template({ name: 'tmpl2', requiredVars: [{ varName: 'var1' }, { varName: 'var2' }] }); -const tmpl3: Template = new Template({ name: 'tmpl3' }); -const ws11 = newWorkspace('ws11', user1, tmpl1); -const ws12 = newWorkspace('ws12', user1, tmpl2); -const ws13 = newWorkspace('ws13', user1, tmpl1); -const ws14 = newWorkspace('ws14', user1, tmpl1); //add -const ws15 = newWorkspace('ws15', user1, tmpl1); +const user1: User = new User({ + name: "user1", + roles: ["cosmo-admin"], + displayName: "user1 name", +}); +const user2: User = new User({ name: "user2", displayName: "user2 name" }); +const user3: User = new User({ name: "user3", displayName: "user2 name" }); +const tmpl1: Template = new Template({ name: "tmpl1" }); +const tmpl2: Template = new Template({ + name: "tmpl2", + requiredVars: [{ varName: "var1" }, { varName: "var2" }], +}); +const tmpl3: Template = new Template({ name: "tmpl3" }); +const ws11 = newWorkspace("ws11", user1, tmpl1); +const ws12 = newWorkspace("ws12", user1, tmpl2); +const ws13 = newWorkspace("ws13", user1, tmpl1); +const ws14 = newWorkspace("ws14", user1, tmpl1); //add +const ws15 = newWorkspace("ws15", user1, tmpl1); //----------------------------------------------- // test //----------------------------------------------- -describe('computeStatus', () => { - const wsStarting = new Workspace({ name: '', ownerName: '', spec: { template: '', replicas: protoInt64.parse(1) }, status: { phase: 'Stopped' } }); - const wsPending = new Workspace({ name: '', ownerName: '', spec: { template: '', replicas: protoInt64.parse(-1) }, status: { phase: 'Pending' } }); - const wsStoping = new Workspace({ name: '', ownerName: '', spec: { template: '', replicas: protoInt64.parse(0) }, status: { phase: 'Running' } }); - const wsStopped = new Workspace({ name: '', ownerName: '', spec: { template: '', replicas: protoInt64.parse(0) }, status: { phase: 'Stopped' } }); - const wsRunning = new Workspace({ name: '', ownerName: '', spec: { template: '', replicas: protoInt64.parse(1) }, status: { phase: 'Running' } }); - - it('Stopping', () => { expect(computeStatus(wsStoping)).toEqual('Stopping') }); - it('Stopped', () => { expect(computeStatus(wsStopped)).toEqual('Stopped') }); - it('Starting', () => { expect(computeStatus(wsStarting)).toEqual('Starting'); }); - it('Running', () => { expect(computeStatus(wsRunning)).toEqual('Running') }); - it('Other', async () => { expect(computeStatus(wsPending)).toEqual('Pending') }); -}); - +describe("computeStatus", () => { + const wsStarting = new Workspace({ + name: "", + ownerName: "", + spec: { template: "", replicas: protoInt64.parse(1) }, + status: { phase: "Stopped" }, + }); + const wsPending = new Workspace({ + name: "", + ownerName: "", + spec: { template: "", replicas: protoInt64.parse(-1) }, + status: { phase: "Pending" }, + }); + const wsStoping = new Workspace({ + name: "", + ownerName: "", + spec: { template: "", replicas: protoInt64.parse(0) }, + status: { phase: "Running" }, + }); + const wsStopped = new Workspace({ + name: "", + ownerName: "", + spec: { template: "", replicas: protoInt64.parse(0) }, + status: { phase: "Stopped" }, + }); + const wsRunning = new Workspace({ + name: "", + ownerName: "", + spec: { template: "", replicas: protoInt64.parse(1) }, + status: { phase: "Running" }, + }); -describe('useWorkspace', () => { + it("Stopping", () => { + expect(computeStatus(wsStoping)).toEqual("Stopping"); + }); + it("Stopped", () => { + expect(computeStatus(wsStopped)).toEqual("Stopped"); + }); + it("Starting", () => { + expect(computeStatus(wsStarting)).toEqual("Starting"); + }); + it("Running", () => { + expect(computeStatus(wsRunning)).toEqual("Running"); + }); + it("Other", async () => { + expect(computeStatus(wsPending)).toEqual("Pending"); + }); +}); +describe("useWorkspace", () => { beforeEach(async () => { useSnackbarMock.mockReturnValue(snackbarMock); useProgressMock.mockReturnValue(progressMock); useWorkspaceServiceMock.mockReturnValue(wsMock); useLoginMock.mockReturnValue(loginMock); - wsMock.getWorkspaces.mockResolvedValue(new GetWorkspacesResponse({ message: "", items: [ws11, ws13, ws12] })); + wsMock.getWorkspaces.mockResolvedValue( + new GetWorkspacesResponse({ message: "", items: [ws11, ws13, ws12] }) + ); }); afterEach(() => { @@ -132,86 +224,120 @@ describe('useWorkspace', () => { function renderUseWorkspaceModule() { return renderHook(() => useWorkspaceModule(), { - wrapper: ({ children }) => ({children}), + wrapper: ({ children }) => ( + {children} + ), }); } - describe('useWorkspace getWorkspaces', () => { - - it('normal', async () => { + describe("useWorkspace getWorkspaces", () => { + it("normal", async () => { const { result } = renderUseWorkspaceModule(); - await act(async () => { result.current.getWorkspaces(user1.name) }); + await act(async () => { + result.current.getWorkspaces(user1.name); + }); expect(result.current.workspaces).toMatchSnapshot(); }); - it('normal empty', async () => { + it("normal empty", async () => { const { result } = renderUseWorkspaceModule(); - wsMock.getWorkspaces.mockResolvedValue(new GetWorkspacesResponse({ message: "", items: [] })); - await act(async () => { result.current.getWorkspaces(user1.name) }); + wsMock.getWorkspaces.mockResolvedValue( + new GetWorkspacesResponse({ message: "", items: [] }) + ); + await act(async () => { + result.current.getWorkspaces(user1.name); + }); expect(result.current.workspaces).toMatchSnapshot(); }); - - it('get error', async () => { + it("get error", async () => { const { result } = renderUseWorkspaceModule(); // wsMock.getWorkspaces.mockRejectedValue(new Error('[mock] getWorkspaces error')); - wsMock.getWorkspaces.mockRejectedValue(new ConnectError('[mock] getWorkspaces error', Code.Unauthenticated)); - await expect(result.current.getWorkspaces(user1.name)).rejects.toMatchSnapshot(); + wsMock.getWorkspaces.mockRejectedValue( + new ConnectError("[mock] getWorkspaces error", Code.Unauthenticated) + ); + await expect( + result.current.getWorkspaces(user1.name) + ).rejects.toMatchSnapshot(); expect(result.current.workspaces).toMatchSnapshot(); }); }); - - describe('useWorkspace refreshWorkspaces', () => { - - it('normal', async () => { + describe("useWorkspace refreshWorkspaces", () => { + it("normal", async () => { const { result } = renderUseWorkspaceModule(); - await act(async () => { result.current.refreshWorkspaces(user1.name) }); + await act(async () => { + result.current.refreshWorkspaces(user1.name); + }); expect(result.current.workspaces).toMatchSnapshot(); }); - }); - describe('useWorkspace refreshWorkspace', () => { - - it('normal creating', async () => { - - const wsCreateing = wsStat(ws12, 1, 'Creating'); - const wsStarting = wsStat(ws12, 1, 'NotRunning'); - const wsPending = wsStat(ws12, 1, 'Pending'); - const wsRunning = wsStat(ws12, 1, 'Running'); + describe("useWorkspace refreshWorkspace", () => { + it("normal creating", async () => { + const wsCreateing = wsStat(ws12, 1, "Creating"); + const wsStarting = wsStat(ws12, 1, "NotRunning"); + const wsPending = wsStat(ws12, 1, "Pending"); + const wsRunning = wsStat(ws12, 1, "Running"); vi.useFakeTimers(); - vi.spyOn(global, 'setTimeout'); + vi.spyOn(global, "setTimeout"); const { result } = renderUseWorkspaceModule(); // getWorkspaces then setWorkspaces - wsMock.getWorkspaces.mockResolvedValueOnce(new GetWorkspacesResponse({ message: "", items: [ws11, wsCreateing, ws13] })); - await act(async () => { result.current.getWorkspaces(user1.name) }); + wsMock.getWorkspaces.mockResolvedValueOnce( + new GetWorkspacesResponse({ + message: "", + items: [ws11, wsCreateing, ws13], + }) + ); + await act(async () => { + result.current.getWorkspaces(user1.name); + }); expect(result.current.workspaces).toMatchSnapshot(); // refReshWorkspace - await act(async () => { result.current.refreshWorkspace(wsCreateing) }); + await act(async () => { + result.current.refreshWorkspace(wsCreateing); + }); wsMock.getWorkspace.mockRejectedValueOnce(new Error()); - await act(async () => { vi.runAllTimers(); }); + await act(async () => { + vi.runAllTimers(); + }); expect(result.current.workspaces).toMatchSnapshot(); //  refReshWorkspace starting - wsMock.getWorkspace.mockResolvedValueOnce(new GetWorkspaceResponse({ workspace: wsStarting })); - await act(async () => { vi.runAllTimers(); }); + wsMock.getWorkspace.mockResolvedValueOnce( + new GetWorkspaceResponse({ workspace: wsStarting }) + ); + await act(async () => { + vi.runAllTimers(); + }); expect(result.current.workspaces).toMatchSnapshot(); //  refReshWorkspace starting - wsMock.getWorkspace.mockResolvedValueOnce(new GetWorkspaceResponse({ workspace: wsStarting })); - await act(async () => { vi.runAllTimers(); }); + wsMock.getWorkspace.mockResolvedValueOnce( + new GetWorkspaceResponse({ workspace: wsStarting }) + ); + await act(async () => { + vi.runAllTimers(); + }); expect(result.current.workspaces).toMatchSnapshot(); //  refReshWorkspace pending - wsMock.getWorkspace.mockResolvedValueOnce(new GetWorkspaceResponse({ workspace: wsPending })); - await act(async () => { vi.runAllTimers(); }); + wsMock.getWorkspace.mockResolvedValueOnce( + new GetWorkspaceResponse({ workspace: wsPending }) + ); + await act(async () => { + vi.runAllTimers(); + }); expect(result.current.workspaces).toMatchSnapshot(); //  refReshWorkspace running - wsMock.getWorkspace.mockResolvedValueOnce(new GetWorkspaceResponse({ workspace: wsRunning })); - await act(async () => { vi.runAllTimers(); }); + wsMock.getWorkspace.mockResolvedValueOnce( + new GetWorkspaceResponse({ workspace: wsRunning }) + ); + await act(async () => { + vi.runAllTimers(); + }); expect(result.current.workspaces).toMatchSnapshot(); // expect(setTimeout).toHaveBeenCalledTimes(10); @@ -219,224 +345,348 @@ describe('useWorkspace', () => { // expect(setTimeout).toHaveBeenCalledTimes(10); }); - it('normal stopping', async () => { - - const wsStoping = wsStat(ws12, 0, 'Running'); - const wsStopped = wsStat(ws12, 0, 'NotRunning'); + it("normal stopping", async () => { + const wsStoping = wsStat(ws12, 0, "Running"); + const wsStopped = wsStat(ws12, 0, "NotRunning"); const { result } = renderUseWorkspaceModule(); // getWorkspaces then setWorkspaces - wsMock.getWorkspaces.mockResolvedValueOnce(new GetWorkspacesResponse({ message: "", items: [ws11, wsStoping, ws13] })); - await act(async () => { result.current.getWorkspaces(user1.name) }); + wsMock.getWorkspaces.mockResolvedValueOnce( + new GetWorkspacesResponse({ + message: "", + items: [ws11, wsStoping, ws13], + }) + ); + await act(async () => { + result.current.getWorkspaces(user1.name); + }); expect(result.current.workspaces).toMatchSnapshot(); vi.useFakeTimers(); - vi.spyOn(global, 'setTimeout'); + vi.spyOn(global, "setTimeout"); // refReshWorkspace - await act(async () => { result.current.refreshWorkspace(wsStoping) }); + await act(async () => { + result.current.refreshWorkspace(wsStoping); + }); - wsMock.getWorkspace.mockResolvedValueOnce(new GetWorkspaceResponse({ workspace: wsStoping })); - await act(async () => { vi.runOnlyPendingTimers(); }); + wsMock.getWorkspace.mockResolvedValueOnce( + new GetWorkspaceResponse({ workspace: wsStoping }) + ); + await act(async () => { + vi.runOnlyPendingTimers(); + }); expect(result.current.workspaces).toMatchSnapshot(); //  refReshWorkspace stoping - wsMock.getWorkspace.mockResolvedValueOnce(new GetWorkspaceResponse({ workspace: wsStoping })); - await act(async () => { vi.runOnlyPendingTimers(); }); + wsMock.getWorkspace.mockResolvedValueOnce( + new GetWorkspaceResponse({ workspace: wsStoping }) + ); + await act(async () => { + vi.runOnlyPendingTimers(); + }); expect(result.current.workspaces).toMatchSnapshot(); //  refReshWorkspace stopped - wsMock.getWorkspace.mockResolvedValueOnce(new GetWorkspaceResponse({ workspace: wsStopped })); - await act(async () => { vi.runOnlyPendingTimers(); }); + wsMock.getWorkspace.mockResolvedValueOnce( + new GetWorkspaceResponse({ workspace: wsStopped }) + ); + await act(async () => { + vi.runOnlyPendingTimers(); + }); expect(result.current.workspaces).toMatchSnapshot(); }); - it('normal network modify', async () => { - - - const wsRunning1 = wsStat(ws12, 1, 'Running'); + it("normal network modify", async () => { + const wsRunning1 = wsStat(ws12, 1, "Running"); wsRunning1.spec!.additionalNetwork = [ - new NetworkRule({ name: 'portname1', portNumber: 3000, url: 'url1', public: false }), - new NetworkRule({ name: 'portname2', portNumber: 3000, public: false }), + new NetworkRule({ + name: "portname1", + portNumber: 3000, + url: "url1", + public: false, + }), + new NetworkRule({ name: "portname2", portNumber: 3000, public: false }), ]; - const wsRunning2 = wsStat(ws12, 1, 'Running'); + const wsRunning2 = wsStat(ws12, 1, "Running"); wsRunning1.spec!.additionalNetwork = [ - new NetworkRule({ name: 'portname1', portNumber: 3000, url: 'url1', public: false }), - new NetworkRule({ name: 'portname2', portNumber: 3000, url: 'url2', public: false }), + new NetworkRule({ + name: "portname1", + portNumber: 3000, + url: "url1", + public: false, + }), + new NetworkRule({ + name: "portname2", + portNumber: 3000, + url: "url2", + public: false, + }), ]; vi.useFakeTimers(); - vi.spyOn(global, 'setTimeout'); + vi.spyOn(global, "setTimeout"); const { result } = renderUseWorkspaceModule(); // getWorkspaces then setWorkspaces - wsMock.getWorkspaces.mockResolvedValueOnce(new GetWorkspacesResponse({ message: "", items: [ws11, wsRunning1, ws13] })); - await act(async () => { result.current.getWorkspaces(user1.name) }); + wsMock.getWorkspaces.mockResolvedValueOnce( + new GetWorkspacesResponse({ + message: "", + items: [ws11, wsRunning1, ws13], + }) + ); + await act(async () => { + result.current.getWorkspaces(user1.name); + }); expect(result.current.workspaces).toMatchSnapshot(); // refReshWorkspace - await act(async () => { result.current.refreshWorkspace(wsRunning1) }); + await act(async () => { + result.current.refreshWorkspace(wsRunning1); + }); - wsMock.getWorkspace.mockResolvedValueOnce(new GetWorkspaceResponse({ workspace: wsRunning1 })); - await act(async () => { vi.runAllTimers(); }); + wsMock.getWorkspace.mockResolvedValueOnce( + new GetWorkspaceResponse({ workspace: wsRunning1 }) + ); + await act(async () => { + vi.runAllTimers(); + }); expect(result.current.workspaces).toMatchSnapshot(); //  refReshWorkspace starting - wsMock.getWorkspace.mockResolvedValueOnce(new GetWorkspaceResponse({ workspace: wsRunning2 })); - await act(async () => { vi.runAllTimers(); }); + wsMock.getWorkspace.mockResolvedValueOnce( + new GetWorkspaceResponse({ workspace: wsRunning2 }) + ); + await act(async () => { + vi.runAllTimers(); + }); expect(result.current.workspaces).toMatchSnapshot(); const timerCount = (setTimeout as any).mock.calls.length; - await act(async () => { vi.runOnlyPendingTimers(); }); + await act(async () => { + vi.runOnlyPendingTimers(); + }); expect(setTimeout).toHaveBeenCalledTimes(timerCount); }); - it('normal timeout', async () => { - - const wsStoping = wsStat(ws12, 0, 'Running'); + it("normal timeout", async () => { + const wsStoping = wsStat(ws12, 0, "Running"); const { result } = renderUseWorkspaceModule(); // getWorkspaces then setWorkspaces - wsMock.getWorkspaces.mockResolvedValueOnce(new GetWorkspacesResponse({ message: "", items: [ws11, wsStoping, ws13] })); - await act(async () => { result.current.getWorkspaces(user1.name) }); + wsMock.getWorkspaces.mockResolvedValueOnce( + new GetWorkspacesResponse({ + message: "", + items: [ws11, wsStoping, ws13], + }) + ); + await act(async () => { + result.current.getWorkspaces(user1.name); + }); expect(result.current.workspaces).toMatchSnapshot(); vi.useFakeTimers(); - vi.spyOn(global, 'setTimeout'); + vi.spyOn(global, "setTimeout"); // refReshWorkspace - await act(async () => { result.current.refreshWorkspace(wsStoping) }); + await act(async () => { + result.current.refreshWorkspace(wsStoping); + }); - wsMock.getWorkspace.mockResolvedValue(new GetWorkspaceResponse({ workspace: wsStoping })); + wsMock.getWorkspace.mockResolvedValue( + new GetWorkspaceResponse({ workspace: wsStoping }) + ); let i; for (i = 120000; 0 < i; i -= 1000) { - await act(async () => { vi.runOnlyPendingTimers(); }); + await act(async () => { + vi.runOnlyPendingTimers(); + }); expect(result.current.workspaces).toMatchSnapshot(); } const timerCount = (setTimeout as any).mock.calls.length; - await act(async () => { vi.runOnlyPendingTimers(); }); + await act(async () => { + vi.runOnlyPendingTimers(); + }); expect(setTimeout).toHaveBeenCalledTimes(timerCount); }); - }); - it('error', async () => { - - const wsStoping = wsStat(ws12, 0, 'Running'); + it("error", async () => { + const wsStoping = wsStat(ws12, 0, "Running"); vi.useFakeTimers(); - vi.spyOn(global, 'setTimeout'); + vi.spyOn(global, "setTimeout"); const { result } = renderUseWorkspaceModule(); - await act(async () => { result.current.refreshWorkspace(wsStoping) }); + await act(async () => { + result.current.refreshWorkspace(wsStoping); + }); - wsMock.getWorkspace.mockRejectedValueOnce(new Error('[mock] getWorkspace error')); - await act(async () => { vi.runOnlyPendingTimers(); }); + wsMock.getWorkspace.mockRejectedValueOnce( + new Error("[mock] getWorkspace error") + ); + await act(async () => { + vi.runOnlyPendingTimers(); + }); expect(setTimeout).toHaveBeenCalledTimes(1); }); - - describe('useWorkspace createWorkspace', () => { - - it('normal', async () => { + describe("useWorkspace createWorkspace", () => { + it("normal", async () => { vi.useFakeTimers(); const { result } = renderUseWorkspaceModule(); - wsMock.getWorkspaces.mockResolvedValueOnce(new GetWorkspacesResponse({ message: "", items: [ws11, ws12, ws13, ws15] })); - await act(async () => { result.current.getWorkspaces(user1.name) }); + wsMock.getWorkspaces.mockResolvedValueOnce( + new GetWorkspacesResponse({ + message: "", + items: [ws11, ws12, ws13, ws15], + }) + ); + await act(async () => { + result.current.getWorkspaces(user1.name); + }); expect(result.current.workspaces).toMatchSnapshot(); - wsMock.createWorkspace.mockResolvedValueOnce(new CreateWorkspaceResponse({ message: "ok", workspace: ws14 })); - await act(async () => { result.current.createWorkspace(ws14.ownerName, ws14.name, ws14.spec!.template, {}) }); + wsMock.createWorkspace.mockResolvedValueOnce( + new CreateWorkspaceResponse({ message: "ok", workspace: ws14 }) + ); + await act(async () => { + result.current.createWorkspace( + ws14.ownerName, + ws14.name, + ws14.spec!.template, + {} + ); + }); expect(result.current.workspaces).toMatchSnapshot(); }); - it('get error', async () => { + it("get error", async () => { const { result } = renderUseWorkspaceModule(); - wsMock.createWorkspace.mockRejectedValue(new Error('[mock] createWorkspace error')); - await expect(result.current.createWorkspace(ws14.ownerName, ws14.name, ws14.spec!.template, {})) - .rejects.toMatchSnapshot(); + wsMock.createWorkspace.mockRejectedValue( + new Error("[mock] createWorkspace error") + ); + await expect( + result.current.createWorkspace( + ws14.ownerName, + ws14.name, + ws14.spec!.template, + {} + ) + ).rejects.toMatchSnapshot(); expect(result.current.workspaces).toMatchSnapshot(); }); }); - - describe('useWorkspace runWorkspace', () => { - - it('normal', async () => { + describe("useWorkspace runWorkspace", () => { + it("normal", async () => { vi.useFakeTimers(); const { result } = renderUseWorkspaceModule(); - wsMock.getWorkspaces.mockResolvedValueOnce(new GetWorkspacesResponse({ message: "", items: [ws11, ws12, ws13] })); - await act(async () => { result.current.getWorkspaces(user1.name) }); + wsMock.getWorkspaces.mockResolvedValueOnce( + new GetWorkspacesResponse({ message: "", items: [ws11, ws12, ws13] }) + ); + await act(async () => { + result.current.getWorkspaces(user1.name); + }); expect(result.current.workspaces).toMatchSnapshot(); - wsMock.updateWorkspace.mockResolvedValueOnce(new UpdateWorkspaceResponse({ message: "ok", workspace: ws11 })); - wsMock.getWorkspace.mockResolvedValueOnce(new GetWorkspaceResponse({ workspace: ws11 })); - await act(async () => { result.current.runWorkspace(ws11) }); + wsMock.updateWorkspace.mockResolvedValueOnce( + new UpdateWorkspaceResponse({ message: "ok", workspace: ws11 }) + ); + wsMock.getWorkspace.mockResolvedValueOnce( + new GetWorkspaceResponse({ workspace: ws11 }) + ); + await act(async () => { + result.current.runWorkspace(ws11); + }); expect(result.current.workspaces).toMatchSnapshot(); }); - it('error', async () => { + it("error", async () => { const { result } = renderUseWorkspaceModule(); - wsMock.updateWorkspace.mockRejectedValue(new Error('[mock] updateWorkspace error')); + wsMock.updateWorkspace.mockRejectedValue( + new Error("[mock] updateWorkspace error") + ); await expect(result.current.runWorkspace(ws11)).rejects.toMatchSnapshot(); expect(result.current.workspaces).toMatchSnapshot(); }); }); - - describe('useWorkspace stopWorkspace', () => { - - it('normal', async () => { + describe("useWorkspace stopWorkspace", () => { + it("normal", async () => { vi.useFakeTimers(); const { result } = renderUseWorkspaceModule(); - wsMock.getWorkspaces.mockResolvedValueOnce(new GetWorkspacesResponse({ message: "", items: [ws11, ws12, ws13] })); - await act(async () => { result.current.getWorkspaces(user1.name) }); + wsMock.getWorkspaces.mockResolvedValueOnce( + new GetWorkspacesResponse({ message: "", items: [ws11, ws12, ws13] }) + ); + await act(async () => { + result.current.getWorkspaces(user1.name); + }); expect(result.current.workspaces).toMatchSnapshot(); - wsMock.updateWorkspace.mockResolvedValueOnce(new UpdateWorkspaceResponse({ message: "ok", workspace: ws11 })); - wsMock.getWorkspace.mockResolvedValueOnce(new GetWorkspaceResponse({ workspace: ws11 })); - await act(async () => { result.current.stopWorkspace(ws11) }); + wsMock.updateWorkspace.mockResolvedValueOnce( + new UpdateWorkspaceResponse({ message: "ok", workspace: ws11 }) + ); + wsMock.getWorkspace.mockResolvedValueOnce( + new GetWorkspaceResponse({ workspace: ws11 }) + ); + await act(async () => { + result.current.stopWorkspace(ws11); + }); expect(result.current.workspaces).toMatchSnapshot(); }); - it('error', async () => { + it("error", async () => { const { result } = renderUseWorkspaceModule(); - wsMock.updateWorkspace.mockRejectedValue(new Error('[mock] patchWorkspace error')); - await expect(result.current.stopWorkspace(ws11)).rejects.toMatchSnapshot(); + wsMock.updateWorkspace.mockRejectedValue( + new Error("[mock] patchWorkspace error") + ); + await expect( + result.current.stopWorkspace(ws11) + ).rejects.toMatchSnapshot(); expect(result.current.workspaces).toMatchSnapshot(); }); }); - describe('useWorkspace deleteWorkspace', () => { - - it('normal', async () => { + describe("useWorkspace deleteWorkspace", () => { + it("normal", async () => { vi.useFakeTimers(); const { result } = renderUseWorkspaceModule(); - wsMock.getWorkspaces.mockResolvedValueOnce(new GetWorkspacesResponse({ message: "", items: [ws11, ws12, ws13] })); - await act(async () => { result.current.getWorkspaces(user1.name) }); + wsMock.getWorkspaces.mockResolvedValueOnce( + new GetWorkspacesResponse({ message: "", items: [ws11, ws12, ws13] }) + ); + await act(async () => { + result.current.getWorkspaces(user1.name); + }); expect(result.current.workspaces).toMatchSnapshot(); - wsMock.deleteWorkspace.mockResolvedValueOnce(new DeleteWorkspaceResponse({ message: "ok", workspace: ws11 })); - wsMock.getWorkspace.mockResolvedValueOnce(new GetWorkspaceResponse({ workspace: ws11 })); - await act(async () => { result.current.deleteWorkspace(ws11) }); + wsMock.deleteWorkspace.mockResolvedValueOnce( + new DeleteWorkspaceResponse({ message: "ok", workspace: ws11 }) + ); + wsMock.getWorkspace.mockResolvedValueOnce( + new GetWorkspaceResponse({ workspace: ws11 }) + ); + await act(async () => { + result.current.deleteWorkspace(ws11); + }); expect(result.current.workspaces).toMatchSnapshot(); }); - it('error', async () => { + it("error", async () => { const { result } = renderUseWorkspaceModule(); - wsMock.deleteWorkspace.mockRejectedValue(new Error('[mock] updateUserRole error')); - await expect(result.current.deleteWorkspace(ws11)).rejects.toMatchSnapshot(); + wsMock.deleteWorkspace.mockRejectedValue( + new Error("[mock] updateUserRole error") + ); + await expect( + result.current.deleteWorkspace(ws11) + ).rejects.toMatchSnapshot(); expect(result.current.workspaces).toMatchSnapshot(); }); }); - }); - -describe('useTemplates', () => { - +describe("useTemplates", () => { beforeEach(async () => { useSnackbarMock.mockReturnValue(snackbarMock); useProgressMock.mockReturnValue(progressMock); @@ -450,32 +700,48 @@ describe('useTemplates', () => { cleanup(); }); - it('normal', async () => { + it("normal", async () => { const { result } = renderHook(() => useTemplates()); - templateMock.getWorkspaceTemplates.mockResolvedValue(new GetWorkspaceTemplatesResponse({ message: "", items: [tmpl1, tmpl3, tmpl2] })); - await act(async () => { result.current.getTemplates() }); + templateMock.getWorkspaceTemplates.mockResolvedValue( + new GetWorkspaceTemplatesResponse({ + message: "", + items: [tmpl1, tmpl3, tmpl2], + }) + ); + await act(async () => { + result.current.getTemplates(); + }); expect(result.current.templates).toMatchSnapshot(); }); - it('normal empty', async () => { + it("normal empty", async () => { const { result } = renderHook(() => useTemplates()); - templateMock.getWorkspaceTemplates.mockResolvedValue(new GetWorkspaceTemplatesResponse({ message: "", items: [] })); - await act(async () => { result.current.getTemplates() }); + templateMock.getWorkspaceTemplates.mockResolvedValue( + new GetWorkspaceTemplatesResponse({ message: "", items: [] }) + ); + await act(async () => { + result.current.getTemplates(); + }); expect(result.current.templates).toMatchSnapshot(); }); - it('get error', async () => { + it("get error", async () => { const { result } = renderHook(() => useTemplates()); - templateMock.getWorkspaceTemplates.mockRejectedValue(new Error('[mock] getWorkspaceTemplates error')); + templateMock.getWorkspaceTemplates.mockRejectedValue( + new Error("[mock] getWorkspaceTemplates error") + ); await expect(result.current.getTemplates()).rejects.toMatchSnapshot(); expect(result.current.templates).toMatchSnapshot(); }); }); - -describe('useNetworkRule', () => { - - const nw111 = new NetworkRule({ name: 'nw1', httpPath: '/path1', portNumber: 1111, public: false }); +describe("useNetworkRule", () => { + const nw111 = new NetworkRule({ + name: "nw1", + httpPath: "/path1", + portNumber: 1111, + public: false, + }); beforeEach(async () => { vi.useFakeTimers(); @@ -493,47 +759,58 @@ describe('useNetworkRule', () => { function renderUseNetworkRule() { return renderHook(() => useNetworkRule(), { - wrapper: ({ children }) => ({children}), + wrapper: ({ children }) => ( + {children} + ), }); } - describe('useNetworkRule upsertNetwork', () => { - - it('normal', async () => { + describe("useNetworkRule upsertNetwork", () => { + it("normal", async () => { const { result } = renderUseNetworkRule(); - wsMock.upsertNetworkRule.mockResolvedValue(new UpsertNetworkRuleResponse({ message: "ok", networkRule: nw111 })); - await act(async () => { result.current.upsertNetwork(ws11, nw111) }); + wsMock.upsertNetworkRule.mockResolvedValue( + new UpsertNetworkRuleResponse({ message: "ok", networkRule: nw111 }) + ); + await act(async () => { + result.current.upsertNetwork(ws11, nw111); + }); }); - it('error', async () => { + it("error", async () => { const { result } = renderUseNetworkRule(); - wsMock.upsertNetworkRule.mockRejectedValue(new Error('[mock] upsertNetworkRule error')); - await expect(result.current.upsertNetwork(ws11, nw111)).rejects.toMatchSnapshot(); + wsMock.upsertNetworkRule.mockRejectedValue( + new Error("[mock] upsertNetworkRule error") + ); + await expect( + result.current.upsertNetwork(ws11, nw111) + ).rejects.toMatchSnapshot(); }); - }); - describe('useNetworkRule removeNetwork', () => { - - it('normal', async () => { + describe("useNetworkRule removeNetwork", () => { + it("normal", async () => { const { result } = renderUseNetworkRule(); - wsMock.deleteNetworkRule.mockResolvedValue(new DeleteNetworkRuleResponse({ message: "ok", networkRule: nw111 })); - await act(async () => { result.current.removeNetwork(ws11, nw111.name) }); + wsMock.deleteNetworkRule.mockResolvedValue( + new DeleteNetworkRuleResponse({ message: "ok", networkRule: nw111 }) + ); + await act(async () => { + result.current.removeNetwork(ws11, nw111.name); + }); }); - it('error', async () => { + it("error", async () => { const { result } = renderUseNetworkRule(); - wsMock.deleteNetworkRule.mockRejectedValue(new Error('[mock] deleteNetworkRule error')); - await expect(result.current.removeNetwork(ws11, nw111.name)).rejects.toMatchSnapshot(); + wsMock.deleteNetworkRule.mockRejectedValue( + new Error("[mock] deleteNetworkRule error") + ); + await expect( + result.current.removeNetwork(ws11, nw111.name) + ).rejects.toMatchSnapshot(); }); - }); - }); - -describe('useWorkspaceUsers', () => { - +describe("useWorkspaceUsers", () => { const useLoginMock = useLogin as MockedFunction; const loginMock: MockedMemberFunction = { loginUser: {} as any, @@ -543,8 +820,10 @@ describe('useWorkspaceUsers', () => { updataPassword: vi.fn(), refreshUserInfo: vi.fn(), clearLoginUser: vi.fn(), - } - const useUserServiceMock = useUserService as MockedFunction; + }; + const useUserServiceMock = useUserService as MockedFunction< + typeof useUserService + >; const userMock: MockedMemberFunction = { getUser: vi.fn(), getUsers: vi.fn(), @@ -552,8 +831,8 @@ describe('useWorkspaceUsers', () => { createUser: vi.fn(), updateUserDisplayName: vi.fn(), updateUserPassword: vi.fn(), - updateUserRole: vi.fn() - } + updateUserRole: vi.fn(), + }; beforeEach(async () => { useSnackbarMock.mockReturnValue(snackbarMock); @@ -566,9 +845,8 @@ describe('useWorkspaceUsers', () => { cleanup(); }); - describe('useWorkspaceUsers not login', () => { - - it('normal', async () => { + describe("useWorkspaceUsers not login", () => { + it("normal", async () => { useLoginMock.mockReturnValue({ loginUser: undefined as any, verifyLogin: vi.fn(), @@ -579,7 +857,11 @@ describe('useWorkspaceUsers', () => { clearLoginUser: vi.fn(), }); const result = renderHook(() => useWorkspaceUsersModule(), { - wrapper: ({ children }) => ({children}), + wrapper: ({ children }) => ( + + {children} + + ), }).result; expect(result.current.user).toMatchSnapshot(); @@ -587,9 +869,7 @@ describe('useWorkspaceUsers', () => { }); }); - - describe('useWorkspaceUsers getUsers', () => { - + describe("useWorkspaceUsers getUsers", () => { beforeEach(async () => { useLoginMock.mockReturnValue(loginMock); useUserServiceMock.mockReturnValue(userMock); @@ -597,38 +877,54 @@ describe('useWorkspaceUsers', () => { function renderUseWorkspaceUsersModule() { return renderHook(() => useWorkspaceUsersModule(), { - wrapper: ({ children }) => ({children}), + wrapper: ({ children }) => ( + + {children} + + ), }); } - it('normal', async () => { + it("normal", async () => { const { result } = renderUseWorkspaceUsersModule(); - userMock.getUsers.mockResolvedValue(new GetUsersResponse({ message: "ok", items: [user2, user1, user3] })); - await act(async () => { result.current.getUsers() }); + userMock.getUsers.mockResolvedValue( + new GetUsersResponse({ message: "ok", items: [user2, user1, user3] }) + ); + await act(async () => { + result.current.getUsers(); + }); expect(result.current.users).toMatchSnapshot(); }); - it('normal empty', async () => { + it("normal empty", async () => { const { result } = renderUseWorkspaceUsersModule(); - userMock.getUsers.mockResolvedValue(new GetUsersResponse({ message: "ok", items: [] })); - await act(async () => { result.current.getUsers() }); + userMock.getUsers.mockResolvedValue( + new GetUsersResponse({ message: "ok", items: [] }) + ); + await act(async () => { + result.current.getUsers(); + }); expect(result.current.users).toMatchSnapshot(); }); - it('normal no change', async () => { + it("normal no change", async () => { const { result } = renderUseWorkspaceUsersModule(); - userMock.getUsers.mockResolvedValue(new GetUsersResponse({ message: "ok", items: [user2, user1, user3] })); - await act(async () => { result.current.getUsers() }); - await act(async () => { result.current.getUsers() }); + userMock.getUsers.mockResolvedValue( + new GetUsersResponse({ message: "ok", items: [user2, user1, user3] }) + ); + await act(async () => { + result.current.getUsers(); + }); + await act(async () => { + result.current.getUsers(); + }); expect(result.current.users).toMatchSnapshot(); }); - it('error', async () => { + it("error", async () => { const { result } = renderUseWorkspaceUsersModule(); - userMock.getUsers.mockRejectedValue(new Error('[mock] getUsers error')); + userMock.getUsers.mockRejectedValue(new Error("[mock] getUsers error")); await expect(result.current.getUsers()).rejects.toMatchSnapshot(); }); - }); - -}); \ No newline at end of file +}); diff --git a/web/dashboard-ui/src/__tests__/views/pages/SignIn.spec.tsx b/web/dashboard-ui/src/__tests__/views/pages/SignIn.spec.tsx index 4fa2f61e..b19a8958 100644 --- a/web/dashboard-ui/src/__tests__/views/pages/SignIn.spec.tsx +++ b/web/dashboard-ui/src/__tests__/views/pages/SignIn.spec.tsx @@ -1,63 +1,100 @@ -import '@testing-library/jest-dom'; -import { assert, describe, it } from 'vitest'; -import { extractDomainFromHostname, isValidRedirectURLDomain } from '../../../views/pages/SignIn'; +import "@testing-library/jest-dom"; +import { assert, beforeEach, describe, it } from "vitest"; +import { + extractDomainFromHostname, + isValidRedirectURLDomain, +} from "../../../views/pages/SignIn"; //----------------------------------------------- // test //----------------------------------------------- -describe('extractDomainFromHostname', () => { - it('returns domain from hostname', () => { - assert.equal(extractDomainFromHostname('dashboard.cosmo.github.io'), 'cosmo.github.io'); - assert.equal(extractDomainFromHostname('cosmo-dashboard.github.com'), 'github.com'); - assert.equal(extractDomainFromHostname('main-ws1-tom-k3d-code-server.example.cosmo.github.io'), 'example.cosmo.github.io'); - assert.equal(extractDomainFromHostname('localhost'), 'localhost'); +describe("extractDomainFromHostname", () => { + it("returns domain from hostname", () => { + assert.equal( + extractDomainFromHostname("dashboard.cosmo.github.io"), + "cosmo.github.io" + ); + assert.equal( + extractDomainFromHostname("cosmo-dashboard.github.com"), + "github.com" + ); + assert.equal( + extractDomainFromHostname( + "main-ws1-tom-k3d-code-server.example.cosmo.github.io" + ), + "example.cosmo.github.io" + ); + assert.equal(extractDomainFromHostname("localhost"), "localhost"); }); }); -describe('isValidRedirectURLDomain', () => { +describe("isValidRedirectURLDomain", () => { beforeEach(() => { - mockWindowHostname('localhost'); + mockWindowHostname("localhost"); }); - describe('when redirect url has the same domain as current url', () => { - it('returns true', () => { - mockWindowHostname('dashboard.cosmo.github.io'); - assert.equal(isValidRedirectURLDomain('https://tom-workspace1.cosmo.github.io/api/foo/bar'), true); + describe("when redirect url has the same domain as current url", () => { + it("returns true", () => { + mockWindowHostname("dashboard.cosmo.github.io"); + assert.equal( + isValidRedirectURLDomain( + "https://tom-workspace1.cosmo.github.io/api/foo/bar" + ), + true + ); }); }); - describe('when redirect url with port has the same domain as current url', () => { - it('returns true', () => { - mockWindowHostname('cosmo-dashboard.github.com'); - assert.equal(isValidRedirectURLDomain('wss://main-workspace-tom.github.com:3000/api/foo/bar'), true); + describe("when redirect url with port has the same domain as current url", () => { + it("returns true", () => { + mockWindowHostname("cosmo-dashboard.github.com"); + assert.equal( + isValidRedirectURLDomain( + "wss://main-workspace-tom.github.com:3000/api/foo/bar" + ), + true + ); }); }); - describe('when redirect url which subdomain is long has the same domain as current url', () => { - it('returns true', () => { - mockWindowHostname('dashboard.example.cosmo.github.io'); - assert.equal(isValidRedirectURLDomain('http://main-ws1-tom-k3d-code-server.example.cosmo.github.io/api/foo/bar'), true); + describe("when redirect url which subdomain is long has the same domain as current url", () => { + it("returns true", () => { + mockWindowHostname("dashboard.example.cosmo.github.io"); + assert.equal( + isValidRedirectURLDomain( + "http://main-ws1-tom-k3d-code-server.example.cosmo.github.io/api/foo/bar" + ), + true + ); }); }); - describe('when current url and redirect url are localhost', () => { - it('returns true', () => { - assert.equal(isValidRedirectURLDomain('http://localhost:5000/api/foo/bar'), true); + describe("when current url and redirect url are localhost", () => { + it("returns true", () => { + assert.equal( + isValidRedirectURLDomain("http://localhost:5000/api/foo/bar"), + true + ); }); }); - describe('when redirect url does NOT have the same domain as current url', () => { - it('returns false', () => { - mockWindowHostname('dashboard.cosmo.github.io'); - assert.equal(isValidRedirectURLDomain('https://tom-workspace1.c0smo.github.io/api/foo/bar'), false); + describe("when redirect url does NOT have the same domain as current url", () => { + it("returns false", () => { + mockWindowHostname("dashboard.cosmo.github.io"); + assert.equal( + isValidRedirectURLDomain( + "https://tom-workspace1.c0smo.github.io/api/foo/bar" + ), + false + ); }); }); }); const mockWindowHostname = (hostname: string) => { global.window = Object.create(window); - Object.defineProperty(window, 'location', { + Object.defineProperty(window, "location", { value: { hostname: hostname, }, }); -} \ No newline at end of file +}; diff --git a/web/dashboard-ui/src/components/AuthRoute.tsx b/web/dashboard-ui/src/components/AuthRoute.tsx index f95141c7..6d18412f 100644 --- a/web/dashboard-ui/src/components/AuthRoute.tsx +++ b/web/dashboard-ui/src/components/AuthRoute.tsx @@ -1,12 +1,12 @@ -import React, { ReactElement } from 'react'; -import { Navigate, useLocation } from 'react-router-dom'; -import { isAdminUser } from '../views/organisms/UserModule'; -import { useLogin } from './LoginProvider'; +import React, { ReactElement } from "react"; +import { Navigate, useLocation } from "react-router-dom"; +import { isAdminUser } from "../views/organisms/UserModule"; +import { useLogin } from "./LoginProvider"; type Props = { children: ReactElement; admin?: boolean; -} +}; export const AuthRoute: React.VFC = ({ children, admin }) => { const { loginUser } = useLogin(); @@ -14,14 +14,16 @@ export const AuthRoute: React.VFC = ({ children, admin }) => { let location = useLocation(); if (!loginUser) { - return (); + return ( + + ); } else if (admin && !isAdmin) { - return (); + return ; } else { return children; } -} \ No newline at end of file +}; diff --git a/web/dashboard-ui/src/components/Base64.ts b/web/dashboard-ui/src/components/Base64.ts index 00edbc05..7263d87b 100644 --- a/web/dashboard-ui/src/components/Base64.ts +++ b/web/dashboard-ui/src/components/Base64.ts @@ -1,57 +1,54 @@ // https://github.com/google/webauthndemo/blob/main/src/public/scripts/base64url.ts -const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; +const chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; // Use a lookup table to find the index. const lookup = new Uint8Array(256); for (let i = 0; i < chars.length; i++) { - lookup[chars.charCodeAt(i)] = i; + lookup[chars.charCodeAt(i)] = i; } -const encode = function ( - arraybuffer: ArrayBuffer -): string { - const bytes = new Uint8Array(arraybuffer); - const len = bytes.length; - let base64 = ''; - - for (let i = 0; i < len; i += 3) { - base64 += chars[bytes[i] >> 2]; - base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)]; - base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)]; - base64 += chars[bytes[i + 2] & 63]; - } - - if (len % 3 === 2) { - base64 = base64.substring(0, base64.length - 1); - } else if (len % 3 === 1) { - base64 = base64.substring(0, base64.length - 2); - } - - return base64; +const encode = function (arraybuffer: ArrayBuffer): string { + const bytes = new Uint8Array(arraybuffer); + const len = bytes.length; + let base64 = ""; + + for (let i = 0; i < len; i += 3) { + base64 += chars[bytes[i] >> 2]; + base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)]; + base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)]; + base64 += chars[bytes[i + 2] & 63]; + } + + if (len % 3 === 2) { + base64 = base64.substring(0, base64.length - 1); + } else if (len % 3 === 1) { + base64 = base64.substring(0, base64.length - 2); + } + + return base64; }; -const decode = function ( - base64: string -): ArrayBuffer { - const len = base64.length; - const bufferLength = base64.length * 0.75; - const arraybuffer = new ArrayBuffer(bufferLength); - const bytes = new Uint8Array(arraybuffer); - - let p = 0; - for (let i = 0; i < len; i += 4) { - const encoded1 = lookup[base64.charCodeAt(i)]; - const encoded2 = lookup[base64.charCodeAt(i + 1)]; - const encoded3 = lookup[base64.charCodeAt(i + 2)]; - const encoded4 = lookup[base64.charCodeAt(i + 3)]; - - bytes[p++] = (encoded1 << 2) | (encoded2 >> 4); - bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2); - bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63); - } - - return arraybuffer; +const decode = function (base64: string): ArrayBuffer { + const len = base64.length; + const bufferLength = base64.length * 0.75; + const arraybuffer = new ArrayBuffer(bufferLength); + const bytes = new Uint8Array(arraybuffer); + + let p = 0; + for (let i = 0; i < len; i += 4) { + const encoded1 = lookup[base64.charCodeAt(i)]; + const encoded2 = lookup[base64.charCodeAt(i + 1)]; + const encoded3 = lookup[base64.charCodeAt(i + 2)]; + const encoded4 = lookup[base64.charCodeAt(i + 3)]; + + bytes[p++] = (encoded1 << 2) | (encoded2 >> 4); + bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2); + bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63); + } + + return arraybuffer; }; const base64url = { encode, decode }; diff --git a/web/dashboard-ui/src/components/ContextProvider.tsx b/web/dashboard-ui/src/components/ContextProvider.tsx index 2c988efa..6e81b5ce 100644 --- a/web/dashboard-ui/src/components/ContextProvider.tsx +++ b/web/dashboard-ui/src/components/ContextProvider.tsx @@ -3,13 +3,15 @@ import { createContext, ReactNode, useContext, useState } from "react"; /** * DialogContext */ -type DialogState = { open: boolean, dialogProps?: T } -type DialogProps = T & { onClose: () => void } +type DialogState = { open: boolean; dialogProps?: T }; +type DialogProps = T & { onClose: () => void }; type Dispatch = (open: boolean, dialogProp?: T) => void; export function DialogContext(dialog: (props: DialogProps) => any) { - - const Context = createContext<{ state: DialogState, dispatch: Dispatch }>(undefined as any); + const Context = createContext<{ + state: DialogState; + dispatch: Dispatch; + }>(undefined as any); return { Provider: ({ children }: { children: ReactNode }) => { @@ -17,17 +19,23 @@ export function DialogContext(dialog: (props: DialogProps) => any) { const dispatch: Dispatch = (open, dialogProps?) => { setState({ open, dialogProps }); - } + }; const closeHandler = () => dispatch(false, state.dialogProps); return ( - <> - {children} - {state.open && dialog({ ...state.dialogProps, onClose: () => closeHandler() } as DialogProps)} - + + <> + {children} + {state.open && + dialog({ + ...state.dialogProps, + onClose: () => closeHandler(), + } as DialogProps)} + + ); }, useDispatch: () => useContext(Context).dispatch, - } + }; } /** @@ -38,8 +46,10 @@ export function ModuleContext any>(useModule: T) { return { Provider: ({ children }: { children: ReactNode }) => { const module = useModule(); - return ({children}); + return ( + {children} + ); }, useContext: () => useContext(Context), - } + }; } diff --git a/web/dashboard-ui/src/components/LoginProvider.tsx b/web/dashboard-ui/src/components/LoginProvider.tsx index ee3fbbe0..dd3055c0 100644 --- a/web/dashboard-ui/src/components/LoginProvider.tsx +++ b/web/dashboard-ui/src/components/LoginProvider.tsx @@ -26,7 +26,7 @@ import { useProgress } from "./ProgressProvider"; * ref: https://github.com/DefinitelyTyped/DefinitelyTyped/pull/24509#issuecomment-774430643 */ const Context = createContext>( - undefined as any, + undefined as any ); /** @@ -58,7 +58,7 @@ const useLoginModule = () => { const handleMyEvents = (events: Event[]) => { for (const event of events) { - const index = myEvents.findIndex((e) => (e.id === event.id)); + const index = myEvents.findIndex((e) => e.id === event.id); if (index >= 0) { // replace event console.log("!!! replace", event.id, index); @@ -80,9 +80,12 @@ const useLoginModule = () => { const watchEvents = async (retryCount: number) => { console.log("Start watching events...", loginUser?.name, retryCount); try { - const result = await streamService.streamingEvents({ - userName: loginUser?.name, - }, {}); + const result = await streamService.streamingEvents( + { + userName: loginUser?.name, + }, + {} + ); for await (const event of result) { updateClock(); setNewEventsCount((v) => v + 1); @@ -129,7 +132,7 @@ const useLoginModule = () => { if (options.publicKey?.challenge) { opt.publicKey!.challenge = base64url.decode( - options.publicKey?.challenge, + options.publicKey?.challenge ); } @@ -144,7 +147,7 @@ const useLoginModule = () => { } if (options.publicKey?.allowCredentials) { opt.publicKey!.allowCredentials![index].id = base64url.decode( - options.publicKey?.allowCredentials[index].id, + options.publicKey?.allowCredentials[index].id ); } } @@ -279,7 +282,7 @@ const useLoginModule = () => { */ const updataPassword = async ( currentPassword: string, - newPassword: string, + newPassword: string ) => { console.log("updataPassword", loginUser?.name); setMask(); @@ -353,21 +356,22 @@ const useLoginModule = () => { /** * Provider */ -export const LoginProvider: React.FC> = ( - { children }, -) => { +export const LoginProvider: React.FC> = ({ + children, +}) => { console.log("LoginProvider"); const loginModule = useLoginModule(); const [isVerified, setIsVerified] = useState(false); useEffect(() => { - loginModule.verifyLogin() - .then(() => setIsVerified(true)); + loginModule.verifyLogin().then(() => setIsVerified(true)); }, []); // eslint-disable-line return ( - {isVerified ? children : ( + {isVerified ? ( + children + ) : (
diff --git a/web/dashboard-ui/src/components/MyThemeProvider.tsx b/web/dashboard-ui/src/components/MyThemeProvider.tsx index 135d59d3..bbb37e32 100644 --- a/web/dashboard-ui/src/components/MyThemeProvider.tsx +++ b/web/dashboard-ui/src/components/MyThemeProvider.tsx @@ -1,13 +1,14 @@ import { colors, useMediaQuery } from "@mui/material"; -import { createTheme, ThemeProvider } from "@mui/material/styles"; +import { ThemeProvider, createTheme } from "@mui/material/styles"; import React, { useMemo } from "react"; const MyTheme = () => { - console.log('MyTheme'); - const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)', { noSsr: true }); + console.log("MyTheme"); + const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)", { + noSsr: true, + }); return useMemo(() => { - return createTheme({ components: { MuiOutlinedInput: { @@ -22,21 +23,18 @@ const MyTheme = () => { }, }, palette: { - mode: prefersDarkMode ? 'dark' : undefined, + mode: prefersDarkMode ? "dark" : undefined, primary: colors.deepPurple, secondary: colors.pink, }, }); }, [prefersDarkMode]); -} - -export const MyThemeProvider: React.FC> = ({ children }) => { +}; +export const MyThemeProvider: React.FC> = ({ + children, +}) => { const myTheme = MyTheme(); - return ( - - {children} - - ); + return {children}; }; diff --git a/web/dashboard-ui/src/components/PageSettingsProvider.tsx b/web/dashboard-ui/src/components/PageSettingsProvider.tsx index 80fd4d23..16fbbf95 100644 --- a/web/dashboard-ui/src/components/PageSettingsProvider.tsx +++ b/web/dashboard-ui/src/components/PageSettingsProvider.tsx @@ -1,61 +1,74 @@ -import React, { createContext, useContext, useEffect, useReducer } from 'react'; +import React, { createContext, useContext, useEffect, useReducer } from "react"; interface PageSettings { isOpen: boolean; -}; +} interface PageSettingsContext { pageSettings: PageSettings; setPageSettings: (setting: PageSettings) => void; updatePageSettings: (setting: PageSettings) => void; -}; +} const Context = createContext({ pageSettings: { isOpen: true, }, - setPageSettings: (_setting) => { }, - updatePageSettings: (_setting) => { }, + setPageSettings: (_setting) => {}, + updatePageSettings: (_setting) => {}, }); interface Action { - type: 'SET' | 'UPDATE'; + type: "SET" | "UPDATE"; pageSettings: PageSettings; } function reducer(state: PageSettings, action: Action) { switch (action.type) { - case 'SET': + case "SET": return action.pageSettings; - case 'UPDATE': - return { ...state, ...action.pageSettings } + case "UPDATE": + return { ...state, ...action.pageSettings }; default: - throw new Error() + throw new Error(); } } -export const PageSettingsProvider: React.FC> = ({ children }) => { - const persistKey = 'csm_PageSettings'; - const persistPageSettings = JSON.parse(localStorage.getItem('csm_PageSettings') || "{}"); - const [pageSettings, dispatch] = useReducer(reducer, persistPageSettings || {}) +export const PageSettingsProvider: React.FC< + React.PropsWithChildren +> = ({ children }) => { + const persistKey = "csm_PageSettings"; + const persistPageSettings = JSON.parse( + localStorage.getItem("csm_PageSettings") || "{}" + ); + const [pageSettings, dispatch] = useReducer( + reducer, + persistPageSettings || {} + ); useEffect(() => { try { - localStorage.setItem(persistKey, JSON.stringify(pageSettings)) + localStorage.setItem(persistKey, JSON.stringify(pageSettings)); } catch (error) { - console.warn(error) + console.warn(error); } - }, [pageSettings]) + }, [pageSettings]); return ( - { dispatch({ type: 'SET', pageSettings: settings }) }, - updatePageSettings: (settings) => { dispatch({ type: 'UPDATE', pageSettings: settings }) } - }}> + { + dispatch({ type: "SET", pageSettings: settings }); + }, + updatePageSettings: (settings) => { + dispatch({ type: "UPDATE", pageSettings: settings }); + }, + }} + > {children} - ) -} + ); +}; export function usePageSettings() { return useContext(Context); diff --git a/web/dashboard-ui/src/components/ProgressProvider.tsx b/web/dashboard-ui/src/components/ProgressProvider.tsx index 4e399ed0..a80aa4e0 100644 --- a/web/dashboard-ui/src/components/ProgressProvider.tsx +++ b/web/dashboard-ui/src/components/ProgressProvider.tsx @@ -1,27 +1,44 @@ -import { Backdrop, CircularProgress } from '@mui/material'; -import React, { createContext, useContext, useState } from 'react'; +import { Backdrop, CircularProgress } from "@mui/material"; +import React, { createContext, useContext, useState } from "react"; -const DispatchContext = createContext>>(undefined as any); +const DispatchContext = createContext< + React.Dispatch> +>(undefined as any); /** * provider */ -export const ProgressProvider: React.FC> = ({ children }) => { +export const ProgressProvider: React.FC> = ({ + children, +}) => { const [count, setCount] = useState(0); - return (<> - - {children} - - theme.zIndex.drawer + 1000 }} open={count > 0}> - - - ); -} + return ( + <> + + {children} + + theme.zIndex.drawer + 1000 }} + open={count > 0} + > + + + + ); +}; export function useProgress() { const setCount = useContext(DispatchContext); return { - setMask: () => setCount(count => { console.log('setMask', count + 1); return count + 1 }), - releaseMask: () => setCount(count => { console.log('releaseMask', count); return count > 0 ? count - 1 : 0 }), - } -} \ No newline at end of file + setMask: () => + setCount((count) => { + console.log("setMask", count + 1); + return count + 1; + }), + releaseMask: () => + setCount((count) => { + console.log("releaseMask", count); + return count > 0 ? count - 1 : 0; + }), + }; +} diff --git a/web/dashboard-ui/src/index.tsx b/web/dashboard-ui/src/index.tsx index 73b4d61a..284d80bb 100644 --- a/web/dashboard-ui/src/index.tsx +++ b/web/dashboard-ui/src/index.tsx @@ -1,13 +1,13 @@ -import { createRoot } from 'react-dom/client'; -import App from './App'; -import './index.css'; -import reportWebVitals from './reportWebVitals'; +import { createRoot } from "react-dom/client"; +import App from "./App"; +import "./index.css"; +import reportWebVitals from "./reportWebVitals"; if (process.env.NODE_ENV !== "development") { - console.log = () => { }; + console.log = () => {}; } -createRoot(document.getElementById('root') as HTMLElement).render( +createRoot(document.getElementById("root") as HTMLElement).render( // // diff --git a/web/dashboard-ui/src/reportWebVitals.ts b/web/dashboard-ui/src/reportWebVitals.ts index 49a2a16e..5fa3583b 100644 --- a/web/dashboard-ui/src/reportWebVitals.ts +++ b/web/dashboard-ui/src/reportWebVitals.ts @@ -1,8 +1,8 @@ -import { ReportHandler } from 'web-vitals'; +import { ReportHandler } from "web-vitals"; const reportWebVitals = (onPerfEntry?: ReportHandler) => { if (onPerfEntry && onPerfEntry instanceof Function) { - import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { + import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { getCLS(onPerfEntry); getFID(onPerfEntry); getFCP(onPerfEntry); diff --git a/web/dashboard-ui/src/services/DashboardServices.ts b/web/dashboard-ui/src/services/DashboardServices.ts index 0a5f1eab..fd8f3262 100644 --- a/web/dashboard-ui/src/services/DashboardServices.ts +++ b/web/dashboard-ui/src/services/DashboardServices.ts @@ -1,5 +1,8 @@ import { createPromiseClient } from "@bufbuild/connect"; -import { createConnectTransport, createGrpcWebTransport } from "@bufbuild/connect-web"; +import { + createConnectTransport, + createGrpcWebTransport, +} from "@bufbuild/connect-web"; import { useMemo } from "react"; import { AuthService } from "../proto/gen/dashboard/v1alpha1/auth_service_connectweb"; import { StreamService } from "../proto/gen/dashboard/v1alpha1/event_service_connectweb"; @@ -9,28 +12,46 @@ import { WebAuthnService } from "../proto/gen/dashboard/v1alpha1/webauthn_connec import { WorkspaceService } from "../proto/gen/dashboard/v1alpha1/workspace_service_connectweb"; const transportX = createConnectTransport({ - baseUrl: import.meta.env.BASE_URL, + baseUrl: import.meta.env.BASE_URL, }); const transport = createGrpcWebTransport({ - baseUrl: import.meta.env.BASE_URL, + baseUrl: import.meta.env.BASE_URL, }); export function useAuthService() { - return useMemo(() => createPromiseClient(AuthService, transport), [AuthService]); + return useMemo( + () => createPromiseClient(AuthService, transport), + [AuthService] + ); } export function useTemplateService() { - return useMemo(() => createPromiseClient(TemplateService, transport), [TemplateService]); + return useMemo( + () => createPromiseClient(TemplateService, transport), + [TemplateService] + ); } export function useUserService() { - return useMemo(() => createPromiseClient(UserService, transport), [UserService]); + return useMemo( + () => createPromiseClient(UserService, transport), + [UserService] + ); } export function useWorkspaceService() { - return useMemo(() => createPromiseClient(WorkspaceService, transport), [WorkspaceService]); + return useMemo( + () => createPromiseClient(WorkspaceService, transport), + [WorkspaceService] + ); } export function useWebAuthnService() { - return useMemo(() => createPromiseClient(WebAuthnService, transport), [WebAuthnService]); + return useMemo( + () => createPromiseClient(WebAuthnService, transport), + [WebAuthnService] + ); } export function useStreamService() { - return useMemo(() => createPromiseClient(StreamService, transport), [StreamService]); -} \ No newline at end of file + return useMemo( + () => createPromiseClient(StreamService, transport), + [StreamService] + ); +} diff --git a/web/dashboard-ui/src/setupTests.ts b/web/dashboard-ui/src/setupTests.ts index d6caba73..82f73f04 100644 --- a/web/dashboard-ui/src/setupTests.ts +++ b/web/dashboard-ui/src/setupTests.ts @@ -2,8 +2,8 @@ // allows you to do things like: // expect(element).toHaveTextContent(/react/i) // learn more: https://github.com/testing-library/jest-dom -import '@testing-library/jest-dom'; -import { createSerializer } from '@emotion/jest'; +import { createSerializer } from "@emotion/jest"; +import "@testing-library/jest-dom"; // https://github.com/mui-org/material-ui/issues/21701 // expect.addSnapshotSerializer({ @@ -17,6 +17,6 @@ import { createSerializer } from '@emotion/jest'; // } // }); -expect.addSnapshotSerializer(createSerializer()) +expect.addSnapshotSerializer(createSerializer()); -//globalThis.IS_REACT_ACT_ENVIRONMENT = true; \ No newline at end of file +//globalThis.IS_REACT_ACT_ENVIRONMENT = true; diff --git a/web/dashboard-ui/src/views/atoms/AlertTooltip.tsx b/web/dashboard-ui/src/views/atoms/AlertTooltip.tsx index 9d6dd779..50e24f7f 100644 --- a/web/dashboard-ui/src/views/atoms/AlertTooltip.tsx +++ b/web/dashboard-ui/src/views/atoms/AlertTooltip.tsx @@ -1,18 +1,28 @@ import { - darken, lighten, styled, Tooltip, tooltipClasses, TooltipProps + darken, + lighten, + styled, + Tooltip, + tooltipClasses, + TooltipProps, } from "@mui/material"; -import React from "react"; export const AlertTooltip = styled(({ className, ...props }: TooltipProps) => ( ))(({ theme }) => ({ [`& .${tooltipClasses.arrow}`]: { - fontSize: '1rem', - color: (theme.palette.mode === 'light' ? lighten : darken)(theme.palette['info'].light, 0.9), + fontSize: "1rem", + color: (theme.palette.mode === "light" ? lighten : darken)( + theme.palette["info"].light, + 0.9 + ), }, [`& .${tooltipClasses.tooltip}`]: { borderRadius: "4px", boxShadow: theme.shadows[5], - backgroundColor: (theme.palette.mode === 'light' ? lighten : darken)(theme.palette['info'].light, 0.9), + backgroundColor: (theme.palette.mode === "light" ? lighten : darken)( + theme.palette["info"].light, + 0.9 + ), }, })); diff --git a/web/dashboard-ui/src/views/atoms/EditableTypography.tsx b/web/dashboard-ui/src/views/atoms/EditableTypography.tsx index f436ecad..9be5fcf7 100644 --- a/web/dashboard-ui/src/views/atoms/EditableTypography.tsx +++ b/web/dashboard-ui/src/views/atoms/EditableTypography.tsx @@ -1,63 +1,99 @@ import { Check, Close, Edit } from "@mui/icons-material"; import { - IconButton, - InputBase, InputBaseProps, - Stack, - Tooltip + IconButton, + InputBase, + InputBaseProps, + Stack, + Tooltip, } from "@mui/material"; import React, { useState } from "react"; -export type EditableTypographyProps = - InputBaseProps - & { children: string, onSave: (inputData: string) => void, showAlways?: boolean }; +export type EditableTypographyProps = InputBaseProps & { + children: string; + onSave: (inputData: string) => void; + showAlways?: boolean; +}; -export const EditableTypography: React.FC = ({ children, onSave, showAlways, ...props }) => { +export const EditableTypography: React.FC = ({ + children, + onSave, + showAlways, + ...props +}) => { + const [showEditIcon, setShowEditIcon] = useState(showAlways); + const [editting, setEditing] = useState(false); + const [inputData, setInputData] = useState(children); - const [showEditIcon, setShowEditIcon] = useState(showAlways); - const [editting, setEditing] = useState(false); - const [inputData, setInputData] = useState(children); + const editIcon = ( + + { + setEditing(true); + }} + > + + + + ); + const editingIcons = ( + + + { + onSave(inputData); + setEditing(false); + }} + > + + + + + { + setInputData(children); + setEditing(false); + }} + > + + + + + ); - const editIcon = ( - - { setEditing(true) }}> - - - - ); - const editingIcons = ( - - - { onSave(inputData); setEditing(false) }}> - - - - - { - setInputData(children); setEditing(false) - }}> - - - - - ); - - - return ( - { setShowEditIcon(showAlways || true) }} - onMouseLeave={() => { setShowEditIcon(showAlways || false) }} - onBlur={(e) => { setInputData(e.currentTarget.value) }} - sx={{ borderBottom: editting ? 1 : 0 }} - {...props} - />) -} + return ( + { + setShowEditIcon(showAlways || true); + }} + onMouseLeave={() => { + setShowEditIcon(showAlways || false); + }} + onBlur={(e) => { + setInputData(e.currentTarget.value); + }} + sx={{ borderBottom: editting ? 1 : 0 }} + {...props} + /> + ); +}; diff --git a/web/dashboard-ui/src/views/atoms/EllipsisTypography.tsx b/web/dashboard-ui/src/views/atoms/EllipsisTypography.tsx index d76e65e2..c229d7ba 100644 --- a/web/dashboard-ui/src/views/atoms/EllipsisTypography.tsx +++ b/web/dashboard-ui/src/views/atoms/EllipsisTypography.tsx @@ -1,32 +1,60 @@ -import { Tooltip, TooltipProps, Typography, TypographyProps } from "@mui/material"; +import { + Tooltip, + TooltipProps, + Typography, + TypographyProps, +} from "@mui/material"; import React from "react"; - - -export type EllipsisTypographyProps = - Omit - & { children: string, placement?: TooltipProps["placement"] }; - -export const EllipsisTypography: React.FC = ({ children, placement }) => { - - const [isOverflow, setIsOverflow] = React.useState(false); - const paragraph = React.useRef(null); - - React.useEffect(() => { - const pElement = paragraph.current; - if (pElement) { - setIsOverflow(Boolean(pElement.offsetWidth < pElement.scrollWidth)); - } - }, [paragraph]); - - const title = children - - // return isOverflow ? ( {typoglaphy} ) : typoglaphy - return isOverflow ? ( - - {children} - - ) : {children} -} +export type EllipsisTypographyProps = Omit & { + children: string; + placement?: TooltipProps["placement"]; +}; + +export const EllipsisTypography: React.FC = ({ + children, + placement, +}) => { + const [isOverflow, setIsOverflow] = React.useState(false); + const paragraph = React.useRef(null); + + React.useEffect(() => { + const pElement = paragraph.current; + if (pElement) { + setIsOverflow(Boolean(pElement.offsetWidth < pElement.scrollWidth)); + } + }, [paragraph]); + + const title = children; + + // return isOverflow ? ( {typoglaphy} ) : typoglaphy + return isOverflow ? ( + + + {children} + + + ) : ( + + {children} + + ); +}; diff --git a/web/dashboard-ui/src/views/atoms/EventsDataGrid.tsx b/web/dashboard-ui/src/views/atoms/EventsDataGrid.tsx index dac2e91f..08058f30 100644 --- a/web/dashboard-ui/src/views/atoms/EventsDataGrid.tsx +++ b/web/dashboard-ui/src/views/atoms/EventsDataGrid.tsx @@ -3,8 +3,8 @@ import { Chip, SxProps } from "@mui/material"; import { DataGrid, DataGridProps, - gridClasses, GridColDef, + gridClasses, } from "@mui/x-data-grid"; import React from "react"; import { Event } from "../../proto/gen/dashboard/v1alpha1/event_pb"; @@ -18,9 +18,13 @@ export type EventsDataGridProp = { clock: Date; }; -export const EventsDataGrid: React.FC = ( - { events, maxHeight, sx, dataGridProps, clock }, -) => { +export const EventsDataGrid: React.FC = ({ + events, + maxHeight, + sx, + dataGridProps, + clock, +}) => { const eventDetailDialogDispatch = EventDetailDialogContext.useDispatch(); const columns: GridColDef[] = [ @@ -41,9 +45,13 @@ export const EventsDataGrid: React.FC = ( flex: 0.25, renderCell: (params) => ( - : } + icon={ + params.api.getRow(params.id).type == "Normal" ? ( + + ) : ( + + ) + } label={params.value} /> ), diff --git a/web/dashboard-ui/src/views/atoms/NameAvatar.tsx b/web/dashboard-ui/src/views/atoms/NameAvatar.tsx index b8f4f053..e0167d44 100644 --- a/web/dashboard-ui/src/views/atoms/NameAvatar.tsx +++ b/web/dashboard-ui/src/views/atoms/NameAvatar.tsx @@ -1,29 +1,38 @@ -import { Avatar, AvatarProps, Typography } from "@mui/material"; import { AccountCircle } from "@mui/icons-material"; +import { Avatar, AvatarProps, Typography } from "@mui/material"; import React from "react"; -export const NameAvatar: React.VFC<{ name?: string } & AvatarProps> = (props) => { - return ( - props.name ? - - (theme.palette.mode === 'light' ? 'white' : 'black') }} fontSize='inherit'> - {props.name.substring(0, 1).toUpperCase()} - - - : - - ) -} +export const NameAvatar: React.VFC<{ name?: string } & AvatarProps> = ( + props +) => { + return props.name ? ( + + + theme.palette.mode === "light" ? "white" : "black", + }} + fontSize="inherit" + > + {props.name.substring(0, 1).toUpperCase()} + + + ) : ( + + + + ); +}; const stringToColor = (str: string) => { let hash = 0; for (let i = 0; i < str.length; i++) { hash = str.charCodeAt(i) + ((hash << 5) - hash); } - let color = '#'; + let color = "#"; for (let i = 0; i < 3; i++) { - let value = (hash >> (i * 8)) & 0xFF; - color += ('00' + value.toString(16)).substr(-2); + let value = (hash >> (i * 8)) & 0xff; + color += ("00" + value.toString(16)).substr(-2); } return color; -} +}; diff --git a/web/dashboard-ui/src/views/atoms/PasswordTextField.tsx b/web/dashboard-ui/src/views/atoms/PasswordTextField.tsx index 40e5ad82..b2c31231 100644 --- a/web/dashboard-ui/src/views/atoms/PasswordTextField.tsx +++ b/web/dashboard-ui/src/views/atoms/PasswordTextField.tsx @@ -1,29 +1,47 @@ +import { Visibility, VisibilityOff, VpnKey } from "@mui/icons-material"; import { IconButton, - InputAdornment, TextField, TextFieldProps + InputAdornment, + TextField, + TextFieldProps, } from "@mui/material"; -import { Visibility, VisibilityOff, VpnKey } from "@mui/icons-material"; import React, { useState } from "react"; - -export const PasswordTextField: React.VFC = ({ type, ...props }) => { +export const PasswordTextField: React.VFC = ({ + type, + ...props +}) => { const [isPassShow, setIsPassShow] = useState(false); return ( - ), - endAdornment: ( - { setIsPassShow(true) }} - onPointerUp={() => { setIsPassShow(false) }} - onPointerOut={() => { setIsPassShow(false) }} - > - {isPassShow ? : } - - ), + startAdornment: ( + + + + ), + endAdornment: ( + + { + setIsPassShow(true); + }} + onPointerUp={() => { + setIsPassShow(false); + }} + onPointerOut={() => { + setIsPassShow(false); + }} + > + {isPassShow ? : } + + + ), }} /> ); -} +}; diff --git a/web/dashboard-ui/src/views/atoms/SelectableChips.tsx b/web/dashboard-ui/src/views/atoms/SelectableChips.tsx index bad44392..b0518cd7 100644 --- a/web/dashboard-ui/src/views/atoms/SelectableChips.tsx +++ b/web/dashboard-ui/src/views/atoms/SelectableChips.tsx @@ -1,35 +1,75 @@ import { Check } from "@mui/icons-material"; import { Chip, ChipProps, ChipTypeMap } from "@mui/material"; import { forwardRef, useState } from "react"; -import { useController, UseControllerProps } from "react-hook-form"; +import { UseControllerProps, useController } from "react-hook-form"; -type toggleChipProps = { variant: ChipTypeMap['props']['variant'], onClick?: () => void } & ChipProps +type toggleChipProps = { + variant: ChipTypeMap["props"]["variant"]; + onClick?: () => void; +} & ChipProps; -export const FormSelectableChip = forwardRef((props: UseControllerProps & ChipProps, ref) => { +export const FormSelectableChip = forwardRef( + (props: UseControllerProps & ChipProps, ref) => { const { field } = useController(props); - return -}) + return ( + + ); + } +); -export const SelectableChip: React.FC<{ checked?: boolean, onChecked?: (...event: any[]) => void } & ChipProps> = ({ onChecked, ...props }) => { - const [mouseovered, setMouseovered] = useState(false); +export const SelectableChip: React.FC< + { checked?: boolean; onChecked?: (...event: any[]) => void } & ChipProps +> = ({ onChecked, ...props }) => { + const [mouseovered, setMouseovered] = useState(false); - const [checked, setChecked] = useState(props.defaultChecked || false); + const [checked, setChecked] = useState( + props.defaultChecked || false + ); - if (props.checked !== undefined && props.checked !== checked) { - setChecked(props.checked); - } + if (props.checked !== undefined && props.checked !== checked) { + setChecked(props.checked); + } - const toggleChecked = () => { setChecked(!checked); onChecked && onChecked(!checked); } + const toggleChecked = () => { + setChecked(!checked); + onChecked && onChecked(!checked); + }; - const unCheckedChipProps: toggleChipProps = { variant: "outlined", onClick: toggleChecked } - const checkedChipProps: toggleChipProps = { variant: "filled", onClick: toggleChecked, onDelete: toggleChecked, deleteIcon: } - const checkedChipPropsMouseovered: toggleChipProps = { variant: "filled", onClick: toggleChecked, onDelete: toggleChecked } + const unCheckedChipProps: toggleChipProps = { + variant: "outlined", + onClick: toggleChecked, + }; + const checkedChipProps: toggleChipProps = { + variant: "filled", + onClick: toggleChecked, + onDelete: toggleChecked, + deleteIcon: , + }; + const checkedChipPropsMouseovered: toggleChipProps = { + variant: "filled", + onClick: toggleChecked, + onDelete: toggleChecked, + }; - return { setMouseovered(true) }} - onMouseLeave={() => { setMouseovered(false) }} - {...props} /> -} + onMouseEnter={() => { + setMouseovered(true); + }} + onMouseLeave={() => { + setMouseovered(false); + }} + {...props} + /> + ); +}; diff --git a/web/dashboard-ui/src/views/atoms/TextFieldLabel.tsx b/web/dashboard-ui/src/views/atoms/TextFieldLabel.tsx index 87f31860..375f3154 100644 --- a/web/dashboard-ui/src/views/atoms/TextFieldLabel.tsx +++ b/web/dashboard-ui/src/views/atoms/TextFieldLabel.tsx @@ -1,21 +1,28 @@ -import { - InputAdornment, TextField, TextFieldProps -} from "@mui/material"; +import { InputAdornment, TextField, TextFieldProps } from "@mui/material"; import React, { ReactNode } from "react"; -export type TextFieldLabelProps = - Omit - & { startAdornmentIcon?: ReactNode }; - -export const TextFieldLabel: React.VFC = ({ InputProps, startAdornmentIcon, ...props }) => { +export type TextFieldLabelProps = Omit & { + startAdornmentIcon?: ReactNode; +}; +export const TextFieldLabel: React.VFC = ({ + InputProps, + startAdornmentIcon, + ...props +}) => { return ( - {startAdornmentIcon}), + startAdornment: startAdornmentIcon && ( + {startAdornmentIcon} + ), }} - />) -} + /> + ); +}; diff --git a/web/dashboard-ui/src/views/organisms/AuthenticatorManageDialog.tsx b/web/dashboard-ui/src/views/organisms/AuthenticatorManageDialog.tsx index 2e072901..a8650d9c 100644 --- a/web/dashboard-ui/src/views/organisms/AuthenticatorManageDialog.tsx +++ b/web/dashboard-ui/src/views/organisms/AuthenticatorManageDialog.tsx @@ -10,80 +10,107 @@ import { IconButton, Tooltip, Typography, - useMediaQuery, useTheme + useMediaQuery, + useTheme, } from "@mui/material"; -import Box from '@mui/material/Box'; -import { useSnackbar } from 'notistack'; +import Box from "@mui/material/Box"; +import { useSnackbar } from "notistack"; import { useEffect, useState } from "react"; -import { base64url } from '../../components/Base64'; +import { base64url } from "../../components/Base64"; import { DialogContext } from "../../components/ContextProvider"; import { User } from "../../proto/gen/dashboard/v1alpha1/user_pb"; import { Credential } from "../../proto/gen/dashboard/v1alpha1/webauthn_pb"; -import { useWebAuthnService } from '../../services/DashboardServices'; +import { useWebAuthnService } from "../../services/DashboardServices"; import { EditableTypography } from "../atoms/EditableTypography"; import { EllipsisTypography } from "../atoms/EllipsisTypography"; /** * view */ -export const AuthenticatorManageDialog: React.VFC<{ onClose: () => void, user: User }> = ({ onClose, user }) => { - console.log('AuthenticatorManageDialog'); +export const AuthenticatorManageDialog: React.VFC<{ + onClose: () => void; + user: User; +}> = ({ onClose, user }) => { + console.log("AuthenticatorManageDialog"); const webauthnService = useWebAuthnService(); const { enqueueSnackbar } = useSnackbar(); const [credentials, setCredentials] = useState([]); - const registerdCredId = localStorage.getItem(`credId`) - const isRegistered = Boolean(registerdCredId && credentials.map(c => c.id).includes(registerdCredId!)); + const registerdCredId = localStorage.getItem(`credId`); + const isRegistered = Boolean( + registerdCredId && credentials.map((c) => c.id).includes(registerdCredId!) + ); const [isWebAuthnAvailable, setIsWebAuthnAvailable] = useState(false); const checkWebAuthnAvailable = () => { if (window.PublicKeyCredential) { - PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable() - .then(uvpaa => { setIsWebAuthnAvailable(uvpaa) }); + PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable().then( + (uvpaa) => { + setIsWebAuthnAvailable(uvpaa); + } + ); } - } - useEffect(() => { checkWebAuthnAvailable() }, []); + }; + useEffect(() => { + checkWebAuthnAvailable(); + }, []); - console.log("credId", registerdCredId, "isRegistered", isRegistered, "isWebAuthnAvailable", isWebAuthnAvailable); + console.log( + "credId", + registerdCredId, + "isRegistered", + isRegistered, + "isWebAuthnAvailable", + isWebAuthnAvailable + ); /** * listCredentials - */ + */ const listCredentials = async () => { console.log("listCredentials"); try { - const resp = await webauthnService.listCredentials({ userName: user.name }); + const resp = await webauthnService.listCredentials({ + userName: user.name, + }); setCredentials(resp.credentials); - } - catch (error) { + } catch (error) { handleError(error); } - } - useEffect(() => { listCredentials() }, []); + }; + useEffect(() => { + listCredentials(); + }, []); /** * registerNewAuthenticator */ const registerNewAuthenticator = async () => { try { - const resp = await webauthnService.beginRegistration({ userName: user.name }); + const resp = await webauthnService.beginRegistration({ + userName: user.name, + }); const options = JSON.parse(resp.credentialCreationOptions); - const opt: CredentialCreationOptions = JSON.parse(JSON.stringify(options)); + const opt: CredentialCreationOptions = JSON.parse( + JSON.stringify(options) + ); if (options.publicKey?.user.id) { opt.publicKey!.user.id = base64url.decode(options.publicKey?.user.id); } if (options.publicKey?.challenge) { - opt.publicKey!.challenge = base64url.decode(options.publicKey?.challenge); + opt.publicKey!.challenge = base64url.decode( + options.publicKey?.challenge + ); } // Credential is allowed to access only id and type so use any. const cred: any = await navigator.credentials.create(opt); if (cred === null) { console.log("cred is null"); - throw Error('credential is null'); + throw Error("credential is null"); } const credential = { @@ -92,52 +119,61 @@ export const AuthenticatorManageDialog: React.VFC<{ onClose: () => void, user: U type: cred.type, response: { clientDataJSON: base64url.encode(cred.response.clientDataJSON), - attestationObject: base64url.encode(cred.response.attestationObject) - } + attestationObject: base64url.encode(cred.response.attestationObject), + }, }; localStorage.setItem(`credId`, credential.rawId); - const finResp = await webauthnService.finishRegistration({ userName: user.name, credentialCreationResponse: JSON.stringify(credential) }); - enqueueSnackbar(finResp.message, { variant: 'success' }); + const finResp = await webauthnService.finishRegistration({ + userName: user.name, + credentialCreationResponse: JSON.stringify(credential), + }); + enqueueSnackbar(finResp.message, { variant: "success" }); listCredentials(); - } - catch (error) { + } catch (error) { handleError(error); } - } + }; /** * removeCredentials - */ + */ const removeCredentials = async (id: string) => { console.log("removeCredentials"); - if (!confirm("Are you sure to REMOVE?\nID: " + id)) { return } + if (!confirm("Are you sure to REMOVE?\nID: " + id)) { + return; + } try { - const resp = await webauthnService.deleteCredential({ userName: user.name, credId: id }); - enqueueSnackbar(resp.message, { variant: 'success' }); + const resp = await webauthnService.deleteCredential({ + userName: user.name, + credId: id, + }); + enqueueSnackbar(resp.message, { variant: "success" }); listCredentials(); if (id === registerdCredId) { localStorage.removeItem(`credId`); } - } - catch (error) { + } catch (error) { handleError(error); } - } + }; /** * updateCredentialName - */ + */ const updateCredentialName = async (id: string, name: string) => { try { - const resp = await webauthnService.updateCredential({ userName: user.name, credId: id, credDisplayName: name }); - enqueueSnackbar(resp.message, { variant: 'success' }); + const resp = await webauthnService.updateCredential({ + userName: user.name, + credId: id, + credDisplayName: name, + }); + enqueueSnackbar(resp.message, { variant: "success" }); listCredentials(); - } - catch (error) { + } catch (error) { handleError(error); } - } + }; /** * error handler @@ -145,60 +181,121 @@ export const AuthenticatorManageDialog: React.VFC<{ onClose: () => void, user: U const handleError = (error: any) => { console.log(error); const msg = error?.message; - error instanceof DOMException || msg && enqueueSnackbar(msg, { variant: 'error' }); - } + error instanceof DOMException || + (msg && enqueueSnackbar(msg, { variant: "error" })); + }; const theme = useTheme(); - const sm = useMediaQuery(theme.breakpoints.up('sm'), { noSsr: true }); + const sm = useMediaQuery(theme.breakpoints.up("sm"), { noSsr: true }); return ( - + WebAuthn Credentials - {credentials.length === 0 - ? No credentials - : - - Created - Credential ID & Name - - + {credentials.length === 0 ? ( + No credentials + ) : ( + + + + {" "} + + Created + + + + + Credential ID & Name + + + + + + {credentials.map((field, index) => { return ( <> - {sm && - {registerdCredId === field.id && - - - - || undefined} - } - - {field.timestamp?.toDate().toLocaleDateString()} - {field.timestamp?.toDate().toLocaleTimeString()} + {sm && ( + + {(registerdCredId === field.id && ( + + + + )) || + undefined} + + )} + + + {field.timestamp?.toDate().toLocaleDateString()} + + + {field.timestamp?.toDate().toLocaleTimeString()} + + + + + {field.id} + + { + updateCredentialName(field.id, input); + }} + > + {field.displayName} + - - {field.id} - { updateCredentialName(field.id, input) }}>{field.displayName} - - < Grid item xs={1} sx={{ m: 'auto', textAlign: 'center' }}> - { removeCredentials(field.id) }}> + + { + removeCredentials(field.id); + }} + > + + - ) + ); })} - } + + )} - + - - {!isRegistered && isWebAuthnAvailable - ? - : undefined} + + {!isRegistered && isWebAuthnAvailable ? ( + + ) : undefined} - + ); }; @@ -206,4 +303,5 @@ export const AuthenticatorManageDialog: React.VFC<{ onClose: () => void, user: U * Context */ export const AuthenticatorManageDialogContext = DialogContext<{ user: User }>( - props => ()); + (props) => +); diff --git a/web/dashboard-ui/src/views/organisms/EventDetailDialog.tsx b/web/dashboard-ui/src/views/organisms/EventDetailDialog.tsx index a5ea739c..dee85346 100644 --- a/web/dashboard-ui/src/views/organisms/EventDetailDialog.tsx +++ b/web/dashboard-ui/src/views/organisms/EventDetailDialog.tsx @@ -39,37 +39,42 @@ const ClipboardTextField = (props: TextFieldProps) => { {...props} onMouseOver={() => setFocused(true)} onMouseLeave={() => setFocused(false)} - InputProps={focused - ? { - endAdornment: ( - - { - onCopy(String(props.value)); - }} - > - - - - ), - } - : undefined} + InputProps={ + focused + ? { + endAdornment: ( + + { + onCopy(String(props.value)); + }} + > + + + + ), + } + : undefined + } /> ); }; -export const EventDetailDialog: React.FC< - { onClose: () => void; event: Event } -> = ({ onClose, event }) => { +export const EventDetailDialog: React.FC<{ + onClose: () => void; + event: Event; +}> = ({ onClose, event }) => { console.log("EventDetailDialog"); return ( onClose()} fullWidth> - {event.type == "Normal" - ? - : } + {event.type == "Normal" ? ( + + ) : ( + + )} {event.reason} @@ -171,5 +176,5 @@ export const EventDetailDialog: React.FC< * Context */ export const EventDetailDialogContext = DialogContext<{ event: Event }>( - (props) => , + (props) => ); diff --git a/web/dashboard-ui/src/views/organisms/EventModule.tsx b/web/dashboard-ui/src/views/organisms/EventModule.tsx index 41ea012c..09970f4b 100644 --- a/web/dashboard-ui/src/views/organisms/EventModule.tsx +++ b/web/dashboard-ui/src/views/organisms/EventModule.tsx @@ -25,7 +25,7 @@ const useEvent = () => { try { const result = await userService.getUsers({}); setUsers( - setUserStateFuncFilteredByLoginUserRole(result.items, loginUser), + setUserStateFuncFilteredByLoginUserRole(result.items, loginUser) ); } catch (error) { handleError(error); @@ -43,7 +43,7 @@ const useEvent = () => { } }; - return ({ + return { user, setUser, users, @@ -52,7 +52,7 @@ const useEvent = () => { setEvents, getEvents, getUsers, - }); + }; }; export function getTime(timestamp?: Timestamp): number { diff --git a/web/dashboard-ui/src/views/organisms/NetworkRuleActionDialog.tsx b/web/dashboard-ui/src/views/organisms/NetworkRuleActionDialog.tsx index 2c5fe78e..9a0d6f1a 100644 --- a/web/dashboard-ui/src/views/organisms/NetworkRuleActionDialog.tsx +++ b/web/dashboard-ui/src/views/organisms/NetworkRuleActionDialog.tsx @@ -2,160 +2,275 @@ import { Close, ExpandLess, ExpandMore } from "@mui/icons-material"; import { Alert, Box, - Button, Checkbox, Collapse, Dialog, DialogActions, DialogContent, DialogTitle, Divider, FormControlLabel, - IconButton, Stack, TextField, Typography + Button, + Checkbox, + Collapse, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Divider, + FormControlLabel, + IconButton, + Stack, + TextField, + Typography, } from "@mui/material"; import { useState } from "react"; import { Controller, UseFormRegisterReturn, useForm } from "react-hook-form"; import { DialogContext } from "../../components/ContextProvider"; -import { NetworkRule, Workspace } from "../../proto/gen/dashboard/v1alpha1/workspace_pb"; +import { + NetworkRule, + Workspace, +} from "../../proto/gen/dashboard/v1alpha1/workspace_pb"; import { TextFieldLabel } from "../atoms/TextFieldLabel"; import { useNetworkRule } from "./WorkspaceModule"; const registerMui = ({ ref, ...rest }: UseFormRegisterReturn) => ({ - inputRef: ref, ...rest -}) + inputRef: ref, + ...rest, +}); /** * view */ -export const NetworkRuleUpsertDialog: React.VFC<{ workspace: Workspace, networkRule?: NetworkRule, index: number, onClose: () => void, defaultOpenHttpOptions?: boolean, isMain?: boolean }> - = ({ workspace, networkRule, onClose, index, defaultOpenHttpOptions, isMain }) => { - console.log('NetworkRuleUpsertDialog', networkRule); - const networkRuleModule = useNetworkRule(); - const { register, handleSubmit, setValue, control, formState: { errors } } = useForm({ - defaultValues: networkRule || { portNumber: 8080, httpPath: '/' }, - }); +export const NetworkRuleUpsertDialog: React.VFC<{ + workspace: Workspace; + networkRule?: NetworkRule; + index: number; + onClose: () => void; + defaultOpenHttpOptions?: boolean; + isMain?: boolean; +}> = ({ + workspace, + networkRule, + onClose, + index, + defaultOpenHttpOptions, + isMain, +}) => { + console.log("NetworkRuleUpsertDialog", networkRule); + const networkRuleModule = useNetworkRule(); + const { + register, + handleSubmit, + setValue, + control, + formState: { errors }, + } = useForm({ + defaultValues: networkRule || { portNumber: 8080, httpPath: "/" }, + }); - const [openHttpOptions, setOpenHttpOptions] = useState(defaultOpenHttpOptions || false); + const [openHttpOptions, setOpenHttpOptions] = useState( + defaultOpenHttpOptions || false + ); - const handleOpenHttpOptionsClick = () => { - setOpenHttpOptions(!openHttpOptions); - }; + const handleOpenHttpOptionsClick = () => { + setOpenHttpOptions(!openHttpOptions); + }; - const upsertRule = (newRule: NetworkRule) => { - if (!(newRule.httpPath || '').startsWith('/')) { - newRule.httpPath = '/' + newRule.httpPath; - } - networkRuleModule.upsertNetwork(workspace, newRule, index).then(() => onClose()); + const upsertRule = (newRule: NetworkRule) => { + if (!(newRule.httpPath || "").startsWith("/")) { + newRule.httpPath = "/" + newRule.httpPath; } + networkRuleModule + .upsertNetwork(workspace, newRule, index) + .then(() => onClose()); + }; - return ( - - - {networkRule ? "Edit NetworkRule" : "Add New NetworkRule"} - theme.palette.grey[500] }} - onClick={() => onClose()}> - - - - -
{ upsertRule(data); })}> - - + + {networkRule ? "Edit NetworkRule" : "Add New NetworkRule"} + theme.palette.grey[500], + }} + onClick={() => onClose()} + > + + + + + { + upsertRule(data); + })} + > + + - + + HTTP Options + - HTTP Options - - {openHttpOptions ? : } - - - - + ) : ( + + )} + + + + /^[^-](.*[^-])?$|^$/.test(v || '') || 'Must start and end with an alphanumeric character', - chars: v => /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$|^$/.test(v || '') || 'Only lowercase alphanumeric charactor and - are allowed', + hyphen: (v) => + /^[^-](.*[^-])?$|^$/.test(v || "") || + "Must start and end with an alphanumeric character", + chars: (v) => + /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$|^$/.test(v || "") || + "Only lowercase alphanumeric charactor and - are allowed", }, - }))} - error={Boolean(errors.customHostPrefix)} - helperText={(errors.customHostPrefix && errors.customHostPrefix.message)} - /> - - + + - - {isMain && Main Network Rule values cannot be changed} - - + + {isMain && ( + + Main Network Rule values cannot be changed + + )} + + } - />} - label={<> + render={({ field }) => ( + + )} + /> + } + label={ + <> public - + No authentication is required for this URL. - } - /> - - - - - - -
- ); - }; + + } + /> +
+ + + + + + + ); +}; export const NetworkRuleDeleteDialog: React.VFC<{ - workspace: Workspace, networkRule: NetworkRule, index: number, onClose: () => void + workspace: Workspace; + networkRule: NetworkRule; + index: number; + onClose: () => void; }> = ({ workspace, networkRule, index, onClose }) => { - console.log('NetworkRuleDeleteDialog', networkRule); + console.log("NetworkRuleDeleteDialog", networkRule); const networkRuleModule = useNetworkRule(); const deleteRule = () => { networkRuleModule.removeNetwork(workspace, index).then(() => onClose()); - } + }; return ( - - + + Network Rule theme.palette.grey[500] }} - onClick={() => onClose()}> + sx={{ + position: "absolute", + right: 8, + top: 8, + color: (theme) => theme.palette.grey[500], + }} + onClick={() => onClose()} + > - + HTTP Options - + - DELETE} - >Are you sure to delete it? + + DELETE + + } + > + Are you sure to delete it? + ); @@ -164,7 +279,15 @@ export const NetworkRuleDeleteDialog: React.VFC<{ /** * Context */ -export const NetworkRuleUpsertDialogContext = DialogContext<{ workspace: Workspace, networkRule?: NetworkRule, index: number, defaultOpenHttpOptions?: boolean, isMain?: boolean }>( - props => ()); -export const NetworkRuleDeleteDialogContext = DialogContext<{ workspace: Workspace, networkRule: NetworkRule, index: number }>( - props => ()); +export const NetworkRuleUpsertDialogContext = DialogContext<{ + workspace: Workspace; + networkRule?: NetworkRule; + index: number; + defaultOpenHttpOptions?: boolean; + isMain?: boolean; +}>((props) => ); +export const NetworkRuleDeleteDialogContext = DialogContext<{ + workspace: Workspace; + networkRule: NetworkRule; + index: number; +}>((props) => ); diff --git a/web/dashboard-ui/src/views/organisms/PasswordChangeDialog.tsx b/web/dashboard-ui/src/views/organisms/PasswordChangeDialog.tsx index 4f1645d0..bf7be54c 100644 --- a/web/dashboard-ui/src/views/organisms/PasswordChangeDialog.tsx +++ b/web/dashboard-ui/src/views/organisms/PasswordChangeDialog.tsx @@ -1,96 +1,153 @@ +import { PersonOutlineTwoTone } from "@mui/icons-material"; import { - Button, Dialog, DialogActions, DialogContent, DialogTitle, Stack + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Stack, } from "@mui/material"; -import { PersonOutlineTwoTone } from "@mui/icons-material"; import React, { useState } from "react"; -import { useForm, UseFormRegisterReturn } from "react-hook-form"; +import { UseFormRegisterReturn, useForm } from "react-hook-form"; import { DialogContext } from "../../components/ContextProvider"; import { useLogin } from "../../components/LoginProvider"; import { PasswordTextField } from "../atoms/PasswordTextField"; import { TextFieldLabel } from "../atoms/TextFieldLabel"; const registerMui = ({ ref, ...rest }: UseFormRegisterReturn) => ({ - inputRef: ref, ...rest + inputRef: ref, + ...rest, }); /** * view */ interface Inputs { - currentPassword: string, - newPassword1: string, - newPassword2: string, -}; + currentPassword: string; + newPassword1: string; + newPassword2: string; +} -export const PasswordChangeDialog: React.VFC<{ onClose: () => void }> = ({ onClose }) => { - console.log('PasswordChangeDialog'); - const { register, watch, handleSubmit, formState: { errors } } = useForm(); +export const PasswordChangeDialog: React.VFC<{ onClose: () => void }> = ({ + onClose, +}) => { + console.log("PasswordChangeDialog"); + const { + register, + watch, + handleSubmit, + formState: { errors }, + } = useForm(); const login = useLogin(); const [isNewPasswordError, setIsNewPasswordError] = useState(false); - watch(data => { + watch((data) => { setIsNewPasswordError( - data.newPassword1 !== '' && data.newPassword2 !== '' && - data.newPassword1 !== data.newPassword2); + data.newPassword1 !== "" && + data.newPassword2 !== "" && + data.newPassword1 !== data.newPassword2 + ); }); const onChangePass = async (data: Inputs) => { if (isNewPasswordError) return; await login.updataPassword(data.currentPassword, data.newPassword1); onClose(); - } + }; return ( - onClose()}> + onClose()}> Change Password 🔒
- } /> + } + /> - - - - - + +
); -} +}; /** * Context */ -export const PasswordChangeDialogContext = DialogContext( - props => ()); +export const PasswordChangeDialogContext = DialogContext((props) => ( + +)); diff --git a/web/dashboard-ui/src/views/organisms/PasswordDialog.tsx b/web/dashboard-ui/src/views/organisms/PasswordDialog.tsx index b6b08289..8c234fe5 100644 --- a/web/dashboard-ui/src/views/organisms/PasswordDialog.tsx +++ b/web/dashboard-ui/src/views/organisms/PasswordDialog.tsx @@ -1,45 +1,82 @@ import { ContentCopy, PersonOutlineTwoTone, VpnKey } from "@mui/icons-material"; import { - Alert, Button, Dialog, DialogActions, DialogContent, DialogTitle, - IconButton, InputAdornment, Stack + Alert, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + IconButton, + InputAdornment, + Stack, } from "@mui/material"; -import copy from 'copy-to-clipboard'; +import copy from "copy-to-clipboard"; import { useSnackbar } from "notistack"; import React from "react"; import { DialogContext } from "../../components/ContextProvider"; import { User } from "../../proto/gen/dashboard/v1alpha1/user_pb"; import { TextFieldLabel } from "../atoms/TextFieldLabel"; -export const PasswordDialog: React.VFC<{ onClose: () => void, user: User }> = ({ onClose, user }) => { - console.log('PasswordDialog'); +export const PasswordDialog: React.VFC<{ onClose: () => void; user: User }> = ({ + onClose, + user, +}) => { + console.log("PasswordDialog"); const { enqueueSnackbar } = useSnackbar(); const onCopy = (text: string) => { copy(text); - enqueueSnackbar('Copied!', { variant: 'success' }); - } + enqueueSnackbar("Copied!", { variant: "success" }); + }; return ( - + Here you go 🚀 - } /> - } + } + /> + } InputProps={{ - endAdornment: ( - { onCopy(user.defaultPassword!) }}> - - - ), + endAdornment: ( + + { + onCopy(user.defaultPassword!); + }} + > + + + + ), }} /> - onClose()} >OK} - >Make sure to copy default password now. You won’t be able to see it again! + onClose()} + > + OK + + } + > + Make sure to copy default password now. You won’t be able to see it + again! + ); @@ -48,5 +85,6 @@ export const PasswordDialog: React.VFC<{ onClose: () => void, user: User }> = ({ /** * Context */ -export const PasswordDialogContext = DialogContext<{ user: User }>( - props => ()); +export const PasswordDialogContext = DialogContext<{ user: User }>((props) => ( + +)); diff --git a/web/dashboard-ui/src/views/organisms/RoleChangeDialog.tsx b/web/dashboard-ui/src/views/organisms/RoleChangeDialog.tsx index d429b98d..ee0ea4f5 100644 --- a/web/dashboard-ui/src/views/organisms/RoleChangeDialog.tsx +++ b/web/dashboard-ui/src/views/organisms/RoleChangeDialog.tsx @@ -1,6 +1,16 @@ import { Add, PersonOutlineTwoTone, Remove } from "@mui/icons-material"; import { - Button, Dialog, DialogActions, DialogContent, DialogTitle, FormHelperText, Grid, IconButton, Stack, TextField, Typography + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + FormHelperText, + Grid, + IconButton, + Stack, + TextField, + Typography, } from "@mui/material"; import React from "react"; import { UseFormRegisterReturn, useFieldArray, useForm } from "react-hook-form"; @@ -12,94 +22,167 @@ import { TextFieldLabel } from "../atoms/TextFieldLabel"; import { useUserModule } from "./UserModule"; const registerMui = ({ ref, ...rest }: UseFormRegisterReturn) => ({ - inputRef: ref, ...rest -}) + inputRef: ref, + ...rest, +}); /** * view */ interface Inputs { - existingRoles: { name: string, enabled: boolean }[]; + existingRoles: { name: string; enabled: boolean }[]; roles: { name: string }[]; } -export const RoleChangeDialog: React.VFC<{ onClose: () => void, user: User }> = ({ onClose, user }) => { - console.log('RoleChangeDialog'); +export const RoleChangeDialog: React.VFC<{ + onClose: () => void; + user: User; +}> = ({ onClose, user }) => { + console.log("RoleChangeDialog"); const hooks = useUserModule(); const { refreshUserInfo } = useLogin(); - const { register, handleSubmit, control, formState: { errors, defaultValues } } = useForm({ + const { + register, + handleSubmit, + control, + formState: { errors, defaultValues }, + } = useForm({ defaultValues: { - existingRoles: hooks.existingRoles.map(v => ({ name: v, enabled: user.roles.includes(v) })) + existingRoles: hooks.existingRoles.map((v) => ({ + name: v, + enabled: user.roles.includes(v), + })), }, }); - const { fields: rolesFields, append: appendRoles, remove: removeRoles } = useFieldArray({ + const { + fields: rolesFields, + append: appendRoles, + remove: removeRoles, + } = useFieldArray({ control, name: "roles", rules: { validate: (fieldArrayValues) => { // check that no duplicates exist - let values = fieldArrayValues.map((item) => item.name).filter((v) => v !== ""); + let values = fieldArrayValues + .map((item) => item.name) + .filter((v) => v !== ""); values.push(...hooks.existingRoles); const uniqueValues = [...new Set(values)]; return values.length === uniqueValues.length || "No duplicates allowed"; - } - } + }, + }, }); return ( - + Change Role -
{ - console.log(inp) - let protoRoles = inp.roles.filter((v) => { return v.name !== "" }).map((v) => { return v.name }) - console.log(protoRoles) - inp.existingRoles.forEach((v, i) => { - if (v.enabled) { - protoRoles.push(v.name) - } - }) - protoRoles = [...new Set(protoRoles)]; // remove duplicates - console.log("protoRoles", protoRoles) - await hooks.updateRole(user.name, protoRoles); - await refreshUserInfo(); - onClose(); - })} - autoComplete="new-password"> + { + console.log(inp); + let protoRoles = inp.roles + .filter((v) => { + return v.name !== ""; + }) + .map((v) => { + return v.name; + }); + console.log(protoRoles); + inp.existingRoles.forEach((v, i) => { + if (v.enabled) { + protoRoles.push(v.name); + } + }); + protoRoles = [...new Set(protoRoles)]; // remove duplicates + console.log("protoRoles", protoRoles); + await hooks.updateRole(user.name, protoRoles); + await refreshUserInfo(); + onClose(); + })} + autoComplete="new-password" + > - } /> - Roles + } + /> + + Roles + - {hooks.existingRoles.map((v, index) => - - )} + {hooks.existingRoles.map((v, index) => ( + + ))} - {rolesFields.map((field, index) => - ( + { removeRoles(index) }} > + endAdornment: ( + { + removeRoles(index); + }} + > + + + ), }} error={Boolean(errors.roles?.[index]?.name)} helperText={errors.roles?.[index]?.name?.message} /> + ))} + {Boolean(errors.roles?.root?.message) && ( + + {errors.roles?.root?.message} + )} - {Boolean(errors.roles?.root?.message) && - {errors.roles?.root?.message} - } - - - + +
@@ -110,4 +193,5 @@ export const RoleChangeDialog: React.VFC<{ onClose: () => void, user: User }> = * Context */ export const RoleChangeDialogContext = DialogContext<{ user: User }>( - props => ()); + (props) => +); diff --git a/web/dashboard-ui/src/views/organisms/UserActionDialog.tsx b/web/dashboard-ui/src/views/organisms/UserActionDialog.tsx index 6155fd64..2da0aff4 100644 --- a/web/dashboard-ui/src/views/organisms/UserActionDialog.tsx +++ b/web/dashboard-ui/src/views/organisms/UserActionDialog.tsx @@ -1,10 +1,42 @@ -import { Add, Close, ExpandLess, ExpandMore, PersonOutlineTwoTone, Remove, SecurityOutlined } from "@mui/icons-material"; import { - Alert, Button, Checkbox, Chip, Collapse, Dialog, DialogActions, DialogContent, DialogTitle, + Add, + Close, + ExpandLess, + ExpandMore, + PersonOutlineTwoTone, + Remove, + SecurityOutlined, +} from "@mui/icons-material"; +import { + Alert, + Button, + Checkbox, + Chip, + Collapse, + Dialog, + DialogActions, + DialogContent, + DialogTitle, Divider, FormControlLabel, FormHelperText, - Grid, IconButton, InputAdornment, List, ListItem, ListItemText, MenuItem, Paper, Stack, Table, TableBody, TableCell, TableContainer, TableRow, TextField, Tooltip, Typography + Grid, + IconButton, + InputAdornment, + List, + ListItem, + ListItemText, + MenuItem, + Paper, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableRow, + TextField, + Tooltip, + Typography, } from "@mui/material"; import React, { useEffect, useState } from "react"; import { UseFormRegisterReturn, useFieldArray, useForm } from "react-hook-form"; @@ -15,103 +47,182 @@ import { NameAvatar } from "../atoms/NameAvatar"; import { FormSelectableChip } from "../atoms/SelectableChips"; import { TextFieldLabel } from "../atoms/TextFieldLabel"; import { PasswordDialogContext } from "./PasswordDialog"; -import { isAdminRole, isPrivilegedRole, useTemplates, useUserModule } from "./UserModule"; +import { + isAdminRole, + isPrivilegedRole, + useTemplates, + useUserModule, +} from "./UserModule"; const registerMui = ({ ref, ...rest }: UseFormRegisterReturn) => ({ - inputRef: ref, ...rest -}) + inputRef: ref, + ...rest, +}); /** * UserActionDialog */ interface UserActionDialogProps { - title: string - actions: React.ReactNode - user: User, - onClose: () => void, - defaultOpenUserAddon?: boolean + title: string; + actions: React.ReactNode; + user: User; + onClose: () => void; + defaultOpenUserAddon?: boolean; } -const UserActionDialog: React.FC = ({ title, actions, user, onClose, defaultOpenUserAddon }) => { - console.log(user) - const [openUserAddon, setOpenUserAddon] = useState(defaultOpenUserAddon || false); +const UserActionDialog: React.FC = ({ + title, + actions, + user, + onClose, + defaultOpenUserAddon, +}) => { + console.log(user); + const [openUserAddon, setOpenUserAddon] = useState( + defaultOpenUserAddon || false + ); const handleOpenUserAddonClick = () => { setOpenUserAddon(!openUserAddon); }; return ( - onClose()} fullWidth maxWidth={'xs'}> - {title} + onClose()} fullWidth maxWidth={"xs"}> + + {title} theme.palette.grey[500] }} - onClick={() => onClose()}> + sx={{ + position: "absolute", + right: 8, + top: 8, + color: (theme) => theme.palette.grey[500], + }} + onClick={() => onClose()} + > - - + + - } /> - } /> - } /> - Roles - - - {user?.roles && user.roles.map((v, i) => { - return ( - - - ) - })} + } + /> + } + /> + } + /> + + Roles + + + + {user?.roles && + user.roles.map((v, i) => { + return ( + + + + ); + })} - {Boolean(user.addons.length) && - - User Addons - - {openUserAddon ? : } - - - - - {user.addons.map((v, i) => - - * {v.template}} - secondary={ - - - - {Object.keys(v.vars).map((key, j) => - - {key} - {v.vars[key]} - - )} - -
-
- } - /> -
- )} -
-
-
} + {Boolean(user.addons.length) && ( + + + User Addons + + {openUserAddon ? ( + + ) : ( + + )} + + + + + {user.addons.map((v, i) => ( + + + * {v.template} + + } + secondary={ + + + + {Object.keys(v.vars).map((key, j) => ( + + + {key} + + + {v.vars[key]} + + + ))} + +
+
+ } + /> +
+ ))} +
+
+
+ )}
{actions} @@ -122,74 +233,127 @@ const UserActionDialog: React.FC = ({ title, actions, use /** * Info */ -export const UserInfoDialog: React.VFC<{ onClose: () => void, user: User, defaultOpenUserAddon?: boolean }> = ({ onClose, user, defaultOpenUserAddon }) => { - console.log('UserInfoDialog'); +export const UserInfoDialog: React.VFC<{ + onClose: () => void; + user: User; + defaultOpenUserAddon?: boolean; +}> = ({ onClose, user, defaultOpenUserAddon }) => { + console.log("UserInfoDialog"); return ( onClose()} user={user} defaultOpenUserAddon={defaultOpenUserAddon} - actions={} /> + actions={ + + } + /> ); -} +}; /** * Delete */ -export const UserDeleteDialog: React.VFC<{ onClose: () => void, user: User }> = ({ onClose, user }) => { - console.log('UserDeleteDialog'); +export const UserDeleteDialog: React.VFC<{ + onClose: () => void; + user: User; +}> = ({ onClose, user }) => { + console.log("UserDeleteDialog"); const hooks = useUserModule(); const [lock, setLock] = useState(false); return ( onClose()} user={user} - actions={ - setLock(e.target.checked)} /> - - } - >This action is NOT recoverable. Are you sure to delete it?} /> + actions={ + + setLock(e.target.checked)} + /> + + + } + > + This action is NOT recoverable. Are you sure to delete it? + + } + /> ); }; -export const UserCreateConfirmDialog: React.VFC<{ onClose: () => void, onConfirm: () => void, user: User }> = ({ onClose, onConfirm, user }) => { - console.log('UserCreateConfirmDialog'); +export const UserCreateConfirmDialog: React.VFC<{ + onClose: () => void; + onConfirm: () => void; + user: User; +}> = ({ onClose, onConfirm, user }) => { + console.log("UserCreateConfirmDialog"); const hooks = useUserModule(); const passwordDialogDispatch = PasswordDialogContext.useDispatch(); return ( onClose()} user={user} defaultOpenUserAddon={true} - actions={ - - - - } /> + actions={ + + + + + } + /> ); -} +}; /** * Create @@ -205,208 +369,356 @@ type Inputs = { enable: boolean; vars: string[]; }[]; -} -export const UserCreateDialog: React.VFC<{ onClose: () => void }> = ({ onClose }) => { - console.log('UserCreateDialog'); +}; +export const UserCreateDialog: React.VFC<{ onClose: () => void }> = ({ + onClose, +}) => { + console.log("UserCreateDialog"); const hooks = useUserModule(); - const userCreateConfirmDialogDispatch = UserCreateConfirmDialogContext.useDispatch(); + const userCreateConfirmDialogDispatch = + UserCreateConfirmDialogContext.useDispatch(); - const { register, handleSubmit, watch, control, formState: { errors } } = useForm({ - defaultValues: {} + const { + register, + handleSubmit, + watch, + control, + formState: { errors }, + } = useForm({ + defaultValues: {}, }); - const { fields: addonsFields, replace: replaceAddons } = useFieldArray({ control, name: "addons" }); + const { fields: addonsFields, replace: replaceAddons } = useFieldArray({ + control, + name: "addons", + }); const templ = useTemplates(); - useEffect(() => { templ.getUserAddonTemplates(); }, []); // eslint-disable-line useEffect(() => { - replaceAddons(templ.templates.map(t => ({ template: t, enable: false, vars: [] }))); - }, [templ.templates]); // eslint-disable-line + templ.getUserAddonTemplates(); + }, []); // eslint-disable-line + useEffect(() => { + replaceAddons( + templ.templates.map((t) => ({ template: t, enable: false, vars: [] })) + ); + }, [templ.templates]); // eslint-disable-line - const { fields: rolesFields, append: appendRoles, remove: removeRoles } = useFieldArray({ + const { + fields: rolesFields, + append: appendRoles, + remove: removeRoles, + } = useFieldArray({ control, name: "roles", rules: { validate: (fieldArrayValues) => { // check that no duplicates exist - let values = fieldArrayValues.map((item) => item.name).filter((v) => v !== ""); + let values = fieldArrayValues + .map((item) => item.name) + .filter((v) => v !== ""); values.push(...hooks.existingRoles); const uniqueValues = [...new Set(values)]; return values.length === uniqueValues.length || "No duplicates allowed"; - } - } + }, + }, }); return ( - + Create New User 🎉 -
{ - console.log(inp) - const userAddons = inp.addons.filter(v => v.enable) - .map((inpAddon) => { - const vars: { [key: string]: string; } = {}; - inpAddon.vars.forEach((v, i) => { - vars[inpAddon.template.requiredVars?.[i].varName!] = v; + { + console.log(inp); + const userAddons = inp.addons + .filter((v) => v.enable) + .map((inpAddon) => { + const vars: { [key: string]: string } = {}; + inpAddon.vars.forEach((v, i) => { + vars[inpAddon.template.requiredVars?.[i].varName!] = v; + }); + return { + template: inpAddon.template.name, + vars: vars, + clusterScoped: inpAddon.template.isClusterScope, + }; }); - return { template: inpAddon.template.name, vars: vars, clusterScoped: inpAddon.template.isClusterScope } - }); - const protoUserAddons = userAddons.map(ua => new UserAddon(ua)); - console.log("protoUserAddons", protoUserAddons) + const protoUserAddons = userAddons.map((ua) => new UserAddon(ua)); + console.log("protoUserAddons", protoUserAddons); - let protoRoles = inp.roles.map((v) => { return v.name }) - inp.existingRoles.forEach((v, i) => { - if (v.enabled) { - protoRoles.push(hooks.existingRoles[i]) - } - }) - protoRoles = [...new Set(protoRoles)]; // remove duplicates - console.log("protoRoles", protoRoles) + let protoRoles = inp.roles.map((v) => { + return v.name; + }); + inp.existingRoles.forEach((v, i) => { + if (v.enabled) { + protoRoles.push(hooks.existingRoles[i]); + } + }); + protoRoles = [...new Set(protoRoles)]; // remove duplicates + console.log("protoRoles", protoRoles); - userCreateConfirmDialogDispatch(true, { - onConfirm: () => { onClose(); }, - user: - new User({ name: inp.id, displayName: inp.name, roles: protoRoles, authType: inp.authType, addons: protoUserAddons }) - }); - })} - autoComplete="new-password"> + userCreateConfirmDialogDispatch(true, { + onConfirm: () => { + onClose(); + }, + user: new User({ + name: inp.id, + displayName: inp.name, + roles: protoRoles, + authType: inp.authType, + addons: protoUserAddons, + }), + }); + })} + autoComplete="new-password" + > - ), + startAdornment: ( + + + + ), }} /> - ), + startAdornment: ( + + + + ), }} /> - ), + startAdornment: ( + + + + ), }} > - +
password-secret
- +
ldap
- Roles + + Roles + - {hooks.existingRoles.map((label, index) => - - )} + {hooks.existingRoles.map((label, index) => ( + + ))} - {rolesFields.map((field, index) => - ( + { removeRoles(index) }} > + endAdornment: ( + { + removeRoles(index); + }} + > + + + ), }} error={Boolean(errors.roles?.[index]?.name)} helperText={errors.roles?.[index]?.name?.message} /> - )} + ))} {errors.roles?.root?.message} - - {Boolean(templ.templates.length) && - Enable User Addons - } - {addonsFields.map((field, index) => + {Boolean(templ.templates.length) && ( + + Enable User Addons + + )} + {addonsFields.map((field, index) => ( - - - } + + + + } /> - + {errors.addons?.[index]?.enable?.message} - + - {field.template.requiredVars?.map((required, j) => - ( + - )} + ))} - )} + ))}
- - + +
-
+
); }; /** * Context */ -export const UserInfoDialogContext = DialogContext<{ user: User, defaultOpenUserAddon?: boolean }>( - props => ()); +export const UserInfoDialogContext = DialogContext<{ + user: User; + defaultOpenUserAddon?: boolean; +}>((props) => ); export const UserDeleteDialogContext = DialogContext<{ user: User }>( - props => ()); -export const UserCreateDialogContext = DialogContext( - props => ()); -export const UserCreateConfirmDialogContext = DialogContext<{ onConfirm: () => void, user: User }>( - props => ()); + (props) => +); +export const UserCreateDialogContext = DialogContext((props) => ( + +)); +export const UserCreateConfirmDialogContext = DialogContext<{ + onConfirm: () => void; + user: User; +}>((props) => ); diff --git a/web/dashboard-ui/src/views/organisms/UserAddonsChangeDialog.tsx b/web/dashboard-ui/src/views/organisms/UserAddonsChangeDialog.tsx index b3dbf695..8569a0a7 100644 --- a/web/dashboard-ui/src/views/organisms/UserAddonsChangeDialog.tsx +++ b/web/dashboard-ui/src/views/organisms/UserAddonsChangeDialog.tsx @@ -1,11 +1,18 @@ import { PersonOutlineTwoTone } from "@mui/icons-material"; import { - Button, Checkbox, - Collapse, Dialog, DialogActions, DialogContent, DialogTitle, - FormControlLabel, - FormHelperText, - Stack, - TextField, Tooltip, Typography + Button, + Checkbox, + Collapse, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + FormControlLabel, + FormHelperText, + Stack, + TextField, + Tooltip, + Typography, } from "@mui/material"; import React, { useEffect } from "react"; import { UseFormRegisterReturn, useFieldArray, useForm } from "react-hook-form"; @@ -17,124 +24,192 @@ import { TextFieldLabel } from "../atoms/TextFieldLabel"; import { useTemplates, useUserModule } from "./UserModule"; const registerMui = ({ ref, ...rest }: UseFormRegisterReturn) => ({ - inputRef: ref, ...rest -}) + inputRef: ref, + ...rest, +}); /** * view */ interface Inputs { - addons: { - template: Template; - enable: boolean; - vars: string[]; - }[]; + addons: { + template: Template; + enable: boolean; + vars: string[]; + }[]; } -export const UserAddonChangeDialog: React.FC<{ onClose: () => void, user: User }> = ({ onClose, user }) => { - console.log('UserAddonChangeDialog'); - const hooks = useUserModule(); - const { refreshUserInfo } = useLogin(); +export const UserAddonChangeDialog: React.FC<{ + onClose: () => void; + user: User; +}> = ({ onClose, user }) => { + console.log("UserAddonChangeDialog"); + const hooks = useUserModule(); + const { refreshUserInfo } = useLogin(); - const { register, handleSubmit, watch, control, formState: { errors } } = useForm({ - defaultValues: {} - }); + const { + register, + handleSubmit, + watch, + control, + formState: { errors }, + } = useForm({ + defaultValues: {}, + }); - const { fields: addonsFields, replace: replaceAddons } = useFieldArray({ control, name: "addons" }); + const { fields: addonsFields, replace: replaceAddons } = useFieldArray({ + control, + name: "addons", + }); - const currentAddons = new Map(); - user.addons.forEach((v) => { - currentAddons.set(v.template, v) - }) + const currentAddons = new Map(); + user.addons.forEach((v) => { + currentAddons.set(v.template, v); + }); - const templ = useTemplates(); - useEffect(() => { templ.getAllUserAddonTemplates(); }, []); // eslint-disable-line - useEffect(() => { - const tt = templ.templates.map(t => ({ template: t, enable: false, vars: [] })); - replaceAddons(tt); - }, [templ.templates]); // eslint-disable-line + const templ = useTemplates(); + useEffect(() => { + templ.getAllUserAddonTemplates(); + }, []); // eslint-disable-line + useEffect(() => { + const tt = templ.templates.map((t) => ({ + template: t, + enable: false, + vars: [], + })); + replaceAddons(tt); + }, [templ.templates]); // eslint-disable-line - return ( - - Change UserAddons -
{ - console.log(inp) - const userAddons = inp.addons.filter(v => v.enable) - .map((inpAddon) => { - const vars: { [key: string]: string; } = {}; - inpAddon.vars.forEach((v, i) => { - vars[inpAddon.template.requiredVars?.[i].varName!] = v; - }); - return { template: inpAddon.template.name, vars: vars, clusterScoped: inpAddon.template.isClusterScope } - }); - const protoUserAddons = userAddons.map(ua => new UserAddon(ua)); - console.log("protoUserAddons", protoUserAddons) + return ( + + Change UserAddons + { + console.log(inp); + const userAddons = inp.addons + .filter((v) => v.enable) + .map((inpAddon) => { + const vars: { [key: string]: string } = {}; + inpAddon.vars.forEach((v, i) => { + vars[inpAddon.template.requiredVars?.[i].varName!] = v; + }); + return { + template: inpAddon.template.name, + vars: vars, + clusterScoped: inpAddon.template.isClusterScope, + }; + }); + const protoUserAddons = userAddons.map((ua) => new UserAddon(ua)); + console.log("protoUserAddons", protoUserAddons); - // call API - await hooks.updateAddons(user.name, protoUserAddons); - await refreshUserInfo(); - onClose(); - })} - autoComplete="new-password"> - - - } /> - - {Boolean(templ.templates.length) && - Enable User Addons - } - {addonsFields.map((field, index) => - - - - } - /> - - {errors.addons?.[index]?.enable?.message} - - - - {field.template.requiredVars?.map((required, j) => - - )} - - - - )} - + // call API + await hooks.updateAddons(user.name, protoUserAddons); + await refreshUserInfo(); + onClose(); + })} + autoComplete="new-password" + > + + + } + /> + + {Boolean(templ.templates.length) && ( + + Enable User Addons + + )} + {addonsFields.map((field, index) => ( + + + + + } + /> + + {errors.addons?.[index]?.enable?.message} + + + + {field.template.requiredVars?.map((required, j) => ( + + ))} - - - - - - -
- ); + + + ))} + + + + + + + + +
+ ); }; /** * Context */ export const UserAddonChangeDialogContext = DialogContext<{ user: User }>( - props => ()); \ No newline at end of file + (props) => +); diff --git a/web/dashboard-ui/src/views/organisms/UserModule.tsx b/web/dashboard-ui/src/views/organisms/UserModule.tsx index e656cdac..cfc92744 100644 --- a/web/dashboard-ui/src/views/organisms/UserModule.tsx +++ b/web/dashboard-ui/src/views/organisms/UserModule.tsx @@ -5,80 +5,98 @@ import { useHandleError } from "../../components/LoginProvider"; import { useProgress } from "../../components/ProgressProvider"; import { Template } from "../../proto/gen/dashboard/v1alpha1/template_pb"; import { User, UserAddon } from "../../proto/gen/dashboard/v1alpha1/user_pb"; -import { useTemplateService, useUserService } from "../../services/DashboardServices"; +import { + useTemplateService, + useUserService, +} from "../../services/DashboardServices"; -export const PrivilegedRole = 'cosmo-admin' +export const PrivilegedRole = "cosmo-admin"; -const AdminRoleSufix = '-admin' +const AdminRoleSufix = "-admin"; export const isPrivilegedRole = (role: string) => { - return role === PrivilegedRole -} + return role === PrivilegedRole; +}; export const isAdminRole = (role: string) => { - return role.endsWith(AdminRoleSufix) -} + return role.endsWith(AdminRoleSufix); +}; export const hasPrivilegedRole = (roles: string[]) => { return roles.includes(PrivilegedRole); -} +}; export const isAdminUser = (user?: User) => { if (user && user.roles) { if (hasPrivilegedRole(user.roles)) { - return true + return true; } for (const role of user.roles) { if (isAdminRole(role)) { - return true + return true; } } } - return false -} + return false; +}; export const excludeAdminRolePrefix = (role: string): string => { // given "xxx-admin" return "xxx" - return role.endsWith(AdminRoleSufix) ? role.slice(0, -AdminRoleSufix.length) : role -} + return role.endsWith(AdminRoleSufix) + ? role.slice(0, -AdminRoleSufix.length) + : role; +}; export const hasAdminForRole = (myRoles: string[], userrole: string) => { for (const myRole of myRoles) { if (myRole == userrole) { - return true + return true; } - if (isAdminRole(myRole) && userrole.startsWith(excludeAdminRolePrefix(myRole))) { - return true + if ( + isAdminRole(myRole) && + userrole.startsWith(excludeAdminRolePrefix(myRole)) + ) { + return true; } } - return false -} + return false; +}; function filterUsersByRoles(users: User[], myRoles: string[]) { - return hasPrivilegedRole(myRoles) ? users : users.filter((u) => { - for (const userRole of u.roles) { - if (hasAdminForRole(myRoles, userRole)) { - return true - } - } - return false - }) + return hasPrivilegedRole(myRoles) + ? users + : users.filter((u) => { + for (const userRole of u.roles) { + if (hasAdminForRole(myRoles, userRole)) { + return true; + } + } + return false; + }); } -export function setUserStateFuncFilteredByLoginUserRole(users: User[], loginUser?: User) { +export function setUserStateFuncFilteredByLoginUserRole( + users: User[], + loginUser?: User +) { const f = (prev: User[]) => { - const newUsers = users.sort((a, b) => (a.name < b.name) ? -1 : 1); - const roleFilteredNewUsers = filterUsersByRoles(newUsers, (loginUser?.roles || [])) - return JSON.stringify(prev) === JSON.stringify(roleFilteredNewUsers) ? prev : roleFilteredNewUsers; - } - return f + const newUsers = users.sort((a, b) => (a.name < b.name ? -1 : 1)); + const roleFilteredNewUsers = filterUsersByRoles( + newUsers, + loginUser?.roles || [] + ); + return JSON.stringify(prev) === JSON.stringify(roleFilteredNewUsers) + ? prev + : roleFilteredNewUsers; + }; + return f; } /** * hooks */ const useUser = () => { - console.log('useUserModule'); + console.log("useUserModule"); const { enqueueSnackbar } = useSnackbar(); const { setMask, releaseMask } = useProgress(); @@ -88,178 +106,213 @@ const useUser = () => { const [existingRoles, setExistingRoles] = useState([]); /** - * WorkspaceList: workspace list + * WorkspaceList: workspace list */ const getUsers = async () => { - console.log('getUsers'); + console.log("getUsers"); try { const result = await userService.getUsers({}); - setUsers(result.items?.sort((a, b) => (a.name < b.name) ? -1 : 1)); + setUsers(result.items?.sort((a, b) => (a.name < b.name ? -1 : 1))); updateExistingRoles(result.items); } catch (error) { handleError(error); } - } + }; const updateExistingRoles = (users: User[]) => { - setExistingRoles([...new Set(users.map(user => user.roles).flat())].sort((a, b) => a < b ? -1 : 1)); - } + setExistingRoles( + [...new Set(users.map((user) => user.roles).flat())].sort((a, b) => + a < b ? -1 : 1 + ) + ); + }; /** - * CreateDialog: Add user + * CreateDialog: Add user */ - const createUser = async (userName: string, displayName: string, authType: string, roles?: string[], addons?: UserAddon[]) => { - console.log('addUser'); + const createUser = async ( + userName: string, + displayName: string, + authType: string, + roles?: string[], + addons?: UserAddon[] + ) => { + console.log("addUser"); setMask(); try { try { - const result = await userService.createUser({ userName, displayName, authType, roles, addons }); - enqueueSnackbar(result.message, { variant: 'success' }); + const result = await userService.createUser({ + userName, + displayName, + authType, + roles, + addons, + }); + enqueueSnackbar(result.message, { variant: "success" }); return result.user; - } - catch (error) { + } catch (error) { handleError(error); } + } finally { + releaseMask(); } - finally { releaseMask(); } - } + }; /** * updateNameDialog: Update user name */ const updateName = async (userName: string, displayName: string) => { - console.log('updateUserName', userName, displayName); + console.log("updateUserName", userName, displayName); setMask(); try { try { - const result = await userService.updateUserDisplayName({ userName, displayName }); + const result = await userService.updateUserDisplayName({ + userName, + displayName, + }); const newUser = result.user; - enqueueSnackbar(result.message, { variant: 'success' }); + enqueueSnackbar(result.message, { variant: "success" }); if (users && newUser) { - setUsers(prev => prev.map(us => us.name === newUser.name ? new User(newUser) : us)); + setUsers((prev) => + prev.map((us) => + us.name === newUser.name ? new User(newUser) : us + ) + ); } return newUser; - } - catch (error) { + } catch (error) { handleError(error); } + } finally { + releaseMask(); } - finally { releaseMask(); } - } + }; /** - * updateRoleDialog: Update user + * updateRoleDialog: Update user */ const updateRole = async (userName: string, roles: string[]) => { - console.log('updateRole', userName, roles); + console.log("updateRole", userName, roles); setMask(); try { try { const result = await userService.updateUserRole({ userName, roles }); const newUser = result.user; - enqueueSnackbar(result.message, { variant: 'success' }); + enqueueSnackbar(result.message, { variant: "success" }); if (users && newUser) { - const newUsers = users.map(us => us.name === newUser.name ? new User(newUser) : us); + const newUsers = users.map((us) => + us.name === newUser.name ? new User(newUser) : us + ); setUsers(newUsers); updateExistingRoles(newUsers); } return newUser; - } - catch (error) { + } catch (error) { handleError(error); } + } finally { + releaseMask(); } - finally { releaseMask(); } - } + }; /** - * updateAddonsDialog: Update user + * updateAddonsDialog: Update user */ const updateAddons = async (userName: string, addons: UserAddon[]) => { - console.log('updateAddons', userName, addons); + console.log("updateAddons", userName, addons); setMask(); try { try { const result = await userService.updateUserAddons({ userName, addons }); const newUser = result.user; - enqueueSnackbar(result.message, { variant: 'success' }); + enqueueSnackbar(result.message, { variant: "success" }); if (users && newUser) { - const newUsers = users.map(us => us.name === newUser.name ? new User(newUser) : us); + const newUsers = users.map((us) => + us.name === newUser.name ? new User(newUser) : us + ); setUsers(newUsers); } return newUser; - } - catch (error) { + } catch (error) { handleError(error); } + } finally { + releaseMask(); } - finally { releaseMask(); } - } + }; /** - * DeleteDialog: Delete user + * DeleteDialog: Delete user */ const deleteUser = async (userName: string) => { - console.log('deleteUser'); + console.log("deleteUser"); setMask(); try { try { const result = await userService.deleteUser({ userName }); - enqueueSnackbar(result.message, { variant: 'success' }); + enqueueSnackbar(result.message, { variant: "success" }); setUsers(users.filter((u) => u.name !== userName)); return result; - } - catch (error) { + } catch (error) { handleError(error); } + } finally { + releaseMask(); } - finally { releaseMask(); } - } + }; - return ( - { - existingRoles, - users, - getUsers, - createUser, - updateName, - updateRole, - updateAddons, - deleteUser, - } - ); -} + return { + existingRoles, + users, + getUsers, + createUser, + updateName, + updateRole, + updateAddons, + deleteUser, + }; +}; /** * TemplateModule */ export const useTemplates = () => { - console.log('useTemplates'); + console.log("useTemplates"); const [templates, setTemplates] = useState([]); const templateService = useTemplateService(); const { handleError } = useHandleError(); const getAllUserAddonTemplates = () => { - console.log('getUserAddonTemplates'); - return templateService.getUserAddonTemplates({}) - .then(result => { setTemplates(result.items.sort((a, b) => (a.name < b.name) ? -1 : 1)); }) - .catch(error => { handleError(error) }); - } + console.log("getUserAddonTemplates"); + return templateService + .getUserAddonTemplates({}) + .then((result) => { + setTemplates(result.items.sort((a, b) => (a.name < b.name ? -1 : 1))); + }) + .catch((error) => { + handleError(error); + }); + }; const getUserAddonTemplates = () => { - console.log('getUserAddonTemplates'); - return templateService.getUserAddonTemplates({ useRoleFilter: true }) - .then(result => { setTemplates(result.items.sort((a, b) => (a.name < b.name) ? -1 : 1)); }) - .catch(error => { handleError(error) }); - } + console.log("getUserAddonTemplates"); + return templateService + .getUserAddonTemplates({ useRoleFilter: true }) + .then((result) => { + setTemplates(result.items.sort((a, b) => (a.name < b.name ? -1 : 1))); + }) + .catch((error) => { + handleError(error); + }); + }; - return ({ + return { templates, getUserAddonTemplates, getAllUserAddonTemplates, - }); -} + }; +}; /** * UserProvider diff --git a/web/dashboard-ui/src/views/organisms/UserNameChangeDialog.tsx b/web/dashboard-ui/src/views/organisms/UserNameChangeDialog.tsx index 90cc25ae..2641ce6b 100644 --- a/web/dashboard-ui/src/views/organisms/UserNameChangeDialog.tsx +++ b/web/dashboard-ui/src/views/organisms/UserNameChangeDialog.tsx @@ -1,9 +1,16 @@ import { PersonOutlineTwoTone } from "@mui/icons-material"; import { - Button, Dialog, DialogActions, DialogContent, DialogTitle, InputAdornment, Stack, TextField + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + InputAdornment, + Stack, + TextField, } from "@mui/material"; import React from "react"; -import { useForm, UseFormRegisterReturn } from "react-hook-form"; +import { UseFormRegisterReturn, useForm } from "react-hook-form"; import { DialogContext } from "../../components/ContextProvider"; import { useLogin } from "../../components/LoginProvider"; import { User } from "../../proto/gen/dashboard/v1alpha1/user_pb"; @@ -11,8 +18,9 @@ import { TextFieldLabel } from "../atoms/TextFieldLabel"; import { useUserModule } from "./UserModule"; const registerMui = ({ ref, ...rest }: UseFormRegisterReturn) => ({ - inputRef: ref, ...rest -}) + inputRef: ref, + ...rest, +}); /** * view @@ -21,12 +29,19 @@ interface Inputs { name: string; } -export const UserNameChangeDialog: React.VFC<{ onClose: () => void, user: User }> = ({ onClose, user }) => { - console.log('UserNameChangeDialog'); +export const UserNameChangeDialog: React.VFC<{ + onClose: () => void; + user: User; +}> = ({ onClose, user }) => { + console.log("UserNameChangeDialog"); const hooks = useUserModule(); const login = useLogin(); - const { register, handleSubmit, formState: { errors } } = useForm({ + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ defaultValues: { name: user.displayName }, }); @@ -35,33 +50,49 @@ export const UserNameChangeDialog: React.VFC<{ onClose: () => void, user: User } await hooks.updateName(user.name, data.name); await login.refreshUserInfo(); onClose(); - } + }; return ( - + Change Name
- } /> - } + /> + ), + startAdornment: ( + + + + ), }} /> - - + +
@@ -72,4 +103,5 @@ export const UserNameChangeDialog: React.VFC<{ onClose: () => void, user: User } * Context */ export const UserNameChangeDialogContext = DialogContext<{ user: User }>( - props => ()); + (props) => +); diff --git a/web/dashboard-ui/src/views/organisms/WorkspaceActionDialog.tsx b/web/dashboard-ui/src/views/organisms/WorkspaceActionDialog.tsx index 740ac9b9..8dfdbe16 100644 --- a/web/dashboard-ui/src/views/organisms/WorkspaceActionDialog.tsx +++ b/web/dashboard-ui/src/views/organisms/WorkspaceActionDialog.tsx @@ -1,7 +1,18 @@ import { Close, PersonOutlineTwoTone, WebTwoTone } from "@mui/icons-material"; import { - Alert, Button, Checkbox, Dialog, DialogActions, DialogContent, DialogTitle, - IconButton, InputAdornment, MenuItem, Stack, TextField, Tooltip + Alert, + Button, + Checkbox, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + IconButton, + InputAdornment, + MenuItem, + Stack, + TextField, + Tooltip, } from "@mui/material"; import { useEffect, useState } from "react"; import { UseFormRegisterReturn, useForm } from "react-hook-form"; @@ -11,31 +22,50 @@ import { Workspace } from "../../proto/gen/dashboard/v1alpha1/workspace_pb"; import { TextFieldLabel } from "../atoms/TextFieldLabel"; import { useTemplates, useWorkspaceModule } from "./WorkspaceModule"; - const registerMui = ({ ref, ...rest }: UseFormRegisterReturn) => ({ - inputRef: ref, ...rest -}) + inputRef: ref, + ...rest, +}); /** * WorkspaceActionDialog */ const WorkspaceActionDialog: React.VFC<{ - workspace: Workspace, onClose: () => void, title: string, actions: React.ReactNode + workspace: Workspace; + onClose: () => void; + title: string; + actions: React.ReactNode; }> = ({ workspace, onClose, title, actions }) => { - return ( - onClose()} fullWidth maxWidth={'xs'}> - {title} + onClose()} fullWidth maxWidth={"xs"}> + + {title} theme.palette.grey[500] }} - onClick={() => onClose()}> + sx={{ + position: "absolute", + right: 8, + top: 8, + color: (theme) => theme.palette.grey[500], + }} + onClick={() => onClose()} + > - } /> - } /> + } + /> + } + /> {actions} @@ -43,59 +73,108 @@ const WorkspaceActionDialog: React.VFC<{ ); }; - /** * Start */ -export const WorkspaceStartDialog: React.VFC<{ workspace: Workspace, onClose: () => void }> = (props) => { - console.log('WorkspaceStartDialog'); +export const WorkspaceStartDialog: React.VFC<{ + workspace: Workspace; + onClose: () => void; +}> = (props) => { + console.log("WorkspaceStartDialog"); const { workspace, onClose } = props; const hooks = useWorkspaceModule(); return ( - { hooks.runWorkspace(workspace).then(() => onClose()); }}>Start} />); + + } + /> + ); }; - /** - * Stop + * Stop */ -export const WorkspaceStopDialog: React.VFC<{ workspace: Workspace, onClose: () => void }> = (props) => { - console.log('WorkspaceStopDialog'); +export const WorkspaceStopDialog: React.VFC<{ + workspace: Workspace; + onClose: () => void; +}> = (props) => { + console.log("WorkspaceStopDialog"); const { workspace, onClose } = props; const hooks = useWorkspaceModule(); return ( - { hooks.stopWorkspace(workspace).then(() => onClose()); }}>Stop} />); + + } + /> + ); }; - /** * Delete */ -export const WorkspaceDeleteDialog: React.VFC<{ workspace: Workspace, onClose: () => void }> = (props) => { - console.log('WorkspaceDeleteDialog'); +export const WorkspaceDeleteDialog: React.VFC<{ + workspace: Workspace; + onClose: () => void; +}> = (props) => { + console.log("WorkspaceDeleteDialog"); const [lock, setLock] = useState(false); const { workspace, onClose } = props; const hooks = useWorkspaceModule(); return ( - - setLock(e.target.checked)} /> - - } - >This action is NOT recoverable. Are you sure to delete it?} /> + + setLock(e.target.checked)} + /> + + + } + > + This action is NOT recoverable. Are you sure to delete it? + + } + /> ); }; - /** * Create */ @@ -104,94 +183,158 @@ interface Inputs { templateName: string; vars: string[]; } -export const WorkspaceCreateDialog: React.VFC<{ onClose: () => void }> = ({ onClose }) => { - - console.log('WorkspaceCreateDialog'); +export const WorkspaceCreateDialog: React.VFC<{ onClose: () => void }> = ({ + onClose, +}) => { + console.log("WorkspaceCreateDialog"); const hooks = useWorkspaceModule(); const { user } = useWorkspaceModule(); const [template, setTemplate] = useState