From 2aa74dc044bc64c1b705c5409daa14d3e37419b1 Mon Sep 17 00:00:00 2001 From: Piyush Kumar Date: Mon, 7 Oct 2024 15:34:39 +0530 Subject: [PATCH] devx ui changes --- .../components/common-console-components.tsx | 19 +++- .../_main+/$account+/settings+/_layout.tsx | 54 +++++++++- .../_main+/$account+/settings+/general.tsx | 55 ++++++++--- .../settings+/user-management/handle-user.tsx | 19 ++-- .../settings+/user-management/route.tsx | 99 ++++++++++++------- .../user-management/user-access-resource.tsx | 62 +++++++----- .../console/routes/_main+/_layout/_layout.tsx | 15 ++- 7 files changed, 222 insertions(+), 101 deletions(-) diff --git a/web/src/apps/console/components/common-console-components.tsx b/web/src/apps/console/components/common-console-components.tsx index 86979d4ef..7ea1a4404 100644 --- a/web/src/apps/console/components/common-console-components.tsx +++ b/web/src/apps/console/components/common-console-components.tsx @@ -1,27 +1,36 @@ import { ReactNode, useState } from 'react'; import { Button } from '~/components/atoms/button'; +import TooltipV2 from '~/components/atoms/tooltipV2'; +import { toast } from '~/components/molecule/toast'; import { cn } from '~/components/utils'; +import { Check, Copy } from '~/console/components/icons'; import useClipboard from '~/root/lib/client/hooks/use-clipboard'; -import { toast } from '~/components/molecule/toast'; -import { Copy, Check } from '~/console/components/icons'; import { Truncate } from '~/root/lib/utils/common'; -import TooltipV2 from '~/components/atoms/tooltipV2'; interface IDeleteContainer { title: ReactNode; children: ReactNode; action: () => void; + content?: string; + disabled?: boolean; } export const DeleteContainer = ({ title, children, action, + content, + disabled = false, }: IDeleteContainer) => { return (
{title}
{children}
-
); }; @@ -48,7 +57,7 @@ export const Box = ({ children, title, className }: IBox) => {
{title}
diff --git a/web/src/apps/console/routes/_main+/$account+/settings+/_layout.tsx b/web/src/apps/console/routes/_main+/$account+/settings+/_layout.tsx index cbbcbbcc1..6f849185b 100644 --- a/web/src/apps/console/routes/_main+/$account+/settings+/_layout.tsx +++ b/web/src/apps/console/routes/_main+/$account+/settings+/_layout.tsx @@ -1,11 +1,18 @@ -import { Outlet, useOutletContext } from '@remix-run/react'; +import { Outlet, useLoaderData, useOutletContext } from '@remix-run/react'; import SidebarLayout from '~/console/components/sidebar-layout'; +import { GQLServerHandler } from '~/console/server/gql/saved-queries'; +import { ensureAccountSet } from '~/console/server/utils/auth-utils'; import { useHandleFromMatches } from '~/root/lib/client/hooks/use-custom-matches'; +import { IExtRemixCtx, LoaderResult } from '~/root/lib/types/common'; +import { IAccountContext } from '../_layout'; const Settings = () => { - const rootContext = useOutletContext(); + // const rootContext = useOutletContext(); + const rootContext = useOutletContext(); const noLayout = useHandleFromMatches('noLayout', null); + const { teamMembers, currentUser } = useLoaderData(); + if (noLayout) { return ; } @@ -20,12 +27,49 @@ const Settings = () => { // { label: 'VPN', value: 'vpn' }, ]} parentPath="/settings" - // headerTitle="Settings" - // headerActions={subNavAction.data} + // headerTitle="Settings" + // headerActions={subNavAction.data} > - + ); }; +export const loader = async (ctx: IExtRemixCtx) => { + const { account } = ctx.params; + ensureAccountSet(ctx); + const { data, errors } = await GQLServerHandler( + ctx.request + ).listMembershipsForAccount({ + accountName: account, + }); + if (errors) { + throw errors[0]; + } + + const { data: currentUser, errors: cErrors } = await GQLServerHandler( + ctx.request + ).whoAmI({}); + + if (cErrors) { + throw cErrors[0]; + } + + return { + teamMembers: data, + currentUser, + }; +}; + +export interface ISettingsContext extends IAccountContext { + teamMembers: LoaderResult['teamMembers']; + currentUser: LoaderResult['currentUser']; +} + export default Settings; diff --git a/web/src/apps/console/routes/_main+/$account+/settings+/general.tsx b/web/src/apps/console/routes/_main+/$account+/settings+/general.tsx index 5be042c33..c1b17df4e 100644 --- a/web/src/apps/console/routes/_main+/$account+/settings+/general.tsx +++ b/web/src/apps/console/routes/_main+/$account+/settings+/general.tsx @@ -1,29 +1,30 @@ -import { Buildings, CopySimple } from '~/console/components/icons'; import { useNavigate, useOutletContext } from '@remix-run/react'; -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { Avatar } from '~/components/atoms/avatar'; import { Button } from '~/components/atoms/button'; import { TextInput } from '~/components/atoms/input'; import { toast } from '~/components/molecule/toast'; +import { Buildings, CopySimple } from '~/console/components/icons'; +import { parseName } from '~/console/server/r-utils/common'; import useClipboard from '~/root/lib/client/hooks/use-clipboard'; import useForm from '~/root/lib/client/hooks/use-form'; import { useUnsavedChanges } from '~/root/lib/client/hooks/use-unsaved-changes'; import { consoleBaseUrl } from '~/root/lib/configs/base-url.cjs'; import Yup from '~/root/lib/server/helpers/yup'; import { handleError } from '~/root/lib/utils/common'; -import { parseName } from '~/console/server/r-utils/common'; -import SecondarySubHeader from '~/console/components/secondary-sub-header'; -import { useConsoleApi } from '~/console/server/gql/api-provider'; -import { ConsoleApiType } from '~/console/server/gql/saved-queries'; import { Box, DeleteContainer, } from '~/console/components/common-console-components'; -import { IAccount } from '~/console/server/gql/queries/account-queries'; import DeleteDialog from '~/console/components/delete-dialog'; +import SecondarySubHeader from '~/console/components/secondary-sub-header'; +import { useConsoleApi } from '~/console/server/gql/api-provider'; +import { IAccount } from '~/console/server/gql/queries/account-queries'; +import { ConsoleApiType } from '~/console/server/gql/saved-queries'; import { useReload } from '~/root/lib/client/helpers/reloader'; import { IAccountContext } from '../_layout'; +import { ISettingsContext } from './_layout'; // import SubNavAction from '../components/sub-nav-action'; // import { useConsoleApi } from '../server/gql/api-provider'; // import { IAccount } from '../server/gql/queries/access-queries'; @@ -59,6 +60,7 @@ export const updateAccount = async ({ const SettingGeneral = () => { const { account } = useOutletContext(); + const { teamMembers, currentUser } = useOutletContext(); const [deleteAccount, setDeleteAccount] = useState(false); const { setHasChanges, resetAndReload } = useUnsavedChanges(); @@ -72,6 +74,12 @@ const SettingGeneral = () => { }, }); + const isOwner = useMemo(() => { + if (!teamMembers || !currentUser) return false; + const owner = teamMembers.find((member) => member.role === 'account_owner'); + return owner?.user?.email === currentUser?.email; + }, [teamMembers, currentUser]); + const { values, handleChange, submit, isLoading, resetValues } = useForm({ initialValues: { displayName: account.displayName, @@ -127,7 +135,7 @@ const SettingGeneral = () => {
} />{' '} -
@@ -135,6 +143,7 @@ const SettingGeneral = () => { label="Account name" value={values.displayName} onChange={handleChange('displayName')} + disabled={!isOwner} />
@@ -213,20 +222,40 @@ const SettingGeneral = () => { { setDeleteAccount(true); }} + content="Disable" + disabled={!isOwner} > - Permanently remove your Account and all of its contents from the - Kloudlite platform. This action is not reversible — please continue - with caution. + For permanent delete or reverse your account and all of its contents + from the Kloudlite platform, please contact us at support@kloudlite.io + Are you sure you want to disable “{parseName(account)} + ”? + + ), + prompt: ( + <> +
Enter the account name
+
+ {' '} + {parseName(account)}{' '} +
+
to continue:
+ + ), + }} onSubmit={async () => { try { const { errors } = await api.deleteAccount({ @@ -237,7 +266,7 @@ const SettingGeneral = () => { throw errors[0]; } reload(); - toast.success(`Account deleted successfully`); + toast.success(`Account disabled successfully`); setDeleteAccount(false); navigate(`/`); } catch (err) { diff --git a/web/src/apps/console/routes/_main+/$account+/settings+/user-management/handle-user.tsx b/web/src/apps/console/routes/_main+/$account+/settings+/user-management/handle-user.tsx index b2ebc1e9e..234c6050a 100644 --- a/web/src/apps/console/routes/_main+/$account+/settings+/user-management/handle-user.tsx +++ b/web/src/apps/console/routes/_main+/$account+/settings+/user-management/handle-user.tsx @@ -1,20 +1,18 @@ import { useOutletContext } from '@remix-run/react'; import { TextInput } from '~/components/atoms/input'; -import SelectPrimitive from '~/components/atoms/select-primitive'; import Popup from '~/components/molecule/popup'; import { toast } from '~/components/molecule/toast'; +import CommonPopupHandle from '~/console/components/common-popup-handle'; import { IDialogBase } from '~/console/components/types.d'; +import { + IMemberType +} from '~/console/routes/_main+/$account+/settings+/user-management/user-access-resource'; import { useConsoleApi } from '~/console/server/gql/api-provider'; +import { parseName } from '~/console/server/r-utils/common'; import useForm from '~/root/lib/client/hooks/use-form'; import Yup from '~/root/lib/server/helpers/yup'; import { handleError } from '~/root/lib/utils/common'; import { Github__Com___Kloudlite___Api___Apps___Iam___Types__Role as Role } from '~/root/src/generated/gql/server'; -import { parseName } from '~/console/server/r-utils/common'; -import CommonPopupHandle from '~/console/components/common-popup-handle'; -import { - IMemberType, - mapRoleToDisplayName, -} from '~/console/routes/_main+/$account+/settings+/user-management/user-access-resource'; import { IAccountContext } from '../../_layout'; type IDialog = IDialogBase; @@ -40,7 +38,8 @@ const Root = (props: IDialog) => { initialValues: isUpdate ? { email: props?.data.email || '', - role: props?.data.role || 'account_member', + // role: props?.data.role || 'account_member', + role: 'account_member', } : { email: '', @@ -98,7 +97,7 @@ const Root = (props: IDialog) => { )}
- { ); })} - + */}
diff --git a/web/src/apps/console/routes/_main+/$account+/settings+/user-management/route.tsx b/web/src/apps/console/routes/_main+/$account+/settings+/user-management/route.tsx index 5a44d1034..1ea3b1a2b 100644 --- a/web/src/apps/console/routes/_main+/$account+/settings+/user-management/route.tsx +++ b/web/src/apps/console/routes/_main+/$account+/settings+/user-management/route.tsx @@ -1,22 +1,23 @@ -import { Plus, SmileySad } from '~/console/components/icons'; import { useOutletContext } from '@remix-run/react'; -import { useCallback, useState } from 'react'; +import { motion } from 'framer-motion'; +import { useCallback, useMemo, useState } from 'react'; import { Button } from '~/components/atoms/button'; +import { dayjs } from '~/components/molecule/dayjs'; import Profile from '~/components/molecule/profile'; +import { useSort } from '~/components/utils'; +import { EmptyState } from '~/console/components/empty-state'; import ExtendedFilledTab from '~/console/components/extended-filled-tab'; +import { Plus, SmileySad } from '~/console/components/icons'; import SecondarySubHeader from '~/console/components/secondary-sub-header'; import Wrapper from '~/console/components/wrapper'; import { useConsoleApi } from '~/console/server/gql/api-provider'; -import { useSearch } from '~/root/lib/client/helpers/search-filter'; -import { ExtractArrayType, NonNullableString } from '~/root/lib/types/common'; -import { EmptyState } from '~/console/components/empty-state'; -import useCustomSwr from '~/root/lib/client/hooks/use-custom-swr'; -import { motion } from 'framer-motion'; import { parseName } from '~/console/server/r-utils/common'; -import { useSort } from '~/components/utils'; -import { dayjs } from '~/components/molecule/dayjs'; import Pulsable from '~/root/lib/client/components/pulsable'; +import { useSearch } from '~/root/lib/client/helpers/search-filter'; +import useCustomSwr from '~/root/lib/client/hooks/use-custom-swr'; +import { ExtractArrayType, NonNullableString } from '~/root/lib/types/common'; import { IAccountContext } from '../../_layout'; +import { ISettingsContext } from '../_layout'; import HandleUser from './handle-user'; import Tools from './tools'; import UserAccessResources from './user-access-resource'; @@ -28,6 +29,7 @@ interface ITeams { sortByProperty: string; sortByTime: string; }; + isOwner?: boolean; } const placeHolderUsers = Array(3) @@ -39,7 +41,12 @@ const placeHolderUsers = Array(3) email: 'sampleuser@gmail.com', })); -const Teams = ({ setShowUserInvite, searchText, sortTeamMembers }: ITeams) => { +const Teams = ({ + setShowUserInvite, + searchText, + sortTeamMembers, + isOwner, +}: ITeams) => { const { account } = useOutletContext(); const api = useConsoleApi(); const { data: teamMembers, isLoading } = useCustomSwr( @@ -134,6 +141,7 @@ const Teams = ({ setShowUserInvite, searchText, sortTeamMembers }: ITeams) => { })) } isPendingInvitation={false} + isOwner={isOwner || false} /> )} @@ -146,6 +154,7 @@ const Invitations = ({ setShowUserInvite, searchText, sortTeamMembers, + isOwner, }: ITeams) => { const { account } = useOutletContext(); const api = useConsoleApi(); @@ -241,6 +250,7 @@ const Invitations = ({ })) } isPendingInvitation + isOwner={isOwner || false} /> @@ -253,7 +263,8 @@ const SettingUserManagement = () => { 'team' | 'invitations' | NonNullableString >('team'); const [visible, setVisible] = useState(false); - const { account } = useOutletContext(); + // const { account } = useOutletContext(); + const { teamMembers, currentUser } = useOutletContext(); const [searchText, setSearchText] = useState(''); @@ -262,21 +273,29 @@ const SettingUserManagement = () => { sortByTime: 'des', }); - const api = useConsoleApi(); + // const api = useConsoleApi(); - const { data: teamMembers, isLoading } = useCustomSwr( - `${parseName(account)}-owners`, - async () => { - return api.listMembershipsForAccount({ - accountName: parseName(account), - }); - } - ); + const isOwner = useMemo(() => { + if (!teamMembers || !currentUser) return false; + const owner = teamMembers.find((member) => member.role === 'account_owner'); + return owner?.user?.email === currentUser?.email; + }, [teamMembers, currentUser]); + + // const { data: teamMembers, isLoading } = useCustomSwr( + // `${parseName(account)}-owners`, + // async () => { + // return api.listMembershipsForAccount({ + // accountName: parseName(account), + // }); + // } + // ); + + // const owners = useCallback( + // () => teamMembers?.filter((i) => i.role === 'account_owner') || [], + // [teamMembers] + // )(); - const owners = useCallback( - () => teamMembers?.filter((i) => i.role === 'account_owner') || [], - [teamMembers] - )(); + const accountOwner = teamMembers?.find((i) => i.role === 'account_owner'); return (
@@ -284,18 +303,30 @@ const SettingUserManagement = () => { setVisible(true)} - /> + isOwner && ( +
@@ -328,9 +359,9 @@ const SettingUserManagement = () => { value={active} onChange={setActive} items={[ - { label: 'Team member', to: 'team-member', value: 'team' }, + { label: 'Team members', to: 'team-member', value: 'team' }, { - label: 'Pending invitation', + label: 'Pending invitations', to: 'pending-invitation', value: 'invitations', }, @@ -348,12 +379,14 @@ const SettingUserManagement = () => { setShowUserInvite={setVisible} searchText={searchText} sortTeamMembers={sortByProperty} + isOwner={isOwner} /> ) : ( )}
diff --git a/web/src/apps/console/routes/_main+/$account+/settings+/user-management/user-access-resource.tsx b/web/src/apps/console/routes/_main+/$account+/settings+/user-management/user-access-resource.tsx index ebc36c13f..fbaed561f 100644 --- a/web/src/apps/console/routes/_main+/$account+/settings+/user-management/user-access-resource.tsx +++ b/web/src/apps/console/routes/_main+/$account+/settings+/user-management/user-access-resource.tsx @@ -1,26 +1,24 @@ -import { PencilSimple, Trash } from '~/console/components/icons'; import { useOutletContext } from '@remix-run/react'; import { useState } from 'react'; import { Avatar } from '~/components/atoms/avatar'; import { toast } from '~/components/molecule/toast'; import { titleCase } from '~/components/utils'; import { - ListBody, ListItemV2, - ListTitle, ListTitleV2, } from '~/console/components/console-list-components'; import DeleteDialog from '~/console/components/delete-dialog'; +import { Trash } from '~/console/components/icons'; import List from '~/console/components/list'; import ListGridView from '~/console/components/list-grid-view'; import ResourceExtraAction, { IResourceExtraItem, } from '~/console/components/resource-extra-action'; +import HandleUser from '~/console/routes/_main+/$account+/settings+/user-management/handle-user'; import { useConsoleApi } from '~/console/server/gql/api-provider'; +import { parseName } from '~/console/server/r-utils/common'; import { useReload } from '~/root/lib/client/helpers/reloader'; import { handleError } from '~/root/lib/utils/common'; -import { parseName } from '~/console/server/r-utils/common'; -import HandleUser from '~/console/routes/_main+/$account+/settings+/user-management/handle-user'; import { IAccountContext } from '../../_layout'; const RESOURCE_NAME = 'user'; @@ -59,7 +57,7 @@ export const mapRoleToDisplayName = (role: string): string => { }; const ExtraButton = ({ onAction, item, isInvite }: IExtraButton) => { - let items: IResourceExtraItem[] = [ + const items: IResourceExtraItem[] = [ { label: 'Remove', icon: , @@ -69,18 +67,18 @@ const ExtraButton = ({ onAction, item, isInvite }: IExtraButton) => { className: '!text-text-critical', }, ]; - if (!isInvite) { - items = [ - { - label: 'Edit', - icon: , - type: 'item', - onClick: () => onAction({ action: 'edit', item }), - key: 'edit', - }, - ...items, - ]; - } + // if (!isInvite) { + // items = [ + // { + // label: 'Edit', + // icon: , + // type: 'item', + // onClick: () => onAction({ action: 'edit', item }), + // key: 'edit', + // }, + // ...items, + // ]; + // } return ; }; @@ -88,9 +86,10 @@ interface IResource { items: BaseType[]; onAction: OnAction; isInvite: boolean; + isOwner: boolean; } -const ListView = ({ items = [], onAction, isInvite }: IResource) => { +const ListView = ({ items = [], onAction, isInvite, isOwner }: IResource) => { return ( {items.map((item) => ( @@ -118,13 +117,19 @@ const ListView = ({ items = [], onAction, isInvite }: IResource) => { }, { key: 3, - render: () => ( - - ), + render: () => { + if (item.role === 'account_owner') return null; + if (isOwner) { + return ( + + ); + } + return null; + }, }, ]} /> @@ -136,12 +141,14 @@ const ListView = ({ items = [], onAction, isInvite }: IResource) => { const UserAccessResources = ({ items = [], isPendingInvitation = false, + isOwner, }: { items: BaseType[]; isPendingInvitation: boolean; + isOwner: boolean; }) => { const [showDeleteDialog, setShowDeleteDialog] = useState( - null, + null ); const [showUserInvite, setShowUserInvite] = useState(null); @@ -153,6 +160,7 @@ const UserAccessResources = ({ const props: IResource = { items, isInvite: isPendingInvitation, + isOwner, onAction: ({ action, item }) => { switch (action) { case 'edit': diff --git a/web/src/apps/console/routes/_main+/_layout/_layout.tsx b/web/src/apps/console/routes/_main+/_layout/_layout.tsx index 9610b8a58..0f764bca3 100644 --- a/web/src/apps/console/routes/_main+/_layout/_layout.tsx +++ b/web/src/apps/console/routes/_main+/_layout/_layout.tsx @@ -65,7 +65,7 @@ export type IConsoleRootContext = { export const meta = (c: IRemixCtx) => { return [ - { title: `Account ${constants.metadot} ${c.params?.account || ''}` }, + { title: `Team ${constants.metadot} ${c.params?.account || ''}` }, { name: 'theme-color', content: LightTitlebarColor }, ]; }; @@ -143,9 +143,9 @@ const AccountTabs = () => { }; const Logo = () => { - const { account } = useParams(); + // const { account } = useParams(); return ( - + ); @@ -171,11 +171,10 @@ const ProfileMenu = ({ hideProfileName }: { hideProfileName: boolean }) => {
- {!hideProfileName ? ( - - ) : ( - - )} +