From 1d8362b8e4dfd7d2bfdbafe7137a11beae7f70f7 Mon Sep 17 00:00:00 2001 From: jlandowner Date: Wed, 19 Jun 2024 22:56:46 +0900 Subject: [PATCH 1/4] UI: use MUI X DataGrid on UserPages --- .../src/views/atoms/EllipsisTypography.tsx | 19 +- .../src/views/atoms/NameAvatar.tsx | 10 +- .../src/views/organisms/UserActionDialog.tsx | 72 ++-- .../organisms/UserAddonsChangeDialog.tsx | 70 ++-- web/dashboard-ui/src/views/pages/UserPage.tsx | 392 ++++++++++-------- .../src/views/templates/PageTemplate.tsx | 7 +- 6 files changed, 306 insertions(+), 264 deletions(-) diff --git a/web/dashboard-ui/src/views/atoms/EllipsisTypography.tsx b/web/dashboard-ui/src/views/atoms/EllipsisTypography.tsx index c229d7ba..19f2964d 100644 --- a/web/dashboard-ui/src/views/atoms/EllipsisTypography.tsx +++ b/web/dashboard-ui/src/views/atoms/EllipsisTypography.tsx @@ -7,14 +7,13 @@ import { import React from "react"; export type EllipsisTypographyProps = Omit & { - children: string; + children?: string; placement?: TooltipProps["placement"]; }; -export const EllipsisTypography: React.FC = ({ - children, - placement, -}) => { +export const EllipsisTypography: React.FC = ( + props +) => { const [isOverflow, setIsOverflow] = React.useState(false); const paragraph = React.useRef(null); @@ -25,11 +24,11 @@ export const EllipsisTypography: React.FC = ({ } }, [paragraph]); - const title = children; + const title = props.children || ""; // return isOverflow ? ( {typoglaphy} ) : typoglaphy return isOverflow ? ( - + = ({ whiteSpace: "nowrap", overflow: "hidden", }} + {...props} > - {children} + {props.children} ) : ( @@ -53,8 +53,9 @@ export const EllipsisTypography: React.FC = ({ whiteSpace: "nowrap", overflow: "hidden", }} + {...props} > - {children} + {props.children} ); }; diff --git a/web/dashboard-ui/src/views/atoms/NameAvatar.tsx b/web/dashboard-ui/src/views/atoms/NameAvatar.tsx index e0167d44..2b2ac4d8 100644 --- a/web/dashboard-ui/src/views/atoms/NameAvatar.tsx +++ b/web/dashboard-ui/src/views/atoms/NameAvatar.tsx @@ -6,13 +6,19 @@ export const NameAvatar: React.VFC<{ name?: string } & AvatarProps> = ( props ) => { return props.name ? ( - + theme.palette.mode === "light" ? "white" : "black", }} - fontSize="inherit" + variant="body1" > {props.name.substring(0, 1).toUpperCase()} diff --git a/web/dashboard-ui/src/views/organisms/UserActionDialog.tsx b/web/dashboard-ui/src/views/organisms/UserActionDialog.tsx index 2da0aff4..13103e13 100644 --- a/web/dashboard-ui/src/views/organisms/UserActionDialog.tsx +++ b/web/dashboard-ui/src/views/organisms/UserActionDialog.tsx @@ -177,7 +177,7 @@ const UserActionDialog: React.FC = ({ - + {user.addons.map((v, i) => ( void }> = ({ }); return ( - + Create New User 🎉
{ console.log(inp); const userAddons = inp.addons - .filter((v) => v.enable) + .filter((v) => v.enable || v.template.isDefaultUserAddon) .map((inpAddon) => { const vars: { [key: string]: string } = {}; inpAddon.vars.forEach((v, i) => { @@ -483,9 +483,9 @@ export const UserCreateDialog: React.VFC<{ onClose: () => void }> = ({ pattern: { value: /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/, message: - 'Only lowercase alphanumeric characters or "-" are allowed', + 'Only lowercase alphanumeric characters or "-" are allowed (start and end with an alphanumeric character)', }, - maxLength: { value: 128, message: "Max 128 characters" }, + maxLength: { value: 50, message: "Max 50 characters" }, }) )} error={Boolean(errors.id)} @@ -503,12 +503,11 @@ export const UserCreateDialog: React.VFC<{ onClose: () => void }> = ({ }} /> void }> = ({ label="Auth Type" select fullWidth - defaultValue="" + defaultValue="password-secret" {...registerMui( register("authType", { required: { value: true, message: "Required" }, @@ -647,14 +646,9 @@ export const UserCreateDialog: React.VFC<{ onClose: () => void }> = ({ defaultChecked={ field.template.isDefaultUserAddon || false } + disabled={field.template.isDefaultUserAddon || false} {...registerMui( - register(`addons.${index}.enable` as const, { - required: { - value: - field.template.isDefaultUserAddon || false, - message: "Required", - }, - }) + register(`addons.${index}.enable` as const, {}) )} /> @@ -665,28 +659,30 @@ export const UserCreateDialog: React.VFC<{ onClose: () => void }> = ({ > {errors.addons?.[index]?.enable?.message} - - - {field.template.requiredVars?.map((required, j) => ( - - ))} - - + {(watch("addons")[index].template.isDefaultUserAddon || + watch("addons")[index].enable) && + field.template.requiredVars.length > 0 && ( + + {field.template.requiredVars?.map((required, j) => ( + + ))} + + )} ))} diff --git a/web/dashboard-ui/src/views/organisms/UserAddonsChangeDialog.tsx b/web/dashboard-ui/src/views/organisms/UserAddonsChangeDialog.tsx index 8569a0a7..41123ff7 100644 --- a/web/dashboard-ui/src/views/organisms/UserAddonsChangeDialog.tsx +++ b/web/dashboard-ui/src/views/organisms/UserAddonsChangeDialog.tsx @@ -2,7 +2,6 @@ import { PersonOutlineTwoTone } from "@mui/icons-material"; import { Button, Checkbox, - Collapse, Dialog, DialogActions, DialogContent, @@ -86,7 +85,7 @@ export const UserAddonChangeDialog: React.FC<{ onSubmit={handleSubmit(async (inp: Inputs) => { console.log(inp); const userAddons = inp.addons - .filter((v) => v.enable) + .filter((v) => v.enable || v.template.isDefaultUserAddon) .map((inpAddon) => { const vars: { [key: string]: string } = {}; inpAddon.vars.forEach((v, i) => { @@ -143,14 +142,9 @@ export const UserAddonChangeDialog: React.FC<{ field.template.isDefaultUserAddon || false } + disabled={field.template.isDefaultUserAddon || false} {...registerMui( - register(`addons.${index}.enable` as const, { - required: { - value: - field.template.isDefaultUserAddon || false, - message: "Required", - }, - }) + register(`addons.${index}.enable` as const, {}) )} /> @@ -161,34 +155,36 @@ export const UserAddonChangeDialog: React.FC<{ > {errors.addons?.[index]?.enable?.message} - - - {field.template.requiredVars?.map((required, j) => ( - - ))} - - + {(watch("addons")[index].template.isDefaultUserAddon || + watch("addons")[index].enable) && + field.template.requiredVars.length > 0 && ( + + {field.template.requiredVars?.map((required, j) => ( + + ))} + + )} ))} diff --git a/web/dashboard-ui/src/views/pages/UserPage.tsx b/web/dashboard-ui/src/views/pages/UserPage.tsx index 964ab9b1..8dabe464 100644 --- a/web/dashboard-ui/src/views/pages/UserPage.tsx +++ b/web/dashboard-ui/src/views/pages/UserPage.tsx @@ -1,25 +1,20 @@ import useUrlState from "@ahooksjs/use-url-state"; import { AddTwoTone, + AdminPanelSettingsTwoTone, Badge, Clear, - DeleteTwoTone, - ExpandLess, - ExpandMore, - ManageAccountsTwoTone, + DeleteOutlined, + Edit, MoreVert, RefreshTwoTone, SearchTwoTone, + Settings, } from "@mui/icons-material"; import { Box, - Card, - CardHeader, Chip, - Collapse, - Divider, Fab, - Grid, IconButton, InputAdornment, ListItemIcon, @@ -31,12 +26,19 @@ import { TextField, Tooltip, Typography, + useMediaQuery, + useTheme, } from "@mui/material"; -import React, { useEffect, useState } from "react"; +import { + DataGrid, + GridColDef, + GridRenderCellParams, + gridClasses, +} from "@mui/x-data-grid"; +import React, { useEffect } from "react"; import { useLogin } from "../../components/LoginProvider"; -import { User } from "../../proto/gen/dashboard/v1alpha1/user_pb"; -import { NameAvatar } from "../atoms/NameAvatar"; -import { SelectableChip } from "../atoms/SelectableChips"; +import { User, UserAddon } from "../../proto/gen/dashboard/v1alpha1/user_pb"; +import { EllipsisTypography } from "../atoms/EllipsisTypography"; import { PasswordDialogContext } from "../organisms/PasswordDialog"; import { RoleChangeDialogContext } from "../organisms/RoleChangeDialog"; import { @@ -91,7 +93,7 @@ const UserMenu: React.VFC<{ user: User }> = ({ user: us }) => { - Change Name... + Change DisplayName... { @@ -100,7 +102,7 @@ const UserMenu: React.VFC<{ user: User }> = ({ user: us }) => { }} > - + Change Role... @@ -111,7 +113,7 @@ const UserMenu: React.VFC<{ user: User }> = ({ user: us }) => { }} > - + Change Addons... @@ -122,7 +124,7 @@ const UserMenu: React.VFC<{ user: User }> = ({ user: us }) => { }} > - + Remove User... @@ -132,13 +134,193 @@ const UserMenu: React.VFC<{ user: User }> = ({ user: us }) => { ); }; +export type UserDataGridProp = { + users: User[]; +}; + +export const UserDataGrid: React.FC = ({ users }) => { + const userNameChangeDispatch = UserNameChangeDialogContext.useDispatch(); + const roleChangeDialogDispatch = RoleChangeDialogContext.useDispatch(); + const userAddonChangeDispatch = UserAddonChangeDialogContext.useDispatch(); + + const theme = useTheme(); + const isUpSM = useMediaQuery(theme.breakpoints.up("sm"), { noSsr: true }); + + const columns: GridColDef[] = [ + { + field: "id", + headerName: "ID", + flex: 0.8, + renderCell: (params: GridRenderCellParams) => ( + {params.value} + ), + }, + { + field: "status", + headerName: "Status", + flex: 0.6, + minWidth: 80, + renderCell: (params: GridRenderCellParams) => ( + + ), + }, + { field: "authType", headerName: "Auth Type", flex: 0.8 }, + { + field: "displayName", + headerName: "Display Name", + flex: 0.8, + renderCell: (params: GridRenderCellParams) => ( + <> + {params.hasFocus ? ( + + + {params.value} + + + userNameChangeDispatch(true, { user: params.row }) + } + > + + + + ) : ( + + {params.value} + + )} + + ), + }, + { + field: "roles", + headerName: "Roles", + flex: 0.8, + renderCell: (params: GridRenderCellParams) => ( + + {params.value?.map((v, i) => ( + + ))} + {params.hasFocus && ( + + roleChangeDialogDispatch(true, { user: params.row }) + } + > + + + )} + + ), + }, + { + field: "addons", + headerName: "Addons", + valueGetter: (addons: UserAddon[]) => addons.map((v) => v.template), + renderCell: (params: GridRenderCellParams) => ( + + {params.value?.map((v, i) => ( + + {v} + + ))} + {params.hasFocus && ( + + + userAddonChangeDispatch(true, { user: params.row }) + } + > + + + + + )} + + ), + flex: 1, + }, + { + field: "actions", + type: "actions", + getActions: () => [], + flex: 0.2, + renderCell: (params: GridRenderCellParams) => ( + + ), + }, + ]; + + return ( + <> +
+ ({ ...v, id: v.name }))} + columns={columns} + getRowHeight={() => "auto"} + sx={{ + [`& .${gridClasses.cell}`]: { + py: 1, + }, + }} + initialState={{ + columns: { + columnVisibilityModel: { + addons: isUpSM, + displayName: isUpSM, + authType: isUpSM, + }, + }, + pagination: { paginationModel: { pageSize: 10 } }, + }} + pageSizeOptions={[10, 50, 100]} + /> +
+ + ); +}; + const UserList: React.VFC = () => { const hooks = useUserModule(); const { loginUser } = useLogin(); const userCreateDialogDispatch = UserCreateDialogContext.useDispatch(); const userInfoDialogDispatch = UserInfoDialogContext.useDispatch(); - const [showFilter, setShowFilter] = useState(false); const [urlParam, setUrlParam] = useUrlState( { search: "", @@ -154,16 +336,6 @@ const UserList: React.VFC = () => { typeof urlParam.filterRoles === "string" ? [urlParam.filterRoles] : urlParam.filterRoles; - const pushFilterRoles = (role: string) => { - const f = [...new Set([...filterRoles, role])].sort((a, b) => - a < b ? -1 : 1 - ); - filterRoles && setUrlParam({ filterRoles: f }); - }; - const popFilterRoles = (role: string) => { - const f = filterRoles.filter((v: string) => v !== role); - filterRoles && setUrlParam({ filterRoles: f }); - }; useEffect(() => { if ( @@ -177,7 +349,7 @@ const UserList: React.VFC = () => { ), }); } - }, [loginUser, hooks.existingRoles.length]); + }, []); const isUserMatchedToFilterRoles = (user: User) => { for (const v of user.roles) { @@ -257,158 +429,28 @@ const UserList: React.VFC = () => { - - { - setShowFilter(!showFilter); - }} - > - {showFilter ? : } - - - Filter by Roles - - {filterRoles.length > 0 && ( - - {filterRoles.map((v, i) => ( - { - popFilterRoles(v); - }} - /> - ))} - - )} - - - - Existing Roles - - - {hooks.existingRoles.map((v, i) => ( - { - checked ? pushFilterRoles(v) : popFilterRoles(v); - }} - /> - ))} - - - - {!hooks.users.filter( - (us) => - urlParam.search === "" || Boolean(us.name.match(urlParam.search)) - ).length && ( - - - No Users found. - - - )} - - {hooks.users + - urlParam.search === "" || Boolean(us.name.match(urlParam.search)) + urlParam.search === "" || + Boolean(us.name.match(urlParam.search)) || + Boolean(us.status.match(urlParam.search)) || + Boolean(us.authType.match(urlParam.search)) || + Boolean(us.displayName.match(urlParam.search)) || + Boolean( + us.roles.filter((v) => v.match(urlParam.search)).length > 0 + ) || + Boolean( + us.addons.filter((v) => v.template.match(urlParam.search)) + .length > 0 + ) ) - .filter((us) => us.status === "Active") .filter( (us) => filterRoles.length == 0 || isUserMatchedToFilterRoles(us) - ) - .map((us) => ( - - - { - userInfoDialogDispatch(true, { user: us }); - }} - /> - } - title={ - { - userInfoDialogDispatch(true, { user: us }); - }} - > - {us.name} - -
- - {us.roles && - us.roles.map((v, i) => { - return ( - - ); - })} - -
-
- } - subheader={us.displayName} - action={} - /> -
-
- ))} -
+ )} + /> ); }; diff --git a/web/dashboard-ui/src/views/templates/PageTemplate.tsx b/web/dashboard-ui/src/views/templates/PageTemplate.tsx index 16328fb9..18d16b0b 100644 --- a/web/dashboard-ui/src/views/templates/PageTemplate.tsx +++ b/web/dashboard-ui/src/views/templates/PageTemplate.tsx @@ -8,6 +8,7 @@ import { Menu as MenuIcon, Notifications, ReportProblem, + Settings, SupervisorAccountTwoTone, VpnKey, Warning, @@ -371,7 +372,7 @@ export const PageTemplate: React.FC< openUserInfoDialog()} /> {loginUser?.displayName} @@ -423,13 +424,13 @@ export const PageTemplate: React.FC< - Change Name... + Change DisplayName... )} {isSignIn && isAdmin && ( changeAddons()}> - + Change Addons... From 7af27345a13efc70c50e53d95f4c6258b49b98a9 Mon Sep 17 00:00:00 2001 From: jlandowner Date: Wed, 19 Jun 2024 23:49:02 +0900 Subject: [PATCH 2/4] Fix EventPage datagrid column visibility --- web/dashboard-ui/src/views/pages/EventPage.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/web/dashboard-ui/src/views/pages/EventPage.tsx b/web/dashboard-ui/src/views/pages/EventPage.tsx index 5e0ded0b..dae0e894 100644 --- a/web/dashboard-ui/src/views/pages/EventPage.tsx +++ b/web/dashboard-ui/src/views/pages/EventPage.tsx @@ -124,17 +124,19 @@ const EventList: React.VFC = () => { events={events} clock={clock} dataGridProps={{ - columnVisibilityModel: { - type: false, - reportingController: false, - series: false, - regardingWorkspace: isUpSM, - note: isUpSM, - }, initialState: { sorting: { sortModel: [{ field: "eventTime", sort: "desc" }], }, + columns: { + columnVisibilityModel: { + type: false, + reportingController: false, + series: false, + regardingWorkspace: isUpSM, + note: isUpSM, + }, + }, }, }} /> From c0c16fcefa54885a3ef2fb10eacef98fe9e4c8ab Mon Sep 17 00:00:00 2001 From: jlandowner Date: Fri, 21 Jun 2024 08:50:28 +0900 Subject: [PATCH 3/4] Fix web/dashboard-ui/src/views/organisms/NetworkRuleActionDialog.tsx --- .../organisms/NetworkRuleActionDialog.tsx | 63 +++++-------------- 1 file changed, 15 insertions(+), 48 deletions(-) diff --git a/web/dashboard-ui/src/views/organisms/NetworkRuleActionDialog.tsx b/web/dashboard-ui/src/views/organisms/NetworkRuleActionDialog.tsx index d0a765be..24908231 100644 --- a/web/dashboard-ui/src/views/organisms/NetworkRuleActionDialog.tsx +++ b/web/dashboard-ui/src/views/organisms/NetworkRuleActionDialog.tsx @@ -264,38 +264,6 @@ export const NetworkRuleUpsertDialog: React.VFC<{ )} /> - {/* */} {openAllowedUsers && ( @@ -312,9 +280,23 @@ export const NetworkRuleUpsertDialog: React.VFC<{ {openInputAllowedUser ? : } + + + {fields.map((v, i) => ( + + remove(i)} + /> + + ))} + + {openInputAllowedUser && ( )} - - - {fields.map((v, i) => ( - - remove(i)} - /> - - ))} - - )} {isMain && ( From 91dede4cd6c1790440308be702af29cdd0f024c8 Mon Sep 17 00:00:00 2001 From: jlandowner Date: Fri, 21 Jun 2024 08:51:11 +0900 Subject: [PATCH 4/4] Fix polling --- web/dashboard-ui/src/views/organisms/WorkspaceModule.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/dashboard-ui/src/views/organisms/WorkspaceModule.tsx b/web/dashboard-ui/src/views/organisms/WorkspaceModule.tsx index 7b46838c..16711541 100644 --- a/web/dashboard-ui/src/views/organisms/WorkspaceModule.tsx +++ b/web/dashboard-ui/src/views/organisms/WorkspaceModule.tsx @@ -219,6 +219,7 @@ const useWorkspace = () => { setWorkspaces((prev) => { if (prev[wsName]) { const pws = prev[wsName]; + clearInterval(pws.timer); pws.timer = timer; return { ...prev, [wsName]: pws }; }