diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..e69de29bb diff --git a/lib/app-setup/root.tsx b/lib/app-setup/root.tsx index 696bf1f5f..7c22f3087 100644 --- a/lib/app-setup/root.tsx +++ b/lib/app-setup/root.tsx @@ -32,6 +32,7 @@ import tailwindBase from '~/design-system/tailwind-base.js'; import { ReloadIndicator } from '~/lib/client/components/reload-indicator'; import { isDev } from '~/lib/client/helpers/log'; import { getClientEnv, getServerEnv } from '../configs/base-url.cjs'; +import { useDataFromMatches } from '../client/hooks/use-custom-matches'; export const links: LinksFunction = () => [ { rel: 'stylesheet', href: stylesUrl }, @@ -167,6 +168,8 @@ const Root = ({ }) => { const env = useLoaderData(); + const error = useDataFromMatches('error', ''); + return ( @@ -235,9 +238,13 @@ const Root = ({ - - - + {error ? ( +
{JSON.stringify(error)}
+ ) : ( + + + + )} diff --git a/lib/client/components/logger/index.tsx b/lib/client/components/logger/index.tsx index 85bd5c3ea..f85dd9a34 100644 --- a/lib/client/components/logger/index.tsx +++ b/lib/client/components/logger/index.tsx @@ -669,7 +669,7 @@ const LogComp = ({ } }, [fullScreen]); - const { logs, subscribed, errors } = useSocketLogs(websocket); + const { logs, subscribed, errors, isLoading } = useSocketLogs(websocket); const [isClientSide, setIsClientSide] = useState(false); @@ -744,7 +744,7 @@ const LogComp = ({ )} - {!subscribed && logs.length === 0 && } + {isLoading && } {errors.length ? (
{JSON.stringify(errors)}
diff --git a/lib/client/helpers/log.ts b/lib/client/helpers/log.ts index 0e18c6651..ed4525f45 100644 --- a/lib/client/helpers/log.ts +++ b/lib/client/helpers/log.ts @@ -1,7 +1,4 @@ -// import axios from 'axios'; -// import { consoleBaseUrl } from '../../configs/base-url.cjs'; import { serverError } from '../../server/helpers/server-error'; -// import { parseError } from '../../utils/common'; const getNodeEnv = () => { const env = (() => { @@ -19,43 +16,6 @@ const getNodeEnv = () => { return 'development'; }; -/* eslint-disable no-unused-vars */ -/* eslint-disable @typescript-eslint/no-unused-vars */ -export const PostErr = async (message: string, source: string) => { - // try { - // await axios.post( - // 'https://hooks.slack.com/services/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', - // { - // body: { - // channel: source === 'server' ? '#bugs' : '#web-errors', - // username: - // typeof window === 'undefined' ? 'server-error' : 'web-error', - // text: message, - // icon_emoji: ':ghost:', - // }, - // } - // ); - // } catch (err) { - // console.log(parseError(err).message); - // } - return {}; -}; - -const PostToHook = (message: string) => { - // if (typeof window === 'undefined') { - // return PostErr(message, 'server'); - // } - // - // try { - // axios.post(`${consoleBaseUrl}/api/error`, { - // body: { error: message }, - // }); - // } catch (err) { - // console.log(err); - // } - return {}; -}; - export const isDev = getNodeEnv() === 'development'; const logger = { @@ -63,8 +23,6 @@ const logger = { timeEnd: isDev ? console.timeEnd : () => {}, log: isDev ? console.log : () => {}, - // log: console.log, - warn: console.warn, trace: (...args: any[]) => { let err; @@ -90,14 +48,16 @@ const logger = { } if (err) { - console.log(err); if (!isDev) { - PostToHook(`\`\`\`${err}\`\`\``); + console.trace(`\n\n${args}\n\n`); + return; } - } else { - console.trace(args); + console.error(`\n\n${err}\n\n`); + return; } + console.trace(`\n\n${args}\n\n`); + if (isDev && typeof window === 'undefined') { serverError(args); } diff --git a/lib/client/helpers/socket/context.tsx b/lib/client/helpers/socket/context.tsx index e4d3859be..07c66fecb 100644 --- a/lib/client/helpers/socket/context.tsx +++ b/lib/client/helpers/socket/context.tsx @@ -62,8 +62,8 @@ export const useSubscribe = ( const { sendMsg, responses, - infos: i, - errors: e, + infos: mInfos, + errors: mErrors, clear, } = useContext(Context); @@ -85,9 +85,10 @@ export const useSubscribe = ( const m = msg[k]; tr.push(...(responses[m.for]?.[m.data.id || 'default'] || [])); - terr.push(...(e[m.for]?.[m.data.id || 'default'] || [])); - ti.push(...(i[m.for]?.[m.data.id || 'default'] || [])); + terr.push(...(mErrors[m.for]?.[m.data.id || 'default'] || [])); + ti.push(...(mInfos[m.for]?.[m.data.id || 'default'] || [])); } + setResp(tr); setErrors(terr); setInfos(ti); @@ -97,16 +98,19 @@ export const useSubscribe = ( } return; } + const tempResp = responses[msg.for]?.[msg.data.id || 'default'] || []; + setResp(tempResp); + + setErrors(mErrors[msg.for]?.[msg.data.id || 'default'] || []); - setResp(responses[msg.for]?.[msg.data.id || 'default'] || []); - setErrors(e[msg.for]?.[msg.data.id || 'default'] || []); - setInfos(i[msg.for]?.[msg.data.id || 'default'] || []); + const tempInfo = mInfos[msg.for]?.[msg.data.id || 'default'] || []; + setInfos(tempInfo); - if (resp.length || i[msg.for]?.[msg.data.id || 'default']?.length) { + if (tempResp.length || tempInfo.length) { setSubscribed(true); } })(); - }, [responses]); + }, [responses, mInfos, mErrors]); useDebounce( () => { @@ -236,7 +240,7 @@ export const SockProvider = ({ children }: ChildrenProps) => { }; w.onerror = (e) => { - console.error(e); + console.error('socket closed:', e); if (!rejected) { rejected = true; rej(e); diff --git a/lib/client/helpers/socket/useSockLogs.tsx b/lib/client/helpers/socket/useSockLogs.tsx index 81e28e5cc..b1831166f 100644 --- a/lib/client/helpers/socket/useSockLogs.tsx +++ b/lib/client/helpers/socket/useSockLogs.tsx @@ -11,7 +11,7 @@ interface IuseLog { export const useSocketLogs = ({ account, cluster, trackingId }: IuseLog) => { const [logs, setLogs] = useState[]>([]); - const { responses, subscribed, errors } = useSubscribe( + const { responses, infos, subscribed, errors } = useSubscribe( { for: 'logs', data: { @@ -26,16 +26,6 @@ export const useSocketLogs = ({ account, cluster, trackingId }: IuseLog) => { [] ); - const [isLoading, setIsLoading] = useState(true); - - useEffect(() => { - if (subscribed && isLoading) { - setIsLoading(false); - } else if (!subscribed && !isLoading) { - setIsLoading(true); - } - }, []); - useEffect(() => { const sorted = responses.sort((a, b) => { const resp = b.data.podName.localeCompare(a.data.podName); @@ -55,7 +45,7 @@ export const useSocketLogs = ({ account, cluster, trackingId }: IuseLog) => { return { logs, errors, - isLoading, + isLoading: !subscribed && (logs.length === 0 || infos.length === 0), subscribed, }; }; diff --git a/lib/client/hooks/use-custom-loader-data.tsx b/lib/client/hooks/use-custom-loader-data.tsx new file mode 100644 index 000000000..40ca49199 --- /dev/null +++ b/lib/client/hooks/use-custom-loader-data.tsx @@ -0,0 +1,10 @@ +import { useLoaderData } from '@remix-run/react'; + +type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ( + k: infer I +) => void + ? I + : never; + +export const useExtLoaderData = Promise>() => + useLoaderData() as UnionToIntersection>>; diff --git a/lib/server/helpers/execute-query-with-context.ts b/lib/server/helpers/execute-query-with-context.ts index 91f24e7da..43eea48a6 100644 --- a/lib/server/helpers/execute-query-with-context.ts +++ b/lib/server/helpers/execute-query-with-context.ts @@ -112,12 +112,12 @@ export const ExecuteQueryWithContext = ( return { ...resp.data, data }; } catch (err) { if ((err as AxiosError).response) { - console.trace('ErrorIn:', apiName, (err as Error).name); + console.log('\nErrorIn:', apiName, (err as Error).name, '\n'); return (err as AxiosError).response?.data; } - console.trace('ErrorIn:', apiName, (err as Error).message); + console.log('\nErrorIn:', apiName, (err as Error).message, '\n'); return { data: null, diff --git a/lib/utils/common.tsx b/lib/utils/common.tsx index fb71ad287..38469880d 100644 --- a/lib/utils/common.tsx +++ b/lib/utils/common.tsx @@ -1,10 +1,27 @@ import { toast } from '~/components/molecule/toast'; import logger from '../client/helpers/log'; -export const handleError = (e: unknown): void => { +export const handleError = ( + e: unknown +): { + error?: { + message: string; + }; +} => { const err = e as Error; + + if (typeof window === 'undefined') { + return { + error: { + message: err.message, + }, + }; + } + toast.error(err.message); logger.error(e); + + return {}; }; export const parseError = (e: unknown): Error => { @@ -27,8 +44,9 @@ export const Truncate = ({ }; export function sleep(time: number) { - // eslint-disable-next-line no-promise-executor-return - return new Promise((resolve) => setTimeout(resolve, time)); + return new Promise((resolve) => { + setTimeout(resolve, time); + }); } export const anyUndefined: any = undefined; diff --git a/src/apps/auth/routes/_main+/logout.tsx b/src/apps/auth/routes/_main+/logout.tsx index 6d06933d7..ecea7e1a8 100644 --- a/src/apps/auth/routes/_main+/logout.tsx +++ b/src/apps/auth/routes/_main+/logout.tsx @@ -1,19 +1,34 @@ -import { getCookie } from '~/root/lib/app-setup/cookies'; -import withContext from '~/root/lib/app-setup/with-contxt'; -import { useNavigate, useLoaderData } from '@remix-run/react'; -import { useEffect } from 'react'; +import { useNavigate } from '@remix-run/react'; import { BrandLogo } from '~/components/branding/brand-logo'; -import { IExtRemixCtx } from '~/root/lib/types/common'; +import { handleError, sleep } from '~/root/lib/utils/common'; +import { useAuthApi } from '~/auth/server/gql/api-provider'; +import { toast } from 'react-toastify'; +import useDebounce from '~/root/lib/client/hooks/use-debounce'; const LogoutPage = () => { const navigate = useNavigate(); - const { done } = useLoaderData(); + const api = useAuthApi(); - useEffect(() => { - if (done) { - navigate('/'); - } - }, [done]); + useDebounce( + () => { + (async () => { + try { + const { errors } = await api.logout({}); + if (errors) { + throw errors[0]; + } + + toast.warn('Logged out successfully'); + await sleep(1000); + navigate('/login'); + } catch (error) { + handleError(error); + } + })(); + }, + 1000, + [] + ); return (
@@ -22,21 +37,4 @@ const LogoutPage = () => { ); }; -export const loader = async (ctx: IExtRemixCtx) => { - const cookie = getCookie(ctx); - - const keys = Object.keys(cookie.getAll()); - - for (let i = 0; i < keys.length; i += 1) { - const key = keys[i]; - if (key === 'hotspot-session') { - cookie.remove(key); - } - } - - return withContext(ctx, { - done: 'true', - }); -}; - export default LogoutPage; diff --git a/src/apps/auth/routes/_main+/verify-email.tsx b/src/apps/auth/routes/_main+/verify-email.tsx index c34b6e724..b85459f96 100644 --- a/src/apps/auth/routes/_main+/verify-email.tsx +++ b/src/apps/auth/routes/_main+/verify-email.tsx @@ -46,6 +46,7 @@ const VerifyEmail = () => { (async () => { try { if (!email) { + // TODO: handle this case, by taking email from user toast.error('Something went wrong! Please try again.'); return; } @@ -137,8 +138,9 @@ export const loader = async (ctx: IRemixCtx) => { const query = getQueries(ctx); const { data, errors } = await GQLServerHandler(ctx.request).whoAmI(); if (errors) { - console.error(errors[0].message); - return redirect('/'); + return { + query, + }; } const { email, verified } = data || {}; diff --git a/src/apps/console/routes/_main+/$account+/settings+/user-management/handle-user.tsx b/src/apps/console/routes/_main+/$account+/settings+/user-management/handle-user.tsx index f38daa433..b2ebc1e9e 100644 --- a/src/apps/console/routes/_main+/$account+/settings+/user-management/handle-user.tsx +++ b/src/apps/console/routes/_main+/$account+/settings+/user-management/handle-user.tsx @@ -3,42 +3,55 @@ 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 { IDialog } from '~/console/components/types.d'; +import { IDialogBase } from '~/console/components/types.d'; import { useConsoleApi } from '~/console/server/gql/api-provider'; 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; + const validRoles = (role: string): Role => { - switch (role as Role) { + switch (role) { case 'account_owner': - case 'account_admin': + return 'account_owner' as Role; case 'account_member': - return role as Role; + return 'account_member' as Role; default: throw new Error(`invalid role ${role}`); } }; -const HandleUser = ({ show, setShow }: IDialog) => { +const Root = (props: IDialog) => { + const { setVisible, isUpdate } = props; const api = useConsoleApi(); const { account } = useOutletContext(); - const { values, handleChange, handleSubmit, resetValues, isLoading } = - useForm({ - initialValues: { - email: '', - role: 'account_member', - }, - validationSchema: Yup.object({ - email: Yup.string().required().email(), - }), - onSubmit: async (val) => { - try { + const { values, handleChange, handleSubmit, isLoading } = useForm({ + initialValues: isUpdate + ? { + email: props?.data.email || '', + role: props?.data.role || 'account_member', + } + : { + email: '', + role: 'account_member', + }, + validationSchema: Yup.object({ + email: Yup.string().required().email(), + }), + onSubmit: async (val) => { + try { + if (!isUpdate) { const { errors: e } = await api.inviteMembersForAccount({ accountName: parseName(account), invitations: { @@ -49,68 +62,81 @@ const HandleUser = ({ show, setShow }: IDialog) => { if (e) { throw e[0]; } - toast.success('user invited'); - setShow(null); - } catch (err) { - handleError(err); + } else if (isUpdate) { + const { errors: e } = await api.updateAccountMembership({ + accountName: parseName(account), + memberId: props?.data.id, + role: validRoles(val.role), + }); + if (e) { + throw e[0]; + } } - }, - }); + toast.success(`user ${isUpdate ? 'role updated' : 'invited'}`); + setVisible(false); + } catch (err) { + handleError(err); + } + }, + }); - const roles: Role[] = ['account_owner', 'account_admin', 'account_member']; + const roles: string[] = ['account_owner', 'account_member']; return ( - { - if (!e) { - resetValues(); - } - - setShow(e); - }} - > - Invite user -
- -
-
+ + +
+
+ {!isUpdate ? ( -
- - - - -- not-selected -- - - {roles.map((role) => { - return ( - - {role} - - ); - })} - + ) : ( + + )}
-
- - - - - - + + + + -- not-selected -- + + {roles.map((role) => { + return ( + + {mapRoleToDisplayName(role)} + + ); + })} + +
+ + + + + + + ); +}; + +const HandleUser = (props: IDialog) => { + return ( + ); }; diff --git a/src/apps/console/routes/_main+/$account+/settings+/user-management/route.tsx b/src/apps/console/routes/_main+/$account+/settings+/user-management/route.tsx index 55c77fd00..c46fc7e8c 100644 --- a/src/apps/console/routes/_main+/$account+/settings+/user-management/route.tsx +++ b/src/apps/console/routes/_main+/$account+/settings+/user-management/route.tsx @@ -5,10 +5,8 @@ import { Button } from '~/components/atoms/button'; import Profile from '~/components/molecule/profile'; import ExtendedFilledTab from '~/console/components/extended-filled-tab'; import SecondarySubHeader from '~/console/components/secondary-sub-header'; -import { IShowDialog } from '~/console/components/types.d'; import Wrapper from '~/console/components/wrapper'; import { useConsoleApi } from '~/console/server/gql/api-provider'; -import { DIALOG_DATA_NONE } from '~/console/utils/commons'; import { useSearch } from '~/root/lib/client/helpers/search-filter'; import { NonNullableString } from '~/root/lib/types/common'; import Pulsable from '~/console/components/pulsable'; @@ -22,7 +20,7 @@ import Tools from './tools'; import UserAccessResources from './user-access-resource'; interface ITeams { - setShowUserInvite: React.Dispatch>; + setShowUserInvite: React.Dispatch>; searchText: string; } @@ -78,7 +76,7 @@ const Teams = ({ setShowUserInvite, searchText }: ITeams) => { content: 'Invite users', prefix: , onClick: () => { - setShowUserInvite(DIALOG_DATA_NONE); + setShowUserInvite(true); }, }, }} @@ -105,6 +103,7 @@ const Teams = ({ setShowUserInvite, searchText }: ITeams) => { email: i.user.email, })) } + isPendingInvitation={false} /> )} @@ -159,7 +158,7 @@ const Invitations = ({ setShowUserInvite, searchText }: ITeams) => { content: 'Invite users', prefix: , onClick: () => { - setShowUserInvite(DIALOG_DATA_NONE); + setShowUserInvite(true); }, }, }} @@ -178,6 +177,7 @@ const Invitations = ({ setShowUserInvite, searchText }: ITeams) => { id: i.id || i.userEmail || '', })) } + isPendingInvitation /> @@ -189,7 +189,7 @@ const SettingUserManagement = () => { const [active, setActive] = useState< 'team' | 'invitations' | NonNullableString >('team'); - const [showUserInvite, setShowUserInvite] = useState(null); + const [visible, setVisible] = useState(false); const { account } = useOutletContext(); const [searchText, setSearchText] = useState(''); @@ -219,7 +219,7 @@ const SettingUserManagement = () => {
{active === 'team' ? ( - + ) : ( - + )}
- + ); }; diff --git a/src/apps/console/routes/_main+/$account+/settings+/user-management/user-access-resource.tsx b/src/apps/console/routes/_main+/$account+/settings+/user-management/user-access-resource.tsx index 22a64d7af..dda01e1a7 100644 --- a/src/apps/console/routes/_main+/$account+/settings+/user-management/user-access-resource.tsx +++ b/src/apps/console/routes/_main+/$account+/settings+/user-management/user-access-resource.tsx @@ -1,4 +1,4 @@ -import { Trash } from '@jengaicons/react'; +import { PencilSimple, Trash } from '@jengaicons/react'; import { useOutletContext } from '@remix-run/react'; import { useState } from 'react'; import { Avatar } from '~/components/atoms/avatar'; @@ -11,11 +11,14 @@ import { import DeleteDialog from '~/console/components/delete-dialog'; import List from '~/console/components/list'; import ListGridView from '~/console/components/list-grid-view'; -import ResourceExtraAction from '~/console/components/resource-extra-action'; +import ResourceExtraAction, { + IResourceExtraItem, +} from '~/console/components/resource-extra-action'; import { useConsoleApi } from '~/console/server/gql/api-provider'; 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'; @@ -26,30 +29,66 @@ type BaseType = { role: string; email: string; }; +export type IMemberType = BaseType; + +type OnAction = ({ + action, + item, +}: { + action: 'delete' | 'edit'; + item: BaseType; +}) => void; + +type IExtraButton = { + onAction: OnAction; + item: BaseType; + isInvite: boolean; +}; + +export const mapRoleToDisplayName = (role: string): string => { + switch (role) { + case 'account_owner': + return 'owner'; + case 'account_member': + return 'member'; + default: + return role; + } +}; + +const ExtraButton = ({ onAction, item, isInvite }: IExtraButton) => { + let items: IResourceExtraItem[] = [ + { + label: 'Remove', + icon: , + type: 'item', + onClick: () => onAction({ action: 'delete', item }), + key: 'remove', + className: '!text-text-critical', + }, + ]; + if (!isInvite) { + items = [ + { + label: 'Edit', + icon: , + type: 'item', + onClick: () => onAction({ action: 'edit', item }), + key: 'edit', + }, + ...items, + ]; + } + return ; +}; interface IResource { items: BaseType[]; - onDelete: (item: BaseType) => void; + onAction: OnAction; + isInvite: boolean; } -const ExtraButton = ({ onDelete }: { onDelete: () => void }) => { - return ( - , - type: 'item', - onClick: onDelete, - key: 'remove', - className: '!text-text-critical', - }, - ]} - /> - ); -}; - -const ListView = ({ items = [], onDelete }: IResource) => { +const ListView = ({ items = [], onAction, isInvite }: IResource) => { return ( {items.map((item) => ( @@ -70,11 +109,17 @@ const ListView = ({ items = [], onDelete }: IResource) => { }, { key: 2, - render: () => , + render: () => , }, { key: 3, - render: () => onDelete(item)} />, + render: () => ( + + ), }, ]} /> @@ -83,10 +128,17 @@ const ListView = ({ items = [], onDelete }: IResource) => { ); }; -const UserAccessResources = ({ items = [] }: { items: BaseType[] }) => { +const UserAccessResources = ({ + items = [], + isPendingInvitation = false, +}: { + items: BaseType[]; + isPendingInvitation: boolean; +}) => { const [showDeleteDialog, setShowDeleteDialog] = useState( null ); + const [showUserInvite, setShowUserInvite] = useState(null); const { account } = useOutletContext(); @@ -95,16 +147,35 @@ const UserAccessResources = ({ items = [] }: { items: BaseType[] }) => { const props: IResource = { items, - onDelete: (item) => { - setShowDeleteDialog(item); + isInvite: isPendingInvitation, + onAction: ({ action, item }) => { + switch (action) { + case 'edit': + setShowUserInvite(item); + break; + case 'delete': + setShowDeleteDialog(item); + break; + default: + break; + } }, }; + return ( <> } gridView={} /> + setShowUserInvite(null), + visible: !!showUserInvite, + }} + /> { setShow={setShowDeleteDialog} onSubmit={async () => { try { - const { errors } = await api.deleteAccountMembership({ - accountName: parseName(account), - memberId: showDeleteDialog!.id, - }); - if (errors) { - throw errors[0]; + if (!isPendingInvitation) { + const { errors } = await api.deleteAccountMembership({ + accountName: parseName(account), + memberId: showDeleteDialog!.id, + }); + if (errors) { + throw errors[0]; + } + } else if (isPendingInvitation) { + const { errors } = await api.deleteAccountInvitation({ + accountName: parseName(account), + invitationId: showDeleteDialog!.id, + }); + if (errors) { + throw errors[0]; + } } reloadPage(); toast.success(`${titleCase(RESOURCE_NAME)} deleted successfully`);