From deb46bf30add2785856e0b49c48636eeee619314 Mon Sep 17 00:00:00 2001 From: jlandowner Date: Tue, 25 Jun 2024 00:01:45 +0900 Subject: [PATCH 1/2] Show yaml --- web/dashboard-ui/package.json | 1 + web/dashboard-ui/src/App.tsx | 2 +- .../src/views/atoms/YAMLTextArea.tsx | 87 ++++++++++++++++++ .../src/views/organisms/UserActionDialog.tsx | 34 ------- .../src/views/organisms/UserInfoDialog.tsx | 88 ++++++++++++++++++ .../src/views/organisms/UserModule.tsx | 2 +- .../views/organisms/WorkspaceInfoDialog.tsx | 89 +++++++++++++++++++ web/dashboard-ui/src/views/pages/UserPage.tsx | 2 +- .../src/views/pages/WorkspacePage.tsx | 26 ++++-- .../src/views/templates/PageTemplate.tsx | 3 +- web/dashboard-ui/yarn.lock | 5 ++ 11 files changed, 292 insertions(+), 47 deletions(-) create mode 100644 web/dashboard-ui/src/views/atoms/YAMLTextArea.tsx create mode 100644 web/dashboard-ui/src/views/organisms/UserInfoDialog.tsx create mode 100644 web/dashboard-ui/src/views/organisms/WorkspaceInfoDialog.tsx diff --git a/web/dashboard-ui/package.json b/web/dashboard-ui/package.json index d9010cca..e2eac7fd 100644 --- a/web/dashboard-ui/package.json +++ b/web/dashboard-ui/package.json @@ -24,6 +24,7 @@ "@types/node": "20.11.15", "base64url": "^3.0.1", "copy-to-clipboard": "3.3.3", + "highlight.js": "^11.9.0", "notistack": "3.0.1", "react": "18.2.0", "react-dom": "18.2.0", diff --git a/web/dashboard-ui/src/App.tsx b/web/dashboard-ui/src/App.tsx index 6aa11dfa..2f0f57d3 100644 --- a/web/dashboard-ui/src/App.tsx +++ b/web/dashboard-ui/src/App.tsx @@ -11,8 +11,8 @@ import { ProgressProvider } from "./components/ProgressProvider"; import { AuthenticatorManageDialogContext } from "./views/organisms/AuthenticatorManageDialog"; import { EventDetailDialogContext } from "./views/organisms/EventDetailDialog"; import { PasswordChangeDialogContext } from "./views/organisms/PasswordChangeDialog"; -import { UserInfoDialogContext } from "./views/organisms/UserActionDialog"; import { UserAddonChangeDialogContext } from "./views/organisms/UserAddonsChangeDialog"; +import { UserInfoDialogContext } from "./views/organisms/UserInfoDialog"; import { UserContext } from "./views/organisms/UserModule"; import { UserNameChangeDialogContext } from "./views/organisms/UserNameChangeDialog"; import { EventPage } from "./views/pages/EventPage"; diff --git a/web/dashboard-ui/src/views/atoms/YAMLTextArea.tsx b/web/dashboard-ui/src/views/atoms/YAMLTextArea.tsx new file mode 100644 index 00000000..40d2d193 --- /dev/null +++ b/web/dashboard-ui/src/views/atoms/YAMLTextArea.tsx @@ -0,0 +1,87 @@ +import { ContentCopy } from "@mui/icons-material"; +import { Fab } from "@mui/material"; +import { styled } from "@mui/material/styles"; +import copy from "copy-to-clipboard"; +import hljs from "highlight.js"; +import "highlight.js/styles/default.css"; +import { useSnackbar } from "notistack"; +import React, { useState } from "react"; + +const StyledPre = styled("pre")({ + fontFamily: "Menlo, Monaco, 'Courier New', monospace", + fontSize: 12, + lineHeight: 1.6, + margin: 0, + padding: 16, + whiteSpace: "pre", + wordWrap: "break-word", + overflow: "auto", + border: "1px solid #ccc", + borderRadius: "4px", + backgroundColor: "#1E1E1E", + "& .hljs-attr": { + color: "#9CDCFE", + }, + "& .hljs-string": { + color: "#CE9178", + }, + "& .hljs-number": { + color: "#B5CEA8", + }, + "& .hljs-literal": { + color: "#569CD6", + }, +}); + +const YAMLTextArea: React.FC<{ + code: string; +}> = ({ code }) => { + const [hover, setHover] = useState(false); + const { enqueueSnackbar } = useSnackbar(); + + const onCopy = (text: string) => { + copy(text); + enqueueSnackbar("Copied!", { variant: "success" }); + }; + + const highlightedCode = hljs.highlight(code, { + language: "yaml", + }).value; + + const highlightedCodeWithSpaces = highlightedCode.replace( + /(^|\n)( +)/g, + function (_, newline, spaces) { + return newline + " ".repeat(spaces.length); + } + ); + + return ( +
setHover(true)} + onMouseLeave={() => setHover(false)} + > + + {hover && ( + { + onCopy(code); + }} + size="medium" + sx={{ + position: "absolute", + bottom: 80, + right: 64, + }} + > + + + )} +
+ ); +}; + +export default YAMLTextArea; diff --git a/web/dashboard-ui/src/views/organisms/UserActionDialog.tsx b/web/dashboard-ui/src/views/organisms/UserActionDialog.tsx index 13103e13..169276b8 100644 --- a/web/dashboard-ui/src/views/organisms/UserActionDialog.tsx +++ b/web/dashboard-ui/src/views/organisms/UserActionDialog.tsx @@ -230,36 +230,6 @@ const UserActionDialog: React.FC = ({ ); }; -/** - * Info - */ -export const UserInfoDialog: React.VFC<{ - onClose: () => void; - user: User; - defaultOpenUserAddon?: boolean; -}> = ({ onClose, user, defaultOpenUserAddon }) => { - console.log("UserInfoDialog"); - return ( - onClose()} - user={user} - defaultOpenUserAddon={defaultOpenUserAddon} - actions={ - - } - /> - ); -}; - /** * Delete */ @@ -704,10 +674,6 @@ export const UserCreateDialog: React.VFC<{ onClose: () => void }> = ({ /** * Context */ -export const UserInfoDialogContext = DialogContext<{ - user: User; - defaultOpenUserAddon?: boolean; -}>((props) => ); export const UserDeleteDialogContext = DialogContext<{ user: User }>( (props) => ); diff --git a/web/dashboard-ui/src/views/organisms/UserInfoDialog.tsx b/web/dashboard-ui/src/views/organisms/UserInfoDialog.tsx new file mode 100644 index 00000000..8ccb7b67 --- /dev/null +++ b/web/dashboard-ui/src/views/organisms/UserInfoDialog.tsx @@ -0,0 +1,88 @@ +import { Close } from "@mui/icons-material"; +import { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + IconButton, + Stack, + Tab, + Tabs, +} from "@mui/material"; +import { styled } from "@mui/material/styles"; +import "highlight.js/styles/default.css"; +import React, { useEffect, useState } from "react"; +import { DialogContext } from "../../components/ContextProvider"; +import { User } from "../../proto/gen/dashboard/v1alpha1/user_pb"; +import { useUserService } from "../../services/DashboardServices"; +import YAMLTextArea from "../atoms/YAMLTextArea"; + +const StyledDialogContent = styled(DialogContent)({ + overflow: "auto", +}); + +const UserInfoDialog: React.FC<{ + onClose: () => void; + user: User; +}> = ({ onClose, user }) => { + const userService = useUserService(); + + const [code, setCode] = useState(""); + const [showTab, setShowTab] = useState<"objects">("objects"); + + useEffect(() => { + userService + .getUser({ + userName: user.name, + withRaw: true, + }) + .then((res) => { + setCode(res.user?.raw || "no yaml"); + }); + }, [user]); + + return ( + + + + User Object + + theme.palette.grey[500], + }} + onClick={() => onClose()} + > + + + + + + + + { + setShowTab(newValue); + }} + aria-label="basic tabs example" + > + + + + {showTab === "objects" && } + + + + + + ); +}; + +export const UserInfoDialogContext = DialogContext<{ + user: User; +}>((props) => ); diff --git a/web/dashboard-ui/src/views/organisms/UserModule.tsx b/web/dashboard-ui/src/views/organisms/UserModule.tsx index cfc92744..32044e50 100644 --- a/web/dashboard-ui/src/views/organisms/UserModule.tsx +++ b/web/dashboard-ui/src/views/organisms/UserModule.tsx @@ -106,7 +106,7 @@ const useUser = () => { const [existingRoles, setExistingRoles] = useState([]); /** - * WorkspaceList: workspace list + * UserList: user list */ const getUsers = async () => { console.log("getUsers"); diff --git a/web/dashboard-ui/src/views/organisms/WorkspaceInfoDialog.tsx b/web/dashboard-ui/src/views/organisms/WorkspaceInfoDialog.tsx new file mode 100644 index 00000000..23dfc6ba --- /dev/null +++ b/web/dashboard-ui/src/views/organisms/WorkspaceInfoDialog.tsx @@ -0,0 +1,89 @@ +import { Close } from "@mui/icons-material"; +import { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + IconButton, + Stack, + Tab, + Tabs, +} from "@mui/material"; +import { styled } from "@mui/material/styles"; +import "highlight.js/styles/default.css"; +import React, { useEffect, useState } from "react"; +import { DialogContext } from "../../components/ContextProvider"; +import { Workspace } from "../../proto/gen/dashboard/v1alpha1/workspace_pb"; +import { useWorkspaceService } from "../../services/DashboardServices"; +import YAMLTextArea from "../atoms/YAMLTextArea"; + +const StyledDialogContent = styled(DialogContent)({ + overflow: "auto", +}); + +const WorkspaceInfoDialog: React.FC<{ + onClose: () => void; + ws: Workspace; +}> = ({ onClose, ws }) => { + const wsService = useWorkspaceService(); + + const [code, setCode] = useState(""); + const [showTab, setShowTab] = useState<"objects">("objects"); + + useEffect(() => { + wsService + .getWorkspace({ + wsName: ws.name, + userName: ws.ownerName, + withRaw: true, + }) + .then((res) => { + setCode(res.workspace?.raw || "no yaml"); + }); + }, [ws]); + + return ( + + + + Workspace Object + + theme.palette.grey[500], + }} + onClick={() => onClose()} + > + + + + + + + + { + setShowTab(newValue); + }} + aria-label="basic tabs example" + > + + + + {showTab === "objects" && } + + + + + + ); +}; + +export const WorkspaceInfoDialogContext = DialogContext<{ + ws: Workspace; +}>((props) => ); diff --git a/web/dashboard-ui/src/views/pages/UserPage.tsx b/web/dashboard-ui/src/views/pages/UserPage.tsx index 8dabe464..759366d7 100644 --- a/web/dashboard-ui/src/views/pages/UserPage.tsx +++ b/web/dashboard-ui/src/views/pages/UserPage.tsx @@ -45,9 +45,9 @@ import { UserCreateConfirmDialogContext, UserCreateDialogContext, UserDeleteDialogContext, - UserInfoDialogContext, } from "../organisms/UserActionDialog"; import { UserAddonChangeDialogContext } from "../organisms/UserAddonsChangeDialog"; +import { UserInfoDialogContext } from "../organisms/UserInfoDialog"; import { hasAdminForRole, hasPrivilegedRole, diff --git a/web/dashboard-ui/src/views/pages/WorkspacePage.tsx b/web/dashboard-ui/src/views/pages/WorkspacePage.tsx index 822ba9d4..b3ac0d9c 100644 --- a/web/dashboard-ui/src/views/pages/WorkspacePage.tsx +++ b/web/dashboard-ui/src/views/pages/WorkspacePage.tsx @@ -81,6 +81,7 @@ import { WorkspaceStartDialogContext, WorkspaceStopDialogContext, } from "../organisms/WorkspaceActionDialog"; +import { WorkspaceInfoDialogContext } from "../organisms/WorkspaceInfoDialog"; import { WorkspaceContext, WorkspaceWrapper, @@ -479,6 +480,8 @@ const WorkspaceItem: React.VFC<{ const sharedWorkspace = ws.isSharedFor(user); const readonly = ws.readonlyFor(user); + const wsInfoDialogDispatch = WorkspaceInfoDialogContext.useDispatch(); + return ( @@ -491,7 +494,12 @@ const WorkspaceItem: React.VFC<{ : theme.palette.grey["A700"], }} avatar={ - + { + wsInfoDialogDispatch(true, { ws: ws }); + }} + > {sharedWorkspace ? : } } @@ -845,13 +853,15 @@ export const WorkspacePage: React.VFC = () => { - - - - - - - + + + + + + + + + diff --git a/web/dashboard-ui/src/views/templates/PageTemplate.tsx b/web/dashboard-ui/src/views/templates/PageTemplate.tsx index 18d16b0b..8d7eb722 100644 --- a/web/dashboard-ui/src/views/templates/PageTemplate.tsx +++ b/web/dashboard-ui/src/views/templates/PageTemplate.tsx @@ -50,8 +50,8 @@ import { AuthenticatorManageDialogContext } from "../organisms/AuthenticatorMana import { EventDetailDialogContext } from "../organisms/EventDetailDialog"; import { latestTime } from "../organisms/EventModule"; import { PasswordChangeDialogContext } from "../organisms/PasswordChangeDialog"; -import { UserInfoDialogContext } from "../organisms/UserActionDialog"; import { UserAddonChangeDialogContext } from "../organisms/UserAddonsChangeDialog"; +import { UserInfoDialogContext } from "../organisms/UserInfoDialog"; import { isAdminRole, isAdminUser, @@ -141,7 +141,6 @@ export const PageTemplate: React.FC< console.log("openUserInfoDialog"); userInfoDialogDispatch(true, { user: loginUser!, - defaultOpenUserAddon: true, }); setAnchorEl(null); }; diff --git a/web/dashboard-ui/yarn.lock b/web/dashboard-ui/yarn.lock index 687669ea..228fcc37 100644 --- a/web/dashboard-ui/yarn.lock +++ b/web/dashboard-ui/yarn.lock @@ -1891,6 +1891,11 @@ hasown@^2.0.0: dependencies: function-bind "^1.1.2" +highlight.js@^11.9.0: + version "11.9.0" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.9.0.tgz#04ab9ee43b52a41a047432c8103e2158a1b8b5b0" + integrity sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw== + hoist-non-react-statics@^3.3.1: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" From f6e4c1081272d40cdc8b01765ef891b982cbdde5 Mon Sep 17 00:00:00 2001 From: jlandowner Date: Tue, 25 Jun 2024 01:02:35 +0900 Subject: [PATCH 2/2] Add NameAvator and Addon tooltip on UserPage --- web/dashboard-ui/src/views/pages/UserPage.tsx | 42 ++++++++++++++++--- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/web/dashboard-ui/src/views/pages/UserPage.tsx b/web/dashboard-ui/src/views/pages/UserPage.tsx index 759366d7..7494b363 100644 --- a/web/dashboard-ui/src/views/pages/UserPage.tsx +++ b/web/dashboard-ui/src/views/pages/UserPage.tsx @@ -39,6 +39,7 @@ import React, { useEffect } from "react"; import { useLogin } from "../../components/LoginProvider"; import { User, UserAddon } from "../../proto/gen/dashboard/v1alpha1/user_pb"; import { EllipsisTypography } from "../atoms/EllipsisTypography"; +import { NameAvatar } from "../atoms/NameAvatar"; import { PasswordDialogContext } from "../organisms/PasswordDialog"; import { RoleChangeDialogContext } from "../organisms/RoleChangeDialog"; import { @@ -142,11 +143,26 @@ export const UserDataGrid: React.FC = ({ users }) => { const userNameChangeDispatch = UserNameChangeDialogContext.useDispatch(); const roleChangeDialogDispatch = RoleChangeDialogContext.useDispatch(); const userAddonChangeDispatch = UserAddonChangeDialogContext.useDispatch(); + const userInfoDispatch = UserInfoDialogContext.useDispatch(); const theme = useTheme(); const isUpSM = useMediaQuery(theme.breakpoints.up("sm"), { noSsr: true }); const columns: GridColDef[] = [ + { + field: "avator", + headerName: "Avator", + type: "singleSelect", + width: 80, + renderCell: (params: GridRenderCellParams) => ( + { + userInfoDispatch(true, { user: params.row }); + }} + /> + ), + }, { field: "id", headerName: "ID", @@ -249,13 +265,29 @@ export const UserDataGrid: React.FC = ({ users }) => { { field: "addons", headerName: "Addons", - valueGetter: (addons: UserAddon[]) => addons.map((v) => v.template), - renderCell: (params: GridRenderCellParams) => ( + renderCell: (params: GridRenderCellParams) => ( {params.value?.map((v, i) => ( - - {v} - + 0 && ( + + {Object.keys(v.vars).map((k) => ( + {`${k} = ${v.vars[k]}`} + ))} + + ) + } + key={i} + > + + {v.template} + + ))} {params.hasFocus && (