From 77f6c45ab32ed5291b8b5f7eabbac66f12fca80b Mon Sep 17 00:00:00 2001 From: nl_0 Date: Thu, 23 May 2024 14:26:03 +0200 Subject: [PATCH 01/69] utils/GlobalDialogs --- catalog/app/utils/Dialogs.tsx | 9 +++++++-- catalog/app/utils/GlobalDialogs.tsx | 29 +++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 catalog/app/utils/GlobalDialogs.tsx diff --git a/catalog/app/utils/Dialogs.tsx b/catalog/app/utils/Dialogs.tsx index 243a2f7dec9..3b78a955ecc 100644 --- a/catalog/app/utils/Dialogs.tsx +++ b/catalog/app/utils/Dialogs.tsx @@ -6,7 +6,7 @@ import type { Resolver } from 'utils/defer' type DialogState = 'open' | 'closing' | 'closed' -type ExtraDialogProps = Omit +export type ExtraDialogProps = Omit export type Close = [R] extends [never] ? () => void : Resolver['resolve'] @@ -24,12 +24,15 @@ export const useDialogs = () => { const open = React.useCallback( (render: Render, props?: ExtraDialogProps) => { + if (state !== 'closed') { + throw new Error('Another dialog is already open') + } const { resolver, promise } = defer() setDialog({ render, props, resolver }) setState('open') return promise }, - [setDialog, setState], + [state, setDialog, setState], ) const close = React.useCallback( @@ -65,3 +68,5 @@ export const useDialogs = () => { export const use = useDialogs export type Dialogs = ReturnType + +export type Open = Dialogs['open'] diff --git a/catalog/app/utils/GlobalDialogs.tsx b/catalog/app/utils/GlobalDialogs.tsx new file mode 100644 index 00000000000..eac15ce8045 --- /dev/null +++ b/catalog/app/utils/GlobalDialogs.tsx @@ -0,0 +1,29 @@ +import invariant from 'invariant' +import * as React from 'react' + +import * as Dialogs from 'utils/Dialogs' + +export type { Open, Close, ExtraDialogProps } from 'utils/Dialogs' + +const Ctx = React.createContext(null) + +export function useOpenDialog() { + const open = React.useContext(Ctx) + invariant(open, 'useOpenDialog() must be used within ') + return open +} + +export { useOpenDialog as use } + +export default function WithGlobalDialogs({ + children, + ...props +}: React.PropsWithChildren) { + const dialogs = Dialogs.use() + return ( + + {children} + {dialogs.render(props)} + + ) +} From e6d68ea75376f71b6449b32393bd76f75a8077e4 Mon Sep 17 00:00:00 2001 From: nl_0 Date: Thu, 23 May 2024 14:26:45 +0200 Subject: [PATCH 02/69] add global dialogs provider --- catalog/app/app.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/catalog/app/app.tsx b/catalog/app/app.tsx index 4caf240de51..0e23af5bc87 100644 --- a/catalog/app/app.tsx +++ b/catalog/app/app.tsx @@ -39,6 +39,7 @@ import * as APIConnector from 'utils/APIConnector' import * as GraphQL from 'utils/GraphQL' import { BucketCacheProvider } from 'utils/BucketCache' import GlobalAPI from 'utils/GlobalAPI' +import WithGlobalDialogs from 'utils/GlobalDialogs' import log from 'utils/Logging' import * as NamedRoutes from 'utils/NamedRoutes' import { PFSCookieManager } from 'utils/PFSCookieManager' @@ -123,6 +124,7 @@ const render = () => { AWS.Athena.Provider, AWS.S3.Provider, Notifications.WithNotifications, + WithGlobalDialogs, Errors.ErrorBoundary, BucketCacheProvider, PFSCookieManager, From b46c5b9b4f8a63b7327710306f469f3ea7674f50 Mon Sep 17 00:00:00 2001 From: nl_0 Date: Wed, 22 May 2024 12:54:23 +0200 Subject: [PATCH 03/69] gql: hack for passing optimistic responses to mutations --- catalog/app/utils/GraphQL/wrappers.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/catalog/app/utils/GraphQL/wrappers.ts b/catalog/app/utils/GraphQL/wrappers.ts index 15c12c58fff..d445be94383 100644 --- a/catalog/app/utils/GraphQL/wrappers.ts +++ b/catalog/app/utils/GraphQL/wrappers.ts @@ -229,13 +229,14 @@ export function useQueryS( throw err } -interface RunMutationContext extends Partial { +interface RunMutationContext extends Partial { silent?: boolean + optimisticResponse?: Data } type RunMutation = ( variables?: Variables, - context?: RunMutationContext, + context?: RunMutationContext, ) => Promise /** @@ -258,9 +259,14 @@ export function useMutation( const [, execMutation] = urql.useMutation(query) return React.useCallback( - async (variables?: Variables, context?: RunMutationContext) => { - const { silent = false, ...ctx } = context || {} - const result = await execMutation(variables, ctx) + async (variables?: Variables, context?: RunMutationContext) => { + // XXX: probably this logic for optimistic responses is not necessary + const { silent = false, optimisticResponse, ...ctx } = context || {} + const computedVariables = { + __optimisticResponse: optimisticResponse, + ...variables, + } as Variables + const result = await execMutation(computedVariables, ctx) if (!result.data) { const err = result.error || new MutationError(result) From 8717bf1db9169425d900ce8b84bbf72345282278 Mon Sep 17 00:00:00 2001 From: nl_0 Date: Wed, 22 May 2024 12:55:58 +0200 Subject: [PATCH 04/69] update gql schema and generated stuff --- catalog/app/model/graphql/schema.generated.ts | 431 ++++++++++++++++++ catalog/app/model/graphql/types.generated.ts | 85 ++++ shared/graphql/schema.graphql | 54 +++ 3 files changed, 570 insertions(+) diff --git a/catalog/app/model/graphql/schema.generated.ts b/catalog/app/model/graphql/schema.generated.ts index daad77b1026..56911ce6145 100644 --- a/catalog/app/model/graphql/schema.generated.ts +++ b/catalog/app/model/graphql/schema.generated.ts @@ -82,6 +82,44 @@ export default { ], interfaces: [], }, + { + kind: 'OBJECT', + name: 'AdminMutations', + fields: [ + { + name: 'user', + type: { + kind: 'NON_NULL', + ofType: { + kind: 'OBJECT', + name: 'UserAdminMutations', + ofType: null, + }, + }, + args: [], + }, + ], + interfaces: [], + }, + { + kind: 'OBJECT', + name: 'AdminQueries', + fields: [ + { + name: 'user', + type: { + kind: 'NON_NULL', + ofType: { + kind: 'OBJECT', + name: 'UserAdminQueries', + ofType: null, + }, + }, + args: [], + }, + ], + interfaces: [], + }, { kind: 'OBJECT', name: 'BooleanPackageUserMetaFacet', @@ -1311,6 +1349,130 @@ export default { ], interfaces: [], }, + { + kind: 'OBJECT', + name: 'MutateUserAdminMutations', + fields: [ + { + name: 'delete', + type: { + kind: 'NON_NULL', + ofType: { + kind: 'UNION', + name: 'OperationResult', + ofType: null, + }, + }, + args: [], + }, + { + name: 'setEmail', + type: { + kind: 'NON_NULL', + ofType: { + kind: 'UNION', + name: 'UserResult', + ofType: null, + }, + }, + args: [ + { + name: 'email', + type: { + kind: 'NON_NULL', + ofType: { + kind: 'SCALAR', + name: 'String', + ofType: null, + }, + }, + }, + ], + }, + { + name: 'setRole', + type: { + kind: 'NON_NULL', + ofType: { + kind: 'UNION', + name: 'UserResult', + ofType: null, + }, + }, + args: [ + { + name: 'roleName', + type: { + kind: 'SCALAR', + name: 'String', + ofType: null, + }, + }, + ], + }, + { + name: 'setAdmin', + type: { + kind: 'NON_NULL', + ofType: { + kind: 'UNION', + name: 'UserResult', + ofType: null, + }, + }, + args: [ + { + name: 'admin', + type: { + kind: 'NON_NULL', + ofType: { + kind: 'SCALAR', + name: 'Boolean', + ofType: null, + }, + }, + }, + ], + }, + { + name: 'setActive', + type: { + kind: 'NON_NULL', + ofType: { + kind: 'UNION', + name: 'UserResult', + ofType: null, + }, + }, + args: [ + { + name: 'active', + type: { + kind: 'NON_NULL', + ofType: { + kind: 'SCALAR', + name: 'Boolean', + ofType: null, + }, + }, + }, + ], + }, + { + name: 'resetPassword', + type: { + kind: 'NON_NULL', + ofType: { + kind: 'UNION', + name: 'OperationResult', + ofType: null, + }, + }, + args: [], + }, + ], + interfaces: [], + }, { kind: 'OBJECT', name: 'Mutation', @@ -1427,6 +1589,18 @@ export default { }, ], }, + { + name: 'admin', + type: { + kind: 'NON_NULL', + ofType: { + kind: 'OBJECT', + name: 'AdminMutations', + ofType: null, + }, + }, + args: [], + }, { name: 'bucketAdd', type: { @@ -2232,6 +2406,24 @@ export default { ], interfaces: [], }, + { + kind: 'UNION', + name: 'OperationResult', + possibleTypes: [ + { + kind: 'OBJECT', + name: 'Ok', + }, + { + kind: 'OBJECT', + name: 'InvalidInput', + }, + { + kind: 'OBJECT', + name: 'OperationError', + }, + ], + }, { kind: 'OBJECT', name: 'Package', @@ -3611,6 +3803,18 @@ export default { }, args: [], }, + { + name: 'admin', + type: { + kind: 'NON_NULL', + ofType: { + kind: 'OBJECT', + name: 'AdminQueries', + ofType: null, + }, + }, + args: [], + }, { name: 'policies', type: { @@ -4751,6 +4955,233 @@ export default { ], interfaces: [], }, + { + kind: 'OBJECT', + name: 'User', + fields: [ + { + name: 'name', + type: { + kind: 'NON_NULL', + ofType: { + kind: 'SCALAR', + name: 'String', + ofType: null, + }, + }, + args: [], + }, + { + name: 'email', + type: { + kind: 'NON_NULL', + ofType: { + kind: 'SCALAR', + name: 'String', + ofType: null, + }, + }, + args: [], + }, + { + name: 'dateJoined', + type: { + kind: 'NON_NULL', + ofType: { + kind: 'SCALAR', + name: 'Datetime', + ofType: null, + }, + }, + args: [], + }, + { + name: 'lastLogin', + type: { + kind: 'NON_NULL', + ofType: { + kind: 'SCALAR', + name: 'Datetime', + ofType: null, + }, + }, + args: [], + }, + { + name: 'isActive', + type: { + kind: 'NON_NULL', + ofType: { + kind: 'SCALAR', + name: 'Boolean', + ofType: null, + }, + }, + args: [], + }, + { + name: 'isAdmin', + type: { + kind: 'NON_NULL', + ofType: { + kind: 'SCALAR', + name: 'Boolean', + ofType: null, + }, + }, + args: [], + }, + { + name: 'isSsoOnly', + type: { + kind: 'NON_NULL', + ofType: { + kind: 'SCALAR', + name: 'Boolean', + ofType: null, + }, + }, + args: [], + }, + { + name: 'isService', + type: { + kind: 'NON_NULL', + ofType: { + kind: 'SCALAR', + name: 'Boolean', + ofType: null, + }, + }, + args: [], + }, + { + name: 'role', + type: { + kind: 'UNION', + name: 'Role', + ofType: null, + }, + args: [], + }, + ], + interfaces: [], + }, + { + kind: 'OBJECT', + name: 'UserAdminMutations', + fields: [ + { + name: 'create', + type: { + kind: 'NON_NULL', + ofType: { + kind: 'UNION', + name: 'UserResult', + ofType: null, + }, + }, + args: [ + { + name: 'input', + type: { + kind: 'NON_NULL', + ofType: { + kind: 'SCALAR', + name: 'Any', + }, + }, + }, + ], + }, + { + name: 'mutate', + type: { + kind: 'OBJECT', + name: 'MutateUserAdminMutations', + ofType: null, + }, + args: [ + { + name: 'name', + type: { + kind: 'NON_NULL', + ofType: { + kind: 'SCALAR', + name: 'String', + ofType: null, + }, + }, + }, + ], + }, + ], + interfaces: [], + }, + { + kind: 'OBJECT', + name: 'UserAdminQueries', + fields: [ + { + name: 'list', + type: { + kind: 'NON_NULL', + ofType: { + kind: 'LIST', + ofType: { + kind: 'NON_NULL', + ofType: { + kind: 'OBJECT', + name: 'User', + ofType: null, + }, + }, + }, + }, + args: [], + }, + { + name: 'get', + type: { + kind: 'OBJECT', + name: 'User', + ofType: null, + }, + args: [ + { + name: 'name', + type: { + kind: 'NON_NULL', + ofType: { + kind: 'SCALAR', + name: 'String', + ofType: null, + }, + }, + }, + ], + }, + ], + interfaces: [], + }, + { + kind: 'UNION', + name: 'UserResult', + possibleTypes: [ + { + kind: 'OBJECT', + name: 'User', + }, + { + kind: 'OBJECT', + name: 'InvalidInput', + }, + { + kind: 'OBJECT', + name: 'OperationError', + }, + ], + }, { kind: 'SCALAR', name: 'Any', diff --git a/catalog/app/model/graphql/types.generated.ts b/catalog/app/model/graphql/types.generated.ts index ae98dee3755..805e5f366e5 100644 --- a/catalog/app/model/graphql/types.generated.ts +++ b/catalog/app/model/graphql/types.generated.ts @@ -36,6 +36,16 @@ export interface AccessCounts { readonly counts: ReadonlyArray } +export interface AdminMutations { + readonly __typename: 'AdminMutations' + readonly user: UserAdminMutations +} + +export interface AdminQueries { + readonly __typename: 'AdminQueries' + readonly user: UserAdminQueries +} + export interface BooleanPackageUserMetaFacet extends IPackageUserMetaFacet { readonly __typename: 'BooleanPackageUserMetaFacet' readonly path: Scalars['String'] @@ -309,11 +319,38 @@ export interface ManagedRoleInput { readonly policies: ReadonlyArray } +export interface MutateUserAdminMutations { + readonly __typename: 'MutateUserAdminMutations' + readonly delete: OperationResult + readonly setEmail: UserResult + readonly setRole: UserResult + readonly setAdmin: UserResult + readonly setActive: UserResult + readonly resetPassword: OperationResult +} + +export interface MutateUserAdminMutationssetEmailArgs { + email: Scalars['String'] +} + +export interface MutateUserAdminMutationssetRoleArgs { + roleName: Maybe +} + +export interface MutateUserAdminMutationssetAdminArgs { + admin: Scalars['Boolean'] +} + +export interface MutateUserAdminMutationssetActiveArgs { + active: Scalars['Boolean'] +} + export interface Mutation { readonly __typename: 'Mutation' readonly packageConstruct: PackageConstructResult readonly packagePromote: PackagePromoteResult readonly packageRevisionDelete: PackageRevisionDeleteResult + readonly admin: AdminMutations readonly bucketAdd: BucketAddResult readonly bucketUpdate: BucketUpdateResult readonly bucketRemove: BucketRemoveResult @@ -504,6 +541,8 @@ export interface OperationError { readonly context: Maybe } +export type OperationResult = Ok | InvalidInput | OperationError + export interface Package { readonly __typename: 'Package' readonly bucket: Scalars['String'] @@ -768,6 +807,7 @@ export interface Query { readonly searchMoreObjects: ObjectsSearchMoreResult readonly searchMorePackages: PackagesSearchMoreResult readonly subscription: SubscriptionState + readonly admin: AdminQueries readonly policies: ReadonlyArray readonly policy: Maybe readonly roles: ReadonlyArray @@ -1059,3 +1099,48 @@ export interface UnmanagedRoleInput { readonly name: Scalars['String'] readonly arn: Scalars['String'] } + +export interface User { + readonly __typename: 'User' + readonly name: Scalars['String'] + readonly email: Scalars['String'] + readonly dateJoined: Scalars['Datetime'] + readonly lastLogin: Scalars['Datetime'] + readonly isActive: Scalars['Boolean'] + readonly isAdmin: Scalars['Boolean'] + readonly isSsoOnly: Scalars['Boolean'] + readonly isService: Scalars['Boolean'] + readonly role: Maybe +} + +export interface UserAdminMutations { + readonly __typename: 'UserAdminMutations' + readonly create: UserResult + readonly mutate: Maybe +} + +export interface UserAdminMutationscreateArgs { + input: UserInput +} + +export interface UserAdminMutationsmutateArgs { + name: Scalars['String'] +} + +export interface UserAdminQueries { + readonly __typename: 'UserAdminQueries' + readonly list: ReadonlyArray + readonly get: Maybe +} + +export interface UserAdminQueriesgetArgs { + name: Scalars['String'] +} + +export interface UserInput { + readonly name: Scalars['String'] + readonly email: Scalars['String'] + readonly roleName: Maybe +} + +export type UserResult = User | InvalidInput | OperationError diff --git a/shared/graphql/schema.graphql b/shared/graphql/schema.graphql index e04433be231..022a26de908 100644 --- a/shared/graphql/schema.graphql +++ b/shared/graphql/schema.graphql @@ -37,6 +37,8 @@ type Ok { _: Boolean } +union OperationResult = Ok | InvalidInput | OperationError + type Unavailable { _: Boolean } @@ -189,6 +191,18 @@ type RoleBucketPermission implements BucketPermission { level: BucketPermissionLevel! } +type User { + name: String! + email: String! + dateJoined: Datetime! + lastLogin: Datetime! + isActive: Boolean! + isAdmin: Boolean! + isSsoOnly: Boolean! + isService: Boolean! + role: Role +} + type AccessCountForDate { date: Datetime! value: Int! @@ -477,6 +491,15 @@ type SubscriptionState { timestamp: Datetime! } +type UserAdminQueries { + list: [User!]! + get(name: String!): User +} + +type AdminQueries { + user: UserAdminQueries! +} + type Query { config: Config! bucketConfigs: [BucketConfig!]! @@ -498,6 +521,9 @@ type Query { searchMoreObjects(after: String!, size: Int = 30): ObjectsSearchMoreResult! searchMorePackages(after: String!, size: Int = 30): PackagesSearchMoreResult! subscription: SubscriptionState! + + admin: AdminQueries! @admin + policies: [Policy!]! @admin policy(id: ID!): Policy @admin roles: [Role!]! @admin @@ -792,6 +818,32 @@ union BrowsingSessionRefreshResult = union BrowsingSessionDisposeResult = Ok | OperationError +input UserInput { + name: String! + email: String! + roleName: String +} + +union UserResult = User | InvalidInput | OperationError + +type MutateUserAdminMutations { + delete: OperationResult! + setEmail(email: String!): UserResult! + setRole(roleName: String): UserResult! + setAdmin(admin: Boolean!): UserResult! + setActive(active: Boolean!): UserResult! + resetPassword: OperationResult! +} + +type UserAdminMutations { + create(input: UserInput!): UserResult! + mutate(name: String!): MutateUserAdminMutations +} + +type AdminMutations { + user: UserAdminMutations! +} + type Mutation { packageConstruct( params: PackagePushParams! @@ -807,6 +859,8 @@ type Mutation { hash: String! ): PackageRevisionDeleteResult! + admin: AdminMutations! @admin + bucketAdd(input: BucketAddInput!): BucketAddResult! @admin bucketUpdate(name: String!, input: BucketUpdateInput!): BucketUpdateResult! @admin From 844b01cc32186aee441b92b53bd38fc41158d2a3 Mon Sep 17 00:00:00 2001 From: nl_0 Date: Wed, 22 May 2024 12:56:56 +0200 Subject: [PATCH 05/69] gql: keys and updaters for newly added types --- catalog/app/utils/GraphQL/Provider.tsx | 30 ++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/catalog/app/utils/GraphQL/Provider.tsx b/catalog/app/utils/GraphQL/Provider.tsx index b03a302c816..3a3729135c1 100644 --- a/catalog/app/utils/GraphQL/Provider.tsx +++ b/catalog/app/utils/GraphQL/Provider.tsx @@ -16,6 +16,7 @@ const devtools = process.env.NODE_ENV === 'development' ? [DevTools.devtoolsExch const BUCKET_CONFIGS_QUERY = urql.gql`{ bucketConfigs { name } }` const POLICIES_QUERY = urql.gql`{ policies { id } }` const ROLES_QUERY = urql.gql`{ roles { id } }` +const USERS_QUERY = urql.gql`{ admin { user { list { name } } } }` const DEFAULT_ROLE_QUERY = urql.gql`{ defaultRole { id } }` function handlePackageCreation(result: any, cache: GraphCache.Cache) { @@ -110,6 +111,7 @@ export default function GraphQLProvider({ children }: React.PropsWithChildren<{} Status: () => null, StatusReport: (r) => (typeof r.timestamp === 'string' ? r.timestamp : null), StatusReportList: () => null, + Unavailable: () => null, SubscriptionState: () => null, TestStats: () => null, TestStatsTimeSeries: () => null, @@ -130,6 +132,12 @@ export default function GraphQLProvider({ children }: React.PropsWithChildren<{} PackagesSearchResultSet: () => null, InvalidInput: () => null, InputError: () => null, + User: (u) => (u.name as string) ?? null, + AdminQueries: () => null, + UserAdminQueries: () => null, + AdminMutations: () => null, + UserAdminMutations: () => null, + MutateUserAdminMutations: () => null, }, updates: { Mutation: { @@ -305,8 +313,30 @@ export default function GraphQLProvider({ children }: React.PropsWithChildren<{} packagePromote: (result, _vars, cache) => { handlePackageCreation(result.packagePromote, cache) }, + admin: (result: any, _vars, cache, info) => { + // XXX: newer versions of GraphCache support updaters on arbitrary types + if (result.admin?.user?.create?.__typename === 'User') { + // Add created User to user list + // XXX: sort? + const addUser = R.append(result.admin.user.create) + cache.updateQuery( + { query: USERS_QUERY }, + R.evolve({ admin: { user: { list: addUser } } }), + ) + } + if (result.admin?.user?.mutate?.delete?.__typename === 'Ok') { + // XXX: handle "user not found" somehow? + // Remove deleted User from user list + const rmUser = R.reject(R.propEq('name', info.variables.name)) + cache.updateQuery( + { query: USERS_QUERY }, + R.evolve({ admin: { user: { list: rmUser } } }), + ) + } + }, }, }, + // XXX: make an exchange for handling optimistic responses optimistic: { bucketRemove: () => ({ __typename: 'BucketRemoveSuccess' }), roleDelete: () => ({ __typename: 'RoleDeleteSuccess' }), From b4c146bdb3fcfe75e10d450d134296544ed3b241 Mon Sep 17 00:00:00 2001 From: nl_0 Date: Wed, 22 May 2024 12:58:57 +0200 Subject: [PATCH 06/69] admin/Users: TSify, use GraphQL queries/mutations --- catalog/app/containers/Admin/Users/Users.js | 753 ---------------- catalog/app/containers/Admin/Users/Users.tsx | 851 ++++++++++++++++++ .../Admin/Users/gql/UserCreate.generated.ts | 104 +++ .../Admin/Users/gql/UserCreate.graphql | 11 + .../Admin/Users/gql/UserDelete.generated.ts | 176 ++++ .../Admin/Users/gql/UserDelete.graphql | 24 + .../gql/UserResultSelection.generated.ts | 103 +++ .../Users/gql/UserResultSelection.graphql | 19 + .../Users/gql/UserSelection.generated.ts | 86 ++ .../Admin/Users/gql/UserSelection.graphql | 22 + .../Users/gql/UserSetActive.generated.ts | 136 +++ .../Admin/Users/gql/UserSetActive.graphql | 13 + .../Admin/Users/gql/UserSetAdmin.generated.ts | 136 +++ .../Admin/Users/gql/UserSetAdmin.graphql | 13 + .../Admin/Users/gql/UserSetEmail.generated.ts | 136 +++ .../Admin/Users/gql/UserSetEmail.graphql | 13 + .../Admin/Users/gql/UserSetRole.generated.ts | 136 +++ .../Admin/Users/gql/UserSetRole.graphql | 13 + ...{Roles.generated.ts => Users.generated.ts} | 75 +- .../gql/{Roles.graphql => Users.graphql} | 11 + .../{UsersAndRoles.js => UsersAndRoles.tsx} | 9 +- catalog/app/containers/Admin/data.js | 24 - .../containers/Admin/{index.js => index.ts} | 0 23 files changed, 2069 insertions(+), 795 deletions(-) delete mode 100644 catalog/app/containers/Admin/Users/Users.js create mode 100644 catalog/app/containers/Admin/Users/Users.tsx create mode 100644 catalog/app/containers/Admin/Users/gql/UserCreate.generated.ts create mode 100644 catalog/app/containers/Admin/Users/gql/UserCreate.graphql create mode 100644 catalog/app/containers/Admin/Users/gql/UserDelete.generated.ts create mode 100644 catalog/app/containers/Admin/Users/gql/UserDelete.graphql create mode 100644 catalog/app/containers/Admin/Users/gql/UserResultSelection.generated.ts create mode 100644 catalog/app/containers/Admin/Users/gql/UserResultSelection.graphql create mode 100644 catalog/app/containers/Admin/Users/gql/UserSelection.generated.ts create mode 100644 catalog/app/containers/Admin/Users/gql/UserSelection.graphql create mode 100644 catalog/app/containers/Admin/Users/gql/UserSetActive.generated.ts create mode 100644 catalog/app/containers/Admin/Users/gql/UserSetActive.graphql create mode 100644 catalog/app/containers/Admin/Users/gql/UserSetAdmin.generated.ts create mode 100644 catalog/app/containers/Admin/Users/gql/UserSetAdmin.graphql create mode 100644 catalog/app/containers/Admin/Users/gql/UserSetEmail.generated.ts create mode 100644 catalog/app/containers/Admin/Users/gql/UserSetEmail.graphql create mode 100644 catalog/app/containers/Admin/Users/gql/UserSetRole.generated.ts create mode 100644 catalog/app/containers/Admin/Users/gql/UserSetRole.graphql rename catalog/app/containers/Admin/Users/gql/{Roles.generated.ts => Users.generated.ts} (56%) rename catalog/app/containers/Admin/Users/gql/{Roles.graphql => Users.graphql} (59%) rename catalog/app/containers/Admin/{UsersAndRoles.js => UsersAndRoles.tsx} (61%) delete mode 100644 catalog/app/containers/Admin/data.js rename catalog/app/containers/Admin/{index.js => index.ts} (100%) diff --git a/catalog/app/containers/Admin/Users/Users.js b/catalog/app/containers/Admin/Users/Users.js deleted file mode 100644 index fc39586cae4..00000000000 --- a/catalog/app/containers/Admin/Users/Users.js +++ /dev/null @@ -1,753 +0,0 @@ -import cx from 'classnames' -import * as FF from 'final-form' -import * as R from 'ramda' -import * as React from 'react' -import * as RF from 'react-final-form' -import * as M from '@material-ui/core' - -import * as Pagination from 'components/Pagination' -import * as Notifications from 'containers/Notifications' -import * as APIConnector from 'utils/APIConnector' -import * as Dialogs from 'utils/Dialogs' -import { useQueryS } from 'utils/GraphQL' -import * as Cache from 'utils/ResourceCache' -import * as Format from 'utils/format' -import * as validators from 'utils/validators' - -import * as Form from '../Form' -import * as Table from '../Table' -import * as data from '../data' - -import ROLES_QUERY from './gql/Roles.generated' - -const useMonoStyles = M.makeStyles((t) => ({ - root: { - fontFamily: t.typography.monospace.fontFamily, - }, -})) - -function Mono({ className, children }) { - const classes = useMonoStyles() - return {children} -} - -const useInviteStyles = M.makeStyles({ - infoIcon: { - fontSize: '1.25em', - verticalAlign: '-3px', - }, -}) - -// close: PT.func.isRequired, -// roles: PT.array.isRequired, -function Invite({ close, roles, defaultRoleId }) { - const classes = useInviteStyles() - const req = APIConnector.use() - const cache = Cache.use() - const { push } = Notifications.use() - const onSubmit = React.useCallback( - async ({ username, email, roleId }) => { - const role = roles.find((r) => r.id === roleId) - - try { - await req({ - endpoint: '/users/create', - method: 'POST', - body: JSON.stringify({ username, email }), - }) - - const user = { - dateJoined: new Date(), - email, - isActive: true, - isAdmin: false, - lastLogin: new Date(), - username, - } - - try { - await req({ - method: 'POST', - endpoint: '/users/set_role', - body: JSON.stringify({ username, role: role.name }), - }) - user.roleId = role.id - } catch (e) { - // eslint-disable-next-line no-console - console.error('Error setting role', { username, role }) - // eslint-disable-next-line no-console - console.dir(e) - } - - cache.patchOk(data.UsersResource, null, R.append(user)) - push('User invited') - close() - } catch (e) { - if (APIConnector.HTTPError.is(e, 400, /subscription is invalid/)) { - return { [FF.FORM_ERROR]: 'subscriptionInvalid' } - } - - if (APIConnector.HTTPError.is(e, 400, /Username is invalid/)) { - return { - username: 'invalid', - } - } - if (APIConnector.HTTPError.is(e, 409, /Username already taken/)) { - return { - username: 'taken', - } - } - if (APIConnector.HTTPError.is(e, 400, /Invalid email/)) { - return { - email: 'invalid', - } - } - if (APIConnector.HTTPError.is(e, 409, /Email already taken/)) { - return { - email: 'taken', - } - } - if (APIConnector.HTTPError.is(e, 500, /SMTP.*invalid/)) { - return { - [FF.FORM_ERROR]: 'smtp', - } - } - // eslint-disable-next-line no-console - console.error('Error creating user') - // eslint-disable-next-line no-console - console.dir(e) - return { - [FF.FORM_ERROR]: 'unexpected', - } - } - }, - [req, cache, push, close, roles], - ) - - return ( - - {({ - handleSubmit, - submitting, - submitError, - submitFailed, - error, - hasSubmitErrors, - hasValidationErrors, - modifiedSinceLastSubmit, - }) => ( - <> - Invite a user - -
- - Enter a valid username{' '} - - info - - - ), - }} - autoComplete="off" - /> - - - {roles.map((r) => ( - - {r.name} - - ))} - - {(!!error || !!submitError) && ( - - )} - - -
- - close('cancel')} - color="primary" - disabled={submitting} - > - Cancel - - - Invite - - - - )} -
- ) -} - -// close: PT.func.isRequired, -// user: PT.object.isRequired, -function Edit({ close, user: { email: oldEmail, username } }) { - const req = APIConnector.use() - const cache = Cache.use() - const { push } = Notifications.use() - - const onSubmit = React.useCallback( - async ({ email }) => { - if (email === oldEmail) { - close() - return - } - - try { - await req({ - endpoint: '/users/edit_email', - method: 'POST', - body: JSON.stringify({ username, email }), - }) - - cache.patchOk( - data.UsersResource, - null, - R.map((u) => (u.username === username ? { ...u, email } : u)), - ) - push('Changes saved') - close() - } catch (e) { - if (APIConnector.HTTPError.is(e, 400, /Another user already has that email/)) { - return { - email: 'taken', - } - } - if (APIConnector.HTTPError.is(e, 400, /Invalid email/)) { - return { - email: 'invalid', - } - } - // eslint-disable-next-line no-console - console.error('Error changing email') - // eslint-disable-next-line no-console - console.dir(e) - return { - [FF.FORM_ERROR]: 'unexpected', - } - } - }, - [close, username, oldEmail, req, cache, push], - ) - - return ( - - {({ - handleSubmit, - submitting, - submitFailed, - error, - hasSubmitErrors, - hasValidationErrors, - modifiedSinceLastSubmit, - }) => ( - <> - Edit user: "{username}" - -
- - {submitFailed && ( - - )} - - -
- - close('cancel')} - color="primary" - disabled={submitting} - > - Cancel - - - Save - - - - )} -
- ) -} - -// user: PT.object.isRequired, -// close: PT.func.isRequired, -function Delete({ user, close }) { - const req = APIConnector.use() - const cache = Cache.use() - const { push } = Notifications.use() - const doDelete = React.useCallback(() => { - close() - req({ - endpoint: '/users/delete', - method: 'POST', - body: JSON.stringify({ username: user.username }), - }) - .then(() => { - push(`User "${user.username}" deleted`) - }) - .catch((e) => { - // TODO: handle errors once the endpoint is working - cache.patchOk(data.UsersResource, null, R.append(user)) - push(`Error deleting user "${user.username}"`) - // eslint-disable-next-line no-console - console.error('Error deleting user') - // eslint-disable-next-line no-console - console.dir(e) - }) - // optimistically remove the user from cache - cache.patchOk(data.UsersResource, null, R.reject(R.propEq('username', user.username))) - }, [user, close, req, cache, push]) - - return ( - <> - Delete a user - - You are about to delete user "{user.username}". This operation is - irreversible. - - - close('cancel')} color="primary"> - Cancel - - - Delete - - - - ) -} - -// admin: PT.bool.isRequired, -// username: PT.string.isRequired, -// close: PT.func.isRequired, -function AdminRights({ username, admin, close }) { - const req = APIConnector.use() - const cache = Cache.use() - const { push } = Notifications.use() - const doChange = React.useCallback( - () => - close( - req({ - method: 'POST', - endpoint: `/users/${admin ? 'grant' : 'revoke'}_admin`, - body: JSON.stringify({ username }), - }) - .then(() => { - cache.patchOk( - data.UsersResource, - null, - R.map((u) => (u.username === username ? { ...u, isAdmin: admin } : u)), - ) - return 'ok' - }) - .catch((e) => { - push( - `Error ${admin ? 'granting' : 'revoking'} admin status for "${username}"`, - ) - // eslint-disable-next-line no-console - console.error('Error changing user admin status', { username, admin }) - // eslint-disable-next-line no-console - console.dir(e) - throw e - }), - ), - [admin, close, username, req, cache, push], - ) - - return ( - <> - {admin ? 'Grant' : 'Revoke'} admin rights - - You are about to {admin ? 'grant admin rights to' : 'revoke admin rights from'}{' '} - "{username}". - - - close('cancel')} color="primary"> - Cancel - - - {admin ? 'Grant' : 'Revoke'} - - - - ) -} - -const useUsernameStyles = M.makeStyles((t) => ({ - root: { - alignItems: 'center', - display: 'flex', - }, - admin: { - fontWeight: 600, - }, - icon: { - fontSize: '1em', - marginLeft: `calc(-1em - ${t.spacing(0.5)}px)`, - marginRight: t.spacing(0.5), - }, -})) - -// admin: PT.bool, -function Username({ className, admin = false, children, ...props }) { - const classes = useUsernameStyles() - return ( - - {admin && security} - {children} - - ) -} - -function Editable({ value, onChange, children }) { - const [busy, setBusy] = React.useState(false) - const [savedValue, saveValue] = React.useState(value) - const change = React.useCallback( - (newValue) => { - if (savedValue === newValue) return - if (busy) return - setBusy(true) - saveValue(newValue) - Promise.resolve(onChange(newValue)) - .then(() => { - setBusy(false) - }) - .catch((e) => { - saveValue(savedValue) - setBusy(false) - throw e - }) - }, - [onChange, busy, setBusy, savedValue, saveValue], - ) - - return children({ change, busy, value: savedValue }) -} - -// not a valid role name -const emptyRole = '' - -function UsersSkeleton() { - return ( - - - - - ) -} - -// users: PT.object.isRequired, -export default function Users({ users }) { - const rows = Cache.suspend(users) - const { roles, defaultRole } = useQueryS(ROLES_QUERY) - const defaultRoleId = defaultRole?.id - - const req = APIConnector.use() - const cache = Cache.use() - const { push } = Notifications.use() - const dialogs = Dialogs.use() - const { open: openDialog } = dialogs - - const setRole = React.useCallback( - (username, role) => - req({ - method: 'POST', - endpoint: '/users/set_role', - body: JSON.stringify({ username, role }), - }) - .then(() => { - cache.patchOk( - data.UsersResource, - null, - R.map((u) => (u.username === username ? { ...u, role } : u)), - ) - }) - .catch((e) => { - push(`Error changing role for "${username}"`) - // eslint-disable-next-line no-console - console.error('Error chaging role', { username, role }) - // eslint-disable-next-line no-console - console.dir(e) - throw e - }), - [req, cache, push], - ) - - const setIsActive = React.useCallback( - (username, active) => - req({ - method: 'POST', - endpoint: `/users/${active ? 'enable' : 'disable'}`, - body: JSON.stringify({ username }), - }) - .then(() => { - cache.patchOk( - data.UsersResource, - null, - R.map((u) => (u.username === username ? { ...u, isActive: active } : u)), - ) - }) - .catch((e) => { - push(`Error ${active ? 'enabling' : 'disabling'} "${username}"`) - // eslint-disable-next-line no-console - console.error('Error (de)activating user', { username, active }) - // eslint-disable-next-line no-console - console.dir(e) - throw e - }), - [req, cache, push], - ) - - const columns = React.useMemo( - () => [ - { - id: 'isActive', - label: 'Enabled', - getValue: R.prop('isActive'), - getDisplay: (v, u) => ( - setIsActive(u.username, active)}> - {({ change, busy, value }) => ( - change(e.target.checked)} - disabled={busy} - color="default" - /> - )} - - ), - }, - { - id: 'username', - label: 'Username', - getValue: R.prop('username'), - getDisplay: (v, u) => {v}, - props: { component: 'th', scope: 'row' }, - }, - { - id: 'email', - label: 'Email', - getValue: R.prop('email'), - }, - { - id: 'role', - label: 'Role', - getValue: (u) => u.roleId && (roles.find((r) => r.id === u.roleId) || {}).name, - getDisplay: (v, u) => ( - setRole(u.username, role)}> - {({ change, busy, value }) => ( - change(e.target.value)} - disabled={busy} - renderValue={R.identity} - > - {roles.map((r) => ( - - {r.name} - - ))} - - )} - - ), - }, - { - id: 'dateJoined', - label: 'Date joined', - getValue: R.prop('dateJoined'), - getDisplay: (v) => ( - - - - ), - }, - { - id: 'lastLogin', - label: 'Last login', - getValue: R.prop('lastLogin'), - getDisplay: (v) => ( - - - - ), - }, - { - id: 'isAdmin', - label: 'Admin', - hint: 'Admins can see this page, add/remove users, and make/remove admins', - getValue: R.prop('isAdmin'), - getDisplay: (v, u) => ( - { - const res = await openDialog(({ close }) => ( - - )) - if (res !== 'ok') throw new Error('cancelled') - }} - > - {({ change, busy, value }) => ( - change(e.target.checked)} - disabled={busy} - color="default" - /> - )} - - ), - }, - ], - [roles, openDialog, setIsActive, setRole], - ) - - const filtering = Table.useFiltering({ - rows, - filterBy: ({ email, username }) => email + username, - }) - const ordering = Table.useOrdering({ - rows: filtering.filtered, - column: columns[0], - }) - const pagination = Pagination.use(ordering.ordered, { - getItemId: R.prop('username'), - }) - - const toolbarActions = [ - { - title: 'Invite', - icon: add, - fn: React.useCallback(() => { - openDialog(({ close }) => ) - }, [roles, defaultRoleId, openDialog]), - }, - ] - - const inlineActions = (user) => [ - { - title: 'Delete', - icon: delete, - fn: () => { - dialogs.open(({ close }) => ) - }, - }, - { - title: 'Edit', - icon: edit, - fn: () => { - dialogs.open(({ close }) => ) - }, - }, - ] - - return ( - }> - - {dialogs.render({ maxWidth: 'xs', fullWidth: true })} - - - - - - - - {pagination.paginated.map((i) => ( - - {columns.map((col) => ( - - {(col.getDisplay || R.identity)(col.getValue(i), i)} - - ))} - - - - - ))} - - - - - - - ) -} diff --git a/catalog/app/containers/Admin/Users/Users.tsx b/catalog/app/containers/Admin/Users/Users.tsx new file mode 100644 index 00000000000..7767ca564cb --- /dev/null +++ b/catalog/app/containers/Admin/Users/Users.tsx @@ -0,0 +1,851 @@ +import cx from 'classnames' +import * as FF from 'final-form' +import * as R from 'ramda' +import * as React from 'react' +import * as redux from 'react-redux' +import * as RF from 'react-final-form' +import * as M from '@material-ui/core' + +import * as Pagination from 'components/Pagination' +import * as Notifications from 'containers/Notifications' +import * as Auth from 'containers/Auth' +import * as Dialogs from 'utils/GlobalDialogs' +import * as GQL from 'utils/GraphQL' +import assertNever from 'utils/assertNever' +import * as Format from 'utils/format' +import * as validators from 'utils/validators' + +import * as Form from '../Form' +import * as Table from '../Table' + +import USERS_QUERY from './gql/Users.generated' +import USER_CREATE_MUTATION from './gql/UserCreate.generated' +import USER_DELETE_MUTATION from './gql/UserDelete.generated' +import USER_SET_EMAIL_MUTATION from './gql/UserSetEmail.generated' +import USER_SET_ROLE_MUTATION from './gql/UserSetRole.generated' +import USER_SET_ACTIVE_MUTATION from './gql/UserSetActive.generated' +import USER_SET_ADMIN_MUTATION from './gql/UserSetAdmin.generated' + +import { UserSelectionFragment as User } from './gql/UserSelection.generated' + +type Role = GQL.DataForDoc['roles'][number] + +const DIALOG_PROPS: Dialogs.ExtraDialogProps = { maxWidth: 'xs', fullWidth: true } + +const useMonoStyles = M.makeStyles((t) => ({ + root: { + fontFamily: t.typography.monospace.fontFamily, + }, +})) + +interface MonoProps { + className?: string + children: React.ReactNode +} + +function Mono({ className, children }: MonoProps) { + const classes = useMonoStyles() + return {children} +} + +const useInviteStyles = M.makeStyles({ + infoIcon: { + fontSize: '1.25em', + verticalAlign: '-3px', + }, +}) + +interface InviteProps { + close: () => void + roles: readonly Role[] + defaultRoleName: string | undefined +} + +function Invite({ close, roles, defaultRoleName }: InviteProps) { + const classes = useInviteStyles() + const create = GQL.useMutation(USER_CREATE_MUTATION) + const { push } = Notifications.use() + + const onSubmit = React.useCallback( + async ({ username, email, roleName }) => { + try { + // XXX: use formspec to convert/validate form values into gql input? + const input = { + name: username, + email, + roleName, + } + const data = await create({ input }) + const r = data.admin.user.create + switch (r.__typename) { + case 'User': + close() + push('User invited') + return + case 'OperationError': + switch (r.name) { + case 'SubscriptionInvalid': + return { [FF.FORM_ERROR]: 'subscriptionInvalid' } + case 'MailSendError': + return { [FF.FORM_ERROR]: 'smtp' } + case 'Conflict': + if (r.message.match(/Email already taken/)) { + return { email: 'taken' } + } + if (r.message.match(/Username already taken/)) { + return { username: 'taken' } + } + } + throw new Error(`Unexpected operation error: [${r.name}] ${r.message}`) + case 'InvalidInput': + const errors: Record = {} + r.errors.forEach((e) => { + switch (e.path) { + case 'input.name': + // e.name should be one of: + // - InvalidUserNameError + // - ReservedUserNameError + errors.username = 'invalid' + break + case 'input.email': + errors.email = 'invalid' + break + default: + throw new Error( + `Unexpected input error at '${e.path}': [${e.name}] ${e.message}`, + ) + } + }) + return errors + default: + return assertNever(r) + } + } catch (e) { + // eslint-disable-next-line no-console + console.error('Error creating user') + // eslint-disable-next-line no-console + console.dir(e) + // TODO: send to sentry? + return { [FF.FORM_ERROR]: 'unexpected' } + } + }, + [create, push, close], + ) + + return ( + + {({ + handleSubmit, + submitting, + submitError, + submitFailed, + error, + hasSubmitErrors, + hasValidationErrors, + modifiedSinceLastSubmit, + }) => ( + <> + Invite a user + +
+ } + label="Username" + fullWidth + margin="normal" + errors={{ + required: 'Enter a username', + taken: 'Username already taken', + invalid: ( + <> + Enter a valid username{' '} + + info + + + ), + }} + autoComplete="off" + /> + } + label="Email" + fullWidth + margin="normal" + errors={{ + required: 'Enter an email', + taken: 'Email already taken', + invalid: 'Enter a valid email', + }} + autoComplete="off" + /> + + {roles.map((r) => ( + + {r.name} + + ))} + + {(!!error || !!submitError) && ( + + )} + + +
+ + + Cancel + + + Invite + + + + )} +
+ ) +} + +interface EditProps { + close: () => void + user: User +} + +function Edit({ close, user: { email: oldEmail, name } }: EditProps) { + const { push } = Notifications.use() + const setEmail = GQL.useMutation(USER_SET_EMAIL_MUTATION) + + const onSubmit = React.useCallback( + async ({ email }) => { + if (email === oldEmail) { + close() + return + } + + try { + const data = await setEmail({ name, email }) + const r = data.admin.user.mutate?.setEmail + if (!r) { + throw new Error() + } + // TODO: switch result + switch (r.__typename) { + // if (APIConnector.HTTPError.is(e, 400, /Another user already has that email/)) { + // return { + // email: 'taken', + // } + // } + // if (APIConnector.HTTPError.is(e, 400, /Invalid email/)) { + // return { + // email: 'invalid', + // } + // } + default: + } + + close() + push('Changes saved') + } catch (e) { + // eslint-disable-next-line no-console + console.error('Error changing email') + // eslint-disable-next-line no-console + console.dir(e) + return { + [FF.FORM_ERROR]: 'unexpected', + } + } + }, + [close, name, oldEmail, setEmail, push], + ) + + return ( + + {({ + handleSubmit, + submitting, + submitFailed, + error, + hasSubmitErrors, + hasValidationErrors, + modifiedSinceLastSubmit, + }) => ( + <> + Edit user: "{name}" + +
+ } + label="Email" + fullWidth + margin="normal" + errors={{ + required: 'Enter an email', + taken: 'Email already taken', + invalid: 'Enter a valid email', + }} + autoComplete="off" + /> + {submitFailed && ( + + )} + + +
+ + + Cancel + + + Save + + + + )} +
+ ) +} + +function ActionProgress({ children }: React.PropsWithChildren<{}>) { + return ( + + {children} + + ) +} + +interface DeleteProps { + close: () => void + name: string +} + +function Delete({ name, close }: DeleteProps) { + const { push } = Notifications.use() + const del = GQL.useMutation(USER_DELETE_MUTATION) + const onSubmit = React.useCallback(async () => { + try { + const data = await del({ name }) + const r = data.admin.user.mutate?.delete + if (!r) return { [FF.FORM_ERROR]: 'notFound' } + switch (r.__typename) { + case 'Ok': + close() + push(`User "${name}" deleted`) + return + case 'InvalidInput': + const [e] = r.errors + throw new Error( + `Unexpected input error at '${e.path}': [${e.name}] ${e.message}`, + ) + case 'OperationError': + if (r.name === 'DeleteSelf') { + return { [FF.FORM_ERROR]: 'deleteSelf' } + } + throw new Error(`Unexpected operation error: [${r.name}] ${r.message}`) + default: + assertNever(r) + } + } catch (e) { + // eslint-disable-next-line no-console + console.error('Error deleting user') + // eslint-disable-next-line no-console + console.dir(e) + // TODO: send to sentry? + return { [FF.FORM_ERROR]: 'unexpected' } + } + }, [del, name, close, push]) + + return ( + + {({ handleSubmit, submitting, submitError }) => ( + <> + Delete a user + + You are about to delete user "{name}". +
+ This operation is irreversible. +
+ {!!submitError && ( + + )} +
+ + {submitting && Deleting...} + + Cancel + + + Delete + + + + )} +
+ ) +} + +interface ConfirmAdminRightsProps { + admin: boolean + name: string + close: Dialogs.Close +} + +function ConfirmAdminRights({ name, admin, close }: ConfirmAdminRightsProps) { + const { push } = Notifications.use() + const setAdmin = GQL.useMutation(USER_SET_ADMIN_MUTATION) + + const doChange = React.useCallback( + () => + close( + setAdmin({ name, admin }) + .then((data) => { + const r = data.admin.user.mutate?.setAdmin + switch (r?.__typename) { + case 'User': + return true + case undefined: // should not happen + throw new Error('User not found') + case 'InvalidInput': // should not happen + const [e] = r.errors + throw new Error( + `Unexpected input error at '${e.path}': [${e.name}] ${e.message}`, + ) + case 'OperationError': // should not happen + throw new Error(`Unexpected operation error: [${r.name}] ${r.message}`) + default: + assertNever(r) + } + }) + .catch((e) => { + push(`Could not change admin status for "${name}"`) + // eslint-disable-next-line no-console + console.error('Could not change user admin status', { name, admin }) + // eslint-disable-next-line no-console + console.dir(e) + throw e // revert value change in + }), + ), + [admin, close, name, setAdmin, push], + ) + + return ( + <> + {admin ? 'Grant' : 'Revoke'} admin rights + + You are about to {admin ? 'grant admin rights to' : 'revoke admin rights from'}{' '} + "{name}". + + + close(false)} color="primary"> + Cancel + + + {admin ? 'Grant' : 'Revoke'} + + + + ) +} + +const useUsernameStyles = M.makeStyles((t) => ({ + root: { + alignItems: 'center', + display: 'flex', + }, + admin: { + fontWeight: 600, + }, + icon: { + fontSize: '1em', + marginLeft: `calc(-1em - ${t.spacing(0.5)}px)`, + marginRight: t.spacing(0.5), + }, +})) + +interface UsernameProps extends React.HTMLProps { + admin?: boolean +} + +function Username({ className, admin = false, children, ...props }: UsernameProps) { + const classes = useUsernameStyles() + return ( + + {admin && security} + {children} + + ) +} + +interface EditableRenderProps { + change: (v: T) => void + busy: boolean + value: T +} + +interface EditableProps { + value: T + onChange: (v: T) => void + children: (props: EditableRenderProps) => JSX.Element +} + +function Editable({ value, onChange, children }: EditableProps) { + const [busy, setBusy] = React.useState(false) + const [savedValue, saveValue] = React.useState(value) + const change = React.useCallback( + (newValue: T) => { + if (savedValue === newValue) return + if (busy) return + setBusy(true) + saveValue(newValue) + Promise.resolve(onChange(newValue)) + .catch(() => { + saveValue(savedValue) + }) + .finally(() => { + setBusy(false) + }) + }, + [onChange, busy, setBusy, savedValue, saveValue], + ) + + return children({ change, busy, value: savedValue }) +} + +function UsersSkeleton() { + return ( + + + + + ) +} + +// not a valid role name +const emptyRole = '' + +interface ColumnDisplayProps { + roles: readonly Role[] + setActive: (name: string, active: boolean) => Promise + setRole: (name: string, roleName: string) => Promise + openDialog: Dialogs.Open + isSelf: boolean +} + +const columns: Table.Column[] = [ + { + id: 'isActive', + label: 'Enabled', + getValue: (u) => u.isActive, + getDisplay: (v: boolean, u, { setActive, isSelf }: ColumnDisplayProps) => + isSelf ? ( + + ) : ( + setActive(u.name, active)}> + {({ change, busy, value }) => ( + change(e.target.checked)} + disabled={busy} + color="default" + /> + )} + + ), + }, + { + id: 'username', + label: 'Username', + getValue: (u) => u.name, + getDisplay: (_name: string, u) => {u.name}, + props: { component: 'th', scope: 'row' }, + }, + { + id: 'email', + label: 'Email', + getValue: (u) => u.email, + }, + { + id: 'role', + label: 'Role', + getValue: (u) => u.role?.name, + getDisplay: (v: string | undefined, u, { roles, setRole }: ColumnDisplayProps) => ( + setRole(u.name, role)}> + {({ change, busy, value }) => ( + change(e.target.value as string)} + disabled={busy} + renderValue={R.identity} + > + {roles.map((r) => ( + + {r.name} + + ))} + + )} + + ), + }, + { + id: 'dateJoined', + label: 'Date joined', + getValue: (u) => u.dateJoined, + getDisplay: (v: Date) => ( + + + + ), + }, + { + id: 'lastLogin', + label: 'Last login', + getValue: (u) => u.lastLogin, + getDisplay: (v: Date) => ( + + + + ), + }, + { + id: 'isAdmin', + label: 'Admin', + hint: 'Admins can see this page, add/remove users, and make/remove admins', + getValue: (u) => u.isAdmin, + getDisplay: (v: boolean, { name }, { openDialog, isSelf }: ColumnDisplayProps) => + isSelf ? ( + + ) : ( + + openDialog( + ({ close }) => , + DIALOG_PROPS, + ).then((res) => { + if (!res) throw new Error('cancel') + }) + } + > + {({ change, busy, value }) => ( + change(e.target.checked)} + disabled={busy} + color="default" + /> + )} + + ), + }, +] + +function useSetActive() { + const { push } = Notifications.use() + const setActive = GQL.useMutation(USER_SET_ACTIVE_MUTATION) + + return React.useCallback( + async (name: string, active: boolean) => { + try { + const resp = await setActive({ name, active }) + // TODO: switch + switch (resp) { + } + } catch (e) { + push(`Error ${active ? 'enabling' : 'disabling'} "${name}"`) + // eslint-disable-next-line no-console + console.error('Error (de)activating user', { name, active }) + // eslint-disable-next-line no-console + console.dir(e) + // TODO + // throw e + } + }, + [setActive, push], + ) +} + +function useSetRole() { + const { push } = Notifications.use() + const setRole = GQL.useMutation(USER_SET_ROLE_MUTATION) + + return React.useCallback( + async (name: string, roleName: string) => { + try { + const resp = await setRole({ name, roleName }) + const r = resp.admin.user.mutate?.setRole + // TODO: switch result + switch (r) { + } + } catch (e) { + push(`Error changing role for "${name}"`) + // eslint-disable-next-line no-console + console.error('Error chaging role', { name, roleName }) + // eslint-disable-next-line no-console + console.dir(e) + throw e + } + }, + [setRole, push], + ) +} + +export default function Users() { + const data = GQL.useQueryS(USERS_QUERY) + const rows = data.admin.user.list + const roles = data.roles + const defaultRoleName = data.defaultRole?.name + + const openDialog = Dialogs.use() + + const setActive = useSetActive() + const setRole = useSetRole() + + const filtering = Table.useFiltering({ + rows, + filterBy: ({ email, name }) => email + name, + }) + const ordering = Table.useOrdering({ + rows: filtering.filtered, + column: columns[0], + }) + const pagination = Pagination.use(ordering.ordered, { + getItemId: (u: User) => u.name, + } as $TSFixMe) + + const toolbarActions = [ + { + title: 'Invite', + icon: add, + fn: React.useCallback(() => { + openDialog( + ({ close }) => , + DIALOG_PROPS, + ) + }, [roles, defaultRoleName, openDialog]), + }, + ] + + const self: string = redux.useSelector(Auth.selectors.username) + + const inlineActions = (user: User) => [ + user.name === self + ? null + : { + title: 'Delete', + icon: delete, + fn: () => { + openDialog( + ({ close }) => , + DIALOG_PROPS, + ) + }, + }, + { + title: 'Edit', + icon: edit, + fn: () => { + openDialog(({ close }) => , DIALOG_PROPS) + }, + }, + ] + + const getDisplayProps = (u: User): ColumnDisplayProps => ({ + setRole, + setActive, + roles, + openDialog, + isSelf: u.name === self, + }) + + return ( + }> + + + + + + + + + {pagination.paginated.map((i: User) => ( + + {columns.map((col) => ( + + {(col.getDisplay || R.identity)( + col.getValue(i), + i, + getDisplayProps(i), + )} + + ))} + + + + + ))} + + + + + + + ) +} diff --git a/catalog/app/containers/Admin/Users/gql/UserCreate.generated.ts b/catalog/app/containers/Admin/Users/gql/UserCreate.generated.ts new file mode 100644 index 00000000000..ca678da00a1 --- /dev/null +++ b/catalog/app/containers/Admin/Users/gql/UserCreate.generated.ts @@ -0,0 +1,104 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core' +import * as Types from '../../../../model/graphql/types.generated' + +import { + UserResultSelection_User_Fragment, + UserResultSelection_InvalidInput_Fragment, + UserResultSelection_OperationError_Fragment, + UserResultSelectionFragmentDoc, +} from './UserResultSelection.generated' + +export type containers_Admin_Users_gql_UserCreateMutationVariables = Types.Exact<{ + input: Types.UserInput +}> + +export type containers_Admin_Users_gql_UserCreateMutation = { + readonly __typename: 'Mutation' +} & { + readonly admin: { readonly __typename: 'AdminMutations' } & { + readonly user: { readonly __typename: 'UserAdminMutations' } & { + readonly create: + | ({ readonly __typename: 'User' } & UserResultSelection_User_Fragment) + | ({ + readonly __typename: 'InvalidInput' + } & UserResultSelection_InvalidInput_Fragment) + | ({ + readonly __typename: 'OperationError' + } & UserResultSelection_OperationError_Fragment) + } + } +} + +export const containers_Admin_Users_gql_UserCreateDocument = { + kind: 'Document', + definitions: [ + { + kind: 'OperationDefinition', + operation: 'mutation', + name: { kind: 'Name', value: 'containers_Admin_Users_gql_UserCreate' }, + variableDefinitions: [ + { + kind: 'VariableDefinition', + variable: { kind: 'Variable', name: { kind: 'Name', value: 'input' } }, + type: { + kind: 'NonNullType', + type: { kind: 'NamedType', name: { kind: 'Name', value: 'UserInput' } }, + }, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'admin' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'user' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'create' }, + arguments: [ + { + kind: 'Argument', + name: { kind: 'Name', value: 'input' }, + value: { + kind: 'Variable', + name: { kind: 'Name', value: 'input' }, + }, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'FragmentSpread', + name: { kind: 'Name', value: 'UserResultSelection' }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ...UserResultSelectionFragmentDoc.definitions, + ], +} as unknown as DocumentNode< + containers_Admin_Users_gql_UserCreateMutation, + containers_Admin_Users_gql_UserCreateMutationVariables +> + +export { containers_Admin_Users_gql_UserCreateDocument as default } diff --git a/catalog/app/containers/Admin/Users/gql/UserCreate.graphql b/catalog/app/containers/Admin/Users/gql/UserCreate.graphql new file mode 100644 index 00000000000..287d9b2f0f6 --- /dev/null +++ b/catalog/app/containers/Admin/Users/gql/UserCreate.graphql @@ -0,0 +1,11 @@ +# import UserResultSelection from "./UserResultSelection.graphql" + +mutation ($input: UserInput!) { + admin { + user { + create(input: $input) { + ...UserResultSelection + } + } + } +} diff --git a/catalog/app/containers/Admin/Users/gql/UserDelete.generated.ts b/catalog/app/containers/Admin/Users/gql/UserDelete.generated.ts new file mode 100644 index 00000000000..7b8a68057d2 --- /dev/null +++ b/catalog/app/containers/Admin/Users/gql/UserDelete.generated.ts @@ -0,0 +1,176 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core' +import * as Types from '../../../../model/graphql/types.generated' + +export type containers_Admin_Users_gql_UserDeleteMutationVariables = Types.Exact<{ + name: Types.Scalars['String'] +}> + +export type containers_Admin_Users_gql_UserDeleteMutation = { + readonly __typename: 'Mutation' +} & { + readonly admin: { readonly __typename: 'AdminMutations' } & { + readonly user: { readonly __typename: 'UserAdminMutations' } & { + readonly mutate: Types.Maybe< + { readonly __typename: 'MutateUserAdminMutations' } & { + readonly delete: + | { readonly __typename: 'Ok' } + | ({ readonly __typename: 'InvalidInput' } & { + readonly errors: ReadonlyArray< + { readonly __typename: 'InputError' } & Pick< + Types.InputError, + 'path' | 'message' | 'name' | 'context' + > + > + }) + | ({ readonly __typename: 'OperationError' } & Pick< + Types.OperationError, + 'message' | 'name' | 'context' + >) + } + > + } + } +} + +export const containers_Admin_Users_gql_UserDeleteDocument = { + kind: 'Document', + definitions: [ + { + kind: 'OperationDefinition', + operation: 'mutation', + name: { kind: 'Name', value: 'containers_Admin_Users_gql_UserDelete' }, + variableDefinitions: [ + { + kind: 'VariableDefinition', + variable: { kind: 'Variable', name: { kind: 'Name', value: 'name' } }, + type: { + kind: 'NonNullType', + type: { kind: 'NamedType', name: { kind: 'Name', value: 'String' } }, + }, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'admin' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'user' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'mutate' }, + arguments: [ + { + kind: 'Argument', + name: { kind: 'Name', value: 'name' }, + value: { + kind: 'Variable', + name: { kind: 'Name', value: 'name' }, + }, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'delete' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: '__typename' }, + }, + { + kind: 'InlineFragment', + typeCondition: { + kind: 'NamedType', + name: { kind: 'Name', value: 'InvalidInput' }, + }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'errors' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'path' }, + }, + { + kind: 'Field', + name: { kind: 'Name', value: 'message' }, + }, + { + kind: 'Field', + name: { kind: 'Name', value: 'name' }, + }, + { + kind: 'Field', + name: { kind: 'Name', value: 'context' }, + }, + ], + }, + }, + ], + }, + }, + { + kind: 'InlineFragment', + typeCondition: { + kind: 'NamedType', + name: { kind: 'Name', value: 'OperationError' }, + }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'message' }, + }, + { + kind: 'Field', + name: { kind: 'Name', value: 'name' }, + }, + { + kind: 'Field', + name: { kind: 'Name', value: 'context' }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ], +} as unknown as DocumentNode< + containers_Admin_Users_gql_UserDeleteMutation, + containers_Admin_Users_gql_UserDeleteMutationVariables +> + +export { containers_Admin_Users_gql_UserDeleteDocument as default } diff --git a/catalog/app/containers/Admin/Users/gql/UserDelete.graphql b/catalog/app/containers/Admin/Users/gql/UserDelete.graphql new file mode 100644 index 00000000000..78d453b9b9f --- /dev/null +++ b/catalog/app/containers/Admin/Users/gql/UserDelete.graphql @@ -0,0 +1,24 @@ +mutation ($name: String!) { + admin { + user { + mutate(name: $name) { + delete { + __typename + ... on InvalidInput { + errors { + path + message + name + context + } + } + ... on OperationError { + message + name + context + } + } + } + } + } +} diff --git a/catalog/app/containers/Admin/Users/gql/UserResultSelection.generated.ts b/catalog/app/containers/Admin/Users/gql/UserResultSelection.generated.ts new file mode 100644 index 00000000000..1833ee2e578 --- /dev/null +++ b/catalog/app/containers/Admin/Users/gql/UserResultSelection.generated.ts @@ -0,0 +1,103 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core' +import * as Types from '../../../../model/graphql/types.generated' + +import { + UserSelectionFragment, + UserSelectionFragmentDoc, +} from './UserSelection.generated' + +export type UserResultSelection_User_Fragment = { + readonly __typename: 'User' +} & UserSelectionFragment + +export type UserResultSelection_InvalidInput_Fragment = { + readonly __typename: 'InvalidInput' +} & { + readonly errors: ReadonlyArray< + { readonly __typename: 'InputError' } & Pick< + Types.InputError, + 'path' | 'message' | 'name' | 'context' + > + > +} + +export type UserResultSelection_OperationError_Fragment = { + readonly __typename: 'OperationError' +} & Pick + +export type UserResultSelectionFragment = + | UserResultSelection_User_Fragment + | UserResultSelection_InvalidInput_Fragment + | UserResultSelection_OperationError_Fragment + +export const UserResultSelectionFragmentDoc = { + kind: 'Document', + definitions: [ + { + kind: 'FragmentDefinition', + name: { kind: 'Name', value: 'UserResultSelection' }, + typeCondition: { kind: 'NamedType', name: { kind: 'Name', value: 'UserResult' } }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, + { + kind: 'InlineFragment', + typeCondition: { kind: 'NamedType', name: { kind: 'Name', value: 'User' } }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'FragmentSpread', + name: { kind: 'Name', value: 'UserSelection' }, + }, + ], + }, + }, + { + kind: 'InlineFragment', + typeCondition: { + kind: 'NamedType', + name: { kind: 'Name', value: 'InvalidInput' }, + }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'errors' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'path' } }, + { kind: 'Field', name: { kind: 'Name', value: 'message' } }, + { kind: 'Field', name: { kind: 'Name', value: 'name' } }, + { kind: 'Field', name: { kind: 'Name', value: 'context' } }, + ], + }, + }, + ], + }, + }, + { + kind: 'InlineFragment', + typeCondition: { + kind: 'NamedType', + name: { kind: 'Name', value: 'OperationError' }, + }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'message' } }, + { kind: 'Field', name: { kind: 'Name', value: 'name' } }, + { kind: 'Field', name: { kind: 'Name', value: 'context' } }, + ], + }, + }, + ], + }, + }, + ...UserSelectionFragmentDoc.definitions, + ], +} as unknown as DocumentNode diff --git a/catalog/app/containers/Admin/Users/gql/UserResultSelection.graphql b/catalog/app/containers/Admin/Users/gql/UserResultSelection.graphql new file mode 100644 index 00000000000..86ae6e80d97 --- /dev/null +++ b/catalog/app/containers/Admin/Users/gql/UserResultSelection.graphql @@ -0,0 +1,19 @@ +# import UserSelection from "./UserSelection.graphql" + +fragment UserResultSelection on UserResult { + __typename + ... on User { ...UserSelection } + ... on InvalidInput { + errors { + path + message + name + context + } + } + ... on OperationError { + message + name + context + } +} diff --git a/catalog/app/containers/Admin/Users/gql/UserSelection.generated.ts b/catalog/app/containers/Admin/Users/gql/UserSelection.generated.ts new file mode 100644 index 00000000000..0368d91f12b --- /dev/null +++ b/catalog/app/containers/Admin/Users/gql/UserSelection.generated.ts @@ -0,0 +1,86 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core' +import * as Types from '../../../../model/graphql/types.generated' + +export type UserSelectionFragment = { readonly __typename: 'User' } & Pick< + Types.User, + | 'name' + | 'email' + | 'dateJoined' + | 'lastLogin' + | 'isActive' + | 'isAdmin' + | 'isSsoOnly' + | 'isService' +> & { + readonly role: Types.Maybe< + | ({ readonly __typename: 'UnmanagedRole' } & Pick< + Types.UnmanagedRole, + 'id' | 'name' + >) + | ({ readonly __typename: 'ManagedRole' } & Pick) + > + } + +export const UserSelectionFragmentDoc = { + kind: 'Document', + definitions: [ + { + kind: 'FragmentDefinition', + name: { kind: 'Name', value: 'UserSelection' }, + typeCondition: { kind: 'NamedType', name: { kind: 'Name', value: 'User' } }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, + { kind: 'Field', name: { kind: 'Name', value: 'name' } }, + { kind: 'Field', name: { kind: 'Name', value: 'email' } }, + { kind: 'Field', name: { kind: 'Name', value: 'dateJoined' } }, + { kind: 'Field', name: { kind: 'Name', value: 'lastLogin' } }, + { kind: 'Field', name: { kind: 'Name', value: 'isActive' } }, + { kind: 'Field', name: { kind: 'Name', value: 'isAdmin' } }, + { kind: 'Field', name: { kind: 'Name', value: 'isSsoOnly' } }, + { kind: 'Field', name: { kind: 'Name', value: 'isService' } }, + { + kind: 'Field', + name: { kind: 'Name', value: 'role' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, + { + kind: 'InlineFragment', + typeCondition: { + kind: 'NamedType', + name: { kind: 'Name', value: 'ManagedRole' }, + }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'id' } }, + { kind: 'Field', name: { kind: 'Name', value: 'name' } }, + ], + }, + }, + { + kind: 'InlineFragment', + typeCondition: { + kind: 'NamedType', + name: { kind: 'Name', value: 'UnmanagedRole' }, + }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'id' } }, + { kind: 'Field', name: { kind: 'Name', value: 'name' } }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ], +} as unknown as DocumentNode diff --git a/catalog/app/containers/Admin/Users/gql/UserSelection.graphql b/catalog/app/containers/Admin/Users/gql/UserSelection.graphql new file mode 100644 index 00000000000..349a0304e63 --- /dev/null +++ b/catalog/app/containers/Admin/Users/gql/UserSelection.graphql @@ -0,0 +1,22 @@ +fragment UserSelection on User { + __typename + name + email + dateJoined + lastLogin + isActive + isAdmin + isSsoOnly + isService + role { + __typename + ... on ManagedRole { + id + name + } + ... on UnmanagedRole { + id + name + } + } +} diff --git a/catalog/app/containers/Admin/Users/gql/UserSetActive.generated.ts b/catalog/app/containers/Admin/Users/gql/UserSetActive.generated.ts new file mode 100644 index 00000000000..343a4cff8fd --- /dev/null +++ b/catalog/app/containers/Admin/Users/gql/UserSetActive.generated.ts @@ -0,0 +1,136 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core' +import * as Types from '../../../../model/graphql/types.generated' + +import { + UserResultSelection_User_Fragment, + UserResultSelection_InvalidInput_Fragment, + UserResultSelection_OperationError_Fragment, + UserResultSelectionFragmentDoc, +} from './UserResultSelection.generated' + +export type containers_Admin_Users_gql_UserSetActiveMutationVariables = Types.Exact<{ + name: Types.Scalars['String'] + active: Types.Scalars['Boolean'] +}> + +export type containers_Admin_Users_gql_UserSetActiveMutation = { + readonly __typename: 'Mutation' +} & { + readonly admin: { readonly __typename: 'AdminMutations' } & { + readonly user: { readonly __typename: 'UserAdminMutations' } & { + readonly mutate: Types.Maybe< + { readonly __typename: 'MutateUserAdminMutations' } & { + readonly setActive: + | ({ readonly __typename: 'User' } & UserResultSelection_User_Fragment) + | ({ + readonly __typename: 'InvalidInput' + } & UserResultSelection_InvalidInput_Fragment) + | ({ + readonly __typename: 'OperationError' + } & UserResultSelection_OperationError_Fragment) + } + > + } + } +} + +export const containers_Admin_Users_gql_UserSetActiveDocument = { + kind: 'Document', + definitions: [ + { + kind: 'OperationDefinition', + operation: 'mutation', + name: { kind: 'Name', value: 'containers_Admin_Users_gql_UserSetActive' }, + variableDefinitions: [ + { + kind: 'VariableDefinition', + variable: { kind: 'Variable', name: { kind: 'Name', value: 'name' } }, + type: { + kind: 'NonNullType', + type: { kind: 'NamedType', name: { kind: 'Name', value: 'String' } }, + }, + }, + { + kind: 'VariableDefinition', + variable: { kind: 'Variable', name: { kind: 'Name', value: 'active' } }, + type: { + kind: 'NonNullType', + type: { kind: 'NamedType', name: { kind: 'Name', value: 'Boolean' } }, + }, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'admin' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'user' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'mutate' }, + arguments: [ + { + kind: 'Argument', + name: { kind: 'Name', value: 'name' }, + value: { + kind: 'Variable', + name: { kind: 'Name', value: 'name' }, + }, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'setActive' }, + arguments: [ + { + kind: 'Argument', + name: { kind: 'Name', value: 'active' }, + value: { + kind: 'Variable', + name: { kind: 'Name', value: 'active' }, + }, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'FragmentSpread', + name: { kind: 'Name', value: 'UserResultSelection' }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ...UserResultSelectionFragmentDoc.definitions, + ], +} as unknown as DocumentNode< + containers_Admin_Users_gql_UserSetActiveMutation, + containers_Admin_Users_gql_UserSetActiveMutationVariables +> + +export { containers_Admin_Users_gql_UserSetActiveDocument as default } diff --git a/catalog/app/containers/Admin/Users/gql/UserSetActive.graphql b/catalog/app/containers/Admin/Users/gql/UserSetActive.graphql new file mode 100644 index 00000000000..0c1089ea8a6 --- /dev/null +++ b/catalog/app/containers/Admin/Users/gql/UserSetActive.graphql @@ -0,0 +1,13 @@ +# import UserResultSelection from "./UserResultSelection.graphql" + +mutation ($name: String!, $active: Boolean!) { + admin { + user { + mutate(name: $name) { + setActive(active: $active) { + ...UserResultSelection + } + } + } + } +} diff --git a/catalog/app/containers/Admin/Users/gql/UserSetAdmin.generated.ts b/catalog/app/containers/Admin/Users/gql/UserSetAdmin.generated.ts new file mode 100644 index 00000000000..87c7a5c85ac --- /dev/null +++ b/catalog/app/containers/Admin/Users/gql/UserSetAdmin.generated.ts @@ -0,0 +1,136 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core' +import * as Types from '../../../../model/graphql/types.generated' + +import { + UserResultSelection_User_Fragment, + UserResultSelection_InvalidInput_Fragment, + UserResultSelection_OperationError_Fragment, + UserResultSelectionFragmentDoc, +} from './UserResultSelection.generated' + +export type containers_Admin_Users_gql_UserSetAdminMutationVariables = Types.Exact<{ + name: Types.Scalars['String'] + admin: Types.Scalars['Boolean'] +}> + +export type containers_Admin_Users_gql_UserSetAdminMutation = { + readonly __typename: 'Mutation' +} & { + readonly admin: { readonly __typename: 'AdminMutations' } & { + readonly user: { readonly __typename: 'UserAdminMutations' } & { + readonly mutate: Types.Maybe< + { readonly __typename: 'MutateUserAdminMutations' } & { + readonly setAdmin: + | ({ readonly __typename: 'User' } & UserResultSelection_User_Fragment) + | ({ + readonly __typename: 'InvalidInput' + } & UserResultSelection_InvalidInput_Fragment) + | ({ + readonly __typename: 'OperationError' + } & UserResultSelection_OperationError_Fragment) + } + > + } + } +} + +export const containers_Admin_Users_gql_UserSetAdminDocument = { + kind: 'Document', + definitions: [ + { + kind: 'OperationDefinition', + operation: 'mutation', + name: { kind: 'Name', value: 'containers_Admin_Users_gql_UserSetAdmin' }, + variableDefinitions: [ + { + kind: 'VariableDefinition', + variable: { kind: 'Variable', name: { kind: 'Name', value: 'name' } }, + type: { + kind: 'NonNullType', + type: { kind: 'NamedType', name: { kind: 'Name', value: 'String' } }, + }, + }, + { + kind: 'VariableDefinition', + variable: { kind: 'Variable', name: { kind: 'Name', value: 'admin' } }, + type: { + kind: 'NonNullType', + type: { kind: 'NamedType', name: { kind: 'Name', value: 'Boolean' } }, + }, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'admin' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'user' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'mutate' }, + arguments: [ + { + kind: 'Argument', + name: { kind: 'Name', value: 'name' }, + value: { + kind: 'Variable', + name: { kind: 'Name', value: 'name' }, + }, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'setAdmin' }, + arguments: [ + { + kind: 'Argument', + name: { kind: 'Name', value: 'admin' }, + value: { + kind: 'Variable', + name: { kind: 'Name', value: 'admin' }, + }, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'FragmentSpread', + name: { kind: 'Name', value: 'UserResultSelection' }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ...UserResultSelectionFragmentDoc.definitions, + ], +} as unknown as DocumentNode< + containers_Admin_Users_gql_UserSetAdminMutation, + containers_Admin_Users_gql_UserSetAdminMutationVariables +> + +export { containers_Admin_Users_gql_UserSetAdminDocument as default } diff --git a/catalog/app/containers/Admin/Users/gql/UserSetAdmin.graphql b/catalog/app/containers/Admin/Users/gql/UserSetAdmin.graphql new file mode 100644 index 00000000000..5f400011f04 --- /dev/null +++ b/catalog/app/containers/Admin/Users/gql/UserSetAdmin.graphql @@ -0,0 +1,13 @@ +# import UserResultSelection from "./UserResultSelection.graphql" + +mutation ($name: String!, $admin: Boolean!) { + admin { + user { + mutate(name: $name) { + setAdmin(admin: $admin) { + ...UserResultSelection + } + } + } + } +} diff --git a/catalog/app/containers/Admin/Users/gql/UserSetEmail.generated.ts b/catalog/app/containers/Admin/Users/gql/UserSetEmail.generated.ts new file mode 100644 index 00000000000..8776fb06f11 --- /dev/null +++ b/catalog/app/containers/Admin/Users/gql/UserSetEmail.generated.ts @@ -0,0 +1,136 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core' +import * as Types from '../../../../model/graphql/types.generated' + +import { + UserResultSelection_User_Fragment, + UserResultSelection_InvalidInput_Fragment, + UserResultSelection_OperationError_Fragment, + UserResultSelectionFragmentDoc, +} from './UserResultSelection.generated' + +export type containers_Admin_Users_gql_UserSetEmailMutationVariables = Types.Exact<{ + name: Types.Scalars['String'] + email: Types.Scalars['String'] +}> + +export type containers_Admin_Users_gql_UserSetEmailMutation = { + readonly __typename: 'Mutation' +} & { + readonly admin: { readonly __typename: 'AdminMutations' } & { + readonly user: { readonly __typename: 'UserAdminMutations' } & { + readonly mutate: Types.Maybe< + { readonly __typename: 'MutateUserAdminMutations' } & { + readonly setEmail: + | ({ readonly __typename: 'User' } & UserResultSelection_User_Fragment) + | ({ + readonly __typename: 'InvalidInput' + } & UserResultSelection_InvalidInput_Fragment) + | ({ + readonly __typename: 'OperationError' + } & UserResultSelection_OperationError_Fragment) + } + > + } + } +} + +export const containers_Admin_Users_gql_UserSetEmailDocument = { + kind: 'Document', + definitions: [ + { + kind: 'OperationDefinition', + operation: 'mutation', + name: { kind: 'Name', value: 'containers_Admin_Users_gql_UserSetEmail' }, + variableDefinitions: [ + { + kind: 'VariableDefinition', + variable: { kind: 'Variable', name: { kind: 'Name', value: 'name' } }, + type: { + kind: 'NonNullType', + type: { kind: 'NamedType', name: { kind: 'Name', value: 'String' } }, + }, + }, + { + kind: 'VariableDefinition', + variable: { kind: 'Variable', name: { kind: 'Name', value: 'email' } }, + type: { + kind: 'NonNullType', + type: { kind: 'NamedType', name: { kind: 'Name', value: 'String' } }, + }, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'admin' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'user' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'mutate' }, + arguments: [ + { + kind: 'Argument', + name: { kind: 'Name', value: 'name' }, + value: { + kind: 'Variable', + name: { kind: 'Name', value: 'name' }, + }, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'setEmail' }, + arguments: [ + { + kind: 'Argument', + name: { kind: 'Name', value: 'email' }, + value: { + kind: 'Variable', + name: { kind: 'Name', value: 'email' }, + }, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'FragmentSpread', + name: { kind: 'Name', value: 'UserResultSelection' }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ...UserResultSelectionFragmentDoc.definitions, + ], +} as unknown as DocumentNode< + containers_Admin_Users_gql_UserSetEmailMutation, + containers_Admin_Users_gql_UserSetEmailMutationVariables +> + +export { containers_Admin_Users_gql_UserSetEmailDocument as default } diff --git a/catalog/app/containers/Admin/Users/gql/UserSetEmail.graphql b/catalog/app/containers/Admin/Users/gql/UserSetEmail.graphql new file mode 100644 index 00000000000..f64a6a3ffdd --- /dev/null +++ b/catalog/app/containers/Admin/Users/gql/UserSetEmail.graphql @@ -0,0 +1,13 @@ +# import UserResultSelection from "./UserResultSelection.graphql" + +mutation ($name: String!, $email: String!) { + admin { + user { + mutate(name: $name) { + setEmail(email: $email) { + ...UserResultSelection + } + } + } + } +} diff --git a/catalog/app/containers/Admin/Users/gql/UserSetRole.generated.ts b/catalog/app/containers/Admin/Users/gql/UserSetRole.generated.ts new file mode 100644 index 00000000000..78b0b118444 --- /dev/null +++ b/catalog/app/containers/Admin/Users/gql/UserSetRole.generated.ts @@ -0,0 +1,136 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core' +import * as Types from '../../../../model/graphql/types.generated' + +import { + UserResultSelection_User_Fragment, + UserResultSelection_InvalidInput_Fragment, + UserResultSelection_OperationError_Fragment, + UserResultSelectionFragmentDoc, +} from './UserResultSelection.generated' + +export type containers_Admin_Users_gql_UserSetRoleMutationVariables = Types.Exact<{ + name: Types.Scalars['String'] + roleName: Types.Scalars['String'] +}> + +export type containers_Admin_Users_gql_UserSetRoleMutation = { + readonly __typename: 'Mutation' +} & { + readonly admin: { readonly __typename: 'AdminMutations' } & { + readonly user: { readonly __typename: 'UserAdminMutations' } & { + readonly mutate: Types.Maybe< + { readonly __typename: 'MutateUserAdminMutations' } & { + readonly setRole: + | ({ readonly __typename: 'User' } & UserResultSelection_User_Fragment) + | ({ + readonly __typename: 'InvalidInput' + } & UserResultSelection_InvalidInput_Fragment) + | ({ + readonly __typename: 'OperationError' + } & UserResultSelection_OperationError_Fragment) + } + > + } + } +} + +export const containers_Admin_Users_gql_UserSetRoleDocument = { + kind: 'Document', + definitions: [ + { + kind: 'OperationDefinition', + operation: 'mutation', + name: { kind: 'Name', value: 'containers_Admin_Users_gql_UserSetRole' }, + variableDefinitions: [ + { + kind: 'VariableDefinition', + variable: { kind: 'Variable', name: { kind: 'Name', value: 'name' } }, + type: { + kind: 'NonNullType', + type: { kind: 'NamedType', name: { kind: 'Name', value: 'String' } }, + }, + }, + { + kind: 'VariableDefinition', + variable: { kind: 'Variable', name: { kind: 'Name', value: 'roleName' } }, + type: { + kind: 'NonNullType', + type: { kind: 'NamedType', name: { kind: 'Name', value: 'String' } }, + }, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'admin' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'user' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'mutate' }, + arguments: [ + { + kind: 'Argument', + name: { kind: 'Name', value: 'name' }, + value: { + kind: 'Variable', + name: { kind: 'Name', value: 'name' }, + }, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'setRole' }, + arguments: [ + { + kind: 'Argument', + name: { kind: 'Name', value: 'roleName' }, + value: { + kind: 'Variable', + name: { kind: 'Name', value: 'roleName' }, + }, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'FragmentSpread', + name: { kind: 'Name', value: 'UserResultSelection' }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ...UserResultSelectionFragmentDoc.definitions, + ], +} as unknown as DocumentNode< + containers_Admin_Users_gql_UserSetRoleMutation, + containers_Admin_Users_gql_UserSetRoleMutationVariables +> + +export { containers_Admin_Users_gql_UserSetRoleDocument as default } diff --git a/catalog/app/containers/Admin/Users/gql/UserSetRole.graphql b/catalog/app/containers/Admin/Users/gql/UserSetRole.graphql new file mode 100644 index 00000000000..e6da78863d6 --- /dev/null +++ b/catalog/app/containers/Admin/Users/gql/UserSetRole.graphql @@ -0,0 +1,13 @@ +# import UserResultSelection from "./UserResultSelection.graphql" + +mutation ($name: String!, $roleName: String!) { + admin { + user { + mutate(name: $name) { + setRole(roleName: $roleName) { + ...UserResultSelection + } + } + } + } +} diff --git a/catalog/app/containers/Admin/Users/gql/Roles.generated.ts b/catalog/app/containers/Admin/Users/gql/Users.generated.ts similarity index 56% rename from catalog/app/containers/Admin/Users/gql/Roles.generated.ts rename to catalog/app/containers/Admin/Users/gql/Users.generated.ts index 69e8efd8687..80f5a711c7c 100644 --- a/catalog/app/containers/Admin/Users/gql/Roles.generated.ts +++ b/catalog/app/containers/Admin/Users/gql/Users.generated.ts @@ -2,11 +2,23 @@ import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core' import * as Types from '../../../../model/graphql/types.generated' -export type containers_Admin_Users_gql_RolesQueryVariables = Types.Exact<{ +import { + UserSelectionFragment, + UserSelectionFragmentDoc, +} from './UserSelection.generated' + +export type containers_Admin_Users_gql_UsersQueryVariables = Types.Exact<{ [key: string]: never }> -export type containers_Admin_Users_gql_RolesQuery = { readonly __typename: 'Query' } & { +export type containers_Admin_Users_gql_UsersQuery = { readonly __typename: 'Query' } & { + readonly admin: { readonly __typename: 'AdminQueries' } & { + readonly user: { readonly __typename: 'UserAdminQueries' } & { + readonly list: ReadonlyArray< + { readonly __typename: 'User' } & UserSelectionFragment + > + } + } readonly roles: ReadonlyArray< | ({ readonly __typename: 'UnmanagedRole' } & Pick< Types.UnmanagedRole, @@ -15,21 +27,55 @@ export type containers_Admin_Users_gql_RolesQuery = { readonly __typename: 'Quer | ({ readonly __typename: 'ManagedRole' } & Pick) > readonly defaultRole: Types.Maybe< - | ({ readonly __typename: 'UnmanagedRole' } & Pick) - | ({ readonly __typename: 'ManagedRole' } & Pick) + | ({ readonly __typename: 'UnmanagedRole' } & Pick< + Types.UnmanagedRole, + 'id' | 'name' + >) + | ({ readonly __typename: 'ManagedRole' } & Pick) > } -export const containers_Admin_Users_gql_RolesDocument = { +export const containers_Admin_Users_gql_UsersDocument = { kind: 'Document', definitions: [ { kind: 'OperationDefinition', operation: 'query', - name: { kind: 'Name', value: 'containers_Admin_Users_gql_Roles' }, + name: { kind: 'Name', value: 'containers_Admin_Users_gql_Users' }, selectionSet: { kind: 'SelectionSet', selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'admin' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'user' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'list' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'FragmentSpread', + name: { kind: 'Name', value: 'UserSelection' }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, { kind: 'Field', name: { kind: 'Name', value: 'roles' }, @@ -81,7 +127,10 @@ export const containers_Admin_Users_gql_RolesDocument = { }, selectionSet: { kind: 'SelectionSet', - selections: [{ kind: 'Field', name: { kind: 'Name', value: 'id' } }], + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'id' } }, + { kind: 'Field', name: { kind: 'Name', value: 'name' } }, + ], }, }, { @@ -92,7 +141,10 @@ export const containers_Admin_Users_gql_RolesDocument = { }, selectionSet: { kind: 'SelectionSet', - selections: [{ kind: 'Field', name: { kind: 'Name', value: 'id' } }], + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'id' } }, + { kind: 'Field', name: { kind: 'Name', value: 'name' } }, + ], }, }, ], @@ -101,10 +153,11 @@ export const containers_Admin_Users_gql_RolesDocument = { ], }, }, + ...UserSelectionFragmentDoc.definitions, ], } as unknown as DocumentNode< - containers_Admin_Users_gql_RolesQuery, - containers_Admin_Users_gql_RolesQueryVariables + containers_Admin_Users_gql_UsersQuery, + containers_Admin_Users_gql_UsersQueryVariables > -export { containers_Admin_Users_gql_RolesDocument as default } +export { containers_Admin_Users_gql_UsersDocument as default } diff --git a/catalog/app/containers/Admin/Users/gql/Roles.graphql b/catalog/app/containers/Admin/Users/gql/Users.graphql similarity index 59% rename from catalog/app/containers/Admin/Users/gql/Roles.graphql rename to catalog/app/containers/Admin/Users/gql/Users.graphql index 72109c76ff5..1d87f58c480 100644 --- a/catalog/app/containers/Admin/Users/gql/Roles.graphql +++ b/catalog/app/containers/Admin/Users/gql/Users.graphql @@ -1,4 +1,13 @@ +# import UserSelection from "./UserSelection.graphql" + query { + admin { + user { + list { + ...UserSelection + } + } + } roles { ... on UnmanagedRole { id @@ -12,9 +21,11 @@ query { defaultRole { ... on UnmanagedRole { id + name } ... on ManagedRole { id + name } } } diff --git a/catalog/app/containers/Admin/UsersAndRoles.js b/catalog/app/containers/Admin/UsersAndRoles.tsx similarity index 61% rename from catalog/app/containers/Admin/UsersAndRoles.js rename to catalog/app/containers/Admin/UsersAndRoles.tsx index f7141628385..97cc31cc945 100644 --- a/catalog/app/containers/Admin/UsersAndRoles.js +++ b/catalog/app/containers/Admin/UsersAndRoles.tsx @@ -1,23 +1,18 @@ import * as React from 'react' import * as M from '@material-ui/core' -import * as APIConnector from 'utils/APIConnector' import MetaTitle from 'utils/MetaTitle' -import * as Cache from 'utils/ResourceCache' +// XXX: move the components imported below into a single module import { Roles, Policies } from './RolesAndPolicies' import Users from './Users' -import * as data from './data' export default function UsersAndRoles() { - const req = APIConnector.use() - // TODO: use gql for querying users when implemented - const users = Cache.useData(data.UsersResource, { req }) return ( <> {['Users, Roles and Policies', 'Admin']} - + diff --git a/catalog/app/containers/Admin/data.js b/catalog/app/containers/Admin/data.js deleted file mode 100644 index c1ae65b434f..00000000000 --- a/catalog/app/containers/Admin/data.js +++ /dev/null @@ -1,24 +0,0 @@ -import * as R from 'ramda' - -import * as Cache from 'utils/ResourceCache' - -// TODO: remove after migrating this data to gql -export const UsersResource = Cache.createResource({ - name: 'Admin.data.users', - fetch: ({ req }) => - req({ endpoint: `/users/list?_cachebust=${Math.random()}` }).then( - R.pipe( - R.prop('results'), - R.map((u) => ({ - dateJoined: new Date(u.date_joined), - email: u.email, - isActive: u.is_active, - isAdmin: u.is_superuser, - lastLogin: new Date(u.last_login), - username: u.username, - roleId: u.role_id, - })), - ), - ), - key: () => null, -}) diff --git a/catalog/app/containers/Admin/index.js b/catalog/app/containers/Admin/index.ts similarity index 100% rename from catalog/app/containers/Admin/index.js rename to catalog/app/containers/Admin/index.ts From 3498bd474826bcc883c8a12dd34ea1f7a57bedb0 Mon Sep 17 00:00:00 2001 From: nl_0 Date: Fri, 24 May 2024 11:10:30 +0200 Subject: [PATCH 07/69] handle results/errors --- catalog/app/containers/Admin/Users/Users.tsx | 127 +++++++++++-------- 1 file changed, 72 insertions(+), 55 deletions(-) diff --git a/catalog/app/containers/Admin/Users/Users.tsx b/catalog/app/containers/Admin/Users/Users.tsx index 7767ca564cb..19b9b1eddae 100644 --- a/catalog/app/containers/Admin/Users/Users.tsx +++ b/catalog/app/containers/Admin/Users/Users.tsx @@ -5,6 +5,7 @@ import * as React from 'react' import * as redux from 'react-redux' import * as RF from 'react-final-form' import * as M from '@material-ui/core' +import * as Sentry from '@sentry/react' import * as Pagination from 'components/Pagination' import * as Notifications from 'containers/Notifications' @@ -68,13 +69,13 @@ function Invite({ close, roles, defaultRoleName }: InviteProps) { const onSubmit = React.useCallback( async ({ username, email, roleName }) => { + // XXX: use formspec to convert/validate form values into gql input? + const input = { + name: username, + email, + roleName, + } try { - // XXX: use formspec to convert/validate form values into gql input? - const input = { - name: username, - email, - roleName, - } const data = await create({ input }) const r = data.admin.user.create switch (r.__typename) { @@ -88,13 +89,6 @@ function Invite({ close, roles, defaultRoleName }: InviteProps) { return { [FF.FORM_ERROR]: 'subscriptionInvalid' } case 'MailSendError': return { [FF.FORM_ERROR]: 'smtp' } - case 'Conflict': - if (r.message.match(/Email already taken/)) { - return { email: 'taken' } - } - if (r.message.match(/Username already taken/)) { - return { username: 'taken' } - } } throw new Error(`Unexpected operation error: [${r.name}] ${r.message}`) case 'InvalidInput': @@ -102,13 +96,10 @@ function Invite({ close, roles, defaultRoleName }: InviteProps) { r.errors.forEach((e) => { switch (e.path) { case 'input.name': - // e.name should be one of: - // - InvalidUserNameError - // - ReservedUserNameError - errors.username = 'invalid' + errors.username = e.name === 'Conflict' ? 'taken' : 'invalid' break case 'input.email': - errors.email = 'invalid' + errors.email = e.name === 'Conflict' ? 'taken' : 'invalid' break default: throw new Error( @@ -122,10 +113,10 @@ function Invite({ close, roles, defaultRoleName }: InviteProps) { } } catch (e) { // eslint-disable-next-line no-console - console.error('Error creating user') + console.error('Error creating user', input) // eslint-disable-next-line no-console console.dir(e) - // TODO: send to sentry? + Sentry.captureException(e) return { [FF.FORM_ERROR]: 'unexpected' } } }, @@ -257,34 +248,34 @@ function Edit({ close, user: { email: oldEmail, name } }: EditProps) { try { const data = await setEmail({ name, email }) const r = data.admin.user.mutate?.setEmail - if (!r) { - throw new Error() - } - // TODO: switch result - switch (r.__typename) { - // if (APIConnector.HTTPError.is(e, 400, /Another user already has that email/)) { - // return { - // email: 'taken', - // } - // } - // if (APIConnector.HTTPError.is(e, 400, /Invalid email/)) { - // return { - // email: 'invalid', - // } - // } + switch (r?.__typename) { + case 'User': + close() + push('Changes saved') + return + case undefined: + throw new Error('User not found') // should not happend + case 'OperationError': + if (r.name === 'EmailAlreadyInUse') return { email: 'taken' } + throw new Error(`Unexpected operation error: [${r.name}] ${r.message}`) + case 'InvalidInput': + const [e] = r.errors + if (e.path === 'input.email' && e.name === 'InvalidEmail') { + return { email: 'invalid' } + } + throw new Error( + `Unexpected input error at '${e.path}': [${e.name}] ${e.message}`, + ) default: + assertNever(r) } - - close() - push('Changes saved') } catch (e) { // eslint-disable-next-line no-console - console.error('Error changing email') + console.error('Error changing email', { name, email }) // eslint-disable-next-line no-console console.dir(e) - return { - [FF.FORM_ERROR]: 'unexpected', - } + Sentry.captureException(e) + return { [FF.FORM_ERROR]: 'unexpected' } } }, [close, name, oldEmail, setEmail, push], @@ -400,7 +391,7 @@ function Delete({ name, close }: DeleteProps) { console.error('Error deleting user') // eslint-disable-next-line no-console console.dir(e) - // TODO: send to sentry? + Sentry.captureException(e) return { [FF.FORM_ERROR]: 'unexpected' } } }, [del, name, close, push]) @@ -474,7 +465,7 @@ function ConfirmAdminRights({ name, admin, close }: ConfirmAdminRightsProps) { } }) .catch((e) => { - push(`Could not change admin status for "${name}"`) + push(`Could not change admin status for user "${name}": ${e}`) // eslint-disable-next-line no-console console.error('Could not change user admin status', { name, admin }) // eslint-disable-next-line no-console @@ -704,18 +695,31 @@ function useSetActive() { return React.useCallback( async (name: string, active: boolean) => { try { - const resp = await setActive({ name, active }) - // TODO: switch - switch (resp) { + const data = await setActive({ name, active }) + const r = data.admin.user.mutate?.setActive + switch (r?.__typename) { + case 'User': + return + case undefined: + throw new Error('User not found') // should not happend + case 'OperationError': + throw new Error(`Unexpected operation error: [${r.name}] ${r.message}`) + case 'InvalidInput': + const [e] = r.errors + throw new Error( + `Unexpected input error at '${e.path}': [${e.name}] ${e.message}`, + ) + default: + assertNever(r) } } catch (e) { - push(`Error ${active ? 'enabling' : 'disabling'} "${name}"`) + push(`Could not ${active ? 'enable' : 'disable'} user "${name}": ${e}`) // eslint-disable-next-line no-console console.error('Error (de)activating user', { name, active }) // eslint-disable-next-line no-console console.dir(e) - // TODO - // throw e + Sentry.captureException(e) + throw e } }, [setActive, push], @@ -729,17 +733,30 @@ function useSetRole() { return React.useCallback( async (name: string, roleName: string) => { try { - const resp = await setRole({ name, roleName }) - const r = resp.admin.user.mutate?.setRole - // TODO: switch result - switch (r) { + const data = await setRole({ name, roleName }) + const r = data.admin.user.mutate?.setRole + switch (r?.__typename) { + case 'User': + return + case undefined: + throw new Error('User not found') // should not happend + case 'OperationError': + throw new Error(`Unexpected operation error: [${r.name}] ${r.message}`) + case 'InvalidInput': + const [e] = r.errors + throw new Error( + `Unexpected input error at '${e.path}': [${e.name}] ${e.message}`, + ) + default: + assertNever(r) } } catch (e) { - push(`Error changing role for "${name}"`) + push(`Could not change role for user "${name}": ${e}`) // eslint-disable-next-line no-console console.error('Error chaging role', { name, roleName }) // eslint-disable-next-line no-console console.dir(e) + Sentry.captureException(e) throw e } }, From 298587bd0e40e3b13485bf7004623b2efa43b9db Mon Sep 17 00:00:00 2001 From: nl_0 Date: Fri, 24 May 2024 09:51:42 +0200 Subject: [PATCH 08/69] update gql schema: me, roles, role switching --- catalog/app/model/graphql/schema.generated.ts | 142 ++++++++++++++++++ catalog/app/model/graphql/types.generated.ts | 22 +++ catalog/app/utils/GraphQL/Provider.tsx | 2 + shared/graphql/schema.graphql | 18 +++ 4 files changed, 184 insertions(+) diff --git a/catalog/app/model/graphql/schema.generated.ts b/catalog/app/model/graphql/schema.generated.ts index 56911ce6145..06c6e5d9d78 100644 --- a/catalog/app/model/graphql/schema.generated.ts +++ b/catalog/app/model/graphql/schema.generated.ts @@ -1349,6 +1349,79 @@ export default { ], interfaces: [], }, + { + kind: 'OBJECT', + name: 'Me', + fields: [ + { + name: 'name', + type: { + kind: 'NON_NULL', + ofType: { + kind: 'SCALAR', + name: 'String', + ofType: null, + }, + }, + args: [], + }, + { + name: 'email', + type: { + kind: 'NON_NULL', + ofType: { + kind: 'SCALAR', + name: 'String', + ofType: null, + }, + }, + args: [], + }, + { + name: 'isAdmin', + type: { + kind: 'NON_NULL', + ofType: { + kind: 'SCALAR', + name: 'Boolean', + ofType: null, + }, + }, + args: [], + }, + { + name: 'role', + type: { + kind: 'NON_NULL', + ofType: { + kind: 'OBJECT', + name: 'MyRole', + ofType: null, + }, + }, + args: [], + }, + { + name: 'roles', + type: { + kind: 'NON_NULL', + ofType: { + kind: 'LIST', + ofType: { + kind: 'NON_NULL', + ofType: { + kind: 'OBJECT', + name: 'MyRole', + ofType: null, + }, + }, + }, + }, + args: [], + }, + ], + interfaces: [], + }, { kind: 'OBJECT', name: 'MutateUserAdminMutations', @@ -1477,6 +1550,30 @@ export default { kind: 'OBJECT', name: 'Mutation', fields: [ + { + name: 'switchRole', + type: { + kind: 'NON_NULL', + ofType: { + kind: 'UNION', + name: 'SwitchRoleResult', + ofType: null, + }, + }, + args: [ + { + name: 'roleName', + type: { + kind: 'NON_NULL', + ofType: { + kind: 'SCALAR', + name: 'String', + ofType: null, + }, + }, + }, + ], + }, { name: 'packageConstruct', type: { @@ -2079,6 +2176,25 @@ export default { ], interfaces: [], }, + { + kind: 'OBJECT', + name: 'MyRole', + fields: [ + { + name: 'name', + type: { + kind: 'NON_NULL', + ofType: { + kind: 'SCALAR', + name: 'String', + ofType: null, + }, + }, + args: [], + }, + ], + interfaces: [], + }, { kind: 'OBJECT', name: 'NotificationConfigurationError', @@ -3500,6 +3616,18 @@ export default { kind: 'OBJECT', name: 'Query', fields: [ + { + name: 'me', + type: { + kind: 'NON_NULL', + ofType: { + kind: 'OBJECT', + name: 'Me', + ofType: null, + }, + }, + args: [], + }, { name: 'config', type: { @@ -4768,6 +4896,20 @@ export default { ], interfaces: [], }, + { + kind: 'UNION', + name: 'SwitchRoleResult', + possibleTypes: [ + { + kind: 'OBJECT', + name: 'Me', + }, + { + kind: 'OBJECT', + name: 'OperationError', + }, + ], + }, { kind: 'OBJECT', name: 'TestStats', diff --git a/catalog/app/model/graphql/types.generated.ts b/catalog/app/model/graphql/types.generated.ts index 805e5f366e5..76a586aa76c 100644 --- a/catalog/app/model/graphql/types.generated.ts +++ b/catalog/app/model/graphql/types.generated.ts @@ -319,6 +319,15 @@ export interface ManagedRoleInput { readonly policies: ReadonlyArray } +export interface Me { + readonly __typename: 'Me' + readonly name: Scalars['String'] + readonly email: Scalars['String'] + readonly isAdmin: Scalars['Boolean'] + readonly role: MyRole + readonly roles: ReadonlyArray +} + export interface MutateUserAdminMutations { readonly __typename: 'MutateUserAdminMutations' readonly delete: OperationResult @@ -347,6 +356,7 @@ export interface MutateUserAdminMutationssetActiveArgs { export interface Mutation { readonly __typename: 'Mutation' + readonly switchRole: SwitchRoleResult readonly packageConstruct: PackageConstructResult readonly packagePromote: PackagePromoteResult readonly packageRevisionDelete: PackageRevisionDeleteResult @@ -370,6 +380,10 @@ export interface Mutation { readonly browsingSessionDispose: BrowsingSessionDisposeResult } +export interface MutationswitchRoleArgs { + roleName: Scalars['String'] +} + export interface MutationpackageConstructArgs { params: PackagePushParams src: PackageConstructSource @@ -461,6 +475,11 @@ export interface MutationbrowsingSessionDisposeArgs { id: Scalars['ID'] } +export interface MyRole { + readonly __typename: 'MyRole' + readonly name: Scalars['String'] +} + export interface NotificationConfigurationError { readonly __typename: 'NotificationConfigurationError' readonly _: Maybe @@ -796,6 +815,7 @@ export type PolicyResult = Policy | InvalidInput | OperationError export interface Query { readonly __typename: 'Query' + readonly me: Me readonly config: Config readonly bucketConfigs: ReadonlyArray readonly bucketConfig: Maybe @@ -1054,6 +1074,8 @@ export interface SubscriptionState { readonly timestamp: Scalars['Datetime'] } +export type SwitchRoleResult = Me | OperationError + export interface TestStats { readonly __typename: 'TestStats' readonly passed: Scalars['Int'] diff --git a/catalog/app/utils/GraphQL/Provider.tsx b/catalog/app/utils/GraphQL/Provider.tsx index 3a3729135c1..75197214f48 100644 --- a/catalog/app/utils/GraphQL/Provider.tsx +++ b/catalog/app/utils/GraphQL/Provider.tsx @@ -132,6 +132,8 @@ export default function GraphQLProvider({ children }: React.PropsWithChildren<{} PackagesSearchResultSet: () => null, InvalidInput: () => null, InputError: () => null, + Me: (me) => me.name as string, + MyRole: (r) => r.name as string, User: (u) => (u.name as string) ?? null, AdminQueries: () => null, UserAdminQueries: () => null, diff --git a/shared/graphql/schema.graphql b/shared/graphql/schema.graphql index 022a26de908..e7f38469c6d 100644 --- a/shared/graphql/schema.graphql +++ b/shared/graphql/schema.graphql @@ -500,7 +500,23 @@ type AdminQueries { user: UserAdminQueries! } +type MyRole { + name: String! +} + +type Me { + name: String! + email: String! + isAdmin: Boolean! + role: MyRole! + roles: [MyRole!]! +} + +union SwitchRoleResult = Me | OperationError + type Query { + me: Me! + config: Config! bucketConfigs: [BucketConfig!]! bucketConfig(name: String!): BucketConfig @@ -845,6 +861,8 @@ type AdminMutations { } type Mutation { + switchRole(roleName: String!): SwitchRoleResult! + packageConstruct( params: PackagePushParams! src: PackageConstructSource! From fed8df407c468faaee905265e5c9207888fb1b9b Mon Sep 17 00:00:00 2001 From: nl_0 Date: Fri, 24 May 2024 09:57:15 +0200 Subject: [PATCH 09/69] gql for querying "me" and role switching --- .../app/containers/NavBar/gql/Me.generated.ts | 70 ++++++++++ catalog/app/containers/NavBar/gql/Me.graphql | 9 ++ .../NavBar/gql/SwitchRole.generated.ts | 120 ++++++++++++++++++ .../containers/NavBar/gql/SwitchRole.graphql | 16 +++ 4 files changed, 215 insertions(+) create mode 100644 catalog/app/containers/NavBar/gql/Me.generated.ts create mode 100644 catalog/app/containers/NavBar/gql/Me.graphql create mode 100644 catalog/app/containers/NavBar/gql/SwitchRole.generated.ts create mode 100644 catalog/app/containers/NavBar/gql/SwitchRole.graphql diff --git a/catalog/app/containers/NavBar/gql/Me.generated.ts b/catalog/app/containers/NavBar/gql/Me.generated.ts new file mode 100644 index 00000000000..fa4e5066e3f --- /dev/null +++ b/catalog/app/containers/NavBar/gql/Me.generated.ts @@ -0,0 +1,70 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core' +import * as Types from '../../../model/graphql/types.generated' + +export type containers_NavBar_gql_MeQueryVariables = Types.Exact<{ [key: string]: never }> + +export type containers_NavBar_gql_MeQuery = { readonly __typename: 'Query' } & { + readonly me: { readonly __typename: 'Me' } & Pick< + Types.Me, + 'name' | 'email' | 'isAdmin' + > & { + readonly role: { readonly __typename: 'MyRole' } & Pick + readonly roles: ReadonlyArray< + { readonly __typename: 'MyRole' } & Pick + > + } +} + +export const containers_NavBar_gql_MeDocument = { + kind: 'Document', + definitions: [ + { + kind: 'OperationDefinition', + operation: 'query', + name: { kind: 'Name', value: 'containers_NavBar_gql_Me' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'me' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'name' } }, + { kind: 'Field', name: { kind: 'Name', value: 'email' } }, + { kind: 'Field', name: { kind: 'Name', value: 'isAdmin' } }, + { + kind: 'Field', + name: { kind: 'Name', value: 'role' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'name' } }, + ], + }, + }, + { + kind: 'Field', + name: { kind: 'Name', value: 'roles' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'name' } }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ], +} as unknown as DocumentNode< + containers_NavBar_gql_MeQuery, + containers_NavBar_gql_MeQueryVariables +> + +export { containers_NavBar_gql_MeDocument as default } diff --git a/catalog/app/containers/NavBar/gql/Me.graphql b/catalog/app/containers/NavBar/gql/Me.graphql new file mode 100644 index 00000000000..219f41ac532 --- /dev/null +++ b/catalog/app/containers/NavBar/gql/Me.graphql @@ -0,0 +1,9 @@ +query { + me { + name + email + isAdmin + role { name } + roles { name } + } +} diff --git a/catalog/app/containers/NavBar/gql/SwitchRole.generated.ts b/catalog/app/containers/NavBar/gql/SwitchRole.generated.ts new file mode 100644 index 00000000000..e7493a128f7 --- /dev/null +++ b/catalog/app/containers/NavBar/gql/SwitchRole.generated.ts @@ -0,0 +1,120 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core' +import * as Types from '../../../model/graphql/types.generated' + +export type containers_NavBar_gql_SwitchRoleMutationVariables = Types.Exact<{ + roleName: Types.Scalars['String'] +}> + +export type containers_NavBar_gql_SwitchRoleMutation = { + readonly __typename: 'Mutation' +} & { + readonly switchRole: + | ({ readonly __typename: 'Me' } & Pick & { + readonly role: { readonly __typename: 'MyRole' } & Pick + readonly roles: ReadonlyArray< + { readonly __typename: 'MyRole' } & Pick + > + }) + | ({ readonly __typename: 'OperationError' } & Pick< + Types.OperationError, + 'message' | 'name' + >) +} + +export const containers_NavBar_gql_SwitchRoleDocument = { + kind: 'Document', + definitions: [ + { + kind: 'OperationDefinition', + operation: 'mutation', + name: { kind: 'Name', value: 'containers_NavBar_gql_SwitchRole' }, + variableDefinitions: [ + { + kind: 'VariableDefinition', + variable: { kind: 'Variable', name: { kind: 'Name', value: 'roleName' } }, + type: { + kind: 'NonNullType', + type: { kind: 'NamedType', name: { kind: 'Name', value: 'String' } }, + }, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'switchRole' }, + arguments: [ + { + kind: 'Argument', + name: { kind: 'Name', value: 'roleName' }, + value: { kind: 'Variable', name: { kind: 'Name', value: 'roleName' } }, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, + { + kind: 'InlineFragment', + typeCondition: { + kind: 'NamedType', + name: { kind: 'Name', value: 'Me' }, + }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'name' } }, + { kind: 'Field', name: { kind: 'Name', value: 'email' } }, + { kind: 'Field', name: { kind: 'Name', value: 'isAdmin' } }, + { + kind: 'Field', + name: { kind: 'Name', value: 'role' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'name' } }, + ], + }, + }, + { + kind: 'Field', + name: { kind: 'Name', value: 'roles' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'name' } }, + ], + }, + }, + ], + }, + }, + { + kind: 'InlineFragment', + typeCondition: { + kind: 'NamedType', + name: { kind: 'Name', value: 'OperationError' }, + }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'message' } }, + { kind: 'Field', name: { kind: 'Name', value: 'name' } }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ], +} as unknown as DocumentNode< + containers_NavBar_gql_SwitchRoleMutation, + containers_NavBar_gql_SwitchRoleMutationVariables +> + +export { containers_NavBar_gql_SwitchRoleDocument as default } diff --git a/catalog/app/containers/NavBar/gql/SwitchRole.graphql b/catalog/app/containers/NavBar/gql/SwitchRole.graphql new file mode 100644 index 00000000000..b2858b7cc8e --- /dev/null +++ b/catalog/app/containers/NavBar/gql/SwitchRole.graphql @@ -0,0 +1,16 @@ +mutation ($roleName: String!) { + switchRole(roleName: $roleName) { + __typename + ... on Me { + name + email + isAdmin + role { name } + roles { name } + } + ... on OperationError { + message + name + } + } +} From a83e32e238e575584f207cba7517e5646a73c822 Mon Sep 17 00:00:00 2001 From: Maksim Chervonnyi Date: Fri, 24 May 2024 13:10:55 +0200 Subject: [PATCH 10/69] Switch roles draft UI --- catalog/app/containers/NavBar/NavBar.tsx | 136 +++++++++++++++++++++-- 1 file changed, 126 insertions(+), 10 deletions(-) diff --git a/catalog/app/containers/NavBar/NavBar.tsx b/catalog/app/containers/NavBar/NavBar.tsx index cd36a641d77..60cc4bc1680 100644 --- a/catalog/app/containers/NavBar/NavBar.tsx +++ b/catalog/app/containers/NavBar/NavBar.tsx @@ -14,6 +14,8 @@ import * as style from 'constants/style' import * as URLS from 'constants/urls' import * as authSelectors from 'containers/Auth/selectors' import * as CatalogSettings from 'utils/CatalogSettings' +import * as Dialogs from 'utils/GlobalDialogs' +import * as GQL from 'utils/GraphQL' import HashLink from 'utils/HashLink' import * as NamedRoutes from 'utils/NamedRoutes' @@ -21,6 +23,14 @@ import bg from './bg.png' import Controls from './Controls' import * as Subscription from './Subscription' +import ME_QUERY from './gql/Me.generated' + +type CurrentUser = GQL.DataForDoc['me'] + +const SWITCH_ROLES_DIALOG_PROPS: Dialogs.ExtraDialogProps = { + maxWidth: 'sm', + fullWidth: true, +} const useLogoLinkStyles = M.makeStyles((t) => ({ bgQuilt: { @@ -158,8 +168,72 @@ function Badge({ children, color, invisible, ...props }: BadgeProps) { ) } -function UserDropdown() { - const user = redux.useSelector(selectUser) +const useRolesSwitcherStyles = M.makeStyles((t) => ({ + progress: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + padding: t.spacing(4), + }, +})) + +const useListItemTextStyles = M.makeStyles((t) => ({ + root: { + padding: t.spacing(0, 1), + }, + primary: { + overflow: 'hidden', + textOverflow: 'ellipsis', + }, +})) + +interface RolesSwitcherProps { + user: CurrentUser + close: Dialogs.Close +} + +function RolesSwitcher({ close, user }: RolesSwitcherProps) { + const classes = useRolesSwitcherStyles() + const textClasses = useListItemTextStyles() + const [clicked, setClicked] = React.useState(false) + const handleClick = React.useCallback(() => { + setClicked(true) + setTimeout(() => { + close() + window.location.reload() + }, 1000) + }, [close]) + return ( + <> + Switch role + {clicked ? ( +
+ +
+ ) : ( + + {user.roles.map((role) => ( + + {role.name} + + ))} + + )} + + ) +} + +interface UserDropdownProps { + user: CurrentUser +} + +function UserDropdown({ user }: UserDropdownProps) { const { urls, paths } = NamedRoutes.use() const bookmarks = Bookmarks.use() const isProfile = !!useRouteMatch({ path: paths.profile, exact: true }) @@ -174,16 +248,27 @@ function UserDropdown() { [setAnchor], ) - const close = React.useCallback(() => { + const closeDropdown = React.useCallback(() => { setVisible(false) setAnchor(null) }, [setAnchor]) + const openDialog = Dialogs.use() + const showBookmarks = React.useCallback(() => { if (!bookmarks) return bookmarks.show() - close() - }, [bookmarks, close]) + closeDropdown() + }, [bookmarks, closeDropdown]) + + const showRolesSwitcher = React.useCallback( + () => + openDialog( + ({ close }) => , + SWITCH_ROLES_DIALOG_PROPS, + ), + [openDialog, user], + ) React.useEffect(() => { const hasUpdates = bookmarks?.hasUpdates || false @@ -200,7 +285,7 @@ function UserDropdown() { - + {bookmarks && ( @@ -209,17 +294,22 @@ function UserDropdown() {  Bookmarks )} + {user.roles.length && ( + + loop Switch role + + )} {user.isAdmin && ( - + security Admin settings )} {cfg.mode === 'OPEN' && ( - + Profile )} - + Sign Out @@ -602,6 +692,32 @@ export function NavBar() { const intercom = Intercom.use() const classes = useNavBarStyles() const sub = Subscription.useState() + + const userLegacy = redux.useSelector(selectUser) + const user = { + __typename: 'Me', + email: 'FIXME@FIX.ME', + role: { + __typename: 'MyRole', + name: 'ReadWriteQuilt', + }, + roles: [ + { + __typename: 'MyRole', + name: 'ReadQuilt', + }, + { + __typename: 'MyRole', + name: 'ReadWriteQuilt', + }, + { + __typename: 'MyRole', + name: 'InternalFrameInternalFrameTitlePaneInternalFrameTitlePaneMaximizeButtonWindowNotFocusedState', + }, + ], + ...userLegacy, + } as CurrentUser + return ( @@ -645,7 +761,7 @@ export function NavBar() { cfg.mode !== 'LOCAL' && !useHamburger && (authenticated ? ( - + ) : ( !isSignIn && ))} From c1fb57422411b021822015e4de189ef8c8ffe093 Mon Sep 17 00:00:00 2001 From: nl_0 Date: Fri, 24 May 2024 15:03:52 +0200 Subject: [PATCH 11/69] update gql schema and generated code --- catalog/app/model/graphql/schema.generated.ts | 72 +++++++++++++++++-- catalog/app/model/graphql/types.generated.ts | 11 ++- shared/graphql/schema.graphql | 8 ++- 3 files changed, 80 insertions(+), 11 deletions(-) diff --git a/catalog/app/model/graphql/schema.generated.ts b/catalog/app/model/graphql/schema.generated.ts index 06c6e5d9d78..3853548b941 100644 --- a/catalog/app/model/graphql/schema.generated.ts +++ b/catalog/app/model/graphql/schema.generated.ts @@ -1483,6 +1483,47 @@ export default { }, ], }, + { + name: 'setRoles', + type: { + kind: 'NON_NULL', + ofType: { + kind: 'UNION', + name: 'UserResult', + ofType: null, + }, + }, + args: [ + { + name: 'roleNames', + type: { + kind: 'NON_NULL', + ofType: { + kind: 'LIST', + ofType: { + kind: 'NON_NULL', + ofType: { + kind: 'SCALAR', + name: 'String', + ofType: null, + }, + }, + }, + }, + }, + { + name: 'activeRoleName', + type: { + kind: 'NON_NULL', + ofType: { + kind: 'SCALAR', + name: 'String', + ofType: null, + }, + }, + }, + ], + }, { name: 'setAdmin', type: { @@ -3619,12 +3660,9 @@ export default { { name: 'me', type: { - kind: 'NON_NULL', - ofType: { - kind: 'OBJECT', - name: 'Me', - ofType: null, - }, + kind: 'OBJECT', + name: 'Me', + ofType: null, }, args: [], }, @@ -4904,6 +4942,10 @@ export default { kind: 'OBJECT', name: 'Me', }, + { + kind: 'OBJECT', + name: 'InvalidInput', + }, { kind: 'OBJECT', name: 'OperationError', @@ -5206,6 +5248,24 @@ export default { }, args: [], }, + { + name: 'roles', + type: { + kind: 'NON_NULL', + ofType: { + kind: 'LIST', + ofType: { + kind: 'NON_NULL', + ofType: { + kind: 'UNION', + name: 'Role', + ofType: null, + }, + }, + }, + }, + args: [], + }, ], interfaces: [], }, diff --git a/catalog/app/model/graphql/types.generated.ts b/catalog/app/model/graphql/types.generated.ts index 76a586aa76c..ab6e4d49dc6 100644 --- a/catalog/app/model/graphql/types.generated.ts +++ b/catalog/app/model/graphql/types.generated.ts @@ -333,6 +333,7 @@ export interface MutateUserAdminMutations { readonly delete: OperationResult readonly setEmail: UserResult readonly setRole: UserResult + readonly setRoles: UserResult readonly setAdmin: UserResult readonly setActive: UserResult readonly resetPassword: OperationResult @@ -346,6 +347,11 @@ export interface MutateUserAdminMutationssetRoleArgs { roleName: Maybe } +export interface MutateUserAdminMutationssetRolesArgs { + roleNames: ReadonlyArray + activeRoleName: Scalars['String'] +} + export interface MutateUserAdminMutationssetAdminArgs { admin: Scalars['Boolean'] } @@ -815,7 +821,7 @@ export type PolicyResult = Policy | InvalidInput | OperationError export interface Query { readonly __typename: 'Query' - readonly me: Me + readonly me: Maybe readonly config: Config readonly bucketConfigs: ReadonlyArray readonly bucketConfig: Maybe @@ -1074,7 +1080,7 @@ export interface SubscriptionState { readonly timestamp: Scalars['Datetime'] } -export type SwitchRoleResult = Me | OperationError +export type SwitchRoleResult = Me | InvalidInput | OperationError export interface TestStats { readonly __typename: 'TestStats' @@ -1133,6 +1139,7 @@ export interface User { readonly isSsoOnly: Scalars['Boolean'] readonly isService: Scalars['Boolean'] readonly role: Maybe + readonly roles: ReadonlyArray } export interface UserAdminMutations { diff --git a/shared/graphql/schema.graphql b/shared/graphql/schema.graphql index e7f38469c6d..fcae6dd2ffd 100644 --- a/shared/graphql/schema.graphql +++ b/shared/graphql/schema.graphql @@ -200,7 +200,8 @@ type User { isAdmin: Boolean! isSsoOnly: Boolean! isService: Boolean! - role: Role + role: Role # XXX: activeRole? + roles: [Role!]! # XXX: better name } type AccessCountForDate { @@ -512,10 +513,10 @@ type Me { roles: [MyRole!]! } -union SwitchRoleResult = Me | OperationError +union SwitchRoleResult = Me | InvalidInput | OperationError type Query { - me: Me! + me: Me config: Config! bucketConfigs: [BucketConfig!]! @@ -846,6 +847,7 @@ type MutateUserAdminMutations { delete: OperationResult! setEmail(email: String!): UserResult! setRole(roleName: String): UserResult! + setRoles(roleNames: [String!]!, activeRoleName: String!): UserResult! setAdmin(admin: Boolean!): UserResult! setActive(active: Boolean!): UserResult! resetPassword: OperationResult! From 447ec9aa7e7935761620b367f410ff37c6b89346 Mon Sep 17 00:00:00 2001 From: nl_0 Date: Fri, 24 May 2024 15:06:49 +0200 Subject: [PATCH 12/69] add roles to UserSelection --- .../Users/gql/UserSelection.generated.ts | 45 +++++++++++++++++++ .../Admin/Users/gql/UserSelection.graphql | 11 +++++ 2 files changed, 56 insertions(+) diff --git a/catalog/app/containers/Admin/Users/gql/UserSelection.generated.ts b/catalog/app/containers/Admin/Users/gql/UserSelection.generated.ts index 0368d91f12b..2993119988d 100644 --- a/catalog/app/containers/Admin/Users/gql/UserSelection.generated.ts +++ b/catalog/app/containers/Admin/Users/gql/UserSelection.generated.ts @@ -20,6 +20,13 @@ export type UserSelectionFragment = { readonly __typename: 'User' } & Pick< >) | ({ readonly __typename: 'ManagedRole' } & Pick) > + readonly roles: ReadonlyArray< + | ({ readonly __typename: 'UnmanagedRole' } & Pick< + Types.UnmanagedRole, + 'id' | 'name' + >) + | ({ readonly __typename: 'ManagedRole' } & Pick) + > } export const UserSelectionFragmentDoc = { @@ -79,6 +86,44 @@ export const UserSelectionFragmentDoc = { ], }, }, + { + kind: 'Field', + name: { kind: 'Name', value: 'roles' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, + { + kind: 'InlineFragment', + typeCondition: { + kind: 'NamedType', + name: { kind: 'Name', value: 'ManagedRole' }, + }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'id' } }, + { kind: 'Field', name: { kind: 'Name', value: 'name' } }, + ], + }, + }, + { + kind: 'InlineFragment', + typeCondition: { + kind: 'NamedType', + name: { kind: 'Name', value: 'UnmanagedRole' }, + }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'id' } }, + { kind: 'Field', name: { kind: 'Name', value: 'name' } }, + ], + }, + }, + ], + }, + }, ], }, }, diff --git a/catalog/app/containers/Admin/Users/gql/UserSelection.graphql b/catalog/app/containers/Admin/Users/gql/UserSelection.graphql index 349a0304e63..17fddeaddee 100644 --- a/catalog/app/containers/Admin/Users/gql/UserSelection.graphql +++ b/catalog/app/containers/Admin/Users/gql/UserSelection.graphql @@ -19,4 +19,15 @@ fragment UserSelection on User { name } } + roles { + __typename + ... on ManagedRole { + id + name + } + ... on UnmanagedRole { + id + name + } + } } From da795db3b79ff8951d78214767c3ab11e115629e Mon Sep 17 00:00:00 2001 From: nl_0 Date: Fri, 24 May 2024 15:07:44 +0200 Subject: [PATCH 13/69] utils/Dialogs: Close: default type param to never --- catalog/app/utils/Dialogs.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/catalog/app/utils/Dialogs.tsx b/catalog/app/utils/Dialogs.tsx index 3b78a955ecc..efd4370c056 100644 --- a/catalog/app/utils/Dialogs.tsx +++ b/catalog/app/utils/Dialogs.tsx @@ -8,7 +8,7 @@ type DialogState = 'open' | 'closing' | 'closed' export type ExtraDialogProps = Omit -export type Close = [R] extends [never] ? () => void : Resolver['resolve'] +export type Close = [R] extends [never] ? () => void : Resolver['resolve'] type Render = (props: { close: Close }) => JSX.Element From c0a1e0082e37c59e39ddd0b4593349e8a4e462bb Mon Sep 17 00:00:00 2001 From: nl_0 Date: Fri, 24 May 2024 15:08:53 +0200 Subject: [PATCH 14/69] NavBar: regen gql --- .../app/containers/NavBar/gql/Me.generated.ts | 17 ++++++++--------- .../NavBar/gql/SwitchRole.generated.ts | 1 + 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/catalog/app/containers/NavBar/gql/Me.generated.ts b/catalog/app/containers/NavBar/gql/Me.generated.ts index fa4e5066e3f..33bce341948 100644 --- a/catalog/app/containers/NavBar/gql/Me.generated.ts +++ b/catalog/app/containers/NavBar/gql/Me.generated.ts @@ -5,15 +5,14 @@ import * as Types from '../../../model/graphql/types.generated' export type containers_NavBar_gql_MeQueryVariables = Types.Exact<{ [key: string]: never }> export type containers_NavBar_gql_MeQuery = { readonly __typename: 'Query' } & { - readonly me: { readonly __typename: 'Me' } & Pick< - Types.Me, - 'name' | 'email' | 'isAdmin' - > & { - readonly role: { readonly __typename: 'MyRole' } & Pick - readonly roles: ReadonlyArray< - { readonly __typename: 'MyRole' } & Pick - > - } + readonly me: Types.Maybe< + { readonly __typename: 'Me' } & Pick & { + readonly role: { readonly __typename: 'MyRole' } & Pick + readonly roles: ReadonlyArray< + { readonly __typename: 'MyRole' } & Pick + > + } + > } export const containers_NavBar_gql_MeDocument = { diff --git a/catalog/app/containers/NavBar/gql/SwitchRole.generated.ts b/catalog/app/containers/NavBar/gql/SwitchRole.generated.ts index e7493a128f7..af6b4f146a2 100644 --- a/catalog/app/containers/NavBar/gql/SwitchRole.generated.ts +++ b/catalog/app/containers/NavBar/gql/SwitchRole.generated.ts @@ -16,6 +16,7 @@ export type containers_NavBar_gql_SwitchRoleMutation = { { readonly __typename: 'MyRole' } & Pick > }) + | { readonly __typename: 'InvalidInput' } | ({ readonly __typename: 'OperationError' } & Pick< Types.OperationError, 'message' | 'name' From 9e26deec666b478658942e4290c0efab4a6dfb09 Mon Sep 17 00:00:00 2001 From: nl_0 Date: Fri, 24 May 2024 15:51:51 +0200 Subject: [PATCH 15/69] NavBar: get user from gql --- catalog/app/containers/NavBar/NavBar.tsx | 153 +++++++++++------------ 1 file changed, 71 insertions(+), 82 deletions(-) diff --git a/catalog/app/containers/NavBar/NavBar.tsx b/catalog/app/containers/NavBar/NavBar.tsx index 60cc4bc1680..9fc4d9239d6 100644 --- a/catalog/app/containers/NavBar/NavBar.tsx +++ b/catalog/app/containers/NavBar/NavBar.tsx @@ -1,3 +1,4 @@ +import invariant from 'invariant' import * as R from 'ramda' import * as React from 'react' import * as redux from 'react-redux' @@ -25,7 +26,20 @@ import Controls from './Controls' import * as Subscription from './Subscription' import ME_QUERY from './gql/Me.generated' -type CurrentUser = GQL.DataForDoc['me'] +type MaybeMe = GQL.DataForDoc['me'] +type Me = NonNullable + +function useMe(pause: boolean) { + const res = GQL.useQuery(ME_QUERY, undefined, { pause }) + return GQL.fold(res, { + data: (d) => { + invariant(d.me, 'Expected "me" to be non-null') + return d.me + }, + fetching: () => 'fetching' as const, + error: () => 'error' as const, + }) +} const SWITCH_ROLES_DIALOG_PROPS: Dialogs.ExtraDialogProps = { maxWidth: 'sm', @@ -125,12 +139,7 @@ const Item = React.forwardRef( }, ) -const selectUser = createStructuredSelector({ - name: authSelectors.username, - isAdmin: authSelectors.isAdmin, -}) - -const userDisplay = (user: $TSFixMe) => ( +const userDisplay = (user: Me) => ( <> {user.isAdmin && ( <> @@ -188,7 +197,7 @@ const useListItemTextStyles = M.makeStyles((t) => ({ })) interface RolesSwitcherProps { - user: CurrentUser + user: Me close: Dialogs.Close } @@ -203,6 +212,7 @@ function RolesSwitcher({ close, user }: RolesSwitcherProps) { window.location.reload() }, 1000) }, [close]) + // TODO: don't show role switcher if only one role return ( <> Switch role @@ -230,7 +240,7 @@ function RolesSwitcher({ close, user }: RolesSwitcherProps) { } interface UserDropdownProps { - user: CurrentUser + user: Me } function UserDropdown({ user }: UserDropdownProps) { @@ -294,7 +304,7 @@ function UserDropdown({ user }: UserDropdownProps) {  Bookmarks )} - {user.roles.length && ( + {user.roles.length > 1 && ( loop Switch role @@ -365,58 +375,59 @@ interface AuthHamburgerProps { } function AuthHamburger({ authenticated, waiting, error }: AuthHamburgerProps) { - const user = redux.useSelector(selectUser) const { urls, paths } = NamedRoutes.use() const isProfile = !!useRouteMatch({ path: paths.profile, exact: true }) const isAdmin = !!useRouteMatch(paths.admin) const ham = useHam() const links = useLinks() + + const user = useMe(!authenticated || waiting) + + let children: React.ReactNode[] = [] + if (!authenticated || user === 'error') { + children = [ + + {error && ( + <> + error_outline{' '} + + )} + Sign In + , + ] + } else if (waiting || user === 'fetching') { + children = [ + + + , + ] + } else { + children = [ + + {userDisplay(user)} + , + user.isAdmin && ( + + + security +  Admin settings + + ), + cfg.mode === 'OPEN' && ( + + + Profile + + ), + + + Sign Out + , + ] + } + return ham.render([ - ...// eslint-disable-next-line no-nested-ternary - (authenticated - ? [ - - {userDisplay(user)} - , - user.isAdmin && ( - - - security -  Admin settings - - ), - cfg.mode === 'OPEN' && ( - - - Profile - - ), - - - Sign Out - , - ] - : waiting - ? [ - - - , - ] - : [ - - {error && ( - <> - error_outline{' '} - - )} - Sign In - , - ]), + ...children, , ...links.map(({ label, ...rest }) => ( @@ -693,30 +704,7 @@ export function NavBar() { const classes = useNavBarStyles() const sub = Subscription.useState() - const userLegacy = redux.useSelector(selectUser) - const user = { - __typename: 'Me', - email: 'FIXME@FIX.ME', - role: { - __typename: 'MyRole', - name: 'ReadWriteQuilt', - }, - roles: [ - { - __typename: 'MyRole', - name: 'ReadQuilt', - }, - { - __typename: 'MyRole', - name: 'ReadWriteQuilt', - }, - { - __typename: 'MyRole', - name: 'InternalFrameInternalFrameTitlePaneInternalFrameTitlePaneMaximizeButtonWindowNotFocusedState', - }, - ], - ...userLegacy, - } as CurrentUser + const user = useMe(!authenticated || waiting) return ( @@ -760,10 +748,11 @@ export function NavBar() { {!cfg.disableNavigator && cfg.mode !== 'LOCAL' && !useHamburger && - (authenticated ? ( + (authenticated && user !== 'error' && user !== 'fetching' ? ( + // TODO: refactor gql query states ) : ( - !isSignIn && + !isSignIn && ))} {useHamburger && From 1258d7aeda2e388937d966d141acfe7caec8603d Mon Sep 17 00:00:00 2001 From: nl_0 Date: Fri, 24 May 2024 16:17:44 +0200 Subject: [PATCH 16/69] admin: set roles ui tracer bullet --- catalog/app/containers/Admin/Users/Users.tsx | 219 +++++++++++++----- ...generated.ts => UserSetRoles.generated.ts} | 49 ++-- ...erSetRole.graphql => UserSetRoles.graphql} | 4 +- 3 files changed, 198 insertions(+), 74 deletions(-) rename catalog/app/containers/Admin/Users/gql/{UserSetRole.generated.ts => UserSetRoles.generated.ts} (75%) rename catalog/app/containers/Admin/Users/gql/{UserSetRole.graphql => UserSetRoles.graphql} (53%) diff --git a/catalog/app/containers/Admin/Users/Users.tsx b/catalog/app/containers/Admin/Users/Users.tsx index 19b9b1eddae..94581e2cfe6 100644 --- a/catalog/app/containers/Admin/Users/Users.tsx +++ b/catalog/app/containers/Admin/Users/Users.tsx @@ -23,7 +23,7 @@ import USERS_QUERY from './gql/Users.generated' import USER_CREATE_MUTATION from './gql/UserCreate.generated' import USER_DELETE_MUTATION from './gql/UserDelete.generated' import USER_SET_EMAIL_MUTATION from './gql/UserSetEmail.generated' -import USER_SET_ROLE_MUTATION from './gql/UserSetRole.generated' +import USER_SET_ROLES_MUTATION from './gql/UserSetRoles.generated' import USER_SET_ACTIVE_MUTATION from './gql/UserSetActive.generated' import USER_SET_ADMIN_MUTATION from './gql/UserSetAdmin.generated' @@ -524,6 +524,156 @@ function Username({ className, admin = false, children, ...props }: UsernameProp ) } +interface EditRolesProps { + close: Dialogs.Close + roles: readonly Role[] + user: User +} + +function EditRoles({ close, roles, user }: EditRolesProps) { + const { push } = Notifications.use() + const setRoles = GQL.useMutation(USER_SET_ROLES_MUTATION) + + const onSubmit = React.useCallback( + async (values) => { + // console.log('submit', values) + // XXX: use formspec to convert/validate form values into gql input? + const input = { + name: user.name, + roleNames: values.roles.map((r: Role) => r.name), + activeRoleName: values.roles[0].name, + } + try { + const data = await setRoles(input) + const r = data.admin.user.mutate?.setRoles + switch (r?.__typename) { + case undefined: + throw new Error('User not found') // should not happend + case 'User': + close() + push('Changes saved') + return + case 'OperationError': + // TODO + throw new Error(`Unexpected operation error: [${r.name}] ${r.message}`) + case 'InvalidInput': + // TODO + const [e] = r.errors + throw new Error( + `Unexpected input error at '${e.path}': [${e.name}] ${e.message}`, + ) + default: + return assertNever(r) + } + } catch (e) { + // eslint-disable-next-line no-console + console.error('Error setting roles for user', input) + // eslint-disable-next-line no-console + console.dir(e) + Sentry.captureException(e) + return { [FF.FORM_ERROR]: 'unexpected' } + } + }, + [push, close, setRoles, user.name], + ) + + const handleToggle = (selected: Role[], role: Role) => + selected.find((r) => r.id === role.id) + ? selected.filter((r) => r.id !== role.id) + : [...selected, role] + + return ( + + {({ + handleSubmit, + submitting, + submitError, + submitFailed, + error, + hasSubmitErrors, + hasValidationErrors, + modifiedSinceLastSubmit, + }) => ( + <> + Configure roles for {user.name} + +
+ + name="roles" + // validate={validators.required as FF.FieldValidator} + // errors={{ + // required: 'Enter a username', + // taken: 'Username already taken', + // invalid: ( + // <> + // Enter a valid username{' '} + // + // info + // + // + // ), + // }} + // autoComplete="off" + > + {({ input: { onChange, value } }) => ( + + {roles.map((role) => ( + onChange(handleToggle(value, role))} + > + + r.id === role.id)} + tabIndex={-1} + disableRipple + /> + + + + ))} + + )} + + {(!!error || !!submitError) && ( + + )} + + +
+ + + Cancel + + + Save + + + + )} +
+ ) +} + interface EditableRenderProps { change: (v: T) => void busy: boolean @@ -574,7 +724,6 @@ const emptyRole = '' interface ColumnDisplayProps { roles: readonly Role[] setActive: (name: string, active: boolean) => Promise - setRole: (name: string, roleName: string) => Promise openDialog: Dialogs.Open isSelf: boolean } @@ -616,23 +765,15 @@ const columns: Table.Column[] = [ id: 'role', label: 'Role', getValue: (u) => u.role?.name, - getDisplay: (v: string | undefined, u, { roles, setRole }: ColumnDisplayProps) => ( - setRole(u.name, role)}> - {({ change, busy, value }) => ( - change(e.target.value as string)} - disabled={busy} - renderValue={R.identity} - > - {roles.map((r) => ( - - {r.name} - - ))} - - )} - + getDisplay: (v: string | undefined, u, { roles, openDialog }: ColumnDisplayProps) => ( +
+ openDialog(({ close }) => ) + } + > + {v ?? emptyRole} + {u.roles.length > 1 && +{u.roles.length - 1}} +
), }, { @@ -726,44 +867,6 @@ function useSetActive() { ) } -function useSetRole() { - const { push } = Notifications.use() - const setRole = GQL.useMutation(USER_SET_ROLE_MUTATION) - - return React.useCallback( - async (name: string, roleName: string) => { - try { - const data = await setRole({ name, roleName }) - const r = data.admin.user.mutate?.setRole - switch (r?.__typename) { - case 'User': - return - case undefined: - throw new Error('User not found') // should not happend - case 'OperationError': - throw new Error(`Unexpected operation error: [${r.name}] ${r.message}`) - case 'InvalidInput': - const [e] = r.errors - throw new Error( - `Unexpected input error at '${e.path}': [${e.name}] ${e.message}`, - ) - default: - assertNever(r) - } - } catch (e) { - push(`Could not change role for user "${name}": ${e}`) - // eslint-disable-next-line no-console - console.error('Error chaging role', { name, roleName }) - // eslint-disable-next-line no-console - console.dir(e) - Sentry.captureException(e) - throw e - } - }, - [setRole, push], - ) -} - export default function Users() { const data = GQL.useQueryS(USERS_QUERY) const rows = data.admin.user.list @@ -773,7 +876,6 @@ export default function Users() { const openDialog = Dialogs.use() const setActive = useSetActive() - const setRole = useSetRole() const filtering = Table.useFiltering({ rows, @@ -825,7 +927,6 @@ export default function Users() { ] const getDisplayProps = (u: User): ColumnDisplayProps => ({ - setRole, setActive, roles, openDialog, diff --git a/catalog/app/containers/Admin/Users/gql/UserSetRole.generated.ts b/catalog/app/containers/Admin/Users/gql/UserSetRoles.generated.ts similarity index 75% rename from catalog/app/containers/Admin/Users/gql/UserSetRole.generated.ts rename to catalog/app/containers/Admin/Users/gql/UserSetRoles.generated.ts index 78b0b118444..6a4b139d83a 100644 --- a/catalog/app/containers/Admin/Users/gql/UserSetRole.generated.ts +++ b/catalog/app/containers/Admin/Users/gql/UserSetRoles.generated.ts @@ -9,19 +9,20 @@ import { UserResultSelectionFragmentDoc, } from './UserResultSelection.generated' -export type containers_Admin_Users_gql_UserSetRoleMutationVariables = Types.Exact<{ +export type containers_Admin_Users_gql_UserSetRolesMutationVariables = Types.Exact<{ name: Types.Scalars['String'] - roleName: Types.Scalars['String'] + roleNames: ReadonlyArray + activeRoleName: Types.Scalars['String'] }> -export type containers_Admin_Users_gql_UserSetRoleMutation = { +export type containers_Admin_Users_gql_UserSetRolesMutation = { readonly __typename: 'Mutation' } & { readonly admin: { readonly __typename: 'AdminMutations' } & { readonly user: { readonly __typename: 'UserAdminMutations' } & { readonly mutate: Types.Maybe< { readonly __typename: 'MutateUserAdminMutations' } & { - readonly setRole: + readonly setRoles: | ({ readonly __typename: 'User' } & UserResultSelection_User_Fragment) | ({ readonly __typename: 'InvalidInput' @@ -35,13 +36,13 @@ export type containers_Admin_Users_gql_UserSetRoleMutation = { } } -export const containers_Admin_Users_gql_UserSetRoleDocument = { +export const containers_Admin_Users_gql_UserSetRolesDocument = { kind: 'Document', definitions: [ { kind: 'OperationDefinition', operation: 'mutation', - name: { kind: 'Name', value: 'containers_Admin_Users_gql_UserSetRole' }, + name: { kind: 'Name', value: 'containers_Admin_Users_gql_UserSetRoles' }, variableDefinitions: [ { kind: 'VariableDefinition', @@ -53,7 +54,21 @@ export const containers_Admin_Users_gql_UserSetRoleDocument = { }, { kind: 'VariableDefinition', - variable: { kind: 'Variable', name: { kind: 'Name', value: 'roleName' } }, + variable: { kind: 'Variable', name: { kind: 'Name', value: 'roleNames' } }, + type: { + kind: 'NonNullType', + type: { + kind: 'ListType', + type: { + kind: 'NonNullType', + type: { kind: 'NamedType', name: { kind: 'Name', value: 'String' } }, + }, + }, + }, + }, + { + kind: 'VariableDefinition', + variable: { kind: 'Variable', name: { kind: 'Name', value: 'activeRoleName' } }, type: { kind: 'NonNullType', type: { kind: 'NamedType', name: { kind: 'Name', value: 'String' } }, @@ -93,14 +108,22 @@ export const containers_Admin_Users_gql_UserSetRoleDocument = { selections: [ { kind: 'Field', - name: { kind: 'Name', value: 'setRole' }, + name: { kind: 'Name', value: 'setRoles' }, arguments: [ { kind: 'Argument', - name: { kind: 'Name', value: 'roleName' }, + name: { kind: 'Name', value: 'roleNames' }, + value: { + kind: 'Variable', + name: { kind: 'Name', value: 'roleNames' }, + }, + }, + { + kind: 'Argument', + name: { kind: 'Name', value: 'activeRoleName' }, value: { kind: 'Variable', - name: { kind: 'Name', value: 'roleName' }, + name: { kind: 'Name', value: 'activeRoleName' }, }, }, ], @@ -129,8 +152,8 @@ export const containers_Admin_Users_gql_UserSetRoleDocument = { ...UserResultSelectionFragmentDoc.definitions, ], } as unknown as DocumentNode< - containers_Admin_Users_gql_UserSetRoleMutation, - containers_Admin_Users_gql_UserSetRoleMutationVariables + containers_Admin_Users_gql_UserSetRolesMutation, + containers_Admin_Users_gql_UserSetRolesMutationVariables > -export { containers_Admin_Users_gql_UserSetRoleDocument as default } +export { containers_Admin_Users_gql_UserSetRolesDocument as default } diff --git a/catalog/app/containers/Admin/Users/gql/UserSetRole.graphql b/catalog/app/containers/Admin/Users/gql/UserSetRoles.graphql similarity index 53% rename from catalog/app/containers/Admin/Users/gql/UserSetRole.graphql rename to catalog/app/containers/Admin/Users/gql/UserSetRoles.graphql index e6da78863d6..5e96e4efda7 100644 --- a/catalog/app/containers/Admin/Users/gql/UserSetRole.graphql +++ b/catalog/app/containers/Admin/Users/gql/UserSetRoles.graphql @@ -1,10 +1,10 @@ # import UserResultSelection from "./UserResultSelection.graphql" -mutation ($name: String!, $roleName: String!) { +mutation ($name: String!, $roleNames: [String!]!, $activeRoleName: String!) { admin { user { mutate(name: $name) { - setRole(roleName: $roleName) { + setRoles(roleNames: $roleNames, activeRoleName: $activeRoleName) { ...UserResultSelection } } From 86859d2f9e0b468ff7b06123b579380f85bb786d Mon Sep 17 00:00:00 2001 From: nl_0 Date: Fri, 24 May 2024 16:19:44 +0200 Subject: [PATCH 17/69] deploy catalog --- .github/workflows/deploy-catalog.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-catalog.yaml b/.github/workflows/deploy-catalog.yaml index fc4f8aed0fe..182b9a5fb5d 100644 --- a/.github/workflows/deploy-catalog.yaml +++ b/.github/workflows/deploy-catalog.yaml @@ -4,6 +4,7 @@ on: push: branches: - master + - users-gql paths: - '.github/workflows/deploy-catalog.yaml' - 'catalog/**' @@ -64,5 +65,5 @@ jobs: -t $ECR_REGISTRY_MP/$ECR_REPOSITORY_MP:$IMAGE_TAG \ . docker push $ECR_REGISTRY_PROD/$ECR_REPOSITORY:$IMAGE_TAG - docker push $ECR_REGISTRY_GOVCLOUD/$ECR_REPOSITORY:$IMAGE_TAG - docker push $ECR_REGISTRY_MP/$ECR_REPOSITORY_MP:$IMAGE_TAG + # docker push $ECR_REGISTRY_GOVCLOUD/$ECR_REPOSITORY:$IMAGE_TAG + # docker push $ECR_REGISTRY_MP/$ECR_REPOSITORY_MP:$IMAGE_TAG From a682fdb067b6ccc041cb48e508569ccbca6ba7ca Mon Sep 17 00:00:00 2001 From: nl_0 Date: Fri, 24 May 2024 16:56:41 +0200 Subject: [PATCH 18/69] role switching wip --- catalog/app/containers/NavBar/NavBar.tsx | 37 +++++++++++++++++------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/catalog/app/containers/NavBar/NavBar.tsx b/catalog/app/containers/NavBar/NavBar.tsx index 9fc4d9239d6..417e0271c94 100644 --- a/catalog/app/containers/NavBar/NavBar.tsx +++ b/catalog/app/containers/NavBar/NavBar.tsx @@ -19,12 +19,14 @@ import * as Dialogs from 'utils/GlobalDialogs' import * as GQL from 'utils/GraphQL' import HashLink from 'utils/HashLink' import * as NamedRoutes from 'utils/NamedRoutes' +import assertNever from 'utils/assertNever' import bg from './bg.png' import Controls from './Controls' import * as Subscription from './Subscription' import ME_QUERY from './gql/Me.generated' +import SWITCH_ROLE_MUTATION from './gql/SwitchRole.generated' type MaybeMe = GQL.DataForDoc['me'] type Me = NonNullable @@ -201,18 +203,33 @@ interface RolesSwitcherProps { close: Dialogs.Close } -function RolesSwitcher({ close, user }: RolesSwitcherProps) { +function RolesSwitcher({ /*close, */ user }: RolesSwitcherProps) { + const switchRole = GQL.useMutation(SWITCH_ROLE_MUTATION) const classes = useRolesSwitcherStyles() const textClasses = useListItemTextStyles() const [clicked, setClicked] = React.useState(false) - const handleClick = React.useCallback(() => { - setClicked(true) - setTimeout(() => { - close() - window.location.reload() - }, 1000) - }, [close]) - // TODO: don't show role switcher if only one role + const handleClick = React.useCallback( + async (roleName: string) => { + setClicked(true) + try { + const { switchRole: r } = await switchRole({ roleName }) + switch (r.__typename) { + case 'Me': + break + case 'OperationError': + case 'InvalidInput': + throw new Error(JSON.stringify(r)) + default: + assertNever(r) + } + window.location.reload() + } catch (err) { + // eslint-disable-next-line no-console + console.log('Error switching role', err) + } + }, + [switchRole], + ) return ( <> Switch role @@ -227,7 +244,7 @@ function RolesSwitcher({ close, user }: RolesSwitcherProps) { button disabled={role.name === user.role.name} key={role.name} - onClick={handleClick} + onClick={() => handleClick(role.name)} selected={role.name === user.role.name} > {role.name} From 1cf05dee90304d3d2084d60ba921a6cf627ac95b Mon Sep 17 00:00:00 2001 From: nl_0 Date: Thu, 30 May 2024 14:10:02 +0200 Subject: [PATCH 19/69] merge with remote and combine both implementations --- catalog/app/containers/NavBar/NavBar.tsx | 65 +++++++++++++----------- 1 file changed, 36 insertions(+), 29 deletions(-) diff --git a/catalog/app/containers/NavBar/NavBar.tsx b/catalog/app/containers/NavBar/NavBar.tsx index 417e0271c94..4a70ad45987 100644 --- a/catalog/app/containers/NavBar/NavBar.tsx +++ b/catalog/app/containers/NavBar/NavBar.tsx @@ -6,6 +6,7 @@ import { Link, useRouteMatch } from 'react-router-dom' import { createStructuredSelector } from 'reselect' import { sanitizeUrl } from '@braintree/sanitize-url' import * as M from '@material-ui/core' +import * as Lab from '@material-ui/lab' import * as Intercom from 'components/Intercom' import Logo from 'components/Logo' @@ -200,57 +201,67 @@ const useListItemTextStyles = M.makeStyles((t) => ({ interface RolesSwitcherProps { user: Me - close: Dialogs.Close } -function RolesSwitcher({ /*close, */ user }: RolesSwitcherProps) { +function RolesSwitcher({ user }: RolesSwitcherProps) { const switchRole = GQL.useMutation(SWITCH_ROLE_MUTATION) const classes = useRolesSwitcherStyles() const textClasses = useListItemTextStyles() - const [clicked, setClicked] = React.useState(false) + const loading = true + const [state, setState] = React.useState(null) const handleClick = React.useCallback( async (roleName: string) => { - setClicked(true) + setState(loading) try { const { switchRole: r } = await switchRole({ roleName }) switch (r.__typename) { case 'Me': + window.location.reload() break - case 'OperationError': case 'InvalidInput': - throw new Error(JSON.stringify(r)) + case 'OperationError': + throw new Error('Failed to switch role. Try again') default: assertNever(r) } - window.location.reload() } catch (err) { // eslint-disable-next-line no-console - console.log('Error switching role', err) + console.error('Error switching role', err) + if (err instanceof Error) { + setState(err) + } else { + setState(new Error('Unexpected error switching role')) + } } }, - [switchRole], + [loading, switchRole], ) return ( <> Switch role - {clicked ? ( + {state !== true ? ( + <> + {state instanceof Error && ( + {state.message} + )} + + {user.roles.map((role) => ( + handleClick(role.name)} + selected={role.name === user.role.name} + > + {role.name} + + ))} + + + ) : (
- ) : ( - - {user.roles.map((role) => ( - handleClick(role.name)} - selected={role.name === user.role.name} - > - {role.name} - - ))} - )} ) @@ -289,11 +300,7 @@ function UserDropdown({ user }: UserDropdownProps) { }, [bookmarks, closeDropdown]) const showRolesSwitcher = React.useCallback( - () => - openDialog( - ({ close }) => , - SWITCH_ROLES_DIALOG_PROPS, - ), + () => openDialog(() => , SWITCH_ROLES_DIALOG_PROPS), [openDialog, user], ) From e91e468a051bfe05f3ca54e3ce02cc5f4ee44ca5 Mon Sep 17 00:00:00 2001 From: Maksim Chervonnyi Date: Fri, 24 May 2024 18:03:20 +0200 Subject: [PATCH 20/69] prettify .graphql --- .../Admin/Users/gql/UserResultSelection.graphql | 4 +++- catalog/app/containers/NavBar/gql/Me.graphql | 8 ++++++-- catalog/app/containers/NavBar/gql/SwitchRole.graphql | 8 ++++++-- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/catalog/app/containers/Admin/Users/gql/UserResultSelection.graphql b/catalog/app/containers/Admin/Users/gql/UserResultSelection.graphql index 86ae6e80d97..d1e1da5b755 100644 --- a/catalog/app/containers/Admin/Users/gql/UserResultSelection.graphql +++ b/catalog/app/containers/Admin/Users/gql/UserResultSelection.graphql @@ -2,7 +2,9 @@ fragment UserResultSelection on UserResult { __typename - ... on User { ...UserSelection } + ... on User { + ...UserSelection + } ... on InvalidInput { errors { path diff --git a/catalog/app/containers/NavBar/gql/Me.graphql b/catalog/app/containers/NavBar/gql/Me.graphql index 219f41ac532..a65a579108a 100644 --- a/catalog/app/containers/NavBar/gql/Me.graphql +++ b/catalog/app/containers/NavBar/gql/Me.graphql @@ -3,7 +3,11 @@ query { name email isAdmin - role { name } - roles { name } + role { + name + } + roles { + name + } } } diff --git a/catalog/app/containers/NavBar/gql/SwitchRole.graphql b/catalog/app/containers/NavBar/gql/SwitchRole.graphql index b2858b7cc8e..e1577f50cc5 100644 --- a/catalog/app/containers/NavBar/gql/SwitchRole.graphql +++ b/catalog/app/containers/NavBar/gql/SwitchRole.graphql @@ -5,8 +5,12 @@ mutation ($roleName: String!) { name email isAdmin - role { name } - roles { name } + role { + name + } + roles { + name + } } ... on OperationError { message From fb2e6a4d7e838cc6580b1c3bff2780994394d193 Mon Sep 17 00:00:00 2001 From: Maksim Chervonnyi Date: Fri, 24 May 2024 18:05:45 +0200 Subject: [PATCH 21/69] rename close back since we dont close SwitchRoles dialog --- catalog/app/containers/NavBar/NavBar.tsx | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/catalog/app/containers/NavBar/NavBar.tsx b/catalog/app/containers/NavBar/NavBar.tsx index 4a70ad45987..781d27b5c3a 100644 --- a/catalog/app/containers/NavBar/NavBar.tsx +++ b/catalog/app/containers/NavBar/NavBar.tsx @@ -286,7 +286,7 @@ function UserDropdown({ user }: UserDropdownProps) { [setAnchor], ) - const closeDropdown = React.useCallback(() => { + const close = React.useCallback(() => { setVisible(false) setAnchor(null) }, [setAnchor]) @@ -296,13 +296,13 @@ function UserDropdown({ user }: UserDropdownProps) { const showBookmarks = React.useCallback(() => { if (!bookmarks) return bookmarks.show() - closeDropdown() - }, [bookmarks, closeDropdown]) + close() + }, [bookmarks, close]) - const showRolesSwitcher = React.useCallback( - () => openDialog(() => , SWITCH_ROLES_DIALOG_PROPS), - [openDialog, user], - ) + const showRolesSwitcher = React.useCallback(() => { + openDialog(() => , SWITCH_ROLES_DIALOG_PROPS) + close() + }, [openDialog, close, user]) React.useEffect(() => { const hasUpdates = bookmarks?.hasUpdates || false @@ -319,7 +319,7 @@ function UserDropdown({ user }: UserDropdownProps) { - + {bookmarks && ( @@ -334,16 +334,16 @@ function UserDropdown({ user }: UserDropdownProps) { )} {user.isAdmin && ( - + security Admin settings )} {cfg.mode === 'OPEN' && ( - + Profile )} - + Sign Out From 54ef45b53966e511cce49a0851496bb9ea936e28 Mon Sep 17 00:00:00 2001 From: nl_0 Date: Mon, 27 May 2024 09:47:42 +0200 Subject: [PATCH 22/69] fancier username display --- catalog/app/containers/Admin/Users/Users.tsx | 24 +++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/catalog/app/containers/Admin/Users/Users.tsx b/catalog/app/containers/Admin/Users/Users.tsx index 94581e2cfe6..3f74ae256af 100644 --- a/catalog/app/containers/Admin/Users/Users.tsx +++ b/catalog/app/containers/Admin/Users/Users.tsx @@ -510,16 +510,26 @@ const useUsernameStyles = M.makeStyles((t) => ({ }, })) -interface UsernameProps extends React.HTMLProps { - admin?: boolean +interface UsernameProps { + name: string + admin: boolean + self: boolean } -function Username({ className, admin = false, children, ...props }: UsernameProps) { +function Username({ name, admin, self }: UsernameProps) { const classes = useUsernameStyles() return ( - + {admin && security} - {children} + + {self ? ( + + {name}* + + ) : ( + name + )} + ) } @@ -753,7 +763,9 @@ const columns: Table.Column[] = [ id: 'username', label: 'Username', getValue: (u) => u.name, - getDisplay: (_name: string, u) => {u.name}, + getDisplay: (_name: string, u, { isSelf }: ColumnDisplayProps) => ( + + ), props: { component: 'th', scope: 'row' }, }, { From dd0dd38c64c8ac2701183d6cb8315f6bdfc93732 Mon Sep 17 00:00:00 2001 From: nl_0 Date: Wed, 29 May 2024 16:06:26 +0200 Subject: [PATCH 23/69] update gql schema, regen --- catalog/app/model/graphql/schema.generated.ts | 50 +++++-------------- catalog/app/model/graphql/types.generated.ts | 14 ++---- shared/graphql/schema.graphql | 10 ++-- 3 files changed, 23 insertions(+), 51 deletions(-) diff --git a/catalog/app/model/graphql/schema.generated.ts b/catalog/app/model/graphql/schema.generated.ts index 3853548b941..8f3adb33896 100644 --- a/catalog/app/model/graphql/schema.generated.ts +++ b/catalog/app/model/graphql/schema.generated.ts @@ -1474,51 +1474,27 @@ export default { }, args: [ { - name: 'roleName', - type: { - kind: 'SCALAR', - name: 'String', - ofType: null, - }, - }, - ], - }, - { - name: 'setRoles', - type: { - kind: 'NON_NULL', - ofType: { - kind: 'UNION', - name: 'UserResult', - ofType: null, - }, - }, - args: [ - { - name: 'roleNames', + name: 'role', type: { kind: 'NON_NULL', ofType: { - kind: 'LIST', - ofType: { - kind: 'NON_NULL', - ofType: { - kind: 'SCALAR', - name: 'String', - ofType: null, - }, - }, + kind: 'SCALAR', + name: 'String', + ofType: null, }, }, }, { - name: 'activeRoleName', + name: 'extraRoles', type: { - kind: 'NON_NULL', + kind: 'LIST', ofType: { - kind: 'SCALAR', - name: 'String', - ofType: null, + kind: 'NON_NULL', + ofType: { + kind: 'SCALAR', + name: 'String', + ofType: null, + }, }, }, }, @@ -5249,7 +5225,7 @@ export default { args: [], }, { - name: 'roles', + name: 'extraRoles', type: { kind: 'NON_NULL', ofType: { diff --git a/catalog/app/model/graphql/types.generated.ts b/catalog/app/model/graphql/types.generated.ts index ab6e4d49dc6..ebd1100ef17 100644 --- a/catalog/app/model/graphql/types.generated.ts +++ b/catalog/app/model/graphql/types.generated.ts @@ -333,7 +333,6 @@ export interface MutateUserAdminMutations { readonly delete: OperationResult readonly setEmail: UserResult readonly setRole: UserResult - readonly setRoles: UserResult readonly setAdmin: UserResult readonly setActive: UserResult readonly resetPassword: OperationResult @@ -344,12 +343,8 @@ export interface MutateUserAdminMutationssetEmailArgs { } export interface MutateUserAdminMutationssetRoleArgs { - roleName: Maybe -} - -export interface MutateUserAdminMutationssetRolesArgs { - roleNames: ReadonlyArray - activeRoleName: Scalars['String'] + role: Scalars['String'] + extraRoles: Maybe> } export interface MutateUserAdminMutationssetAdminArgs { @@ -1139,7 +1134,7 @@ export interface User { readonly isSsoOnly: Scalars['Boolean'] readonly isService: Scalars['Boolean'] readonly role: Maybe - readonly roles: ReadonlyArray + readonly extraRoles: ReadonlyArray } export interface UserAdminMutations { @@ -1169,7 +1164,8 @@ export interface UserAdminQueriesgetArgs { export interface UserInput { readonly name: Scalars['String'] readonly email: Scalars['String'] - readonly roleName: Maybe + readonly role: Scalars['String'] + readonly extraRoles: Maybe> } export type UserResult = User | InvalidInput | OperationError diff --git a/shared/graphql/schema.graphql b/shared/graphql/schema.graphql index fcae6dd2ffd..c3a282b3ebe 100644 --- a/shared/graphql/schema.graphql +++ b/shared/graphql/schema.graphql @@ -200,8 +200,8 @@ type User { isAdmin: Boolean! isSsoOnly: Boolean! isService: Boolean! - role: Role # XXX: activeRole? - roles: [Role!]! # XXX: better name + role: Role + extraRoles: [Role!]! } type AccessCountForDate { @@ -838,7 +838,8 @@ union BrowsingSessionDisposeResult = Ok | OperationError input UserInput { name: String! email: String! - roleName: String + role: String! + extraRoles: [String!] } union UserResult = User | InvalidInput | OperationError @@ -846,8 +847,7 @@ union UserResult = User | InvalidInput | OperationError type MutateUserAdminMutations { delete: OperationResult! setEmail(email: String!): UserResult! - setRole(roleName: String): UserResult! - setRoles(roleNames: [String!]!, activeRoleName: String!): UserResult! + setRole(role: String!, extraRoles: [String!]): UserResult! setAdmin(admin: Boolean!): UserResult! setActive(active: Boolean!): UserResult! resetPassword: OperationResult! From 6b3a65e22e41f0426620df9629b1a675c95e267d Mon Sep 17 00:00:00 2001 From: nl_0 Date: Wed, 29 May 2024 16:08:59 +0200 Subject: [PATCH 24/69] adjust api, draft ui --- catalog/app/containers/Admin/Users/Users.tsx | 282 ++++++++++++------ .../Users/gql/UserSelection.generated.ts | 4 +- .../Admin/Users/gql/UserSelection.graphql | 2 +- ....generated.ts => UserSetRole.generated.ts} | 48 +-- ...erSetRoles.graphql => UserSetRole.graphql} | 4 +- 5 files changed, 224 insertions(+), 116 deletions(-) rename catalog/app/containers/Admin/Users/gql/{UserSetRoles.generated.ts => UserSetRole.generated.ts} (88%) rename catalog/app/containers/Admin/Users/gql/{UserSetRoles.graphql => UserSetRole.graphql} (53%) diff --git a/catalog/app/containers/Admin/Users/Users.tsx b/catalog/app/containers/Admin/Users/Users.tsx index 3f74ae256af..db755197f3a 100644 --- a/catalog/app/containers/Admin/Users/Users.tsx +++ b/catalog/app/containers/Admin/Users/Users.tsx @@ -1,5 +1,6 @@ import cx from 'classnames' import * as FF from 'final-form' +import invariant from 'invariant' import * as R from 'ramda' import * as React from 'react' import * as redux from 'react-redux' @@ -23,7 +24,7 @@ import USERS_QUERY from './gql/Users.generated' import USER_CREATE_MUTATION from './gql/UserCreate.generated' import USER_DELETE_MUTATION from './gql/UserDelete.generated' import USER_SET_EMAIL_MUTATION from './gql/UserSetEmail.generated' -import USER_SET_ROLES_MUTATION from './gql/UserSetRoles.generated' +import USER_SET_ROLE_MUTATION from './gql/UserSetRole.generated' import USER_SET_ACTIVE_MUTATION from './gql/UserSetActive.generated' import USER_SET_ADMIN_MUTATION from './gql/UserSetAdmin.generated' @@ -49,6 +50,154 @@ function Mono({ className, children }: MonoProps) { return {children} } +const useRoleSelectStyles = M.makeStyles((t) => ({ + root: {}, + chips: { + display: 'flex', + flexWrap: 'wrap', + marginTop: t.spacing(2.5), + }, + chip: { + marginRight: t.spacing(0.5), + marginTop: t.spacing(0.5), + }, + addIcon: { + transform: 'rotate(45deg)', + }, +})) + +interface RoleSelectValue { + extra: readonly Role[] + active: Role | null | undefined +} + +const ROLE_SELECT_VALUE_EMPTY: RoleSelectValue = { extra: [], active: undefined } + +interface RoleSelectProps extends RF.FieldRenderProps { + roles: readonly Role[] + label?: React.ReactNode + // value?: RoleSelectValue + // input: { + // onChange: (value: RoleSelectValue) => void + // } +} + +// TODO: +// - [ ] validation +function RoleSelect({ + roles, + input: { value, onChange /*...input*/ }, + label, // ...props +}: RoleSelectProps) { + const classes = useRoleSelectStyles() + const { active, extra } = value ?? ROLE_SELECT_VALUE_EMPTY + + const selected = React.useMemo( + () => extra.concat(active ?? []).sort(R.ascend((r) => r.name)), + [extra, active], + ) + + const available = React.useMemo( + () => + roles + .filter((r) => !selected.find((r2) => r2.id === r.id)) + .sort(R.ascend((r) => r.name)), + [roles, selected], + ) + + const [anchorEl, setAnchorEl] = React.useState(null) + + const openAddMenu = React.useCallback( + (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget) + }, + [setAnchorEl], + ) + + const closeAddMenu = React.useCallback(() => { + setAnchorEl(null) + }, []) + + const add = (r: Role) => + onChange({ + extra: extra.concat(active ? r : []).sort(R.ascend((r2) => r2.name)), + active: active ?? r, + }) + + const remove = (r: Role) => + onChange({ + extra: extra.filter((r2) => r2.id !== r.id), + active: r.id === active?.id ? undefined : active, + }) + + const activate = (r: Role) => + onChange({ + extra: extra + .concat(active ?? []) + .filter((r2) => r2.id !== r.id) + .sort(R.ascend((r2) => r2.name)), + active: r, + }) + + return ( + + {!!label && {label}} +
+ {selected.map((r) => + active?.id === r.id ? ( + remove(r)} + /> + ) : ( + remove(r)} + clickable + onClick={() => activate(r)} + /> + ), + )} + {available.length > 0 && ( + + )} +
+ + {available.map((r) => ( + { + closeAddMenu() + add(r) + }} + > + {r.name} + + ))} + + Assign a role please +
+ ) +} + const useInviteStyles = M.makeStyles({ infoIcon: { fontSize: '1.25em', @@ -59,21 +208,29 @@ const useInviteStyles = M.makeStyles({ interface InviteProps { close: () => void roles: readonly Role[] - defaultRoleName: string | undefined + defaultRole: Role | null } -function Invite({ close, roles, defaultRoleName }: InviteProps) { +function Invite({ close, roles, defaultRole }: InviteProps) { const classes = useInviteStyles() const create = GQL.useMutation(USER_CREATE_MUTATION) const { push } = Notifications.use() + interface FormValues { + username: string + email: string + roles: RoleSelectValue + } + const onSubmit = React.useCallback( - async ({ username, email, roleName }) => { + async (values: FormValues) => { // XXX: use formspec to convert/validate form values into gql input? + invariant(values.roles.active, 'No active role') const input = { - name: username, - email, - roleName, + name: values.username, + email: values.email, + role: values.roles.active.name, + extraRoles: values.roles.extra.map((r) => r.name), } try { const data = await create({ input }) @@ -123,11 +280,11 @@ function Invite({ close, roles, defaultRoleName }: InviteProps) { [create, push, close], ) + const active = defaultRole || roles[0] + const extra = roles.filter((r) => r.id !== active.id) + return ( - + onSubmit={onSubmit} initialValues={{ roles: { active, extra } }}> {({ handleSubmit, submitting, @@ -180,19 +337,8 @@ function Invite({ close, roles, defaultRoleName }: InviteProps) { }} autoComplete="off" /> - - {roles.map((r) => ( - - {r.name} - - ))} + name="roles"> + {(props) => } {(!!error || !!submitError) && ( { - // console.log('submit', values) + async (values: FormValues) => { // XXX: use formspec to convert/validate form values into gql input? - const input = { + invariant(values.roles.active, 'No active role') + const vars = { name: user.name, - roleNames: values.roles.map((r: Role) => r.name), - activeRoleName: values.roles[0].name, + role: values.roles.active.name, + extraRoles: values.roles.extra.map((r) => r.name), } try { - const data = await setRoles(input) - const r = data.admin.user.mutate?.setRoles + const data = await setRole(vars) + const r = data.admin.user.mutate?.setRole switch (r?.__typename) { case undefined: throw new Error('User not found') // should not happend @@ -577,23 +727,21 @@ function EditRoles({ close, roles, user }: EditRolesProps) { } } catch (e) { // eslint-disable-next-line no-console - console.error('Error setting roles for user', input) + console.error('Error setting roles for user', vars) // eslint-disable-next-line no-console console.dir(e) Sentry.captureException(e) return { [FF.FORM_ERROR]: 'unexpected' } } }, - [push, close, setRoles, user.name], + [push, close, setRole, user.name], ) - const handleToggle = (selected: Role[], role: Role) => - selected.find((r) => r.id === role.id) - ? selected.filter((r) => r.id !== role.id) - : [...selected, role] - return ( - + + onSubmit={onSubmit} + initialValues={{ roles: { active: user.role, extra: user.extraRoles } }} + > {({ handleSubmit, submitting, @@ -608,48 +756,8 @@ function EditRoles({ close, roles, user }: EditRolesProps) { Configure roles for {user.name}
- - name="roles" - // validate={validators.required as FF.FieldValidator} - // errors={{ - // required: 'Enter a username', - // taken: 'Username already taken', - // invalid: ( - // <> - // Enter a valid username{' '} - // - // info - // - // - // ), - // }} - // autoComplete="off" - > - {({ input: { onChange, value } }) => ( - - {roles.map((role) => ( - onChange(handleToggle(value, role))} - > - - r.id === role.id)} - tabIndex={-1} - disableRipple - /> - - - - ))} - - )} + name="roles"> + {(props) => } {(!!error || !!submitError) && (
+ {/*TODO: reset?*/} Cancel @@ -784,7 +893,7 @@ const columns: Table.Column[] = [ } > {v ?? emptyRole} - {u.roles.length > 1 && +{u.roles.length - 1}} + {u.extraRoles.length > 0 && +{u.extraRoles.length}} ), }, @@ -882,8 +991,7 @@ function useSetActive() { export default function Users() { const data = GQL.useQueryS(USERS_QUERY) const rows = data.admin.user.list - const roles = data.roles - const defaultRoleName = data.defaultRole?.name + const { roles, defaultRole } = data const openDialog = Dialogs.use() @@ -907,10 +1015,10 @@ export default function Users() { icon: add, fn: React.useCallback(() => { openDialog( - ({ close }) => , + ({ close }) => , DIALOG_PROPS, ) - }, [roles, defaultRoleName, openDialog]), + }, [roles, defaultRole, openDialog]), }, ] diff --git a/catalog/app/containers/Admin/Users/gql/UserSelection.generated.ts b/catalog/app/containers/Admin/Users/gql/UserSelection.generated.ts index 2993119988d..2c045575818 100644 --- a/catalog/app/containers/Admin/Users/gql/UserSelection.generated.ts +++ b/catalog/app/containers/Admin/Users/gql/UserSelection.generated.ts @@ -20,7 +20,7 @@ export type UserSelectionFragment = { readonly __typename: 'User' } & Pick< >) | ({ readonly __typename: 'ManagedRole' } & Pick) > - readonly roles: ReadonlyArray< + readonly extraRoles: ReadonlyArray< | ({ readonly __typename: 'UnmanagedRole' } & Pick< Types.UnmanagedRole, 'id' | 'name' @@ -88,7 +88,7 @@ export const UserSelectionFragmentDoc = { }, { kind: 'Field', - name: { kind: 'Name', value: 'roles' }, + name: { kind: 'Name', value: 'extraRoles' }, selectionSet: { kind: 'SelectionSet', selections: [ diff --git a/catalog/app/containers/Admin/Users/gql/UserSelection.graphql b/catalog/app/containers/Admin/Users/gql/UserSelection.graphql index 17fddeaddee..d9151dc96d5 100644 --- a/catalog/app/containers/Admin/Users/gql/UserSelection.graphql +++ b/catalog/app/containers/Admin/Users/gql/UserSelection.graphql @@ -19,7 +19,7 @@ fragment UserSelection on User { name } } - roles { + extraRoles { __typename ... on ManagedRole { id diff --git a/catalog/app/containers/Admin/Users/gql/UserSetRoles.generated.ts b/catalog/app/containers/Admin/Users/gql/UserSetRole.generated.ts similarity index 88% rename from catalog/app/containers/Admin/Users/gql/UserSetRoles.generated.ts rename to catalog/app/containers/Admin/Users/gql/UserSetRole.generated.ts index 6a4b139d83a..65b3d88258d 100644 --- a/catalog/app/containers/Admin/Users/gql/UserSetRoles.generated.ts +++ b/catalog/app/containers/Admin/Users/gql/UserSetRole.generated.ts @@ -9,20 +9,20 @@ import { UserResultSelectionFragmentDoc, } from './UserResultSelection.generated' -export type containers_Admin_Users_gql_UserSetRolesMutationVariables = Types.Exact<{ +export type containers_Admin_Users_gql_UserSetRoleMutationVariables = Types.Exact<{ name: Types.Scalars['String'] - roleNames: ReadonlyArray - activeRoleName: Types.Scalars['String'] + role: Types.Scalars['String'] + extraRoles: ReadonlyArray }> -export type containers_Admin_Users_gql_UserSetRolesMutation = { +export type containers_Admin_Users_gql_UserSetRoleMutation = { readonly __typename: 'Mutation' } & { readonly admin: { readonly __typename: 'AdminMutations' } & { readonly user: { readonly __typename: 'UserAdminMutations' } & { readonly mutate: Types.Maybe< { readonly __typename: 'MutateUserAdminMutations' } & { - readonly setRoles: + readonly setRole: | ({ readonly __typename: 'User' } & UserResultSelection_User_Fragment) | ({ readonly __typename: 'InvalidInput' @@ -36,13 +36,13 @@ export type containers_Admin_Users_gql_UserSetRolesMutation = { } } -export const containers_Admin_Users_gql_UserSetRolesDocument = { +export const containers_Admin_Users_gql_UserSetRoleDocument = { kind: 'Document', definitions: [ { kind: 'OperationDefinition', operation: 'mutation', - name: { kind: 'Name', value: 'containers_Admin_Users_gql_UserSetRoles' }, + name: { kind: 'Name', value: 'containers_Admin_Users_gql_UserSetRole' }, variableDefinitions: [ { kind: 'VariableDefinition', @@ -54,7 +54,15 @@ export const containers_Admin_Users_gql_UserSetRolesDocument = { }, { kind: 'VariableDefinition', - variable: { kind: 'Variable', name: { kind: 'Name', value: 'roleNames' } }, + variable: { kind: 'Variable', name: { kind: 'Name', value: 'role' } }, + type: { + kind: 'NonNullType', + type: { kind: 'NamedType', name: { kind: 'Name', value: 'String' } }, + }, + }, + { + kind: 'VariableDefinition', + variable: { kind: 'Variable', name: { kind: 'Name', value: 'extraRoles' } }, type: { kind: 'NonNullType', type: { @@ -66,14 +74,6 @@ export const containers_Admin_Users_gql_UserSetRolesDocument = { }, }, }, - { - kind: 'VariableDefinition', - variable: { kind: 'Variable', name: { kind: 'Name', value: 'activeRoleName' } }, - type: { - kind: 'NonNullType', - type: { kind: 'NamedType', name: { kind: 'Name', value: 'String' } }, - }, - }, ], selectionSet: { kind: 'SelectionSet', @@ -108,22 +108,22 @@ export const containers_Admin_Users_gql_UserSetRolesDocument = { selections: [ { kind: 'Field', - name: { kind: 'Name', value: 'setRoles' }, + name: { kind: 'Name', value: 'setRole' }, arguments: [ { kind: 'Argument', - name: { kind: 'Name', value: 'roleNames' }, + name: { kind: 'Name', value: 'role' }, value: { kind: 'Variable', - name: { kind: 'Name', value: 'roleNames' }, + name: { kind: 'Name', value: 'role' }, }, }, { kind: 'Argument', - name: { kind: 'Name', value: 'activeRoleName' }, + name: { kind: 'Name', value: 'extraRoles' }, value: { kind: 'Variable', - name: { kind: 'Name', value: 'activeRoleName' }, + name: { kind: 'Name', value: 'extraRoles' }, }, }, ], @@ -152,8 +152,8 @@ export const containers_Admin_Users_gql_UserSetRolesDocument = { ...UserResultSelectionFragmentDoc.definitions, ], } as unknown as DocumentNode< - containers_Admin_Users_gql_UserSetRolesMutation, - containers_Admin_Users_gql_UserSetRolesMutationVariables + containers_Admin_Users_gql_UserSetRoleMutation, + containers_Admin_Users_gql_UserSetRoleMutationVariables > -export { containers_Admin_Users_gql_UserSetRolesDocument as default } +export { containers_Admin_Users_gql_UserSetRoleDocument as default } diff --git a/catalog/app/containers/Admin/Users/gql/UserSetRoles.graphql b/catalog/app/containers/Admin/Users/gql/UserSetRole.graphql similarity index 53% rename from catalog/app/containers/Admin/Users/gql/UserSetRoles.graphql rename to catalog/app/containers/Admin/Users/gql/UserSetRole.graphql index 5e96e4efda7..3bb2eb35f60 100644 --- a/catalog/app/containers/Admin/Users/gql/UserSetRoles.graphql +++ b/catalog/app/containers/Admin/Users/gql/UserSetRole.graphql @@ -1,10 +1,10 @@ # import UserResultSelection from "./UserResultSelection.graphql" -mutation ($name: String!, $roleNames: [String!]!, $activeRoleName: String!) { +mutation ($name: String!, $role: String!, $extraRoles: [String!]!) { admin { user { mutate(name: $name) { - setRoles(roleNames: $roleNames, activeRoleName: $activeRoleName) { + setRole(role: $role, extraRoles: $extraRoles) { ...UserResultSelection } } From 53a8c8e0fe11176fae4a09cf081765d868a18564 Mon Sep 17 00:00:00 2001 From: Maksim Chervonnyi Date: Thu, 30 May 2024 09:14:24 +0200 Subject: [PATCH 25/69] Don't fold GQL `Me` data, split code to `MobileSignIn`/`DesktopSignIn` (#3988) --- catalog/app/containers/NavBar/NavBar.tsx | 249 ++++++++++++----------- 1 file changed, 130 insertions(+), 119 deletions(-) diff --git a/catalog/app/containers/NavBar/NavBar.tsx b/catalog/app/containers/NavBar/NavBar.tsx index 781d27b5c3a..36717fb17f9 100644 --- a/catalog/app/containers/NavBar/NavBar.tsx +++ b/catalog/app/containers/NavBar/NavBar.tsx @@ -32,18 +32,6 @@ import SWITCH_ROLE_MUTATION from './gql/SwitchRole.generated' type MaybeMe = GQL.DataForDoc['me'] type Me = NonNullable -function useMe(pause: boolean) { - const res = GQL.useQuery(ME_QUERY, undefined, { pause }) - return GQL.fold(res, { - data: (d) => { - invariant(d.me, 'Expected "me" to be non-null') - return d.me - }, - fetching: () => 'fetching' as const, - error: () => 'error' as const, - }) -} - const SWITCH_ROLES_DIALOG_PROPS: Dialogs.ExtraDialogProps = { maxWidth: 'sm', fullWidth: true, @@ -392,117 +380,93 @@ function useHam() { return { open, close, render } } -interface AuthHamburgerProps { - authenticated: boolean - waiting: boolean - error: boolean -} - -function AuthHamburger({ authenticated, waiting, error }: AuthHamburgerProps) { +function useAuthLinks(onHamClose: () => void): React.ReactNode[] { + const { error, waiting, authenticated } = redux.useSelector(selector) const { urls, paths } = NamedRoutes.use() const isProfile = !!useRouteMatch({ path: paths.profile, exact: true }) const isAdmin = !!useRouteMatch(paths.admin) - const ham = useHam() - const links = useLinks() - const user = useMe(!authenticated || waiting) - - let children: React.ReactNode[] = [] - if (!authenticated || user === 'error') { - children = [ - - {error && ( - <> - error_outline{' '} - - )} - Sign In + const res = GQL.useQuery(ME_QUERY, undefined, { pause: waiting || !authenticated }) + + if (error) + return [ + + error_outline Sign In , ] - } else if (waiting || user === 'fetching') { - children = [ - + if (waiting) { + return [ + , ] - } else { - children = [ - - {userDisplay(user)} - , - user.isAdmin && ( - - - security -  Admin settings - - ), - cfg.mode === 'OPEN' && ( - - - Profile - - ), - - - Sign Out - , - ] } - return ham.render([ - ...children, - , - ...links.map(({ label, ...rest }) => ( - - {label} - - )), - ]) -} - -function LinksHamburger() { - const ham = useHam() - return ham.render( - useLinks().map(({ label, ...rest }) => ( - - {label} - - )), - ) + return GQL.fold(res, { + data: (d) => { + invariant(d.me, 'Expected "me" to be non-null') + return [ + + {userDisplay(d.me)} + , + d.me.isAdmin && ( + + + security +  Admin settings + + ), + cfg.mode === 'OPEN' && ( + + + Profile + + ), + + + Sign Out + , + ] + }, + fetching: () => [ + + + , + ], + error: () => [ + + error_outline Sign In + , + ], + }) } -const useSignInStyles = M.makeStyles((t) => ({ +const useSignInErrorStyles = M.makeStyles((t) => ({ icon: { marginRight: t.spacing(1), }, })) -interface AuthError { - message: string -} - -interface SignInProps { - error?: AuthError - waiting: boolean +interface DesktopSignInErrorProps { + error: Error | GQL.ErrorForData } -function SignIn({ error, waiting }: SignInProps) { - const classes = useSignInStyles() +function DesktopSignInError({ error }: DesktopSignInErrorProps) { + const classes = useSignInErrorStyles() const { urls } = NamedRoutes.use() - if (waiting) { - return - } return ( <> - {error && ( - - error_outline - - )} + + error_outline + Sign In @@ -510,6 +474,67 @@ function SignIn({ error, waiting }: SignInProps) { ) } +function DesktopSignIn() { + const { error, waiting, authenticated } = redux.useSelector(selector) + const { paths } = NamedRoutes.use() + const isSignIn = !!useRouteMatch({ path: paths.signIn, exact: true }) + const res = GQL.useQuery(ME_QUERY, undefined, { pause: waiting || !authenticated }) + + if (!authenticated && isSignIn) return null + + if (error) { + return + } + + if (waiting) { + return + } + + return GQL.fold(res, { + data: (d) => { + invariant(d.me, 'Expected "me" to be non-null') + return + }, + fetching: () => , + error: (err) => , + }) +} + +interface MobileSignInProps { + links: LinkDescriptor[] +} + +function MobileSignIn({ links }: MobileSignInProps) { + const ham = useHam() + + const mobileLinks = React.useMemo( + () => + links.map(({ label, ...rest }) => ( + + {label} + + )), + [ham.close, links], + ) + + if (cfg.disableNavigator || cfg.mode === 'LOCAL') { + return ham.render(mobileLinks) + } + + return +} + +interface MobileSignInInnerProps { + ham: ReturnType + links: React.ReactNode[] +} + +// It's just a wrapper to put hook call, so we can hide it under condition +function MobileSignInWithAuth({ ham, links }: MobileSignInInnerProps) { + const authLinks = useAuthLinks(ham.close) + return ham.render([...authLinks, , ...links]) +} + const useAppBarStyles = M.makeStyles((t) => ({ root: { zIndex: t.zIndex.appBar + 1, @@ -720,7 +745,6 @@ export function NavBar() { const settings = CatalogSettings.use() const { paths } = NamedRoutes.use() const isSignIn = !!useRouteMatch({ path: paths.signIn, exact: true }) - const { error, waiting, authenticated } = redux.useSelector(selector) const t = M.useTheme() const useHamburger = M.useMediaQuery(t.breakpoints.down('sm')) const links = useLinks() @@ -728,8 +752,6 @@ export function NavBar() { const classes = useNavBarStyles() const sub = Subscription.useState() - const user = useMe(!authenticated || waiting) - return ( @@ -769,22 +791,11 @@ export function NavBar() { )} - {!cfg.disableNavigator && - cfg.mode !== 'LOCAL' && - !useHamburger && - (authenticated && user !== 'error' && user !== 'fetching' ? ( - // TODO: refactor gql query states - - ) : ( - !isSignIn && - ))} - - {useHamburger && - (cfg.disableNavigator || cfg.mode === 'LOCAL' ? ( - - ) : ( - - ))} + {!cfg.disableNavigator && cfg.mode !== 'LOCAL' && !useHamburger && ( + + )} + + {useHamburger && } {settings?.logo?.url && } From 3e4b9bce742d332430dbac5f446c0184cc10041f Mon Sep 17 00:00:00 2001 From: nl_0 Date: Thu, 30 May 2024 09:36:41 +0200 Subject: [PATCH 26/69] adjust role selection model --- catalog/app/containers/Admin/Users/Users.tsx | 71 ++++++++++---------- 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/catalog/app/containers/Admin/Users/Users.tsx b/catalog/app/containers/Admin/Users/Users.tsx index db755197f3a..c1b259f8fa2 100644 --- a/catalog/app/containers/Admin/Users/Users.tsx +++ b/catalog/app/containers/Admin/Users/Users.tsx @@ -50,6 +50,15 @@ function Mono({ className, children }: MonoProps) { return {children} } +interface RoleSelectValue { + selected: readonly Role[] + active: Role | null | undefined +} + +const ROLE_SELECT_VALUE_EMPTY: RoleSelectValue = { selected: [], active: undefined } + +const ROLE_NAME_ASC = R.ascend((r: Role) => r.name) + const useRoleSelectStyles = M.makeStyles((t) => ({ root: {}, chips: { @@ -66,13 +75,6 @@ const useRoleSelectStyles = M.makeStyles((t) => ({ }, })) -interface RoleSelectValue { - extra: readonly Role[] - active: Role | null | undefined -} - -const ROLE_SELECT_VALUE_EMPTY: RoleSelectValue = { extra: [], active: undefined } - interface RoleSelectProps extends RF.FieldRenderProps { roles: readonly Role[] label?: React.ReactNode @@ -90,18 +92,10 @@ function RoleSelect({ label, // ...props }: RoleSelectProps) { const classes = useRoleSelectStyles() - const { active, extra } = value ?? ROLE_SELECT_VALUE_EMPTY - - const selected = React.useMemo( - () => extra.concat(active ?? []).sort(R.ascend((r) => r.name)), - [extra, active], - ) + const { active, selected } = value ?? ROLE_SELECT_VALUE_EMPTY const available = React.useMemo( - () => - roles - .filter((r) => !selected.find((r2) => r2.id === r.id)) - .sort(R.ascend((r) => r.name)), + () => roles.filter((r) => !selected.find((r2) => r2.id === r.id)).sort(ROLE_NAME_ASC), [roles, selected], ) @@ -120,24 +114,17 @@ function RoleSelect({ const add = (r: Role) => onChange({ - extra: extra.concat(active ? r : []).sort(R.ascend((r2) => r2.name)), + selected: selected.concat(r).sort(ROLE_NAME_ASC), active: active ?? r, }) const remove = (r: Role) => onChange({ - extra: extra.filter((r2) => r2.id !== r.id), + selected: selected.filter((r2) => r2.id !== r.id), active: r.id === active?.id ? undefined : active, }) - const activate = (r: Role) => - onChange({ - extra: extra - .concat(active ?? []) - .filter((r2) => r2.id !== r.id) - .sort(R.ascend((r2) => r2.name)), - active: r, - }) + const activate = (r: Role) => onChange({ selected, active: r }) return ( @@ -226,11 +213,15 @@ function Invite({ close, roles, defaultRole }: InviteProps) { async (values: FormValues) => { // XXX: use formspec to convert/validate form values into gql input? invariant(values.roles.active, 'No active role') + const role = values.roles.active.name + const extraRoles = values.roles.selected + .map((r) => r.name) + .filter((r) => r !== role) const input = { name: values.username, email: values.email, - role: values.roles.active.name, - extraRoles: values.roles.extra.map((r) => r.name), + role, + extraRoles, } try { const data = await create({ input }) @@ -281,10 +272,13 @@ function Invite({ close, roles, defaultRole }: InviteProps) { ) const active = defaultRole || roles[0] - const extra = roles.filter((r) => r.id !== active.id) + const selected = React.useMemo(() => [active], [active]) return ( - onSubmit={onSubmit} initialValues={{ roles: { active, extra } }}> + + onSubmit={onSubmit} + initialValues={{ roles: { active, selected } }} + > {({ handleSubmit, submitting, @@ -698,10 +692,14 @@ function EditRoles({ close, roles, user }: EditRolesProps) { async (values: FormValues) => { // XXX: use formspec to convert/validate form values into gql input? invariant(values.roles.active, 'No active role') + const role = values.roles.active.name + const extraRoles = values.roles.selected + .map((r) => r.name) + .filter((r) => r !== role) const vars = { name: user.name, - role: values.roles.active.name, - extraRoles: values.roles.extra.map((r) => r.name), + role, + extraRoles, } try { const data = await setRole(vars) @@ -737,10 +735,15 @@ function EditRoles({ close, roles, user }: EditRolesProps) { [push, close, setRole, user.name], ) + const selected = React.useMemo( + () => user.extraRoles.concat(user.role ?? []).sort(ROLE_NAME_ASC), + [user.extraRoles, user.role], + ) + return ( onSubmit={onSubmit} - initialValues={{ roles: { active: user.role, extra: user.extraRoles } }} + initialValues={{ roles: { active: user.role, selected } }} > {({ handleSubmit, From 197100bb180bd903b57aaa6cfd9cf724bb3f6178 Mon Sep 17 00:00:00 2001 From: nl_0 Date: Thu, 30 May 2024 10:08:49 +0200 Subject: [PATCH 27/69] unify form helpers --- catalog/app/containers/Admin/Form.tsx | 21 ++++---- catalog/app/containers/Admin/RFForm.tsx | 49 ------------------- .../Admin/RolesAndPolicies/Policies.tsx | 2 +- .../Admin/RolesAndPolicies/Roles.tsx | 2 +- 4 files changed, 11 insertions(+), 63 deletions(-) delete mode 100644 catalog/app/containers/Admin/RFForm.tsx diff --git a/catalog/app/containers/Admin/Form.tsx b/catalog/app/containers/Admin/Form.tsx index 2d9e8ccb66a..ea33e695568 100644 --- a/catalog/app/containers/Admin/Form.tsx +++ b/catalog/app/containers/Admin/Form.tsx @@ -2,12 +2,12 @@ import * as React from 'react' import type * as RF from 'react-final-form' import * as M from '@material-ui/core' -export interface FieldProps { +interface FieldOwnProps { errors: Record - input: RF.FieldInputProps - meta: RF.FieldMetaState } +export type FieldProps = FieldOwnProps & RF.FieldRenderProps & M.TextFieldProps + // TODO: re-use components/Form/TextField export function Field({ input, @@ -16,8 +16,9 @@ export function Field({ helperText, InputLabelProps, ...rest -}: FieldProps & M.TextFieldProps) { - const error = meta.submitFailed && (meta.error || meta.submitError) +}: FieldProps) { + const error = + meta.submitFailed && (meta.error || (!meta.dirtySinceLastSubmit && meta.submitError)) const props = { error: !!error, helperText: error ? errors[error] || error : helperText, @@ -80,16 +81,12 @@ const useFormErrorStyles = M.makeStyles((t) => ({ }, })) -interface FormErrorProps { - errors: Record +interface FormErrorProps extends M.TypographyProps { error?: string + errors: Record } -export function FormError({ - error, - errors, - ...rest -}: FormErrorProps & M.TypographyProps) { +export function FormError({ error, errors, ...rest }: FormErrorProps) { const classes = useFormErrorStyles() if (!error) return null return ( diff --git a/catalog/app/containers/Admin/RFForm.tsx b/catalog/app/containers/Admin/RFForm.tsx deleted file mode 100644 index bf971094fc9..00000000000 --- a/catalog/app/containers/Admin/RFForm.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import * as React from 'react' -import * as RF from 'react-final-form' -import * as M from '@material-ui/core' - -interface FieldOwnProps { - error?: string - errors: Record - helperText?: React.ReactNode - validating?: boolean -} - -type FieldProps = FieldOwnProps & RF.FieldRenderProps & M.TextFieldProps - -export function Field({ input, meta, errors, helperText, ...rest }: FieldProps) { - const error = - meta.submitFailed && (meta.error || (!meta.dirtySinceLastSubmit && meta.submitError)) - const props = { - error: !!error, - helperText: error ? errors[error] || error : helperText, - disabled: meta.submitting || meta.submitSucceeded, - ...input, - ...rest, - } - return -} - -const useFormErrorStyles = M.makeStyles((t) => ({ - root: { - marginTop: t.spacing(3), - - '& a': { - textDecoration: 'underline', - }, - }, -})) - -type FormErrorProps = M.TypographyProps & { - error?: string - errors: Record -} - -export function FormError({ error, errors, ...rest }: FormErrorProps) { - const classes = useFormErrorStyles() - return !error ? null : ( - - {errors[error] || error} - - ) -} diff --git a/catalog/app/containers/Admin/RolesAndPolicies/Policies.tsx b/catalog/app/containers/Admin/RolesAndPolicies/Policies.tsx index aa3b56a336f..6c5caa0af33 100644 --- a/catalog/app/containers/Admin/RolesAndPolicies/Policies.tsx +++ b/catalog/app/containers/Admin/RolesAndPolicies/Policies.tsx @@ -15,7 +15,7 @@ import { mkFormError, mapInputErrors } from 'utils/formTools' import * as Types from 'utils/types' import validate, * as validators from 'utils/validators' -import * as Form from '../RFForm' +import * as Form from '../Form' import * as Table from '../Table' import AssociatedRoles from './AssociatedRoles' diff --git a/catalog/app/containers/Admin/RolesAndPolicies/Roles.tsx b/catalog/app/containers/Admin/RolesAndPolicies/Roles.tsx index cba79130d01..4b0129696a9 100644 --- a/catalog/app/containers/Admin/RolesAndPolicies/Roles.tsx +++ b/catalog/app/containers/Admin/RolesAndPolicies/Roles.tsx @@ -14,7 +14,7 @@ import assertNever from 'utils/assertNever' import * as Types from 'utils/types' import * as validators from 'utils/validators' -import * as Form from '../RFForm' +import * as Form from '../Form' import * as Table from '../Table' import AttachedPolicies from './AttachedPolicies' From 048c251974052fa8b5368ae6ada7f6d69e1b1b6a Mon Sep 17 00:00:00 2001 From: nl_0 Date: Thu, 30 May 2024 13:42:30 +0200 Subject: [PATCH 28/69] role selection ui wip --- catalog/app/containers/Admin/Users/Users.tsx | 40 +++++++++++--------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/catalog/app/containers/Admin/Users/Users.tsx b/catalog/app/containers/Admin/Users/Users.tsx index c1b259f8fa2..32d432edc19 100644 --- a/catalog/app/containers/Admin/Users/Users.tsx +++ b/catalog/app/containers/Admin/Users/Users.tsx @@ -57,6 +57,9 @@ interface RoleSelectValue { const ROLE_SELECT_VALUE_EMPTY: RoleSelectValue = { selected: [], active: undefined } +const validateRoleSelect: FF.FieldValidator = (v) => + v.active ? undefined : 'required' + const ROLE_NAME_ASC = R.ascend((r: Role) => r.name) const useRoleSelectStyles = M.makeStyles((t) => ({ @@ -78,20 +81,14 @@ const useRoleSelectStyles = M.makeStyles((t) => ({ interface RoleSelectProps extends RF.FieldRenderProps { roles: readonly Role[] label?: React.ReactNode - // value?: RoleSelectValue - // input: { - // onChange: (value: RoleSelectValue) => void - // } } -// TODO: -// - [ ] validation -function RoleSelect({ - roles, - input: { value, onChange /*...input*/ }, - label, // ...props -}: RoleSelectProps) { +function RoleSelect({ roles, input: { value, onChange }, meta, label }: RoleSelectProps) { const classes = useRoleSelectStyles() + + const error = meta.submitFailed && meta.error + const disabled = meta.submitting || meta.submitSucceeded + const { active, selected } = value ?? ROLE_SELECT_VALUE_EMPTY const available = React.useMemo( @@ -127,7 +124,7 @@ function RoleSelect({ const activate = (r: Role) => onChange({ selected, active: r }) return ( - + {!!label && {label}}
{selected.map((r) => @@ -139,6 +136,7 @@ function RoleSelect({ color="secondary" className={classes.chip} onDelete={() => remove(r)} + disabled={disabled} /> ) : ( remove(r)} clickable onClick={() => activate(r)} + disabled={disabled} /> ), )} @@ -164,6 +163,7 @@ function RoleSelect({ clickable onDelete={openAddMenu} onClick={openAddMenu} + disabled={disabled} /> )}
@@ -180,7 +180,11 @@ function RoleSelect({ ))}
- Assign a role please + {!!error && ( + + {error === 'required' ? 'Assign a role please' : error} + + )} ) } @@ -298,6 +302,7 @@ function Invite({ close, roles, defaultRole }: InviteProps) { name="username" validate={validators.required as FF.FieldValidator} label="Username" + placeholder="Enter a username" fullWidth margin="normal" errors={{ @@ -322,6 +327,7 @@ function Invite({ close, roles, defaultRole }: InviteProps) { name="email" validate={validators.required as FF.FieldValidator} label="Email" + placeholder="Enter an email" fullWidth margin="normal" errors={{ @@ -331,7 +337,7 @@ function Invite({ close, roles, defaultRole }: InviteProps) { }} autoComplete="off" /> - name="roles"> + name="roles" validate={validateRoleSelect}> {(props) => } {(!!error || !!submitError) && ( @@ -712,10 +718,10 @@ function EditRoles({ close, roles, user }: EditRolesProps) { push('Changes saved') return case 'OperationError': - // TODO + // should not happend throw new Error(`Unexpected operation error: [${r.name}] ${r.message}`) case 'InvalidInput': - // TODO + // should not happend const [e] = r.errors throw new Error( `Unexpected input error at '${e.path}': [${e.name}] ${e.message}`, @@ -759,7 +765,7 @@ function EditRoles({ close, roles, user }: EditRolesProps) { Configure roles for {user.name}
- name="roles"> + name="roles" validate={validateRoleSelect}> {(props) => } {(!!error || !!submitError) && ( From 9c8b8ecbf611858a3f25386f7e1e60c288922609 Mon Sep 17 00:00:00 2001 From: nl_0 Date: Thu, 30 May 2024 15:15:19 +0200 Subject: [PATCH 29/69] fix form re-init --- catalog/app/containers/Admin/Users/Users.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/catalog/app/containers/Admin/Users/Users.tsx b/catalog/app/containers/Admin/Users/Users.tsx index 32d432edc19..d91872f7cb1 100644 --- a/catalog/app/containers/Admin/Users/Users.tsx +++ b/catalog/app/containers/Admin/Users/Users.tsx @@ -276,12 +276,14 @@ function Invite({ close, roles, defaultRole }: InviteProps) { ) const active = defaultRole || roles[0] - const selected = React.useMemo(() => [active], [active]) + const selected = [active] return ( onSubmit={onSubmit} initialValues={{ roles: { active, selected } }} + initialValuesEqual={R.equals} + keepDirtyOnReinitialize > {({ handleSubmit, @@ -750,6 +752,8 @@ function EditRoles({ close, roles, user }: EditRolesProps) { onSubmit={onSubmit} initialValues={{ roles: { active: user.role, selected } }} + initialValuesEqual={R.equals} + keepDirtyOnReinitialize > {({ handleSubmit, From dafac6b062013779ed3adda61079b1e7bea49cda Mon Sep 17 00:00:00 2001 From: nl_0 Date: Thu, 30 May 2024 15:16:43 +0200 Subject: [PATCH 30/69] Amdin/Form: FormErrorAuto component --- catalog/app/containers/Admin/Form.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/catalog/app/containers/Admin/Form.tsx b/catalog/app/containers/Admin/Form.tsx index ea33e695568..ded59440468 100644 --- a/catalog/app/containers/Admin/Form.tsx +++ b/catalog/app/containers/Admin/Form.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import type * as RF from 'react-final-form' +import * as RF from 'react-final-form' import * as M from '@material-ui/core' interface FieldOwnProps { @@ -95,3 +95,15 @@ export function FormError({ error, errors, ...rest }: FormErrorProps) { ) } + +export function FormErrorAuto(props: Omit) { + const state = RF.useFormState({ + subscription: { + error: true, + submitError: true, + submitFailed: true, + }, + }) + const error = state.submitFailed && (state.submitError || state.error) + return +} From 39dd9b8ebf2b60abc1a2c875336b236f93ae57b2 Mon Sep 17 00:00:00 2001 From: nl_0 Date: Thu, 30 May 2024 15:18:35 +0200 Subject: [PATCH 31/69] Admin/Users: use FormErrorAuto, fix submit error handling --- catalog/app/containers/Admin/Users/Users.tsx | 69 ++++++++------------ 1 file changed, 26 insertions(+), 43 deletions(-) diff --git a/catalog/app/containers/Admin/Users/Users.tsx b/catalog/app/containers/Admin/Users/Users.tsx index d91872f7cb1..879efb196e4 100644 --- a/catalog/app/containers/Admin/Users/Users.tsx +++ b/catalog/app/containers/Admin/Users/Users.tsx @@ -288,9 +288,7 @@ function Invite({ close, roles, defaultRole }: InviteProps) { {({ handleSubmit, submitting, - submitError, submitFailed, - error, hasSubmitErrors, hasValidationErrors, modifiedSinceLastSubmit, @@ -342,16 +340,13 @@ function Invite({ close, roles, defaultRole }: InviteProps) { name="roles" validate={validateRoleSelect}> {(props) => } - {(!!error || !!submitError) && ( - - )} +
@@ -408,7 +403,7 @@ function Edit({ close, user: { email: oldEmail, name } }: EditProps) { throw new Error(`Unexpected operation error: [${r.name}] ${r.message}`) case 'InvalidInput': const [e] = r.errors - if (e.path === 'input.email' && e.name === 'InvalidEmail') { + if (e.path === 'email' && e.name === 'InvalidEmail') { return { email: 'invalid' } } throw new Error( @@ -435,7 +430,6 @@ function Edit({ close, user: { email: oldEmail, name } }: EditProps) { handleSubmit, submitting, submitFailed, - error, hasSubmitErrors, hasValidationErrors, modifiedSinceLastSubmit, @@ -458,14 +452,11 @@ function Edit({ close, user: { email: oldEmail, name } }: EditProps) { }} autoComplete="off" /> - {submitFailed && ( - - )} + @@ -546,7 +537,7 @@ function Delete({ name, close }: DeleteProps) { return ( - {({ handleSubmit, submitting, submitError }) => ( + {({ handleSubmit, submitting }) => ( <> Delete a user @@ -554,16 +545,13 @@ function Delete({ name, close }: DeleteProps) {
This operation is irreversible.
- {!!submitError && ( - - )} +
{submitting && Deleting...} @@ -758,9 +746,7 @@ function EditRoles({ close, roles, user }: EditRolesProps) { {({ handleSubmit, submitting, - submitError, submitFailed, - error, hasSubmitErrors, hasValidationErrors, modifiedSinceLastSubmit, @@ -772,14 +758,11 @@ function EditRoles({ close, roles, user }: EditRolesProps) { name="roles" validate={validateRoleSelect}> {(props) => } - {(!!error || !!submitError) && ( - - )} + From beff626c5655e4b2a6d48a0da5fffa958dcce22c Mon Sep 17 00:00:00 2001 From: nl_0 Date: Thu, 30 May 2024 15:50:51 +0200 Subject: [PATCH 32/69] nicer edit dialog --- catalog/app/containers/Admin/Users/Users.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/catalog/app/containers/Admin/Users/Users.tsx b/catalog/app/containers/Admin/Users/Users.tsx index 879efb196e4..fa8427925d3 100644 --- a/catalog/app/containers/Admin/Users/Users.tsx +++ b/catalog/app/containers/Admin/Users/Users.tsx @@ -756,7 +756,7 @@ function EditRoles({ close, roles, user }: EditRolesProps) {
name="roles" validate={validateRoleSelect}> - {(props) => } + {(props) => }
- {/*TODO: reset?*/} Cancel @@ -885,7 +884,10 @@ const columns: Table.Column[] = [ getDisplay: (v: string | undefined, u, { roles, openDialog }: ColumnDisplayProps) => (
- openDialog(({ close }) => ) + openDialog( + ({ close }) => , + DIALOG_PROPS, + ) } > {v ?? emptyRole} From 140ec5958f0dd814c730ed65e7d162f3824662fb Mon Sep 17 00:00:00 2001 From: nl_0 Date: Fri, 31 May 2024 08:09:55 +0200 Subject: [PATCH 33/69] Admin/Form: add XXX note --- catalog/app/containers/Admin/Form.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/catalog/app/containers/Admin/Form.tsx b/catalog/app/containers/Admin/Form.tsx index ded59440468..b13912a750f 100644 --- a/catalog/app/containers/Admin/Form.tsx +++ b/catalog/app/containers/Admin/Form.tsx @@ -9,6 +9,7 @@ interface FieldOwnProps { export type FieldProps = FieldOwnProps & RF.FieldRenderProps & M.TextFieldProps // TODO: re-use components/Form/TextField +// XXX: extract all custom logic into a function and use MUI's TextField (and other components) explicitly export function Field({ input, meta, From 044e3af9ae7cec86e5bd8a35274a818287f10b7f Mon Sep 17 00:00:00 2001 From: nl_0 Date: Fri, 31 May 2024 08:16:04 +0200 Subject: [PATCH 34/69] Admin/Form: adjust FormErrorAuto API --- catalog/app/containers/Admin/Form.tsx | 16 +++++++---- catalog/app/containers/Admin/Users/Users.tsx | 28 +++++++++----------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/catalog/app/containers/Admin/Form.tsx b/catalog/app/containers/Admin/Form.tsx index b13912a750f..be0edabb2c0 100644 --- a/catalog/app/containers/Admin/Form.tsx +++ b/catalog/app/containers/Admin/Form.tsx @@ -2,8 +2,10 @@ import * as React from 'react' import * as RF from 'react-final-form' import * as M from '@material-ui/core' +type ErrorMessageMap = Record + interface FieldOwnProps { - errors: Record + errors: ErrorMessageMap } export type FieldProps = FieldOwnProps & RF.FieldRenderProps & M.TextFieldProps @@ -39,7 +41,7 @@ const useCheckboxStyles = M.makeStyles({ }) export interface CheckboxProps { - errors?: Record + errors?: ErrorMessageMap input?: RF.FieldInputProps meta: RF.FieldMetaState label?: React.ReactNode @@ -84,7 +86,7 @@ const useFormErrorStyles = M.makeStyles((t) => ({ interface FormErrorProps extends M.TypographyProps { error?: string - errors: Record + errors: ErrorMessageMap } export function FormError({ error, errors, ...rest }: FormErrorProps) { @@ -97,7 +99,11 @@ export function FormError({ error, errors, ...rest }: FormErrorProps) { ) } -export function FormErrorAuto(props: Omit) { +interface FormErrorAutoProps extends M.TypographyProps { + children: ErrorMessageMap +} + +export function FormErrorAuto({ children: errors, ...props }: FormErrorAutoProps) { const state = RF.useFormState({ subscription: { error: true, @@ -106,5 +112,5 @@ export function FormErrorAuto(props: Omit) { }, }) const error = state.submitFailed && (state.submitError || state.error) - return + return } diff --git a/catalog/app/containers/Admin/Users/Users.tsx b/catalog/app/containers/Admin/Users/Users.tsx index fa8427925d3..24957c6a739 100644 --- a/catalog/app/containers/Admin/Users/Users.tsx +++ b/catalog/app/containers/Admin/Users/Users.tsx @@ -340,13 +340,13 @@ function Invite({ close, roles, defaultRole }: InviteProps) { name="roles" validate={validateRoleSelect}> {(props) => } - + {{ unexpected: 'Something went wrong', smtp: 'SMTP error: contact your administrator', subscriptionInvalid: 'Invalid subscription', }} - /> + @@ -452,11 +452,9 @@ function Edit({ close, user: { email: oldEmail, name } }: EditProps) { }} autoComplete="off" /> - + + {{ unexpected: 'Something went wrong' }} + @@ -545,13 +543,13 @@ function Delete({ name, close }: DeleteProps) {
This operation is irreversible.
- + {{ unexpected: 'Something went wrong', notFound: 'User not found', // should not happen deleteSelf: 'You cannot delete yourself', // should not happen }} - /> + {submitting && Deleting...} @@ -758,11 +756,9 @@ function EditRoles({ close, roles, user }: EditRolesProps) { name="roles" validate={validateRoleSelect}> {(props) => } - + + {{ unexpected: 'Something went wrong' }} + From 211e4783a17b454adbe6a0801c8ccd510f21a927 Mon Sep 17 00:00:00 2001 From: nl_0 Date: Fri, 31 May 2024 10:17:50 +0200 Subject: [PATCH 35/69] tweak user admin table ui --- catalog/app/containers/Admin/Users/Users.tsx | 275 ++++++++++++------- 1 file changed, 183 insertions(+), 92 deletions(-) diff --git a/catalog/app/containers/Admin/Users/Users.tsx b/catalog/app/containers/Admin/Users/Users.tsx index 24957c6a739..ca6d06921d7 100644 --- a/catalog/app/containers/Admin/Users/Users.tsx +++ b/catalog/app/containers/Admin/Users/Users.tsx @@ -357,6 +357,7 @@ function Invite({ close, roles, defaultRole }: InviteProps) { void user: User } -function Edit({ close, user: { email: oldEmail, name } }: EditProps) { +function EditEmail({ close, user: { email: oldEmail, name } }: EditEmailProps) { const { push } = Notifications.use() const setEmail = GQL.useMutation(USER_SET_EMAIL_MUTATION) @@ -435,7 +436,7 @@ function Edit({ close, user: { email: oldEmail, name } }: EditProps) { modifiedSinceLastSubmit, }) => ( <> - Edit user: "{name}" + Edit email for user "{name}"
Cancel - + Delete @@ -615,13 +622,13 @@ function ConfirmAdminRights({ name, admin, close }: ConfirmAdminRightsProps) { {admin ? 'Grant' : 'Revoke'} admin rights You are about to {admin ? 'grant admin rights to' : 'revoke admin rights from'}{' '} - "{name}". + user "{name}". close(false)} color="primary"> Cancel - + {admin ? 'Grant' : 'Revoke'} @@ -644,24 +651,23 @@ const useUsernameStyles = M.makeStyles((t) => ({ }, })) -interface UsernameProps { - name: string - admin: boolean +interface UsernameDisplayProps { + user: User self: boolean } -function Username({ name, admin, self }: UsernameProps) { +function UsernameDisplay({ user, self }: UsernameDisplayProps) { const classes = useUsernameStyles() return ( - {admin && security} - + {user.isAdmin && security} + {self ? ( - {name}* + {user.name}* ) : ( - name + user.name )} @@ -750,7 +756,7 @@ function EditRoles({ close, roles, user }: EditRolesProps) { modifiedSinceLastSubmit, }) => ( <> - Configure roles for {user.name} + Configure roles for user "{user.name}" name="roles" validate={validateRoleSelect}> @@ -769,6 +775,7 @@ function EditRoles({ close, roles, user }: EditRolesProps) { ({ value, onChange, children }: EditableProps) { return children({ change, busy, value: savedValue }) } +const useEditableStyles = M.makeStyles((t) => ({ + root: { + marginLeft: t.spacing(0.5), + }, +})) + +interface EditableSwitchProps { + disabled?: boolean + checked: boolean + onChange: (v: boolean) => void + hint: NonNullable +} + +function EditableSwitch({ + disabled = false, + checked, + onChange, + hint, +}: EditableSwitchProps) { + const classes = useEditableStyles() + return disabled ? ( + + ) : ( + + {({ change, busy, value }) => ( + + change(e.target.checked)} + disabled={busy} + color="default" + /> + + )} + + ) +} + function UsersSkeleton() { return ( @@ -828,9 +874,80 @@ function UsersSkeleton() { ) } +const useEmailDisplayStyles = M.makeStyles((t) => ({ + root: { + borderBottom: `1px dashed ${t.palette.text.hint}`, + cursor: 'pointer', + }, +})) + +interface EmailDisplayProps { + user: User + openDialog: Dialogs.Open +} + +function EmailDisplay({ user, openDialog }: EmailDisplayProps) { + const classes = useEmailDisplayStyles() + + const edit = () => + openDialog(({ close }) => , DIALOG_PROPS) + + return ( + + + {user.email} + + + ) +} + // not a valid role name const emptyRole = '' +const useRoleDisplayStyles = M.makeStyles((t) => ({ + root: { + borderBottom: `1px dashed ${t.palette.text.hint}`, + cursor: 'pointer', + }, + extra: { + color: t.palette.text.hint, + }, +})) + +interface RoleDisplayProps { + user: User + roles: readonly Role[] + openDialog: Dialogs.Open +} + +function RoleDisplay({ user, roles, openDialog }: RoleDisplayProps) { + const classes = useRoleDisplayStyles() + + const edit = () => + openDialog(({ close }) => , DIALOG_PROPS) + + return ( + + + {user.role?.name ?? emptyRole} + {user.extraRoles.length > 0 && ( + +{user.extraRoles.length} + )} + + + ) +} + +function DateDisplay({ value }: { value: Date }) { + return ( + + + + + + ) +} + interface ColumnDisplayProps { roles: readonly Role[] setActive: (name: string, active: boolean) => Promise @@ -843,28 +960,22 @@ const columns: Table.Column[] = [ id: 'isActive', label: 'Enabled', getValue: (u) => u.isActive, - getDisplay: (v: boolean, u, { setActive, isSelf }: ColumnDisplayProps) => - isSelf ? ( - - ) : ( - setActive(u.name, active)}> - {({ change, busy, value }) => ( - change(e.target.checked)} - disabled={busy} - color="default" - /> - )} - - ), + getDisplay: (_v, u, { setActive, isSelf }: ColumnDisplayProps) => ( + setActive(u.name, active)} + /> + ), + props: { padding: 'none' }, }, { id: 'username', label: 'Username', getValue: (u) => u.name, - getDisplay: (_name: string, u, { isSelf }: ColumnDisplayProps) => ( - + getDisplay: (_v, u, { isSelf }: ColumnDisplayProps) => ( + ), props: { component: 'th', scope: 'row' }, }, @@ -872,75 +983,50 @@ const columns: Table.Column[] = [ id: 'email', label: 'Email', getValue: (u) => u.email, + getDisplay: (_v, u, { openDialog }: ColumnDisplayProps) => ( + + ), }, { id: 'role', label: 'Role', getValue: (u) => u.role?.name, - getDisplay: (v: string | undefined, u, { roles, openDialog }: ColumnDisplayProps) => ( -
- openDialog( - ({ close }) => , - DIALOG_PROPS, - ) - } - > - {v ?? emptyRole} - {u.extraRoles.length > 0 && +{u.extraRoles.length}} -
+ getDisplay: (_v, u, { roles, openDialog }: ColumnDisplayProps) => ( + ), }, { id: 'dateJoined', label: 'Date joined', getValue: (u) => u.dateJoined, - getDisplay: (v: Date) => ( - - - - ), + getDisplay: (_v, u) => , }, { id: 'lastLogin', label: 'Last login', getValue: (u) => u.lastLogin, - getDisplay: (v: Date) => ( - - - - ), + getDisplay: (_v, u) => , }, { id: 'isAdmin', label: 'Admin', - hint: 'Admins can see this page, add/remove users, and make/remove admins', getValue: (u) => u.isAdmin, - getDisplay: (v: boolean, { name }, { openDialog, isSelf }: ColumnDisplayProps) => - isSelf ? ( - - ) : ( - - openDialog( - ({ close }) => , - DIALOG_PROPS, - ).then((res) => { - if (!res) throw new Error('cancel') - }) - } - > - {({ change, busy, value }) => ( - change(e.target.checked)} - disabled={busy} - color="default" - /> - )} - - ), + getDisplay: (_v, u, { openDialog, isSelf }: ColumnDisplayProps) => ( + + openDialog( + ({ close }) => , + DIALOG_PROPS, + ).then((res) => { + if (!res) throw new Error('cancel') + }) + } + /> + ), + props: { padding: 'none' }, }, ] @@ -982,7 +1068,20 @@ function useSetActive() { ) } +const useStyles = M.makeStyles((t) => ({ + table: { + '& th, & td': { + whiteSpace: 'nowrap', + }, + '& tbody td': { + paddingRight: t.spacing(1), + }, + }, +})) + export default function Users() { + const classes = useStyles() + const data = GQL.useQueryS(USERS_QUERY) const rows = data.admin.user.list const { roles, defaultRole } = data @@ -997,7 +1096,7 @@ export default function Users() { }) const ordering = Table.useOrdering({ rows: filtering.filtered, - column: columns[0], + column: columns[1], }) const pagination = Pagination.use(ordering.ordered, { getItemId: (u: User) => u.name, @@ -1024,20 +1123,12 @@ export default function Users() { : { title: 'Delete', icon: delete, - fn: () => { + fn: () => openDialog( ({ close }) => , DIALOG_PROPS, - ) - }, + ), }, - { - title: 'Edit', - icon: edit, - fn: () => { - openDialog(({ close }) => , DIALOG_PROPS) - }, - }, ] const getDisplayProps = (u: User): ColumnDisplayProps => ({ @@ -1054,7 +1145,7 @@ export default function Users() { - + {pagination.paginated.map((i: User) => ( From 23f597fbded69d66f951205aebd5a2474d0f28b9 Mon Sep 17 00:00:00 2001 From: nl_0 Date: Fri, 31 May 2024 10:45:04 +0200 Subject: [PATCH 36/69] adjust dialogs layout --- catalog/app/containers/Admin/Users/Users.tsx | 24 +++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/catalog/app/containers/Admin/Users/Users.tsx b/catalog/app/containers/Admin/Users/Users.tsx index ca6d06921d7..ead0a6d8b96 100644 --- a/catalog/app/containers/Admin/Users/Users.tsx +++ b/catalog/app/containers/Admin/Users/Users.tsx @@ -189,6 +189,17 @@ function RoleSelect({ roles, input: { value, onChange }, meta, label }: RoleSele ) } +const useDialogFormStyles = M.makeStyles((t) => ({ + root: { + marginTop: t.spacing(-3), + }, +})) + +function DialogForm({ className, ...props }: React.FormHTMLAttributes) { + const classes = useDialogFormStyles() + return +} + const useInviteStyles = M.makeStyles({ infoIcon: { fontSize: '1.25em', @@ -296,7 +307,7 @@ function Invite({ close, roles, defaultRole }: InviteProps) { <> Invite a user - + - + @@ -438,12 +449,13 @@ function EditEmail({ close, user: { email: oldEmail, name } }: EditEmailProps) { <> Edit email for user "{name}" -
+ } label="Email" + placeholder="Enter an email" fullWidth margin="normal" errors={{ @@ -457,7 +469,7 @@ function EditEmail({ close, user: { email: oldEmail, name } }: EditEmailProps) { {{ unexpected: 'Something went wrong' }} - +
@@ -758,7 +770,7 @@ function EditRoles({ close, roles, user }: EditRolesProps) { <> Configure roles for user "{user.name}" -
+ name="roles" validate={validateRoleSelect}> {(props) => } @@ -766,7 +778,7 @@ function EditRoles({ close, roles, user }: EditRolesProps) { {{ unexpected: 'Something went wrong' }} - +
From 72109acd2dfe40276ddbb3bd27ea955d884c43c2 Mon Sep 17 00:00:00 2001 From: nl_0 Date: Fri, 31 May 2024 11:24:14 +0200 Subject: [PATCH 37/69] tweak styling --- catalog/app/containers/Admin/Users/Users.tsx | 56 ++++++++++---------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/catalog/app/containers/Admin/Users/Users.tsx b/catalog/app/containers/Admin/Users/Users.tsx index ead0a6d8b96..c2814f4edae 100644 --- a/catalog/app/containers/Admin/Users/Users.tsx +++ b/catalog/app/containers/Admin/Users/Users.tsx @@ -34,22 +34,6 @@ type Role = GQL.DataForDoc['roles'][number] const DIALOG_PROPS: Dialogs.ExtraDialogProps = { maxWidth: 'xs', fullWidth: true } -const useMonoStyles = M.makeStyles((t) => ({ - root: { - fontFamily: t.typography.monospace.fontFamily, - }, -})) - -interface MonoProps { - className?: string - children: React.ReactNode -} - -function Mono({ className, children }: MonoProps) { - const classes = useMonoStyles() - return {children} -} - interface RoleSelectValue { selected: readonly Role[] active: Role | null | undefined @@ -653,8 +637,22 @@ const useUsernameStyles = M.makeStyles((t) => ({ alignItems: 'center', display: 'flex', }, + name: { + maxWidth: '14rem', + overflow: 'hidden', + textOverflow: 'ellipsis', + }, admin: { - fontWeight: 600, + fontWeight: t.typography.fontWeightMedium, + }, + self: { + '&$name': { + maxWidth: '12rem', + }, + }, + you: { + color: t.palette.text.hint, + fontWeight: t.typography.fontWeightLight, }, icon: { fontSize: '1em', @@ -673,15 +671,18 @@ function UsernameDisplay({ user, self }: UsernameDisplayProps) { return ( {user.isAdmin && security} - - {self ? ( - - {user.name}* - - ) : ( - user.name - )} - + + + {user.name} + + + {self &&  (you)} ) } @@ -923,6 +924,7 @@ const useRoleDisplayStyles = M.makeStyles((t) => ({ }, extra: { color: t.palette.text.hint, + fontWeight: t.typography.fontWeightLight, }, })) @@ -1085,7 +1087,7 @@ const useStyles = M.makeStyles((t) => ({ '& th, & td': { whiteSpace: 'nowrap', }, - '& tbody td': { + '& tbody th, & tbody td': { paddingRight: t.spacing(1), }, }, From efa966c01a048005e2b05db53559cdce01e60bd8 Mon Sep 17 00:00:00 2001 From: nl_0 Date: Mon, 3 Jun 2024 09:20:00 +0200 Subject: [PATCH 38/69] adjust spacing --- catalog/app/containers/Admin/UsersAndRoles.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/catalog/app/containers/Admin/UsersAndRoles.tsx b/catalog/app/containers/Admin/UsersAndRoles.tsx index 97cc31cc945..e583bea2e7e 100644 --- a/catalog/app/containers/Admin/UsersAndRoles.tsx +++ b/catalog/app/containers/Admin/UsersAndRoles.tsx @@ -14,12 +14,13 @@ export default function UsersAndRoles() { - + - + + ) } From 2e8d73c949528471f6f60b359d8731ef8eb52792 Mon Sep 17 00:00:00 2001 From: nl_0 Date: Mon, 3 Jun 2024 09:25:36 +0200 Subject: [PATCH 39/69] move stuff --- catalog/app/containers/Admin/RolesAndPolicies/index.ts | 2 -- catalog/app/containers/Admin/Users/index.ts | 1 - .../{RolesAndPolicies => UsersAndRoles}/AssociatedRoles.tsx | 0 .../{RolesAndPolicies => UsersAndRoles}/AttachedPolicies.tsx | 0 .../BucketsPermissions.tsx | 0 .../Admin/{RolesAndPolicies => UsersAndRoles}/Filter.tsx | 0 .../Admin/{RolesAndPolicies => UsersAndRoles}/Policies.tsx | 0 .../Admin/{RolesAndPolicies => UsersAndRoles}/Roles.tsx | 0 .../app/containers/Admin/{Users => UsersAndRoles}/Users.tsx | 0 .../gql/BucketPermissionSelection.generated.ts | 0 .../gql/BucketPermissionSelection.graphql | 0 .../gql/Buckets.generated.ts | 0 .../{RolesAndPolicies => UsersAndRoles}/gql/Buckets.graphql | 0 .../gql/Policies.generated.ts | 0 .../{RolesAndPolicies => UsersAndRoles}/gql/Policies.graphql | 0 .../gql/PolicyCreateManaged.generated.ts | 0 .../gql/PolicyCreateManaged.graphql | 0 .../gql/PolicyCreateUnmanaged.generated.ts | 0 .../gql/PolicyCreateUnmanaged.graphql | 0 .../gql/PolicyDelete.generated.ts | 0 .../gql/PolicyDelete.graphql | 0 .../gql/PolicyResultSelection.generated.ts | 0 .../gql/PolicyResultSelection.graphql | 0 .../gql/PolicySelection.generated.ts | 0 .../gql/PolicySelection.graphql | 0 .../gql/PolicyUpdateManaged.generated.ts | 0 .../gql/PolicyUpdateManaged.graphql | 0 .../gql/PolicyUpdateUnmanaged.generated.ts | 0 .../gql/PolicyUpdateUnmanaged.graphql | 0 .../gql/RoleCreateManaged.generated.ts | 0 .../gql/RoleCreateManaged.graphql | 0 .../gql/RoleCreateUnmanaged.generated.ts | 0 .../gql/RoleCreateUnmanaged.graphql | 0 .../gql/RoleDelete.generated.ts | 0 .../gql/RoleDelete.graphql | 0 .../gql/RoleSelection.generated.ts | 0 .../gql/RoleSelection.graphql | 0 .../gql/RoleSetDefault.generated.ts | 0 .../gql/RoleSetDefault.graphql | 0 .../gql/RoleUpdateManaged.generated.ts | 0 .../gql/RoleUpdateManaged.graphql | 0 .../gql/RoleUpdateUnmanaged.generated.ts | 0 .../gql/RoleUpdateUnmanaged.graphql | 0 .../gql/Roles.generated.ts | 0 .../{RolesAndPolicies => UsersAndRoles}/gql/Roles.graphql | 0 .../{Users => UsersAndRoles}/gql/UserCreate.generated.ts | 0 .../Admin/{Users => UsersAndRoles}/gql/UserCreate.graphql | 0 .../{Users => UsersAndRoles}/gql/UserDelete.generated.ts | 0 .../Admin/{Users => UsersAndRoles}/gql/UserDelete.graphql | 0 .../gql/UserResultSelection.generated.ts | 0 .../{Users => UsersAndRoles}/gql/UserResultSelection.graphql | 0 .../{Users => UsersAndRoles}/gql/UserSelection.generated.ts | 0 .../Admin/{Users => UsersAndRoles}/gql/UserSelection.graphql | 0 .../{Users => UsersAndRoles}/gql/UserSetActive.generated.ts | 0 .../Admin/{Users => UsersAndRoles}/gql/UserSetActive.graphql | 0 .../{Users => UsersAndRoles}/gql/UserSetAdmin.generated.ts | 0 .../Admin/{Users => UsersAndRoles}/gql/UserSetAdmin.graphql | 0 .../{Users => UsersAndRoles}/gql/UserSetEmail.generated.ts | 0 .../Admin/{Users => UsersAndRoles}/gql/UserSetEmail.graphql | 0 .../{Users => UsersAndRoles}/gql/UserSetRole.generated.ts | 0 .../Admin/{Users => UsersAndRoles}/gql/UserSetRole.graphql | 0 .../Admin/{Users => UsersAndRoles}/gql/Users.generated.ts | 0 .../Admin/{Users => UsersAndRoles}/gql/Users.graphql | 0 .../Admin/{UsersAndRoles.tsx => UsersAndRoles/index.tsx} | 4 ++-- .../Admin/{RolesAndPolicies => UsersAndRoles}/shared.ts | 0 65 files changed, 2 insertions(+), 5 deletions(-) delete mode 100644 catalog/app/containers/Admin/RolesAndPolicies/index.ts delete mode 100644 catalog/app/containers/Admin/Users/index.ts rename catalog/app/containers/Admin/{RolesAndPolicies => UsersAndRoles}/AssociatedRoles.tsx (100%) rename catalog/app/containers/Admin/{RolesAndPolicies => UsersAndRoles}/AttachedPolicies.tsx (100%) rename catalog/app/containers/Admin/{RolesAndPolicies => UsersAndRoles}/BucketsPermissions.tsx (100%) rename catalog/app/containers/Admin/{RolesAndPolicies => UsersAndRoles}/Filter.tsx (100%) rename catalog/app/containers/Admin/{RolesAndPolicies => UsersAndRoles}/Policies.tsx (100%) rename catalog/app/containers/Admin/{RolesAndPolicies => UsersAndRoles}/Roles.tsx (100%) rename catalog/app/containers/Admin/{Users => UsersAndRoles}/Users.tsx (100%) rename catalog/app/containers/Admin/{RolesAndPolicies => UsersAndRoles}/gql/BucketPermissionSelection.generated.ts (100%) rename catalog/app/containers/Admin/{RolesAndPolicies => UsersAndRoles}/gql/BucketPermissionSelection.graphql (100%) rename catalog/app/containers/Admin/{RolesAndPolicies => UsersAndRoles}/gql/Buckets.generated.ts (100%) rename catalog/app/containers/Admin/{RolesAndPolicies => UsersAndRoles}/gql/Buckets.graphql (100%) rename catalog/app/containers/Admin/{RolesAndPolicies => UsersAndRoles}/gql/Policies.generated.ts (100%) rename catalog/app/containers/Admin/{RolesAndPolicies => UsersAndRoles}/gql/Policies.graphql (100%) rename catalog/app/containers/Admin/{RolesAndPolicies => UsersAndRoles}/gql/PolicyCreateManaged.generated.ts (100%) rename catalog/app/containers/Admin/{RolesAndPolicies => UsersAndRoles}/gql/PolicyCreateManaged.graphql (100%) rename catalog/app/containers/Admin/{RolesAndPolicies => UsersAndRoles}/gql/PolicyCreateUnmanaged.generated.ts (100%) rename catalog/app/containers/Admin/{RolesAndPolicies => UsersAndRoles}/gql/PolicyCreateUnmanaged.graphql (100%) rename catalog/app/containers/Admin/{RolesAndPolicies => UsersAndRoles}/gql/PolicyDelete.generated.ts (100%) rename catalog/app/containers/Admin/{RolesAndPolicies => UsersAndRoles}/gql/PolicyDelete.graphql (100%) rename catalog/app/containers/Admin/{RolesAndPolicies => UsersAndRoles}/gql/PolicyResultSelection.generated.ts (100%) rename catalog/app/containers/Admin/{RolesAndPolicies => UsersAndRoles}/gql/PolicyResultSelection.graphql (100%) rename catalog/app/containers/Admin/{RolesAndPolicies => UsersAndRoles}/gql/PolicySelection.generated.ts (100%) rename catalog/app/containers/Admin/{RolesAndPolicies => UsersAndRoles}/gql/PolicySelection.graphql (100%) rename catalog/app/containers/Admin/{RolesAndPolicies => UsersAndRoles}/gql/PolicyUpdateManaged.generated.ts (100%) rename catalog/app/containers/Admin/{RolesAndPolicies => UsersAndRoles}/gql/PolicyUpdateManaged.graphql (100%) rename catalog/app/containers/Admin/{RolesAndPolicies => UsersAndRoles}/gql/PolicyUpdateUnmanaged.generated.ts (100%) rename catalog/app/containers/Admin/{RolesAndPolicies => UsersAndRoles}/gql/PolicyUpdateUnmanaged.graphql (100%) rename catalog/app/containers/Admin/{RolesAndPolicies => UsersAndRoles}/gql/RoleCreateManaged.generated.ts (100%) rename catalog/app/containers/Admin/{RolesAndPolicies => UsersAndRoles}/gql/RoleCreateManaged.graphql (100%) rename catalog/app/containers/Admin/{RolesAndPolicies => UsersAndRoles}/gql/RoleCreateUnmanaged.generated.ts (100%) rename catalog/app/containers/Admin/{RolesAndPolicies => UsersAndRoles}/gql/RoleCreateUnmanaged.graphql (100%) rename catalog/app/containers/Admin/{RolesAndPolicies => UsersAndRoles}/gql/RoleDelete.generated.ts (100%) rename catalog/app/containers/Admin/{RolesAndPolicies => UsersAndRoles}/gql/RoleDelete.graphql (100%) rename catalog/app/containers/Admin/{RolesAndPolicies => UsersAndRoles}/gql/RoleSelection.generated.ts (100%) rename catalog/app/containers/Admin/{RolesAndPolicies => UsersAndRoles}/gql/RoleSelection.graphql (100%) rename catalog/app/containers/Admin/{RolesAndPolicies => UsersAndRoles}/gql/RoleSetDefault.generated.ts (100%) rename catalog/app/containers/Admin/{RolesAndPolicies => UsersAndRoles}/gql/RoleSetDefault.graphql (100%) rename catalog/app/containers/Admin/{RolesAndPolicies => UsersAndRoles}/gql/RoleUpdateManaged.generated.ts (100%) rename catalog/app/containers/Admin/{RolesAndPolicies => UsersAndRoles}/gql/RoleUpdateManaged.graphql (100%) rename catalog/app/containers/Admin/{RolesAndPolicies => UsersAndRoles}/gql/RoleUpdateUnmanaged.generated.ts (100%) rename catalog/app/containers/Admin/{RolesAndPolicies => UsersAndRoles}/gql/RoleUpdateUnmanaged.graphql (100%) rename catalog/app/containers/Admin/{RolesAndPolicies => UsersAndRoles}/gql/Roles.generated.ts (100%) rename catalog/app/containers/Admin/{RolesAndPolicies => UsersAndRoles}/gql/Roles.graphql (100%) rename catalog/app/containers/Admin/{Users => UsersAndRoles}/gql/UserCreate.generated.ts (100%) rename catalog/app/containers/Admin/{Users => UsersAndRoles}/gql/UserCreate.graphql (100%) rename catalog/app/containers/Admin/{Users => UsersAndRoles}/gql/UserDelete.generated.ts (100%) rename catalog/app/containers/Admin/{Users => UsersAndRoles}/gql/UserDelete.graphql (100%) rename catalog/app/containers/Admin/{Users => UsersAndRoles}/gql/UserResultSelection.generated.ts (100%) rename catalog/app/containers/Admin/{Users => UsersAndRoles}/gql/UserResultSelection.graphql (100%) rename catalog/app/containers/Admin/{Users => UsersAndRoles}/gql/UserSelection.generated.ts (100%) rename catalog/app/containers/Admin/{Users => UsersAndRoles}/gql/UserSelection.graphql (100%) rename catalog/app/containers/Admin/{Users => UsersAndRoles}/gql/UserSetActive.generated.ts (100%) rename catalog/app/containers/Admin/{Users => UsersAndRoles}/gql/UserSetActive.graphql (100%) rename catalog/app/containers/Admin/{Users => UsersAndRoles}/gql/UserSetAdmin.generated.ts (100%) rename catalog/app/containers/Admin/{Users => UsersAndRoles}/gql/UserSetAdmin.graphql (100%) rename catalog/app/containers/Admin/{Users => UsersAndRoles}/gql/UserSetEmail.generated.ts (100%) rename catalog/app/containers/Admin/{Users => UsersAndRoles}/gql/UserSetEmail.graphql (100%) rename catalog/app/containers/Admin/{Users => UsersAndRoles}/gql/UserSetRole.generated.ts (100%) rename catalog/app/containers/Admin/{Users => UsersAndRoles}/gql/UserSetRole.graphql (100%) rename catalog/app/containers/Admin/{Users => UsersAndRoles}/gql/Users.generated.ts (100%) rename catalog/app/containers/Admin/{Users => UsersAndRoles}/gql/Users.graphql (100%) rename catalog/app/containers/Admin/{UsersAndRoles.tsx => UsersAndRoles/index.tsx} (80%) rename catalog/app/containers/Admin/{RolesAndPolicies => UsersAndRoles}/shared.ts (100%) diff --git a/catalog/app/containers/Admin/RolesAndPolicies/index.ts b/catalog/app/containers/Admin/RolesAndPolicies/index.ts deleted file mode 100644 index b9c6038f761..00000000000 --- a/catalog/app/containers/Admin/RolesAndPolicies/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default as Roles } from './Roles' -export { default as Policies } from './Policies' diff --git a/catalog/app/containers/Admin/Users/index.ts b/catalog/app/containers/Admin/Users/index.ts deleted file mode 100644 index ebbbf9cf16e..00000000000 --- a/catalog/app/containers/Admin/Users/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './Users' diff --git a/catalog/app/containers/Admin/RolesAndPolicies/AssociatedRoles.tsx b/catalog/app/containers/Admin/UsersAndRoles/AssociatedRoles.tsx similarity index 100% rename from catalog/app/containers/Admin/RolesAndPolicies/AssociatedRoles.tsx rename to catalog/app/containers/Admin/UsersAndRoles/AssociatedRoles.tsx diff --git a/catalog/app/containers/Admin/RolesAndPolicies/AttachedPolicies.tsx b/catalog/app/containers/Admin/UsersAndRoles/AttachedPolicies.tsx similarity index 100% rename from catalog/app/containers/Admin/RolesAndPolicies/AttachedPolicies.tsx rename to catalog/app/containers/Admin/UsersAndRoles/AttachedPolicies.tsx diff --git a/catalog/app/containers/Admin/RolesAndPolicies/BucketsPermissions.tsx b/catalog/app/containers/Admin/UsersAndRoles/BucketsPermissions.tsx similarity index 100% rename from catalog/app/containers/Admin/RolesAndPolicies/BucketsPermissions.tsx rename to catalog/app/containers/Admin/UsersAndRoles/BucketsPermissions.tsx diff --git a/catalog/app/containers/Admin/RolesAndPolicies/Filter.tsx b/catalog/app/containers/Admin/UsersAndRoles/Filter.tsx similarity index 100% rename from catalog/app/containers/Admin/RolesAndPolicies/Filter.tsx rename to catalog/app/containers/Admin/UsersAndRoles/Filter.tsx diff --git a/catalog/app/containers/Admin/RolesAndPolicies/Policies.tsx b/catalog/app/containers/Admin/UsersAndRoles/Policies.tsx similarity index 100% rename from catalog/app/containers/Admin/RolesAndPolicies/Policies.tsx rename to catalog/app/containers/Admin/UsersAndRoles/Policies.tsx diff --git a/catalog/app/containers/Admin/RolesAndPolicies/Roles.tsx b/catalog/app/containers/Admin/UsersAndRoles/Roles.tsx similarity index 100% rename from catalog/app/containers/Admin/RolesAndPolicies/Roles.tsx rename to catalog/app/containers/Admin/UsersAndRoles/Roles.tsx diff --git a/catalog/app/containers/Admin/Users/Users.tsx b/catalog/app/containers/Admin/UsersAndRoles/Users.tsx similarity index 100% rename from catalog/app/containers/Admin/Users/Users.tsx rename to catalog/app/containers/Admin/UsersAndRoles/Users.tsx diff --git a/catalog/app/containers/Admin/RolesAndPolicies/gql/BucketPermissionSelection.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/BucketPermissionSelection.generated.ts similarity index 100% rename from catalog/app/containers/Admin/RolesAndPolicies/gql/BucketPermissionSelection.generated.ts rename to catalog/app/containers/Admin/UsersAndRoles/gql/BucketPermissionSelection.generated.ts diff --git a/catalog/app/containers/Admin/RolesAndPolicies/gql/BucketPermissionSelection.graphql b/catalog/app/containers/Admin/UsersAndRoles/gql/BucketPermissionSelection.graphql similarity index 100% rename from catalog/app/containers/Admin/RolesAndPolicies/gql/BucketPermissionSelection.graphql rename to catalog/app/containers/Admin/UsersAndRoles/gql/BucketPermissionSelection.graphql diff --git a/catalog/app/containers/Admin/RolesAndPolicies/gql/Buckets.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/Buckets.generated.ts similarity index 100% rename from catalog/app/containers/Admin/RolesAndPolicies/gql/Buckets.generated.ts rename to catalog/app/containers/Admin/UsersAndRoles/gql/Buckets.generated.ts diff --git a/catalog/app/containers/Admin/RolesAndPolicies/gql/Buckets.graphql b/catalog/app/containers/Admin/UsersAndRoles/gql/Buckets.graphql similarity index 100% rename from catalog/app/containers/Admin/RolesAndPolicies/gql/Buckets.graphql rename to catalog/app/containers/Admin/UsersAndRoles/gql/Buckets.graphql diff --git a/catalog/app/containers/Admin/RolesAndPolicies/gql/Policies.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/Policies.generated.ts similarity index 100% rename from catalog/app/containers/Admin/RolesAndPolicies/gql/Policies.generated.ts rename to catalog/app/containers/Admin/UsersAndRoles/gql/Policies.generated.ts diff --git a/catalog/app/containers/Admin/RolesAndPolicies/gql/Policies.graphql b/catalog/app/containers/Admin/UsersAndRoles/gql/Policies.graphql similarity index 100% rename from catalog/app/containers/Admin/RolesAndPolicies/gql/Policies.graphql rename to catalog/app/containers/Admin/UsersAndRoles/gql/Policies.graphql diff --git a/catalog/app/containers/Admin/RolesAndPolicies/gql/PolicyCreateManaged.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/PolicyCreateManaged.generated.ts similarity index 100% rename from catalog/app/containers/Admin/RolesAndPolicies/gql/PolicyCreateManaged.generated.ts rename to catalog/app/containers/Admin/UsersAndRoles/gql/PolicyCreateManaged.generated.ts diff --git a/catalog/app/containers/Admin/RolesAndPolicies/gql/PolicyCreateManaged.graphql b/catalog/app/containers/Admin/UsersAndRoles/gql/PolicyCreateManaged.graphql similarity index 100% rename from catalog/app/containers/Admin/RolesAndPolicies/gql/PolicyCreateManaged.graphql rename to catalog/app/containers/Admin/UsersAndRoles/gql/PolicyCreateManaged.graphql diff --git a/catalog/app/containers/Admin/RolesAndPolicies/gql/PolicyCreateUnmanaged.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/PolicyCreateUnmanaged.generated.ts similarity index 100% rename from catalog/app/containers/Admin/RolesAndPolicies/gql/PolicyCreateUnmanaged.generated.ts rename to catalog/app/containers/Admin/UsersAndRoles/gql/PolicyCreateUnmanaged.generated.ts diff --git a/catalog/app/containers/Admin/RolesAndPolicies/gql/PolicyCreateUnmanaged.graphql b/catalog/app/containers/Admin/UsersAndRoles/gql/PolicyCreateUnmanaged.graphql similarity index 100% rename from catalog/app/containers/Admin/RolesAndPolicies/gql/PolicyCreateUnmanaged.graphql rename to catalog/app/containers/Admin/UsersAndRoles/gql/PolicyCreateUnmanaged.graphql diff --git a/catalog/app/containers/Admin/RolesAndPolicies/gql/PolicyDelete.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/PolicyDelete.generated.ts similarity index 100% rename from catalog/app/containers/Admin/RolesAndPolicies/gql/PolicyDelete.generated.ts rename to catalog/app/containers/Admin/UsersAndRoles/gql/PolicyDelete.generated.ts diff --git a/catalog/app/containers/Admin/RolesAndPolicies/gql/PolicyDelete.graphql b/catalog/app/containers/Admin/UsersAndRoles/gql/PolicyDelete.graphql similarity index 100% rename from catalog/app/containers/Admin/RolesAndPolicies/gql/PolicyDelete.graphql rename to catalog/app/containers/Admin/UsersAndRoles/gql/PolicyDelete.graphql diff --git a/catalog/app/containers/Admin/RolesAndPolicies/gql/PolicyResultSelection.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/PolicyResultSelection.generated.ts similarity index 100% rename from catalog/app/containers/Admin/RolesAndPolicies/gql/PolicyResultSelection.generated.ts rename to catalog/app/containers/Admin/UsersAndRoles/gql/PolicyResultSelection.generated.ts diff --git a/catalog/app/containers/Admin/RolesAndPolicies/gql/PolicyResultSelection.graphql b/catalog/app/containers/Admin/UsersAndRoles/gql/PolicyResultSelection.graphql similarity index 100% rename from catalog/app/containers/Admin/RolesAndPolicies/gql/PolicyResultSelection.graphql rename to catalog/app/containers/Admin/UsersAndRoles/gql/PolicyResultSelection.graphql diff --git a/catalog/app/containers/Admin/RolesAndPolicies/gql/PolicySelection.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/PolicySelection.generated.ts similarity index 100% rename from catalog/app/containers/Admin/RolesAndPolicies/gql/PolicySelection.generated.ts rename to catalog/app/containers/Admin/UsersAndRoles/gql/PolicySelection.generated.ts diff --git a/catalog/app/containers/Admin/RolesAndPolicies/gql/PolicySelection.graphql b/catalog/app/containers/Admin/UsersAndRoles/gql/PolicySelection.graphql similarity index 100% rename from catalog/app/containers/Admin/RolesAndPolicies/gql/PolicySelection.graphql rename to catalog/app/containers/Admin/UsersAndRoles/gql/PolicySelection.graphql diff --git a/catalog/app/containers/Admin/RolesAndPolicies/gql/PolicyUpdateManaged.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/PolicyUpdateManaged.generated.ts similarity index 100% rename from catalog/app/containers/Admin/RolesAndPolicies/gql/PolicyUpdateManaged.generated.ts rename to catalog/app/containers/Admin/UsersAndRoles/gql/PolicyUpdateManaged.generated.ts diff --git a/catalog/app/containers/Admin/RolesAndPolicies/gql/PolicyUpdateManaged.graphql b/catalog/app/containers/Admin/UsersAndRoles/gql/PolicyUpdateManaged.graphql similarity index 100% rename from catalog/app/containers/Admin/RolesAndPolicies/gql/PolicyUpdateManaged.graphql rename to catalog/app/containers/Admin/UsersAndRoles/gql/PolicyUpdateManaged.graphql diff --git a/catalog/app/containers/Admin/RolesAndPolicies/gql/PolicyUpdateUnmanaged.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/PolicyUpdateUnmanaged.generated.ts similarity index 100% rename from catalog/app/containers/Admin/RolesAndPolicies/gql/PolicyUpdateUnmanaged.generated.ts rename to catalog/app/containers/Admin/UsersAndRoles/gql/PolicyUpdateUnmanaged.generated.ts diff --git a/catalog/app/containers/Admin/RolesAndPolicies/gql/PolicyUpdateUnmanaged.graphql b/catalog/app/containers/Admin/UsersAndRoles/gql/PolicyUpdateUnmanaged.graphql similarity index 100% rename from catalog/app/containers/Admin/RolesAndPolicies/gql/PolicyUpdateUnmanaged.graphql rename to catalog/app/containers/Admin/UsersAndRoles/gql/PolicyUpdateUnmanaged.graphql diff --git a/catalog/app/containers/Admin/RolesAndPolicies/gql/RoleCreateManaged.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/RoleCreateManaged.generated.ts similarity index 100% rename from catalog/app/containers/Admin/RolesAndPolicies/gql/RoleCreateManaged.generated.ts rename to catalog/app/containers/Admin/UsersAndRoles/gql/RoleCreateManaged.generated.ts diff --git a/catalog/app/containers/Admin/RolesAndPolicies/gql/RoleCreateManaged.graphql b/catalog/app/containers/Admin/UsersAndRoles/gql/RoleCreateManaged.graphql similarity index 100% rename from catalog/app/containers/Admin/RolesAndPolicies/gql/RoleCreateManaged.graphql rename to catalog/app/containers/Admin/UsersAndRoles/gql/RoleCreateManaged.graphql diff --git a/catalog/app/containers/Admin/RolesAndPolicies/gql/RoleCreateUnmanaged.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/RoleCreateUnmanaged.generated.ts similarity index 100% rename from catalog/app/containers/Admin/RolesAndPolicies/gql/RoleCreateUnmanaged.generated.ts rename to catalog/app/containers/Admin/UsersAndRoles/gql/RoleCreateUnmanaged.generated.ts diff --git a/catalog/app/containers/Admin/RolesAndPolicies/gql/RoleCreateUnmanaged.graphql b/catalog/app/containers/Admin/UsersAndRoles/gql/RoleCreateUnmanaged.graphql similarity index 100% rename from catalog/app/containers/Admin/RolesAndPolicies/gql/RoleCreateUnmanaged.graphql rename to catalog/app/containers/Admin/UsersAndRoles/gql/RoleCreateUnmanaged.graphql diff --git a/catalog/app/containers/Admin/RolesAndPolicies/gql/RoleDelete.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/RoleDelete.generated.ts similarity index 100% rename from catalog/app/containers/Admin/RolesAndPolicies/gql/RoleDelete.generated.ts rename to catalog/app/containers/Admin/UsersAndRoles/gql/RoleDelete.generated.ts diff --git a/catalog/app/containers/Admin/RolesAndPolicies/gql/RoleDelete.graphql b/catalog/app/containers/Admin/UsersAndRoles/gql/RoleDelete.graphql similarity index 100% rename from catalog/app/containers/Admin/RolesAndPolicies/gql/RoleDelete.graphql rename to catalog/app/containers/Admin/UsersAndRoles/gql/RoleDelete.graphql diff --git a/catalog/app/containers/Admin/RolesAndPolicies/gql/RoleSelection.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/RoleSelection.generated.ts similarity index 100% rename from catalog/app/containers/Admin/RolesAndPolicies/gql/RoleSelection.generated.ts rename to catalog/app/containers/Admin/UsersAndRoles/gql/RoleSelection.generated.ts diff --git a/catalog/app/containers/Admin/RolesAndPolicies/gql/RoleSelection.graphql b/catalog/app/containers/Admin/UsersAndRoles/gql/RoleSelection.graphql similarity index 100% rename from catalog/app/containers/Admin/RolesAndPolicies/gql/RoleSelection.graphql rename to catalog/app/containers/Admin/UsersAndRoles/gql/RoleSelection.graphql diff --git a/catalog/app/containers/Admin/RolesAndPolicies/gql/RoleSetDefault.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/RoleSetDefault.generated.ts similarity index 100% rename from catalog/app/containers/Admin/RolesAndPolicies/gql/RoleSetDefault.generated.ts rename to catalog/app/containers/Admin/UsersAndRoles/gql/RoleSetDefault.generated.ts diff --git a/catalog/app/containers/Admin/RolesAndPolicies/gql/RoleSetDefault.graphql b/catalog/app/containers/Admin/UsersAndRoles/gql/RoleSetDefault.graphql similarity index 100% rename from catalog/app/containers/Admin/RolesAndPolicies/gql/RoleSetDefault.graphql rename to catalog/app/containers/Admin/UsersAndRoles/gql/RoleSetDefault.graphql diff --git a/catalog/app/containers/Admin/RolesAndPolicies/gql/RoleUpdateManaged.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/RoleUpdateManaged.generated.ts similarity index 100% rename from catalog/app/containers/Admin/RolesAndPolicies/gql/RoleUpdateManaged.generated.ts rename to catalog/app/containers/Admin/UsersAndRoles/gql/RoleUpdateManaged.generated.ts diff --git a/catalog/app/containers/Admin/RolesAndPolicies/gql/RoleUpdateManaged.graphql b/catalog/app/containers/Admin/UsersAndRoles/gql/RoleUpdateManaged.graphql similarity index 100% rename from catalog/app/containers/Admin/RolesAndPolicies/gql/RoleUpdateManaged.graphql rename to catalog/app/containers/Admin/UsersAndRoles/gql/RoleUpdateManaged.graphql diff --git a/catalog/app/containers/Admin/RolesAndPolicies/gql/RoleUpdateUnmanaged.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/RoleUpdateUnmanaged.generated.ts similarity index 100% rename from catalog/app/containers/Admin/RolesAndPolicies/gql/RoleUpdateUnmanaged.generated.ts rename to catalog/app/containers/Admin/UsersAndRoles/gql/RoleUpdateUnmanaged.generated.ts diff --git a/catalog/app/containers/Admin/RolesAndPolicies/gql/RoleUpdateUnmanaged.graphql b/catalog/app/containers/Admin/UsersAndRoles/gql/RoleUpdateUnmanaged.graphql similarity index 100% rename from catalog/app/containers/Admin/RolesAndPolicies/gql/RoleUpdateUnmanaged.graphql rename to catalog/app/containers/Admin/UsersAndRoles/gql/RoleUpdateUnmanaged.graphql diff --git a/catalog/app/containers/Admin/RolesAndPolicies/gql/Roles.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/Roles.generated.ts similarity index 100% rename from catalog/app/containers/Admin/RolesAndPolicies/gql/Roles.generated.ts rename to catalog/app/containers/Admin/UsersAndRoles/gql/Roles.generated.ts diff --git a/catalog/app/containers/Admin/RolesAndPolicies/gql/Roles.graphql b/catalog/app/containers/Admin/UsersAndRoles/gql/Roles.graphql similarity index 100% rename from catalog/app/containers/Admin/RolesAndPolicies/gql/Roles.graphql rename to catalog/app/containers/Admin/UsersAndRoles/gql/Roles.graphql diff --git a/catalog/app/containers/Admin/Users/gql/UserCreate.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/UserCreate.generated.ts similarity index 100% rename from catalog/app/containers/Admin/Users/gql/UserCreate.generated.ts rename to catalog/app/containers/Admin/UsersAndRoles/gql/UserCreate.generated.ts diff --git a/catalog/app/containers/Admin/Users/gql/UserCreate.graphql b/catalog/app/containers/Admin/UsersAndRoles/gql/UserCreate.graphql similarity index 100% rename from catalog/app/containers/Admin/Users/gql/UserCreate.graphql rename to catalog/app/containers/Admin/UsersAndRoles/gql/UserCreate.graphql diff --git a/catalog/app/containers/Admin/Users/gql/UserDelete.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/UserDelete.generated.ts similarity index 100% rename from catalog/app/containers/Admin/Users/gql/UserDelete.generated.ts rename to catalog/app/containers/Admin/UsersAndRoles/gql/UserDelete.generated.ts diff --git a/catalog/app/containers/Admin/Users/gql/UserDelete.graphql b/catalog/app/containers/Admin/UsersAndRoles/gql/UserDelete.graphql similarity index 100% rename from catalog/app/containers/Admin/Users/gql/UserDelete.graphql rename to catalog/app/containers/Admin/UsersAndRoles/gql/UserDelete.graphql diff --git a/catalog/app/containers/Admin/Users/gql/UserResultSelection.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/UserResultSelection.generated.ts similarity index 100% rename from catalog/app/containers/Admin/Users/gql/UserResultSelection.generated.ts rename to catalog/app/containers/Admin/UsersAndRoles/gql/UserResultSelection.generated.ts diff --git a/catalog/app/containers/Admin/Users/gql/UserResultSelection.graphql b/catalog/app/containers/Admin/UsersAndRoles/gql/UserResultSelection.graphql similarity index 100% rename from catalog/app/containers/Admin/Users/gql/UserResultSelection.graphql rename to catalog/app/containers/Admin/UsersAndRoles/gql/UserResultSelection.graphql diff --git a/catalog/app/containers/Admin/Users/gql/UserSelection.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/UserSelection.generated.ts similarity index 100% rename from catalog/app/containers/Admin/Users/gql/UserSelection.generated.ts rename to catalog/app/containers/Admin/UsersAndRoles/gql/UserSelection.generated.ts diff --git a/catalog/app/containers/Admin/Users/gql/UserSelection.graphql b/catalog/app/containers/Admin/UsersAndRoles/gql/UserSelection.graphql similarity index 100% rename from catalog/app/containers/Admin/Users/gql/UserSelection.graphql rename to catalog/app/containers/Admin/UsersAndRoles/gql/UserSelection.graphql diff --git a/catalog/app/containers/Admin/Users/gql/UserSetActive.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/UserSetActive.generated.ts similarity index 100% rename from catalog/app/containers/Admin/Users/gql/UserSetActive.generated.ts rename to catalog/app/containers/Admin/UsersAndRoles/gql/UserSetActive.generated.ts diff --git a/catalog/app/containers/Admin/Users/gql/UserSetActive.graphql b/catalog/app/containers/Admin/UsersAndRoles/gql/UserSetActive.graphql similarity index 100% rename from catalog/app/containers/Admin/Users/gql/UserSetActive.graphql rename to catalog/app/containers/Admin/UsersAndRoles/gql/UserSetActive.graphql diff --git a/catalog/app/containers/Admin/Users/gql/UserSetAdmin.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/UserSetAdmin.generated.ts similarity index 100% rename from catalog/app/containers/Admin/Users/gql/UserSetAdmin.generated.ts rename to catalog/app/containers/Admin/UsersAndRoles/gql/UserSetAdmin.generated.ts diff --git a/catalog/app/containers/Admin/Users/gql/UserSetAdmin.graphql b/catalog/app/containers/Admin/UsersAndRoles/gql/UserSetAdmin.graphql similarity index 100% rename from catalog/app/containers/Admin/Users/gql/UserSetAdmin.graphql rename to catalog/app/containers/Admin/UsersAndRoles/gql/UserSetAdmin.graphql diff --git a/catalog/app/containers/Admin/Users/gql/UserSetEmail.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/UserSetEmail.generated.ts similarity index 100% rename from catalog/app/containers/Admin/Users/gql/UserSetEmail.generated.ts rename to catalog/app/containers/Admin/UsersAndRoles/gql/UserSetEmail.generated.ts diff --git a/catalog/app/containers/Admin/Users/gql/UserSetEmail.graphql b/catalog/app/containers/Admin/UsersAndRoles/gql/UserSetEmail.graphql similarity index 100% rename from catalog/app/containers/Admin/Users/gql/UserSetEmail.graphql rename to catalog/app/containers/Admin/UsersAndRoles/gql/UserSetEmail.graphql diff --git a/catalog/app/containers/Admin/Users/gql/UserSetRole.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/UserSetRole.generated.ts similarity index 100% rename from catalog/app/containers/Admin/Users/gql/UserSetRole.generated.ts rename to catalog/app/containers/Admin/UsersAndRoles/gql/UserSetRole.generated.ts diff --git a/catalog/app/containers/Admin/Users/gql/UserSetRole.graphql b/catalog/app/containers/Admin/UsersAndRoles/gql/UserSetRole.graphql similarity index 100% rename from catalog/app/containers/Admin/Users/gql/UserSetRole.graphql rename to catalog/app/containers/Admin/UsersAndRoles/gql/UserSetRole.graphql diff --git a/catalog/app/containers/Admin/Users/gql/Users.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/Users.generated.ts similarity index 100% rename from catalog/app/containers/Admin/Users/gql/Users.generated.ts rename to catalog/app/containers/Admin/UsersAndRoles/gql/Users.generated.ts diff --git a/catalog/app/containers/Admin/Users/gql/Users.graphql b/catalog/app/containers/Admin/UsersAndRoles/gql/Users.graphql similarity index 100% rename from catalog/app/containers/Admin/Users/gql/Users.graphql rename to catalog/app/containers/Admin/UsersAndRoles/gql/Users.graphql diff --git a/catalog/app/containers/Admin/UsersAndRoles.tsx b/catalog/app/containers/Admin/UsersAndRoles/index.tsx similarity index 80% rename from catalog/app/containers/Admin/UsersAndRoles.tsx rename to catalog/app/containers/Admin/UsersAndRoles/index.tsx index e583bea2e7e..0527aa3bc60 100644 --- a/catalog/app/containers/Admin/UsersAndRoles.tsx +++ b/catalog/app/containers/Admin/UsersAndRoles/index.tsx @@ -3,8 +3,8 @@ import * as M from '@material-ui/core' import MetaTitle from 'utils/MetaTitle' -// XXX: move the components imported below into a single module -import { Roles, Policies } from './RolesAndPolicies' +import Policies from './Policies' +import Roles from './Roles' import Users from './Users' export default function UsersAndRoles() { diff --git a/catalog/app/containers/Admin/RolesAndPolicies/shared.ts b/catalog/app/containers/Admin/UsersAndRoles/shared.ts similarity index 100% rename from catalog/app/containers/Admin/RolesAndPolicies/shared.ts rename to catalog/app/containers/Admin/UsersAndRoles/shared.ts From d7c829b20667b549b9640feaec291041493c03c4 Mon Sep 17 00:00:00 2001 From: nl_0 Date: Mon, 3 Jun 2024 09:31:05 +0200 Subject: [PATCH 40/69] cleanup --- .../Admin/UsersAndRoles/Policies.tsx | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/catalog/app/containers/Admin/UsersAndRoles/Policies.tsx b/catalog/app/containers/Admin/UsersAndRoles/Policies.tsx index 6c5caa0af33..cf5a219fdaa 100644 --- a/catalog/app/containers/Admin/UsersAndRoles/Policies.tsx +++ b/catalog/app/containers/Admin/UsersAndRoles/Policies.tsx @@ -601,7 +601,7 @@ function Edit({ policy, close }: EditProps) { interface SettingsMenuProps { policy: Policy - openDialog: (render: (props: DialogsOpenProps) => JSX.Element, props?: $TSFixMe) => void + openDialog: Dialogs.Open } function SettingsMenu({ policy, openDialog }: SettingsMenuProps) { @@ -641,11 +641,6 @@ function SettingsMenu({ policy, openDialog }: SettingsMenuProps) { ) } -// XXX: move to dialogs module -interface DialogsOpenProps { - close: (reason?: string) => void -} - export default function Policies() { const { policies: rows } = GQL.useQueryS(POLICIES_QUERY) @@ -664,7 +659,7 @@ export default function Policies() { title: 'Create', icon: add, fn: React.useCallback(() => { - dialogs.open(({ close }: DialogsOpenProps) => ) + dialogs.open(({ close }) => ) }, [dialogs.open]), // eslint-disable-line react-hooks/exhaustive-deps }, ] @@ -681,14 +676,7 @@ export default function Policies() { title: 'Edit', icon: edit, fn: () => { - dialogs.open(({ close }: DialogsOpenProps) => ( - - )) + dialogs.open(({ close }) => ) }, }, ] From 9729d07f533620faa6abb35fd66e501160ea3bfc Mon Sep 17 00:00:00 2001 From: nl_0 Date: Mon, 3 Jun 2024 09:42:46 +0200 Subject: [PATCH 41/69] proper suspense wrappers --- .../Admin/UsersAndRoles/Policies.tsx | 61 +++++++---------- .../containers/Admin/UsersAndRoles/Roles.tsx | 65 ++++++++---------- .../Admin/UsersAndRoles/SuspenseWrapper.tsx | 26 +++++++ .../containers/Admin/UsersAndRoles/Users.tsx | 67 ++++++++----------- .../containers/Admin/UsersAndRoles/index.tsx | 13 +++- 5 files changed, 118 insertions(+), 114 deletions(-) create mode 100644 catalog/app/containers/Admin/UsersAndRoles/SuspenseWrapper.tsx diff --git a/catalog/app/containers/Admin/UsersAndRoles/Policies.tsx b/catalog/app/containers/Admin/UsersAndRoles/Policies.tsx index cf5a219fdaa..33ccf850c62 100644 --- a/catalog/app/containers/Admin/UsersAndRoles/Policies.tsx +++ b/catalog/app/containers/Admin/UsersAndRoles/Policies.tsx @@ -682,41 +682,32 @@ export default function Policies() { ] return ( - - - -
- } - > - - {dialogs.render({ fullWidth: true, maxWidth: 'sm' })} - - - - - - - - {ordering.ordered.map((i: Policy) => ( - - {columns.map((col) => ( - - {(col.getDisplay || R.identity)(col.getValue(i), i)} - - ))} - - - - + <> + {dialogs.render({ fullWidth: true, maxWidth: 'sm' })} + + + + + + + + {ordering.ordered.map((i: Policy) => ( + + {columns.map((col) => ( + + {(col.getDisplay || R.identity)(col.getValue(i), i)} - - ))} - - - - - + ))} + + + + + + + ))} + + + + ) } diff --git a/catalog/app/containers/Admin/UsersAndRoles/Roles.tsx b/catalog/app/containers/Admin/UsersAndRoles/Roles.tsx index 4b0129696a9..d8e2c8b5517 100644 --- a/catalog/app/containers/Admin/UsersAndRoles/Roles.tsx +++ b/catalog/app/containers/Admin/UsersAndRoles/Roles.tsx @@ -734,43 +734,34 @@ export default function Roles() { ] return ( - - - - - } - > - - {dialogs.render({ fullWidth: true, maxWidth: 'sm' })} - - - - - - - - {ordering.ordered.map((i: Role) => ( - - {columns.map((col) => ( - - {(col.getDisplay || R.identity)(col.getValue(i), i, { - defaultRoleId, - })} - - ))} - - - - + <> + {dialogs.render({ fullWidth: true, maxWidth: 'sm' })} + + + + + + + + {ordering.ordered.map((i: Role) => ( + + {columns.map((col) => ( + + {(col.getDisplay || R.identity)(col.getValue(i), i, { + defaultRoleId, + })} - - ))} - - - - - + ))} + + + + + + + ))} + + + + ) } diff --git a/catalog/app/containers/Admin/UsersAndRoles/SuspenseWrapper.tsx b/catalog/app/containers/Admin/UsersAndRoles/SuspenseWrapper.tsx new file mode 100644 index 00000000000..714e81e4215 --- /dev/null +++ b/catalog/app/containers/Admin/UsersAndRoles/SuspenseWrapper.tsx @@ -0,0 +1,26 @@ +import * as React from 'react' +import * as M from '@material-ui/core' + +import * as Table from '../Table' + +interface SuspenseWrapperProps { + children: React.ReactNode + heading: React.ReactNode +} + +export default function SuspenseWrapper({ children, heading }: SuspenseWrapperProps) { + return ( + + + + + + } + > + {children} + + + ) +} diff --git a/catalog/app/containers/Admin/UsersAndRoles/Users.tsx b/catalog/app/containers/Admin/UsersAndRoles/Users.tsx index c2814f4edae..aa07aa0bcf3 100644 --- a/catalog/app/containers/Admin/UsersAndRoles/Users.tsx +++ b/catalog/app/containers/Admin/UsersAndRoles/Users.tsx @@ -878,15 +878,6 @@ function EditableSwitch({ ) } -function UsersSkeleton() { - return ( - - - - - ) -} - const useEmailDisplayStyles = M.makeStyles((t) => ({ root: { borderBottom: `1px dashed ${t.palette.text.hint}`, @@ -1153,36 +1144,34 @@ export default function Users() { }) return ( - }> - - - - - - - - - {pagination.paginated.map((i: User) => ( - - {columns.map((col) => ( - - {(col.getDisplay || R.identity)( - col.getValue(i), - i, - getDisplayProps(i), - )} - - ))} - - + <> + + + + + + + + {pagination.paginated.map((i: User) => ( + + {columns.map((col) => ( + + {(col.getDisplay || R.identity)( + col.getValue(i), + i, + getDisplayProps(i), + )} - - ))} - - - - - - + ))} + + + + + ))} + + + + + ) } diff --git a/catalog/app/containers/Admin/UsersAndRoles/index.tsx b/catalog/app/containers/Admin/UsersAndRoles/index.tsx index 0527aa3bc60..a91d5ea6dbe 100644 --- a/catalog/app/containers/Admin/UsersAndRoles/index.tsx +++ b/catalog/app/containers/Admin/UsersAndRoles/index.tsx @@ -6,19 +6,26 @@ import MetaTitle from 'utils/MetaTitle' import Policies from './Policies' import Roles from './Roles' import Users from './Users' +import SuspenseWrapper from './SuspenseWrapper' export default function UsersAndRoles() { return ( <> {['Users, Roles and Policies', 'Admin']} - + + + - + + + - + + + From 33b8f241146cfcb51a0ad3e5d269f6d09e71f1d9 Mon Sep 17 00:00:00 2001 From: nl_0 Date: Mon, 3 Jun 2024 10:02:42 +0200 Subject: [PATCH 42/69] dry --- .../containers/Admin/UsersAndRoles/Users.tsx | 67 +++++++++---------- 1 file changed, 32 insertions(+), 35 deletions(-) diff --git a/catalog/app/containers/Admin/UsersAndRoles/Users.tsx b/catalog/app/containers/Admin/UsersAndRoles/Users.tsx index aa07aa0bcf3..72664d82d38 100644 --- a/catalog/app/containers/Admin/UsersAndRoles/Users.tsx +++ b/catalog/app/containers/Admin/UsersAndRoles/Users.tsx @@ -632,6 +632,33 @@ function ConfirmAdminRights({ name, admin, close }: ConfirmAdminRightsProps) { ) } +const useHintStyles = M.makeStyles((t) => ({ + hint: { + color: t.palette.text.hint, + fontWeight: t.typography.fontWeightLight, + }, +})) + +function Hint({ className, ...props }: React.HTMLAttributes) { + const classes = useHintStyles() + return +} + +const useClickableStyles = M.makeStyles((t) => ({ + clickable: { + borderBottom: `1px dashed ${t.palette.text.hint}`, + cursor: 'pointer', + }, +})) + +const Clickable = React.forwardRef< + HTMLSpanElement, + React.HTMLAttributes +>(function Clickable({ className, ...props }, ref) { + const classes = useClickableStyles() + return +}) + const useUsernameStyles = M.makeStyles((t) => ({ root: { alignItems: 'center', @@ -650,10 +677,6 @@ const useUsernameStyles = M.makeStyles((t) => ({ maxWidth: '12rem', }, }, - you: { - color: t.palette.text.hint, - fontWeight: t.typography.fontWeightLight, - }, icon: { fontSize: '1em', marginLeft: `calc(-1em - ${t.spacing(0.5)}px)`, @@ -682,7 +705,7 @@ function UsernameDisplay({ user, self }: UsernameDisplayProps) { {user.name} - {self &&  (you)} + {self &&  (you)} ) } @@ -878,29 +901,18 @@ function EditableSwitch({ ) } -const useEmailDisplayStyles = M.makeStyles((t) => ({ - root: { - borderBottom: `1px dashed ${t.palette.text.hint}`, - cursor: 'pointer', - }, -})) - interface EmailDisplayProps { user: User openDialog: Dialogs.Open } function EmailDisplay({ user, openDialog }: EmailDisplayProps) { - const classes = useEmailDisplayStyles() - const edit = () => openDialog(({ close }) => , DIALOG_PROPS) return ( - - {user.email} - + {user.email} ) } @@ -908,17 +920,6 @@ function EmailDisplay({ user, openDialog }: EmailDisplayProps) { // not a valid role name const emptyRole = '' -const useRoleDisplayStyles = M.makeStyles((t) => ({ - root: { - borderBottom: `1px dashed ${t.palette.text.hint}`, - cursor: 'pointer', - }, - extra: { - color: t.palette.text.hint, - fontWeight: t.typography.fontWeightLight, - }, -})) - interface RoleDisplayProps { user: User roles: readonly Role[] @@ -926,19 +927,15 @@ interface RoleDisplayProps { } function RoleDisplay({ user, roles, openDialog }: RoleDisplayProps) { - const classes = useRoleDisplayStyles() - const edit = () => openDialog(({ close }) => , DIALOG_PROPS) return ( - + {user.role?.name ?? emptyRole} - {user.extraRoles.length > 0 && ( - +{user.extraRoles.length} - )} - + {user.extraRoles.length > 0 && +{user.extraRoles.length}} + ) } From 7fbb9e07d929e9381b6d3df06ffd1db10d30fa67 Mon Sep 17 00:00:00 2001 From: nl_0 Date: Wed, 5 Jun 2024 12:13:04 +0200 Subject: [PATCH 43/69] NavBar: refactor and tweak nav menu logic and layout --- catalog/app/containers/NavBar/NavBar.tsx | 621 ++------------------- catalog/app/containers/NavBar/NavMenu.tsx | 626 ++++++++++++++++++++++ 2 files changed, 656 insertions(+), 591 deletions(-) create mode 100644 catalog/app/containers/NavBar/NavMenu.tsx diff --git a/catalog/app/containers/NavBar/NavBar.tsx b/catalog/app/containers/NavBar/NavBar.tsx index 36717fb17f9..34606c1e9e0 100644 --- a/catalog/app/containers/NavBar/NavBar.tsx +++ b/catalog/app/containers/NavBar/NavBar.tsx @@ -1,41 +1,19 @@ -import invariant from 'invariant' -import * as R from 'ramda' import * as React from 'react' -import * as redux from 'react-redux' import { Link, useRouteMatch } from 'react-router-dom' -import { createStructuredSelector } from 'reselect' -import { sanitizeUrl } from '@braintree/sanitize-url' import * as M from '@material-ui/core' -import * as Lab from '@material-ui/lab' -import * as Intercom from 'components/Intercom' import Logo from 'components/Logo' -import * as Bookmarks from 'containers/Bookmarks' import cfg from 'constants/config' import * as style from 'constants/style' import * as URLS from 'constants/urls' -import * as authSelectors from 'containers/Auth/selectors' import * as CatalogSettings from 'utils/CatalogSettings' -import * as Dialogs from 'utils/GlobalDialogs' -import * as GQL from 'utils/GraphQL' -import HashLink from 'utils/HashLink' import * as NamedRoutes from 'utils/NamedRoutes' -import assertNever from 'utils/assertNever' import bg from './bg.png' import Controls from './Controls' +import * as NavMenu from './NavMenu' import * as Subscription from './Subscription' -import ME_QUERY from './gql/Me.generated' -import SWITCH_ROLE_MUTATION from './gql/SwitchRole.generated' - -type MaybeMe = GQL.DataForDoc['me'] -type Me = NonNullable - -const SWITCH_ROLES_DIALOG_PROPS: Dialogs.ExtraDialogProps = { - maxWidth: 'sm', - fullWidth: true, -} const useLogoLinkStyles = M.makeStyles((t) => ({ bgQuilt: { @@ -96,445 +74,6 @@ function QuiltLink({ className }: QuiltLinkProps) { ) } -const useItemStyles = M.makeStyles({ - root: { - display: 'inline-flex', - maxWidth: '400px', - overflow: 'hidden', - textOverflow: 'ellipsis', - }, -}) - -// type ItemProps = (LinkProps | { href: string }) & M.MenuItemProps -interface ItemProps extends M.MenuItemProps { - to?: string - href?: string -} - -// FIXME: doesn't compile with Ref -// const Item = React.forwardRef((props: ItemProps, ref: React.Ref) => ( -const Item = React.forwardRef( - ({ children, ...props }: ItemProps, ref: React.Ref) => { - const classes = useItemStyles() - return ( - - {children} - - ) - }, -) - -const userDisplay = (user: Me) => ( - <> - {user.isAdmin && ( - <> - security -   - - )} - {user.name} - -) - -const useBadgeStyles = M.makeStyles({ - root: { - alignItems: 'inherit', - }, - badge: { - top: '4px', - }, -}) - -interface BadgeProps extends M.BadgeProps {} - -function Badge({ children, color, invisible, ...props }: BadgeProps) { - const classes = useBadgeStyles() - return ( - - {children} - - ) -} - -const useRolesSwitcherStyles = M.makeStyles((t) => ({ - progress: { - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - padding: t.spacing(4), - }, -})) - -const useListItemTextStyles = M.makeStyles((t) => ({ - root: { - padding: t.spacing(0, 1), - }, - primary: { - overflow: 'hidden', - textOverflow: 'ellipsis', - }, -})) - -interface RolesSwitcherProps { - user: Me -} - -function RolesSwitcher({ user }: RolesSwitcherProps) { - const switchRole = GQL.useMutation(SWITCH_ROLE_MUTATION) - const classes = useRolesSwitcherStyles() - const textClasses = useListItemTextStyles() - const loading = true - const [state, setState] = React.useState(null) - const handleClick = React.useCallback( - async (roleName: string) => { - setState(loading) - try { - const { switchRole: r } = await switchRole({ roleName }) - switch (r.__typename) { - case 'Me': - window.location.reload() - break - case 'InvalidInput': - case 'OperationError': - throw new Error('Failed to switch role. Try again') - default: - assertNever(r) - } - } catch (err) { - // eslint-disable-next-line no-console - console.error('Error switching role', err) - if (err instanceof Error) { - setState(err) - } else { - setState(new Error('Unexpected error switching role')) - } - } - }, - [loading, switchRole], - ) - return ( - <> - Switch role - {state !== true ? ( - <> - {state instanceof Error && ( - {state.message} - )} - - {user.roles.map((role) => ( - handleClick(role.name)} - selected={role.name === user.role.name} - > - {role.name} - - ))} - - - ) : ( -
- -
- )} - - ) -} - -interface UserDropdownProps { - user: Me -} - -function UserDropdown({ user }: UserDropdownProps) { - const { urls, paths } = NamedRoutes.use() - const bookmarks = Bookmarks.use() - const isProfile = !!useRouteMatch({ path: paths.profile, exact: true }) - const isAdmin = !!useRouteMatch(paths.admin) - const [anchor, setAnchor] = React.useState(null) - const [visible, setVisible] = React.useState(true) - - const open = React.useCallback( - (evt) => { - setAnchor(evt.target) - }, - [setAnchor], - ) - - const close = React.useCallback(() => { - setVisible(false) - setAnchor(null) - }, [setAnchor]) - - const openDialog = Dialogs.use() - - const showBookmarks = React.useCallback(() => { - if (!bookmarks) return - bookmarks.show() - close() - }, [bookmarks, close]) - - const showRolesSwitcher = React.useCallback(() => { - openDialog(() => , SWITCH_ROLES_DIALOG_PROPS) - close() - }, [openDialog, close, user]) - - React.useEffect(() => { - const hasUpdates = bookmarks?.hasUpdates || false - if (hasUpdates !== visible) setVisible(!!hasUpdates) - }, [bookmarks, visible]) - - return ( - <> - - - {userDisplay(user)} - {' '} - expand_more - - - - - {bookmarks && ( - - - bookmarks_outlined - -  Bookmarks - - )} - {user.roles.length > 1 && ( - - loop Switch role - - )} - {user.isAdmin && ( - - security Admin settings - - )} - {cfg.mode === 'OPEN' && ( - - Profile - - )} - - Sign Out - - - - - ) -} - -function useHam() { - const [anchor, setAnchor] = React.useState(null) - - const open = React.useCallback( - (evt) => { - setAnchor(evt.target) - }, - [setAnchor], - ) - - const close = React.useCallback(() => { - setAnchor(null) - }, [setAnchor]) - - const render = (children: React.ReactNode) => ( - <> - - menu - - - - {children} - - - - ) - - return { open, close, render } -} - -function useAuthLinks(onHamClose: () => void): React.ReactNode[] { - const { error, waiting, authenticated } = redux.useSelector(selector) - const { urls, paths } = NamedRoutes.use() - const isProfile = !!useRouteMatch({ path: paths.profile, exact: true }) - const isAdmin = !!useRouteMatch(paths.admin) - - const res = GQL.useQuery(ME_QUERY, undefined, { pause: waiting || !authenticated }) - - if (error) - return [ - - error_outline Sign In - , - ] - if (waiting) { - return [ - - - , - ] - } - - return GQL.fold(res, { - data: (d) => { - invariant(d.me, 'Expected "me" to be non-null') - return [ - - {userDisplay(d.me)} - , - d.me.isAdmin && ( - - - security -  Admin settings - - ), - cfg.mode === 'OPEN' && ( - - - Profile - - ), - - - Sign Out - , - ] - }, - fetching: () => [ - - - , - ], - error: () => [ - - error_outline Sign In - , - ], - }) -} - -const useSignInErrorStyles = M.makeStyles((t) => ({ - icon: { - marginRight: t.spacing(1), - }, -})) - -interface DesktopSignInErrorProps { - error: Error | GQL.ErrorForData -} - -function DesktopSignInError({ error }: DesktopSignInErrorProps) { - const classes = useSignInErrorStyles() - const { urls } = NamedRoutes.use() - return ( - <> - - error_outline - - - Sign In - - - ) -} - -function DesktopSignIn() { - const { error, waiting, authenticated } = redux.useSelector(selector) - const { paths } = NamedRoutes.use() - const isSignIn = !!useRouteMatch({ path: paths.signIn, exact: true }) - const res = GQL.useQuery(ME_QUERY, undefined, { pause: waiting || !authenticated }) - - if (!authenticated && isSignIn) return null - - if (error) { - return - } - - if (waiting) { - return - } - - return GQL.fold(res, { - data: (d) => { - invariant(d.me, 'Expected "me" to be non-null') - return - }, - fetching: () => , - error: (err) => , - }) -} - -interface MobileSignInProps { - links: LinkDescriptor[] -} - -function MobileSignIn({ links }: MobileSignInProps) { - const ham = useHam() - - const mobileLinks = React.useMemo( - () => - links.map(({ label, ...rest }) => ( - - {label} - - )), - [ham.close, links], - ) - - if (cfg.disableNavigator || cfg.mode === 'LOCAL') { - return ham.render(mobileLinks) - } - - return -} - -interface MobileSignInInnerProps { - ham: ReturnType - links: React.ReactNode[] -} - -// It's just a wrapper to put hook call, so we can hide it under condition -function MobileSignInWithAuth({ ham, links }: MobileSignInInnerProps) { - const authLinks = useAuthLinks(ham.close) - return ham.render([...authLinks, , ...links]) -} - const useAppBarStyles = M.makeStyles((t) => ({ root: { zIndex: t.zIndex.appBar + 1, @@ -636,166 +175,66 @@ export function Container({ children }: ContainerProps) { ) } -interface NavLinkOwnProps { - to?: string - path?: string -} - -type NavLinkProps = NavLinkOwnProps & M.BoxProps +const useLicenseErrorStyles = M.makeStyles((t) => ({ + licenseError: { + color: t.palette.error.light, + marginRight: t.spacing(0.5), -const NavLink = React.forwardRef((props: NavLinkProps, ref: React.Ref) => { - const isActive = !!useRouteMatch({ path: props.path, exact: true }) - return ( - 10 - ? props.children - : undefined - } - {...props} - ref={ref} - /> - ) -}) + [t.breakpoints.down('sm')]: { + marginLeft: t.spacing(1.5), + marginRight: 0, + }, + }, +})) -interface LinkDescriptor { - label: string - to?: string - href?: string - target?: '_blank' +interface LicenseErrorProps { + restore: () => void } -function useLinks(): LinkDescriptor[] { - const { paths, urls } = NamedRoutes.use() - const settings = CatalogSettings.use() - const customNavLink: LinkDescriptor | null = React.useMemo(() => { - if (!settings?.customNavLink) return null - const href = sanitizeUrl(settings.customNavLink.url) - if (href === 'about:blank') return null - return { - href, - label: settings.customNavLink.label, - target: '_blank', - } - }, [settings?.customNavLink]) - - return [ - process.env.NODE_ENV === 'development' && { - to: urls.example(), - label: 'Example', - }, - customNavLink, - cfg.mode !== 'MARKETING' && { - to: urls.uriResolver(), - label: 'URI', - path: paths.uriResolver, - }, - { href: URLS.docs, label: 'Docs' }, - cfg.mode === 'MARKETING' && { to: `${urls.home()}#pricing`, label: 'Pricing' }, - (cfg.mode === 'MARKETING' || cfg.mode === 'OPEN') && { - href: URLS.jobs, - label: 'Jobs', - }, - cfg.mode !== 'PRODUCT' && { href: URLS.blog, label: 'Blog' }, - cfg.mode === 'MARKETING' && { to: urls.about(), label: 'About' }, - ].filter(Boolean) as LinkDescriptor[] +function LicenseError({ restore }: LicenseErrorProps) { + const classes = useLicenseErrorStyles() + return ( + + + error_outline + + + ) } -const selector = createStructuredSelector( - R.pick(['error', 'waiting', 'authenticated'], authSelectors), -) - -const useNavBarStyles = M.makeStyles((t) => ({ - nav: { - alignItems: 'center', - display: 'flex', - marginLeft: t.spacing(3), - marginRight: t.spacing(2), - }, - navItem: { - '& + &': { - marginLeft: t.spacing(2), - }, - }, +const useNavBarStyles = M.makeStyles({ quiltLogo: { margin: '0 0 3px 8px', }, spacer: { flexGrow: 1, }, - licenseError: { - color: t.palette.error.light, - marginRight: t.spacing(0.5), - - [t.breakpoints.down('sm')]: { - marginLeft: t.spacing(1.5), - marginRight: 0, - }, - }, -})) +}) export function NavBar() { const settings = CatalogSettings.use() const { paths } = NamedRoutes.use() const isSignIn = !!useRouteMatch({ path: paths.signIn, exact: true }) - const t = M.useTheme() - const useHamburger = M.useMediaQuery(t.breakpoints.down('sm')) - const links = useLinks() - const intercom = Intercom.use() const classes = useNavBarStyles() + const t = M.useTheme() + const collapse = M.useMediaQuery(t.breakpoints.down('sm')) const sub = Subscription.useState() return ( - - {cfg.disableNavigator || (cfg.alwaysRequiresAuth && isSignIn) ? (
) : ( )} - {!useHamburger && ( - - )} + - {sub.invalid && ( - - - error_outline - - - )} + {!collapse && } - {!cfg.disableNavigator && cfg.mode !== 'LOCAL' && !useHamburger && ( - - )} + {sub.invalid && } - {useHamburger && } + {settings?.logo?.url && } diff --git a/catalog/app/containers/NavBar/NavMenu.tsx b/catalog/app/containers/NavBar/NavMenu.tsx new file mode 100644 index 00000000000..acf4c5d0d80 --- /dev/null +++ b/catalog/app/containers/NavBar/NavMenu.tsx @@ -0,0 +1,626 @@ +// import cx from 'classnames' +import * as R from 'ramda' +import * as React from 'react' +import * as redux from 'react-redux' +import { Link, useRouteMatch } from 'react-router-dom' +import * as RR from 'react-router-dom' +import { createStructuredSelector } from 'reselect' +import { sanitizeUrl } from '@braintree/sanitize-url' +import * as M from '@material-ui/core' +import * as Lab from '@material-ui/lab' + +import * as Intercom from 'components/Intercom' +import cfg from 'constants/config' +import * as style from 'constants/style' +import * as URLS from 'constants/urls' +import * as Bookmarks from 'containers/Bookmarks' +import * as authSelectors from 'containers/Auth/selectors' +import * as CatalogSettings from 'utils/CatalogSettings' +import * as Dialogs from 'utils/GlobalDialogs' +import * as GQL from 'utils/GraphQL' +import * as NamedRoutes from 'utils/NamedRoutes' +import assertNever from 'utils/assertNever' +import * as tagged from 'utils/taggedV2' + +import ME_QUERY from './gql/Me.generated' +import SWITCH_ROLE_MUTATION from './gql/SwitchRole.generated' + +type MaybeMe = GQL.DataForDoc['me'] +type Me = NonNullable + +const AuthState = tagged.create('app/containers/NavBar/NavMenu:AuthState' as const, { + Loading: () => {}, + Error: (error: Error) => ({ error }), + Ready: (user: MaybeMe) => ({ user }), +}) + +// eslint-disable-next-line @typescript-eslint/no-redeclare +type AuthState = tagged.InstanceOf + +const authSelector = createStructuredSelector( + R.pick(['error', 'waiting', 'authenticated'], authSelectors), +) + +function useAuthState(): AuthState { + const { error, waiting, authenticated } = redux.useSelector(authSelector) + const meQuery = GQL.useQuery(ME_QUERY, {}, { pause: waiting || !authenticated }) + if (error) return AuthState.Error(error) + if (waiting) return AuthState.Loading() + if (!authenticated) return AuthState.Ready(null) + return GQL.fold(meQuery, { + data: (d) => + d.me + ? AuthState.Ready(d.me) + : AuthState.Error(new Error("Couldn't load user data")), + fetching: () => AuthState.Loading(), + error: (err) => AuthState.Error(err), + }) +} + +// XXX: don't use a separate type for this? +const NavItemDescriptor = tagged.create( + 'app/containers/NavBar/NavMenu:NavItemDescriptor' as const, + { + To: (to: string, children: React.ReactNode) => ({ to, children }), + Href: (href: string, children: React.ReactNode) => ({ href, children }), + }, +) + +// eslint-disable-next-line @typescript-eslint/no-redeclare +type NavItemDescriptor = tagged.InstanceOf + +const MenuItemDescriptor = tagged.create( + 'app/containers/NavBar/NavMenu:MenuItemDescriptor' as const, + { + To: (to: string, children: React.ReactNode) => ({ to, children }), + Href: (href: string, children: React.ReactNode) => ({ href, children }), + Click: (onClick: () => void, children: React.ReactNode) => ({ onClick, children }), + Text: (children: React.ReactNode) => ({ children }), + Divider: () => {}, + }, +) + +// eslint-disable-next-line @typescript-eslint/no-redeclare +type MenuItemDescriptor = tagged.InstanceOf + +interface DropdownMenuProps + extends Omit< + M.MenuProps, + 'anchorEl' | 'open' | 'onClose' | 'children' | 'MenuListProps' + > { + trigger: ( + open: React.EventHandler>, + ) => React.ReactNode + items: (MenuItemDescriptor | null | false)[] +} + +function DropdownMenu({ trigger, items, ...rest }: DropdownMenuProps) { + const [anchor, setAnchor] = React.useState(null) + + const open = React.useCallback( + (evt: React.SyntheticEvent) => { + setAnchor(evt.currentTarget) + }, + [setAnchor], + ) + + const close = React.useCallback(() => { + setAnchor(null) + }, [setAnchor]) + + const filtered = items.filter(Boolean) as MenuItemDescriptor[] + const children = filtered.map( + MenuItemDescriptor.match({ + To: (props, i) => ( + + {({ match }) => ( + + )} + + ), + Href: (props, i) => ( + + ), + Click: ({ onClick, ...props }, i) => ( + { + onClick() + close() + }} + {...props} + /> + ), + Text: (props, i) => , + Divider: (_, i) => , + }), + ) + + return ( + <> + {trigger(open)} + + + {children} + + + + ) +} + +const useRoleSwitcherStyles = M.makeStyles((t) => ({ + progress: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + padding: t.spacing(4), + }, +})) + +const useListItemTextStyles = M.makeStyles((t) => ({ + root: { + padding: t.spacing(0, 1), + }, + primary: { + overflow: 'hidden', + textOverflow: 'ellipsis', + }, +})) + +interface RoleSwitcherProps { + user: Me +} + +function RoleSwitcher({ user }: RoleSwitcherProps) { + const switchRole = GQL.useMutation(SWITCH_ROLE_MUTATION) + const classes = useRoleSwitcherStyles() + const textClasses = useListItemTextStyles() + const loading = true + const [state, setState] = React.useState(null) + const handleClick = React.useCallback( + async (roleName: string) => { + setState(loading) + try { + const { switchRole: r } = await switchRole({ roleName }) + switch (r.__typename) { + case 'Me': + window.location.reload() + break + case 'InvalidInput': + case 'OperationError': + throw new Error('Failed to switch role. Try again') + default: + assertNever(r) + } + } catch (err) { + // eslint-disable-next-line no-console + console.error('Error switching role', err) + if (err instanceof Error) { + setState(err) + } else { + setState(new Error('Unexpected error switching role')) + } + } + }, + [loading, switchRole], + ) + return ( + <> + Switch role + {state !== true ? ( + <> + {state instanceof Error && ( + {state.message} + )} + + {user.roles.map((role) => ( + handleClick(role.name)} + selected={role.name === user.role.name} + > + {role.name} + + ))} + + + ) : ( +
+ +
+ )} + + ) +} + +const SWITCH_ROLES_DIALOG_PROPS = { + maxWidth: 'sm' as const, + fullWidth: true, +} + +function useRoleSwitcher() { + const openDialog = Dialogs.use() + return (user: Me) => + openDialog(() => , SWITCH_ROLES_DIALOG_PROPS) +} + +function useLinks(): NavItemDescriptor[] { + const { urls } = NamedRoutes.use() + const settings = CatalogSettings.use() + + const links: NavItemDescriptor[] = [] + const link = (l: NavItemDescriptor | false, cond = true) => cond && l && links.push(l) + + const customNavLink: NavItemDescriptor | false = React.useMemo(() => { + if (!settings?.customNavLink) return false + const href = sanitizeUrl(settings.customNavLink.url) + if (href === 'about:blank') return false + return NavItemDescriptor.Href(href, settings.customNavLink.label) + }, [settings?.customNavLink]) + + link( + NavItemDescriptor.To(urls.example(), 'Example'), + process.env.NODE_ENV === 'development', + ) + link(customNavLink) + link(NavItemDescriptor.To(urls.uriResolver(), 'URI'), cfg.mode !== 'MARKETING') + link(NavItemDescriptor.Href(URLS.docs, 'Docs')) + link( + NavItemDescriptor.To(`${urls.home()}#pricing`, 'Pricing'), + cfg.mode === 'MARKETING', + ) + link( + NavItemDescriptor.Href(URLS.jobs, 'Jobs'), + cfg.mode === 'MARKETING' || cfg.mode === 'OPEN', + ) + link(NavItemDescriptor.Href(URLS.blog, 'Blog'), cfg.mode !== 'PRODUCT') + link(NavItemDescriptor.To(urls.about(), 'About'), cfg.mode === 'MARKETING') + + return links +} + +const useUserDisplayStyles = M.makeStyles({ + role: { + opacity: 0.5, + fontWeight: 'lighter', + }, +}) + +interface UserDisplayProps { + user: Me +} + +function UserDisplay({ user }: UserDisplayProps) { + const classes = useUserDisplayStyles() + return withIcon( + user.isAdmin ? 'security' : 'person', + <> + {user.name} +  ({user.role.name}) + , + ) +} + +const useBadgeStyles = M.makeStyles({ + root: { + alignItems: 'inherit', + }, + badge: { + top: '4px', + }, +}) + +interface BadgeProps extends M.BadgeProps {} + +function Badge({ children, color, invisible, ...props }: BadgeProps) { + const classes = useBadgeStyles() + return ( + + {children} + + ) +} + +const withIcon = (icon: string, children: React.ReactNode) => ( + <> + {icon} {children} + +) + +interface DesktopUserDropdownProps { + user: Me +} + +function DesktopUserDropdown({ user }: DesktopUserDropdownProps) { + const { urls } = NamedRoutes.use() + const switchRole = useRoleSwitcher() + const bookmarks = Bookmarks.use() + // XXX: reset bookmarks updates state on close? + const hasBookmarksUpdates = bookmarks?.hasUpdates ?? false + + const items = [ + cfg.mode === 'OPEN' && + MenuItemDescriptor.To(urls.profile(), withIcon('person', 'Profile')), + user.roles.length > 1 && + MenuItemDescriptor.Click(() => switchRole(user), withIcon('loop', 'Switch role')), + !!bookmarks && + MenuItemDescriptor.Click( + () => bookmarks.show(), + <> + + bookmarks_outlined + +  Bookmarks + , + ), + user.isAdmin && + MenuItemDescriptor.To(urls.admin(), withIcon('security', 'Admin settings')), + MenuItemDescriptor.To(urls.signOut(), withIcon('meeting_room', 'Sign Out')), + ] + + return ( + ( + + + + {' '} + expand_more + + )} + items={items} + /> + ) +} + +const useDesktopSignInStyles = M.makeStyles((t) => ({ + icon: { + marginRight: t.spacing(1), + }, +})) + +interface DesktopSignInProps { + error?: Error +} + +function DesktopSignIn({ error }: DesktopSignInProps) { + const classes = useDesktopSignInStyles() + const { urls } = NamedRoutes.use() + return ( + <> + {!!error && ( + + error_outline + + )} + + Sign In + + + ) +} + +const useDesktopProgressStyles = M.makeStyles((t) => ({ + progress: { + marginLeft: t.spacing(1), + }, +})) + +function DesktopProgress() { + const classes = useDesktopProgressStyles() + return +} + +interface DesktopMenuProps { + auth: AuthState +} + +function DesktopMenu({ auth }: DesktopMenuProps) { + const { paths } = NamedRoutes.use() + const isSignIn = !!useRouteMatch({ path: paths.signIn, exact: true }) + if (isSignIn || cfg.disableNavigator || cfg.mode === 'LOCAL') return null + return AuthState.match( + { + Ready: ({ user }) => + user ? : , + Error: ({ error }) => , + Loading: () => , + }, + auth, + ) +} + +interface MobileMenuProps { + auth: AuthState +} + +function MobileMenu({ auth }: MobileMenuProps) { + const { urls } = NamedRoutes.use() + const links = useLinks() + const switchRole = useRoleSwitcher() + const bookmarks = Bookmarks.use() + // XXX: reset bookmarks updates state on close? + const hasBookmarksUpdates = bookmarks?.hasUpdates ?? false + + const authItems = + cfg.disableNavigator || cfg.mode === 'LOCAL' + ? [] + : [ + ...AuthState.match<(MenuItemDescriptor | false)[]>( + { + Loading: () => [MenuItemDescriptor.Text(withIcon('person', 'Loading...'))], + Error: () => [ + MenuItemDescriptor.To( + urls.signIn(), + withIcon('error_outline', 'Sign In'), + ), + ], + Ready: ({ user }) => + user + ? [ + cfg.mode === 'OPEN' + ? MenuItemDescriptor.To( + urls.profile(), + , + ) + : MenuItemDescriptor.Text(), + user.roles.length > 1 && + MenuItemDescriptor.Click( + () => switchRole(user), + withIcon('loop', 'Switch role'), + ), + !!bookmarks && + MenuItemDescriptor.Click( + () => bookmarks.show(), + <> + + bookmarks_outlined + +  Bookmarks + , + ), + user.isAdmin && + MenuItemDescriptor.To( + urls.admin(), + withIcon('security', 'Admin settings'), + ), + MenuItemDescriptor.To( + urls.signOut(), + withIcon('meeting_room', 'Sign Out'), + ), + ] + : [ + MenuItemDescriptor.To( + urls.signIn(), + withIcon('exit_to_app', 'Sign In'), + ), + ], + }, + auth, + ), + MenuItemDescriptor.Divider(), + ] + + const navItems = links.map( + NavItemDescriptor.match({ + To: (props) => MenuItemDescriptor.To(props.to, props.children), + Href: (props) => MenuItemDescriptor.Href(props.href, props.children), + }), + ) + + const items = [...authItems, ...navItems] + + return ( + ( + + menu + + )} + items={items} + /> + ) +} + +const useNavStyles = M.makeStyles((t) => ({ + nav: { + alignItems: 'center', + display: 'flex', + marginLeft: t.spacing(3), + marginRight: t.spacing(2), + }, + active: {}, + link: { + color: t.palette.text.secondary, + fontSize: t.typography.body2.fontSize, + maxWidth: '64px', + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + + '& + &': { + marginLeft: t.spacing(2), + }, + + '&$active': { + color: t.palette.text.disabled, + }, + }, + intercom: { + marginLeft: t.spacing(2), + }, +})) + +export function Links() { + const classes = useNavStyles() + const intercom = Intercom.use() + const links = useLinks() + + const mkTitle = (children: React.ReactNode) => + typeof children === 'string' && children.length > 10 ? children : undefined + + return ( + + ) +} + +interface MenuProps { + collapse: boolean +} + +export function Menu({ collapse }: MenuProps) { + const auth = useAuthState() + return collapse ? : +} From 6e9cc87e5279654b91b4a979c58c40138da66f8e Mon Sep 17 00:00:00 2001 From: nl_0 Date: Wed, 5 Jun 2024 12:29:32 +0200 Subject: [PATCH 44/69] simplify --- catalog/app/containers/NavBar/NavMenu.tsx | 115 ++++++++-------------- 1 file changed, 43 insertions(+), 72 deletions(-) diff --git a/catalog/app/containers/NavBar/NavMenu.tsx b/catalog/app/containers/NavBar/NavMenu.tsx index acf4c5d0d80..a2f48ff77dc 100644 --- a/catalog/app/containers/NavBar/NavMenu.tsx +++ b/catalog/app/containers/NavBar/NavMenu.tsx @@ -57,20 +57,8 @@ function useAuthState(): AuthState { }) } -// XXX: don't use a separate type for this? -const NavItemDescriptor = tagged.create( - 'app/containers/NavBar/NavMenu:NavItemDescriptor' as const, - { - To: (to: string, children: React.ReactNode) => ({ to, children }), - Href: (href: string, children: React.ReactNode) => ({ href, children }), - }, -) - -// eslint-disable-next-line @typescript-eslint/no-redeclare -type NavItemDescriptor = tagged.InstanceOf - -const MenuItemDescriptor = tagged.create( - 'app/containers/NavBar/NavMenu:MenuItemDescriptor' as const, +const ItemDescriptor = tagged.create( + 'app/containers/NavBar/NavMenu:ItemDescriptor' as const, { To: (to: string, children: React.ReactNode) => ({ to, children }), Href: (href: string, children: React.ReactNode) => ({ href, children }), @@ -81,7 +69,7 @@ const MenuItemDescriptor = tagged.create( ) // eslint-disable-next-line @typescript-eslint/no-redeclare -type MenuItemDescriptor = tagged.InstanceOf +type ItemDescriptor = tagged.InstanceOf interface DropdownMenuProps extends Omit< @@ -91,7 +79,7 @@ interface DropdownMenuProps trigger: ( open: React.EventHandler>, ) => React.ReactNode - items: (MenuItemDescriptor | null | false)[] + items: (ItemDescriptor | false)[] } function DropdownMenu({ trigger, items, ...rest }: DropdownMenuProps) { @@ -108,9 +96,9 @@ function DropdownMenu({ trigger, items, ...rest }: DropdownMenuProps) { setAnchor(null) }, [setAnchor]) - const filtered = items.filter(Boolean) as MenuItemDescriptor[] + const filtered = items.filter(Boolean) as ItemDescriptor[] const children = filtered.map( - MenuItemDescriptor.match({ + ItemDescriptor.match({ To: (props, i) => ( {({ match }) => ( @@ -259,37 +247,32 @@ function useRoleSwitcher() { openDialog(() => , SWITCH_ROLES_DIALOG_PROPS) } -function useLinks(): NavItemDescriptor[] { +function useLinks(): ItemDescriptor[] { const { urls } = NamedRoutes.use() const settings = CatalogSettings.use() - const links: NavItemDescriptor[] = [] - const link = (l: NavItemDescriptor | false, cond = true) => cond && l && links.push(l) - - const customNavLink: NavItemDescriptor | false = React.useMemo(() => { + const customNavLink: ItemDescriptor | false = React.useMemo(() => { if (!settings?.customNavLink) return false const href = sanitizeUrl(settings.customNavLink.url) if (href === 'about:blank') return false - return NavItemDescriptor.Href(href, settings.customNavLink.label) + return ItemDescriptor.Href(href, settings.customNavLink.label) }, [settings?.customNavLink]) - link( - NavItemDescriptor.To(urls.example(), 'Example'), - process.env.NODE_ENV === 'development', - ) - link(customNavLink) - link(NavItemDescriptor.To(urls.uriResolver(), 'URI'), cfg.mode !== 'MARKETING') - link(NavItemDescriptor.Href(URLS.docs, 'Docs')) - link( - NavItemDescriptor.To(`${urls.home()}#pricing`, 'Pricing'), - cfg.mode === 'MARKETING', - ) - link( - NavItemDescriptor.Href(URLS.jobs, 'Jobs'), - cfg.mode === 'MARKETING' || cfg.mode === 'OPEN', - ) - link(NavItemDescriptor.Href(URLS.blog, 'Blog'), cfg.mode !== 'PRODUCT') - link(NavItemDescriptor.To(urls.about(), 'About'), cfg.mode === 'MARKETING') + const links: ItemDescriptor[] = [] + + if (process.env.NODE_ENV === 'development') { + links.push(ItemDescriptor.To(urls.example(), 'Example')) + } + if (customNavLink) links.push(customNavLink) + if (cfg.mode !== 'MARKETING') { + links.push(ItemDescriptor.To(urls.uriResolver(), 'URI')) + } + links.push(ItemDescriptor.Href(URLS.docs, 'Docs')) + if (cfg.mode === 'MARKETING' || cfg.mode === 'OPEN') { + links.push(ItemDescriptor.Href(URLS.jobs, 'Jobs')) + } + if (cfg.mode !== 'PRODUCT') links.push(ItemDescriptor.Href(URLS.blog, 'Blog')) + if (cfg.mode === 'MARKETING') links.push(ItemDescriptor.To(urls.about(), 'About')) return links } @@ -361,11 +344,11 @@ function DesktopUserDropdown({ user }: DesktopUserDropdownProps) { const items = [ cfg.mode === 'OPEN' && - MenuItemDescriptor.To(urls.profile(), withIcon('person', 'Profile')), + ItemDescriptor.To(urls.profile(), withIcon('person', 'Profile')), user.roles.length > 1 && - MenuItemDescriptor.Click(() => switchRole(user), withIcon('loop', 'Switch role')), + ItemDescriptor.Click(() => switchRole(user), withIcon('loop', 'Switch role')), !!bookmarks && - MenuItemDescriptor.Click( + ItemDescriptor.Click( () => bookmarks.show(), <> @@ -375,8 +358,8 @@ function DesktopUserDropdown({ user }: DesktopUserDropdownProps) { , ), user.isAdmin && - MenuItemDescriptor.To(urls.admin(), withIcon('security', 'Admin settings')), - MenuItemDescriptor.To(urls.signOut(), withIcon('meeting_room', 'Sign Out')), + ItemDescriptor.To(urls.admin(), withIcon('security', 'Admin settings')), + ItemDescriptor.To(urls.signOut(), withIcon('meeting_room', 'Sign Out')), ] return ( @@ -472,31 +455,25 @@ function MobileMenu({ auth }: MobileMenuProps) { cfg.disableNavigator || cfg.mode === 'LOCAL' ? [] : [ - ...AuthState.match<(MenuItemDescriptor | false)[]>( + ...AuthState.match<(ItemDescriptor | false)[]>( { - Loading: () => [MenuItemDescriptor.Text(withIcon('person', 'Loading...'))], + Loading: () => [ItemDescriptor.Text(withIcon('person', 'Loading...'))], Error: () => [ - MenuItemDescriptor.To( - urls.signIn(), - withIcon('error_outline', 'Sign In'), - ), + ItemDescriptor.To(urls.signIn(), withIcon('error_outline', 'Sign In')), ], Ready: ({ user }) => user ? [ cfg.mode === 'OPEN' - ? MenuItemDescriptor.To( - urls.profile(), - , - ) - : MenuItemDescriptor.Text(), + ? ItemDescriptor.To(urls.profile(), ) + : ItemDescriptor.Text(), user.roles.length > 1 && - MenuItemDescriptor.Click( + ItemDescriptor.Click( () => switchRole(user), withIcon('loop', 'Switch role'), ), !!bookmarks && - MenuItemDescriptor.Click( + ItemDescriptor.Click( () => bookmarks.show(), <> @@ -506,17 +483,17 @@ function MobileMenu({ auth }: MobileMenuProps) { , ), user.isAdmin && - MenuItemDescriptor.To( + ItemDescriptor.To( urls.admin(), withIcon('security', 'Admin settings'), ), - MenuItemDescriptor.To( + ItemDescriptor.To( urls.signOut(), withIcon('meeting_room', 'Sign Out'), ), ] : [ - MenuItemDescriptor.To( + ItemDescriptor.To( urls.signIn(), withIcon('exit_to_app', 'Sign In'), ), @@ -524,17 +501,10 @@ function MobileMenu({ auth }: MobileMenuProps) { }, auth, ), - MenuItemDescriptor.Divider(), + ItemDescriptor.Divider(), ] - const navItems = links.map( - NavItemDescriptor.match({ - To: (props) => MenuItemDescriptor.To(props.to, props.children), - Href: (props) => MenuItemDescriptor.Href(props.href, props.children), - }), - ) - - const items = [...authItems, ...navItems] + const items = [...authItems, ...links] return ( {links.map( - NavItemDescriptor.match({ + ItemDescriptor.match({ To: (props, i) => ( ), + _: () => null, }), )} {!intercom.dummy && intercom.isCustom && ( From 8f3c663b19cc5593830c43f33bf0d2f2a954683e Mon Sep 17 00:00:00 2001 From: nl_0 Date: Wed, 5 Jun 2024 12:45:41 +0200 Subject: [PATCH 45/69] tweak role switcher a little --- catalog/app/containers/NavBar/NavMenu.tsx | 28 ++++++++++++++++------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/catalog/app/containers/NavBar/NavMenu.tsx b/catalog/app/containers/NavBar/NavMenu.tsx index a2f48ff77dc..95df1e53159 100644 --- a/catalog/app/containers/NavBar/NavMenu.tsx +++ b/catalog/app/containers/NavBar/NavMenu.tsx @@ -168,19 +168,22 @@ const useListItemTextStyles = M.makeStyles((t) => ({ }, })) +const LOADING = Symbol('loading') + interface RoleSwitcherProps { user: Me + close: Dialogs.Close } -function RoleSwitcher({ user }: RoleSwitcherProps) { +function RoleSwitcher({ user, close }: RoleSwitcherProps) { const switchRole = GQL.useMutation(SWITCH_ROLE_MUTATION) const classes = useRoleSwitcherStyles() const textClasses = useListItemTextStyles() - const loading = true - const [state, setState] = React.useState(null) + const [state, setState] = React.useState(null) const handleClick = React.useCallback( async (roleName: string) => { - setState(loading) + if (roleName === user.role.name) return close() + setState(LOADING) try { const { switchRole: r } = await switchRole({ roleName }) switch (r.__typename) { @@ -203,12 +206,12 @@ function RoleSwitcher({ user }: RoleSwitcherProps) { } } }, - [loading, switchRole], + [close, switchRole, user.role.name], ) return ( <> Switch role - {state !== true ? ( + {state !== LOADING ? ( <> {state instanceof Error && ( {state.message} @@ -217,11 +220,17 @@ function RoleSwitcher({ user }: RoleSwitcherProps) { {user.roles.map((role) => ( handleClick(role.name)} selected={role.name === user.role.name} > + + + {role.name} ))} @@ -244,7 +253,10 @@ const SWITCH_ROLES_DIALOG_PROPS = { function useRoleSwitcher() { const openDialog = Dialogs.use() return (user: Me) => - openDialog(() => , SWITCH_ROLES_DIALOG_PROPS) + openDialog( + ({ close }) => , + SWITCH_ROLES_DIALOG_PROPS, + ) } function useLinks(): ItemDescriptor[] { From 7e9e8ae9877f52bc25bcfdc82f7e0a9b10e56b5b Mon Sep 17 00:00:00 2001 From: nl_0 Date: Thu, 6 Jun 2024 09:01:22 +0200 Subject: [PATCH 46/69] mv RoleSwitcher out --- catalog/app/containers/NavBar/NavMenu.tsx | 118 +---------------- .../app/containers/NavBar/RoleSwitcher.tsx | 122 ++++++++++++++++++ 2 files changed, 125 insertions(+), 115 deletions(-) create mode 100644 catalog/app/containers/NavBar/RoleSwitcher.tsx diff --git a/catalog/app/containers/NavBar/NavMenu.tsx b/catalog/app/containers/NavBar/NavMenu.tsx index 95df1e53159..7e8cb8c9887 100644 --- a/catalog/app/containers/NavBar/NavMenu.tsx +++ b/catalog/app/containers/NavBar/NavMenu.tsx @@ -1,4 +1,3 @@ -// import cx from 'classnames' import * as R from 'ramda' import * as React from 'react' import * as redux from 'react-redux' @@ -7,7 +6,6 @@ import * as RR from 'react-router-dom' import { createStructuredSelector } from 'reselect' import { sanitizeUrl } from '@braintree/sanitize-url' import * as M from '@material-ui/core' -import * as Lab from '@material-ui/lab' import * as Intercom from 'components/Intercom' import cfg from 'constants/config' @@ -16,14 +14,13 @@ import * as URLS from 'constants/urls' import * as Bookmarks from 'containers/Bookmarks' import * as authSelectors from 'containers/Auth/selectors' import * as CatalogSettings from 'utils/CatalogSettings' -import * as Dialogs from 'utils/GlobalDialogs' import * as GQL from 'utils/GraphQL' import * as NamedRoutes from 'utils/NamedRoutes' -import assertNever from 'utils/assertNever' import * as tagged from 'utils/taggedV2' +import useRoleSwitcher from './RoleSwitcher' + import ME_QUERY from './gql/Me.generated' -import SWITCH_ROLE_MUTATION from './gql/SwitchRole.generated' type MaybeMe = GQL.DataForDoc['me'] type Me = NonNullable @@ -137,6 +134,7 @@ function DropdownMenu({ trigger, items, ...rest }: DropdownMenuProps) { ({ - progress: { - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - padding: t.spacing(4), - }, -})) - -const useListItemTextStyles = M.makeStyles((t) => ({ - root: { - padding: t.spacing(0, 1), - }, - primary: { - overflow: 'hidden', - textOverflow: 'ellipsis', - }, -})) - -const LOADING = Symbol('loading') - -interface RoleSwitcherProps { - user: Me - close: Dialogs.Close -} - -function RoleSwitcher({ user, close }: RoleSwitcherProps) { - const switchRole = GQL.useMutation(SWITCH_ROLE_MUTATION) - const classes = useRoleSwitcherStyles() - const textClasses = useListItemTextStyles() - const [state, setState] = React.useState(null) - const handleClick = React.useCallback( - async (roleName: string) => { - if (roleName === user.role.name) return close() - setState(LOADING) - try { - const { switchRole: r } = await switchRole({ roleName }) - switch (r.__typename) { - case 'Me': - window.location.reload() - break - case 'InvalidInput': - case 'OperationError': - throw new Error('Failed to switch role. Try again') - default: - assertNever(r) - } - } catch (err) { - // eslint-disable-next-line no-console - console.error('Error switching role', err) - if (err instanceof Error) { - setState(err) - } else { - setState(new Error('Unexpected error switching role')) - } - } - }, - [close, switchRole, user.role.name], - ) - return ( - <> - Switch role - {state !== LOADING ? ( - <> - {state instanceof Error && ( - {state.message} - )} - - {user.roles.map((role) => ( - handleClick(role.name)} - selected={role.name === user.role.name} - > - - - - {role.name} - - ))} - - - ) : ( -
- -
- )} - - ) -} - -const SWITCH_ROLES_DIALOG_PROPS = { - maxWidth: 'sm' as const, - fullWidth: true, -} - -function useRoleSwitcher() { - const openDialog = Dialogs.use() - return (user: Me) => - openDialog( - ({ close }) => , - SWITCH_ROLES_DIALOG_PROPS, - ) -} - function useLinks(): ItemDescriptor[] { const { urls } = NamedRoutes.use() const settings = CatalogSettings.use() diff --git a/catalog/app/containers/NavBar/RoleSwitcher.tsx b/catalog/app/containers/NavBar/RoleSwitcher.tsx new file mode 100644 index 00000000000..41c871cf14f --- /dev/null +++ b/catalog/app/containers/NavBar/RoleSwitcher.tsx @@ -0,0 +1,122 @@ +import * as React from 'react' +import * as M from '@material-ui/core' +import * as Lab from '@material-ui/lab' + +import * as Dialogs from 'utils/GlobalDialogs' +import * as GQL from 'utils/GraphQL' +import assertNever from 'utils/assertNever' + +import ME_QUERY from './gql/Me.generated' +import SWITCH_ROLE_MUTATION from './gql/SwitchRole.generated' + +type Me = NonNullable['me']> + +const useRoleSwitcherStyles = M.makeStyles((t) => ({ + progress: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + padding: t.spacing(4), + }, +})) + +const useListItemTextStyles = M.makeStyles((t) => ({ + root: { + padding: t.spacing(0, 1), + }, + primary: { + overflow: 'hidden', + textOverflow: 'ellipsis', + }, +})) + +const LOADING = Symbol('loading') + +interface RoleSwitcherProps { + user: Me + close: Dialogs.Close +} + +function RoleSwitcher({ user, close }: RoleSwitcherProps) { + const switchRole = GQL.useMutation(SWITCH_ROLE_MUTATION) + const classes = useRoleSwitcherStyles() + const textClasses = useListItemTextStyles() + const [state, setState] = React.useState(null) + const handleClick = React.useCallback( + async (roleName: string) => { + if (roleName === user.role.name) return close() + setState(LOADING) + try { + const { switchRole: r } = await switchRole({ roleName }) + switch (r.__typename) { + case 'Me': + window.location.reload() + break + case 'InvalidInput': + case 'OperationError': + throw new Error('Failed to switch role. Try again') + default: + assertNever(r) + } + } catch (err) { + // eslint-disable-next-line no-console + console.error('Error switching role', err) + if (err instanceof Error) { + setState(err) + } else { + setState(new Error('Unexpected error switching role')) + } + } + }, + [close, switchRole, user.role.name], + ) + return ( + <> + Switch role + {state !== LOADING ? ( + <> + {state instanceof Error && ( + {state.message} + )} + + {user.roles.map((role) => ( + handleClick(role.name)} + selected={role.name === user.role.name} + > + + + + {role.name} + + ))} + + + ) : ( +
+ +
+ )} + + ) +} + +const SWITCH_ROLES_DIALOG_PROPS = { + maxWidth: 'sm' as const, + fullWidth: true, +} + +export default function useRoleSwitcher() { + const openDialog = Dialogs.use() + return (user: Me) => + openDialog( + ({ close }) => , + SWITCH_ROLES_DIALOG_PROPS, + ) +} From 14f8f34ce6d5edd19cc0a45eefea41f5d53af51e Mon Sep 17 00:00:00 2001 From: nl_0 Date: Thu, 6 Jun 2024 09:52:37 +0200 Subject: [PATCH 47/69] polish role switch ui --- .../app/containers/NavBar/RoleSwitcher.tsx | 124 +++++++++++------- .../NavBar/gql/SwitchRole.generated.ts | 33 ++++- .../containers/NavBar/gql/SwitchRole.graphql | 7 + 3 files changed, 113 insertions(+), 51 deletions(-) diff --git a/catalog/app/containers/NavBar/RoleSwitcher.tsx b/catalog/app/containers/NavBar/RoleSwitcher.tsx index 41c871cf14f..6e83780bdf2 100644 --- a/catalog/app/containers/NavBar/RoleSwitcher.tsx +++ b/catalog/app/containers/NavBar/RoleSwitcher.tsx @@ -1,6 +1,7 @@ import * as React from 'react' import * as M from '@material-ui/core' import * as Lab from '@material-ui/lab' +import * as Sentry from '@sentry/react' import * as Dialogs from 'utils/GlobalDialogs' import * as GQL from 'utils/GraphQL' @@ -11,26 +12,34 @@ import SWITCH_ROLE_MUTATION from './gql/SwitchRole.generated' type Me = NonNullable['me']> -const useRoleSwitcherStyles = M.makeStyles((t) => ({ +const useRoleSwitcherStyles = M.makeStyles({ + content: { + position: 'relative', + }, + error: { + borderRadius: 0, + }, + errorIcon: { + fontSize: '24px', + marginLeft: '9px', + marginRight: '23px', + }, progress: { - display: 'flex', alignItems: 'center', + background: 'rgba(255, 255, 255, 0.5)', + bottom: 0, + display: 'flex', justifyContent: 'center', - padding: t.spacing(4), - }, -})) - -const useListItemTextStyles = M.makeStyles((t) => ({ - root: { - padding: t.spacing(0, 1), + left: 0, + position: 'absolute', + right: 0, + top: 0, }, - primary: { + name: { overflow: 'hidden', textOverflow: 'ellipsis', }, -})) - -const LOADING = Symbol('loading') +}) interface RoleSwitcherProps { user: Me @@ -40,12 +49,12 @@ interface RoleSwitcherProps { function RoleSwitcher({ user, close }: RoleSwitcherProps) { const switchRole = GQL.useMutation(SWITCH_ROLE_MUTATION) const classes = useRoleSwitcherStyles() - const textClasses = useListItemTextStyles() - const [state, setState] = React.useState(null) - const handleClick = React.useCallback( + const [loading, setLoading] = React.useState(false) + const [error, setError] = React.useState(null) + const selectRole = React.useCallback( async (roleName: string) => { if (roleName === user.role.name) return close() - setState(LOADING) + setLoading(true) try { const { switchRole: r } = await switchRole({ roleName }) switch (r.__typename) { @@ -53,19 +62,23 @@ function RoleSwitcher({ user, close }: RoleSwitcherProps) { window.location.reload() break case 'InvalidInput': + const [e] = r.errors + throw new Error(`InputError (${e.name}) at '${e.path}': ${e.message}`) case 'OperationError': - throw new Error('Failed to switch role. Try again') + throw new Error(`OperationError (${r.name}): ${r.message}`) default: assertNever(r) } } catch (err) { // eslint-disable-next-line no-console console.error('Error switching role', err) + Sentry.captureException(err) if (err instanceof Error) { - setState(err) + setError(err.message) } else { - setState(new Error('Unexpected error switching role')) + setError(`${err}`) } + setLoading(false) } }, [close, switchRole, user.role.name], @@ -73,36 +86,47 @@ function RoleSwitcher({ user, close }: RoleSwitcherProps) { return ( <> Switch role - {state !== LOADING ? ( - <> - {state instanceof Error && ( - {state.message} - )} - - {user.roles.map((role) => ( - handleClick(role.name)} - selected={role.name === user.role.name} - > - - - - {role.name} - - ))} - - - ) : ( -
- -
- )} +
+ + + Could not switch role + Try again or contact support. +
+ Error details: +
+ {error} +
+
+ + {user.roles.map((role) => ( + selectRole(role.name)} + selected={role.name === user.role.name} + > + + + + + {role.name} + + + ))} + + {loading && ( +
+ +
+ )} +
) } diff --git a/catalog/app/containers/NavBar/gql/SwitchRole.generated.ts b/catalog/app/containers/NavBar/gql/SwitchRole.generated.ts index af6b4f146a2..0945ad5b11e 100644 --- a/catalog/app/containers/NavBar/gql/SwitchRole.generated.ts +++ b/catalog/app/containers/NavBar/gql/SwitchRole.generated.ts @@ -16,7 +16,14 @@ export type containers_NavBar_gql_SwitchRoleMutation = { { readonly __typename: 'MyRole' } & Pick > }) - | { readonly __typename: 'InvalidInput' } + | ({ readonly __typename: 'InvalidInput' } & { + readonly errors: ReadonlyArray< + { readonly __typename: 'InputError' } & Pick< + Types.InputError, + 'name' | 'path' | 'message' + > + > + }) | ({ readonly __typename: 'OperationError' } & Pick< Types.OperationError, 'message' | 'name' @@ -106,6 +113,30 @@ export const containers_NavBar_gql_SwitchRoleDocument = { ], }, }, + { + kind: 'InlineFragment', + typeCondition: { + kind: 'NamedType', + name: { kind: 'Name', value: 'InvalidInput' }, + }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'errors' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'name' } }, + { kind: 'Field', name: { kind: 'Name', value: 'path' } }, + { kind: 'Field', name: { kind: 'Name', value: 'message' } }, + ], + }, + }, + ], + }, + }, ], }, }, diff --git a/catalog/app/containers/NavBar/gql/SwitchRole.graphql b/catalog/app/containers/NavBar/gql/SwitchRole.graphql index e1577f50cc5..944dc8a2fda 100644 --- a/catalog/app/containers/NavBar/gql/SwitchRole.graphql +++ b/catalog/app/containers/NavBar/gql/SwitchRole.graphql @@ -16,5 +16,12 @@ mutation ($roleName: String!) { message name } + ... on InvalidInput { + errors { + name + path + message + } + } } } From 7be023e07bd9b07af44c793667a24323b1f34108 Mon Sep 17 00:00:00 2001 From: nl_0 Date: Thu, 6 Jun 2024 09:53:12 +0200 Subject: [PATCH 48/69] regen gql --- .../UsersAndRoles/gql/Buckets.generated.ts | 14 +++++------ .../UsersAndRoles/gql/Policies.generated.ts | 14 +++++------ .../gql/PolicyCreateManaged.generated.ts | 14 +++++------ .../gql/PolicyCreateUnmanaged.generated.ts | 14 +++++------ .../gql/PolicyDelete.generated.ts | 14 +++++------ .../gql/PolicyUpdateManaged.generated.ts | 14 +++++------ .../gql/PolicyUpdateUnmanaged.generated.ts | 14 +++++------ .../gql/RoleCreateManaged.generated.ts | 14 +++++------ .../gql/RoleCreateUnmanaged.generated.ts | 14 +++++------ .../UsersAndRoles/gql/RoleDelete.generated.ts | 19 ++++++++------- .../gql/RoleSetDefault.generated.ts | 17 ++++++-------- .../gql/RoleUpdateManaged.generated.ts | 14 +++++------ .../gql/RoleUpdateUnmanaged.generated.ts | 14 +++++------ .../UsersAndRoles/gql/Roles.generated.ts | 14 +++++------ .../UsersAndRoles/gql/UserCreate.generated.ts | 14 +++++------ .../UsersAndRoles/gql/UserDelete.generated.ts | 14 +++++------ .../gql/UserSetActive.generated.ts | 21 +++++++++-------- .../gql/UserSetAdmin.generated.ts | 21 +++++++++-------- .../gql/UserSetEmail.generated.ts | 21 +++++++++-------- .../gql/UserSetRole.generated.ts | 23 ++++++++++--------- .../UsersAndRoles/gql/Users.generated.ts | 16 +++++++------ 21 files changed, 168 insertions(+), 166 deletions(-) diff --git a/catalog/app/containers/Admin/UsersAndRoles/gql/Buckets.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/Buckets.generated.ts index 36b9e927b6c..51ec36847aa 100644 --- a/catalog/app/containers/Admin/UsersAndRoles/gql/Buckets.generated.ts +++ b/catalog/app/containers/Admin/UsersAndRoles/gql/Buckets.generated.ts @@ -2,11 +2,11 @@ import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core' import * as Types from '../../../../model/graphql/types.generated' -export type containers_Admin_RolesAndPolicies_gql_BucketsQueryVariables = Types.Exact<{ +export type containers_Admin_UsersAndRoles_gql_BucketsQueryVariables = Types.Exact<{ [key: string]: never }> -export type containers_Admin_RolesAndPolicies_gql_BucketsQuery = { +export type containers_Admin_UsersAndRoles_gql_BucketsQuery = { readonly __typename: 'Query' } & { readonly buckets: ReadonlyArray< @@ -17,13 +17,13 @@ export type containers_Admin_RolesAndPolicies_gql_BucketsQuery = { > } -export const containers_Admin_RolesAndPolicies_gql_BucketsDocument = { +export const containers_Admin_UsersAndRoles_gql_BucketsDocument = { kind: 'Document', definitions: [ { kind: 'OperationDefinition', operation: 'query', - name: { kind: 'Name', value: 'containers_Admin_RolesAndPolicies_gql_Buckets' }, + name: { kind: 'Name', value: 'containers_Admin_UsersAndRoles_gql_Buckets' }, selectionSet: { kind: 'SelectionSet', selections: [ @@ -45,8 +45,8 @@ export const containers_Admin_RolesAndPolicies_gql_BucketsDocument = { }, ], } as unknown as DocumentNode< - containers_Admin_RolesAndPolicies_gql_BucketsQuery, - containers_Admin_RolesAndPolicies_gql_BucketsQueryVariables + containers_Admin_UsersAndRoles_gql_BucketsQuery, + containers_Admin_UsersAndRoles_gql_BucketsQueryVariables > -export { containers_Admin_RolesAndPolicies_gql_BucketsDocument as default } +export { containers_Admin_UsersAndRoles_gql_BucketsDocument as default } diff --git a/catalog/app/containers/Admin/UsersAndRoles/gql/Policies.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/Policies.generated.ts index a7f5df550a5..f8e6709d56f 100644 --- a/catalog/app/containers/Admin/UsersAndRoles/gql/Policies.generated.ts +++ b/catalog/app/containers/Admin/UsersAndRoles/gql/Policies.generated.ts @@ -7,11 +7,11 @@ import { PolicySelectionFragmentDoc, } from './PolicySelection.generated' -export type containers_Admin_RolesAndPolicies_gql_PoliciesQueryVariables = Types.Exact<{ +export type containers_Admin_UsersAndRoles_gql_PoliciesQueryVariables = Types.Exact<{ [key: string]: never }> -export type containers_Admin_RolesAndPolicies_gql_PoliciesQuery = { +export type containers_Admin_UsersAndRoles_gql_PoliciesQuery = { readonly __typename: 'Query' } & { readonly policies: ReadonlyArray< @@ -19,13 +19,13 @@ export type containers_Admin_RolesAndPolicies_gql_PoliciesQuery = { > } -export const containers_Admin_RolesAndPolicies_gql_PoliciesDocument = { +export const containers_Admin_UsersAndRoles_gql_PoliciesDocument = { kind: 'Document', definitions: [ { kind: 'OperationDefinition', operation: 'query', - name: { kind: 'Name', value: 'containers_Admin_RolesAndPolicies_gql_Policies' }, + name: { kind: 'Name', value: 'containers_Admin_UsersAndRoles_gql_Policies' }, selectionSet: { kind: 'SelectionSet', selections: [ @@ -48,8 +48,8 @@ export const containers_Admin_RolesAndPolicies_gql_PoliciesDocument = { ...PolicySelectionFragmentDoc.definitions, ], } as unknown as DocumentNode< - containers_Admin_RolesAndPolicies_gql_PoliciesQuery, - containers_Admin_RolesAndPolicies_gql_PoliciesQueryVariables + containers_Admin_UsersAndRoles_gql_PoliciesQuery, + containers_Admin_UsersAndRoles_gql_PoliciesQueryVariables > -export { containers_Admin_RolesAndPolicies_gql_PoliciesDocument as default } +export { containers_Admin_UsersAndRoles_gql_PoliciesDocument as default } diff --git a/catalog/app/containers/Admin/UsersAndRoles/gql/PolicyCreateManaged.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/PolicyCreateManaged.generated.ts index 11399e561f2..31ca1b9899f 100644 --- a/catalog/app/containers/Admin/UsersAndRoles/gql/PolicyCreateManaged.generated.ts +++ b/catalog/app/containers/Admin/UsersAndRoles/gql/PolicyCreateManaged.generated.ts @@ -9,12 +9,12 @@ import { PolicyResultSelectionFragmentDoc, } from './PolicyResultSelection.generated' -export type containers_Admin_RolesAndPolicies_gql_PolicyCreateManagedMutationVariables = +export type containers_Admin_UsersAndRoles_gql_PolicyCreateManagedMutationVariables = Types.Exact<{ input: Types.ManagedPolicyInput }> -export type containers_Admin_RolesAndPolicies_gql_PolicyCreateManagedMutation = { +export type containers_Admin_UsersAndRoles_gql_PolicyCreateManagedMutation = { readonly __typename: 'Mutation' } & { readonly policyCreate: @@ -27,7 +27,7 @@ export type containers_Admin_RolesAndPolicies_gql_PolicyCreateManagedMutation = } & PolicyResultSelection_OperationError_Fragment) } -export const containers_Admin_RolesAndPolicies_gql_PolicyCreateManagedDocument = { +export const containers_Admin_UsersAndRoles_gql_PolicyCreateManagedDocument = { kind: 'Document', definitions: [ { @@ -35,7 +35,7 @@ export const containers_Admin_RolesAndPolicies_gql_PolicyCreateManagedDocument = operation: 'mutation', name: { kind: 'Name', - value: 'containers_Admin_RolesAndPolicies_gql_PolicyCreateManaged', + value: 'containers_Admin_UsersAndRoles_gql_PolicyCreateManaged', }, variableDefinitions: [ { @@ -80,8 +80,8 @@ export const containers_Admin_RolesAndPolicies_gql_PolicyCreateManagedDocument = ...PolicyResultSelectionFragmentDoc.definitions, ], } as unknown as DocumentNode< - containers_Admin_RolesAndPolicies_gql_PolicyCreateManagedMutation, - containers_Admin_RolesAndPolicies_gql_PolicyCreateManagedMutationVariables + containers_Admin_UsersAndRoles_gql_PolicyCreateManagedMutation, + containers_Admin_UsersAndRoles_gql_PolicyCreateManagedMutationVariables > -export { containers_Admin_RolesAndPolicies_gql_PolicyCreateManagedDocument as default } +export { containers_Admin_UsersAndRoles_gql_PolicyCreateManagedDocument as default } diff --git a/catalog/app/containers/Admin/UsersAndRoles/gql/PolicyCreateUnmanaged.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/PolicyCreateUnmanaged.generated.ts index 8c9d9d1a4a5..fb112d39165 100644 --- a/catalog/app/containers/Admin/UsersAndRoles/gql/PolicyCreateUnmanaged.generated.ts +++ b/catalog/app/containers/Admin/UsersAndRoles/gql/PolicyCreateUnmanaged.generated.ts @@ -9,12 +9,12 @@ import { PolicyResultSelectionFragmentDoc, } from './PolicyResultSelection.generated' -export type containers_Admin_RolesAndPolicies_gql_PolicyCreateUnmanagedMutationVariables = +export type containers_Admin_UsersAndRoles_gql_PolicyCreateUnmanagedMutationVariables = Types.Exact<{ input: Types.UnmanagedPolicyInput }> -export type containers_Admin_RolesAndPolicies_gql_PolicyCreateUnmanagedMutation = { +export type containers_Admin_UsersAndRoles_gql_PolicyCreateUnmanagedMutation = { readonly __typename: 'Mutation' } & { readonly policyCreate: @@ -27,7 +27,7 @@ export type containers_Admin_RolesAndPolicies_gql_PolicyCreateUnmanagedMutation } & PolicyResultSelection_OperationError_Fragment) } -export const containers_Admin_RolesAndPolicies_gql_PolicyCreateUnmanagedDocument = { +export const containers_Admin_UsersAndRoles_gql_PolicyCreateUnmanagedDocument = { kind: 'Document', definitions: [ { @@ -35,7 +35,7 @@ export const containers_Admin_RolesAndPolicies_gql_PolicyCreateUnmanagedDocument operation: 'mutation', name: { kind: 'Name', - value: 'containers_Admin_RolesAndPolicies_gql_PolicyCreateUnmanaged', + value: 'containers_Admin_UsersAndRoles_gql_PolicyCreateUnmanaged', }, variableDefinitions: [ { @@ -80,8 +80,8 @@ export const containers_Admin_RolesAndPolicies_gql_PolicyCreateUnmanagedDocument ...PolicyResultSelectionFragmentDoc.definitions, ], } as unknown as DocumentNode< - containers_Admin_RolesAndPolicies_gql_PolicyCreateUnmanagedMutation, - containers_Admin_RolesAndPolicies_gql_PolicyCreateUnmanagedMutationVariables + containers_Admin_UsersAndRoles_gql_PolicyCreateUnmanagedMutation, + containers_Admin_UsersAndRoles_gql_PolicyCreateUnmanagedMutationVariables > -export { containers_Admin_RolesAndPolicies_gql_PolicyCreateUnmanagedDocument as default } +export { containers_Admin_UsersAndRoles_gql_PolicyCreateUnmanagedDocument as default } diff --git a/catalog/app/containers/Admin/UsersAndRoles/gql/PolicyDelete.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/PolicyDelete.generated.ts index 5cef95b64d5..90920054ad6 100644 --- a/catalog/app/containers/Admin/UsersAndRoles/gql/PolicyDelete.generated.ts +++ b/catalog/app/containers/Admin/UsersAndRoles/gql/PolicyDelete.generated.ts @@ -2,12 +2,12 @@ import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core' import * as Types from '../../../../model/graphql/types.generated' -export type containers_Admin_RolesAndPolicies_gql_PolicyDeleteMutationVariables = +export type containers_Admin_UsersAndRoles_gql_PolicyDeleteMutationVariables = Types.Exact<{ id: Types.Scalars['ID'] }> -export type containers_Admin_RolesAndPolicies_gql_PolicyDeleteMutation = { +export type containers_Admin_UsersAndRoles_gql_PolicyDeleteMutation = { readonly __typename: 'Mutation' } & { readonly policyDelete: @@ -23,13 +23,13 @@ export type containers_Admin_RolesAndPolicies_gql_PolicyDeleteMutation = { | ({ readonly __typename: 'OperationError' } & Pick) } -export const containers_Admin_RolesAndPolicies_gql_PolicyDeleteDocument = { +export const containers_Admin_UsersAndRoles_gql_PolicyDeleteDocument = { kind: 'Document', definitions: [ { kind: 'OperationDefinition', operation: 'mutation', - name: { kind: 'Name', value: 'containers_Admin_RolesAndPolicies_gql_PolicyDelete' }, + name: { kind: 'Name', value: 'containers_Admin_UsersAndRoles_gql_PolicyDelete' }, variableDefinitions: [ { kind: 'VariableDefinition', @@ -101,8 +101,8 @@ export const containers_Admin_RolesAndPolicies_gql_PolicyDeleteDocument = { }, ], } as unknown as DocumentNode< - containers_Admin_RolesAndPolicies_gql_PolicyDeleteMutation, - containers_Admin_RolesAndPolicies_gql_PolicyDeleteMutationVariables + containers_Admin_UsersAndRoles_gql_PolicyDeleteMutation, + containers_Admin_UsersAndRoles_gql_PolicyDeleteMutationVariables > -export { containers_Admin_RolesAndPolicies_gql_PolicyDeleteDocument as default } +export { containers_Admin_UsersAndRoles_gql_PolicyDeleteDocument as default } diff --git a/catalog/app/containers/Admin/UsersAndRoles/gql/PolicyUpdateManaged.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/PolicyUpdateManaged.generated.ts index 22d2a478d31..7f6146556be 100644 --- a/catalog/app/containers/Admin/UsersAndRoles/gql/PolicyUpdateManaged.generated.ts +++ b/catalog/app/containers/Admin/UsersAndRoles/gql/PolicyUpdateManaged.generated.ts @@ -9,13 +9,13 @@ import { PolicyResultSelectionFragmentDoc, } from './PolicyResultSelection.generated' -export type containers_Admin_RolesAndPolicies_gql_PolicyUpdateManagedMutationVariables = +export type containers_Admin_UsersAndRoles_gql_PolicyUpdateManagedMutationVariables = Types.Exact<{ id: Types.Scalars['ID'] input: Types.ManagedPolicyInput }> -export type containers_Admin_RolesAndPolicies_gql_PolicyUpdateManagedMutation = { +export type containers_Admin_UsersAndRoles_gql_PolicyUpdateManagedMutation = { readonly __typename: 'Mutation' } & { readonly policyUpdate: @@ -28,7 +28,7 @@ export type containers_Admin_RolesAndPolicies_gql_PolicyUpdateManagedMutation = } & PolicyResultSelection_OperationError_Fragment) } -export const containers_Admin_RolesAndPolicies_gql_PolicyUpdateManagedDocument = { +export const containers_Admin_UsersAndRoles_gql_PolicyUpdateManagedDocument = { kind: 'Document', definitions: [ { @@ -36,7 +36,7 @@ export const containers_Admin_RolesAndPolicies_gql_PolicyUpdateManagedDocument = operation: 'mutation', name: { kind: 'Name', - value: 'containers_Admin_RolesAndPolicies_gql_PolicyUpdateManaged', + value: 'containers_Admin_UsersAndRoles_gql_PolicyUpdateManaged', }, variableDefinitions: [ { @@ -94,8 +94,8 @@ export const containers_Admin_RolesAndPolicies_gql_PolicyUpdateManagedDocument = ...PolicyResultSelectionFragmentDoc.definitions, ], } as unknown as DocumentNode< - containers_Admin_RolesAndPolicies_gql_PolicyUpdateManagedMutation, - containers_Admin_RolesAndPolicies_gql_PolicyUpdateManagedMutationVariables + containers_Admin_UsersAndRoles_gql_PolicyUpdateManagedMutation, + containers_Admin_UsersAndRoles_gql_PolicyUpdateManagedMutationVariables > -export { containers_Admin_RolesAndPolicies_gql_PolicyUpdateManagedDocument as default } +export { containers_Admin_UsersAndRoles_gql_PolicyUpdateManagedDocument as default } diff --git a/catalog/app/containers/Admin/UsersAndRoles/gql/PolicyUpdateUnmanaged.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/PolicyUpdateUnmanaged.generated.ts index 1b27f3c11ed..dbfed5a02bd 100644 --- a/catalog/app/containers/Admin/UsersAndRoles/gql/PolicyUpdateUnmanaged.generated.ts +++ b/catalog/app/containers/Admin/UsersAndRoles/gql/PolicyUpdateUnmanaged.generated.ts @@ -9,13 +9,13 @@ import { PolicyResultSelectionFragmentDoc, } from './PolicyResultSelection.generated' -export type containers_Admin_RolesAndPolicies_gql_PolicyUpdateUnmanagedMutationVariables = +export type containers_Admin_UsersAndRoles_gql_PolicyUpdateUnmanagedMutationVariables = Types.Exact<{ id: Types.Scalars['ID'] input: Types.UnmanagedPolicyInput }> -export type containers_Admin_RolesAndPolicies_gql_PolicyUpdateUnmanagedMutation = { +export type containers_Admin_UsersAndRoles_gql_PolicyUpdateUnmanagedMutation = { readonly __typename: 'Mutation' } & { readonly policyUpdate: @@ -28,7 +28,7 @@ export type containers_Admin_RolesAndPolicies_gql_PolicyUpdateUnmanagedMutation } & PolicyResultSelection_OperationError_Fragment) } -export const containers_Admin_RolesAndPolicies_gql_PolicyUpdateUnmanagedDocument = { +export const containers_Admin_UsersAndRoles_gql_PolicyUpdateUnmanagedDocument = { kind: 'Document', definitions: [ { @@ -36,7 +36,7 @@ export const containers_Admin_RolesAndPolicies_gql_PolicyUpdateUnmanagedDocument operation: 'mutation', name: { kind: 'Name', - value: 'containers_Admin_RolesAndPolicies_gql_PolicyUpdateUnmanaged', + value: 'containers_Admin_UsersAndRoles_gql_PolicyUpdateUnmanaged', }, variableDefinitions: [ { @@ -94,8 +94,8 @@ export const containers_Admin_RolesAndPolicies_gql_PolicyUpdateUnmanagedDocument ...PolicyResultSelectionFragmentDoc.definitions, ], } as unknown as DocumentNode< - containers_Admin_RolesAndPolicies_gql_PolicyUpdateUnmanagedMutation, - containers_Admin_RolesAndPolicies_gql_PolicyUpdateUnmanagedMutationVariables + containers_Admin_UsersAndRoles_gql_PolicyUpdateUnmanagedMutation, + containers_Admin_UsersAndRoles_gql_PolicyUpdateUnmanagedMutationVariables > -export { containers_Admin_RolesAndPolicies_gql_PolicyUpdateUnmanagedDocument as default } +export { containers_Admin_UsersAndRoles_gql_PolicyUpdateUnmanagedDocument as default } diff --git a/catalog/app/containers/Admin/UsersAndRoles/gql/RoleCreateManaged.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/RoleCreateManaged.generated.ts index cc7deac7e2f..118e737d626 100644 --- a/catalog/app/containers/Admin/UsersAndRoles/gql/RoleCreateManaged.generated.ts +++ b/catalog/app/containers/Admin/UsersAndRoles/gql/RoleCreateManaged.generated.ts @@ -8,12 +8,12 @@ import { RoleSelectionFragmentDoc, } from './RoleSelection.generated' -export type containers_Admin_RolesAndPolicies_gql_RoleCreateManagedMutationVariables = +export type containers_Admin_UsersAndRoles_gql_RoleCreateManagedMutationVariables = Types.Exact<{ input: Types.ManagedRoleInput }> -export type containers_Admin_RolesAndPolicies_gql_RoleCreateManagedMutation = { +export type containers_Admin_UsersAndRoles_gql_RoleCreateManagedMutation = { readonly __typename: 'Mutation' } & { readonly roleCreate: @@ -30,7 +30,7 @@ export type containers_Admin_RolesAndPolicies_gql_RoleCreateManagedMutation = { | { readonly __typename: 'RoleHasTooManyPoliciesToAttach' } } -export const containers_Admin_RolesAndPolicies_gql_RoleCreateManagedDocument = { +export const containers_Admin_UsersAndRoles_gql_RoleCreateManagedDocument = { kind: 'Document', definitions: [ { @@ -38,7 +38,7 @@ export const containers_Admin_RolesAndPolicies_gql_RoleCreateManagedDocument = { operation: 'mutation', name: { kind: 'Name', - value: 'containers_Admin_RolesAndPolicies_gql_RoleCreateManaged', + value: 'containers_Admin_UsersAndRoles_gql_RoleCreateManaged', }, variableDefinitions: [ { @@ -104,8 +104,8 @@ export const containers_Admin_RolesAndPolicies_gql_RoleCreateManagedDocument = { ...RoleSelectionFragmentDoc.definitions, ], } as unknown as DocumentNode< - containers_Admin_RolesAndPolicies_gql_RoleCreateManagedMutation, - containers_Admin_RolesAndPolicies_gql_RoleCreateManagedMutationVariables + containers_Admin_UsersAndRoles_gql_RoleCreateManagedMutation, + containers_Admin_UsersAndRoles_gql_RoleCreateManagedMutationVariables > -export { containers_Admin_RolesAndPolicies_gql_RoleCreateManagedDocument as default } +export { containers_Admin_UsersAndRoles_gql_RoleCreateManagedDocument as default } diff --git a/catalog/app/containers/Admin/UsersAndRoles/gql/RoleCreateUnmanaged.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/RoleCreateUnmanaged.generated.ts index 9404f1c5126..e25b6ab444e 100644 --- a/catalog/app/containers/Admin/UsersAndRoles/gql/RoleCreateUnmanaged.generated.ts +++ b/catalog/app/containers/Admin/UsersAndRoles/gql/RoleCreateUnmanaged.generated.ts @@ -8,12 +8,12 @@ import { RoleSelectionFragmentDoc, } from './RoleSelection.generated' -export type containers_Admin_RolesAndPolicies_gql_RoleCreateUnmanagedMutationVariables = +export type containers_Admin_UsersAndRoles_gql_RoleCreateUnmanagedMutationVariables = Types.Exact<{ input: Types.UnmanagedRoleInput }> -export type containers_Admin_RolesAndPolicies_gql_RoleCreateUnmanagedMutation = { +export type containers_Admin_UsersAndRoles_gql_RoleCreateUnmanagedMutation = { readonly __typename: 'Mutation' } & { readonly roleCreate: @@ -30,7 +30,7 @@ export type containers_Admin_RolesAndPolicies_gql_RoleCreateUnmanagedMutation = | { readonly __typename: 'RoleHasTooManyPoliciesToAttach' } } -export const containers_Admin_RolesAndPolicies_gql_RoleCreateUnmanagedDocument = { +export const containers_Admin_UsersAndRoles_gql_RoleCreateUnmanagedDocument = { kind: 'Document', definitions: [ { @@ -38,7 +38,7 @@ export const containers_Admin_RolesAndPolicies_gql_RoleCreateUnmanagedDocument = operation: 'mutation', name: { kind: 'Name', - value: 'containers_Admin_RolesAndPolicies_gql_RoleCreateUnmanaged', + value: 'containers_Admin_UsersAndRoles_gql_RoleCreateUnmanaged', }, variableDefinitions: [ { @@ -104,8 +104,8 @@ export const containers_Admin_RolesAndPolicies_gql_RoleCreateUnmanagedDocument = ...RoleSelectionFragmentDoc.definitions, ], } as unknown as DocumentNode< - containers_Admin_RolesAndPolicies_gql_RoleCreateUnmanagedMutation, - containers_Admin_RolesAndPolicies_gql_RoleCreateUnmanagedMutationVariables + containers_Admin_UsersAndRoles_gql_RoleCreateUnmanagedMutation, + containers_Admin_UsersAndRoles_gql_RoleCreateUnmanagedMutationVariables > -export { containers_Admin_RolesAndPolicies_gql_RoleCreateUnmanagedDocument as default } +export { containers_Admin_UsersAndRoles_gql_RoleCreateUnmanagedDocument as default } diff --git a/catalog/app/containers/Admin/UsersAndRoles/gql/RoleDelete.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/RoleDelete.generated.ts index 01abd6b0f22..22fc3c0e923 100644 --- a/catalog/app/containers/Admin/UsersAndRoles/gql/RoleDelete.generated.ts +++ b/catalog/app/containers/Admin/UsersAndRoles/gql/RoleDelete.generated.ts @@ -2,12 +2,11 @@ import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core' import * as Types from '../../../../model/graphql/types.generated' -export type containers_Admin_RolesAndPolicies_gql_RoleDeleteMutationVariables = - Types.Exact<{ - id: Types.Scalars['ID'] - }> +export type containers_Admin_UsersAndRoles_gql_RoleDeleteMutationVariables = Types.Exact<{ + id: Types.Scalars['ID'] +}> -export type containers_Admin_RolesAndPolicies_gql_RoleDeleteMutation = { +export type containers_Admin_UsersAndRoles_gql_RoleDeleteMutation = { readonly __typename: 'Mutation' } & { readonly roleDelete: @@ -17,13 +16,13 @@ export type containers_Admin_RolesAndPolicies_gql_RoleDeleteMutation = { | { readonly __typename: 'RoleAssigned' } } -export const containers_Admin_RolesAndPolicies_gql_RoleDeleteDocument = { +export const containers_Admin_UsersAndRoles_gql_RoleDeleteDocument = { kind: 'Document', definitions: [ { kind: 'OperationDefinition', operation: 'mutation', - name: { kind: 'Name', value: 'containers_Admin_RolesAndPolicies_gql_RoleDelete' }, + name: { kind: 'Name', value: 'containers_Admin_UsersAndRoles_gql_RoleDelete' }, variableDefinitions: [ { kind: 'VariableDefinition', @@ -59,8 +58,8 @@ export const containers_Admin_RolesAndPolicies_gql_RoleDeleteDocument = { }, ], } as unknown as DocumentNode< - containers_Admin_RolesAndPolicies_gql_RoleDeleteMutation, - containers_Admin_RolesAndPolicies_gql_RoleDeleteMutationVariables + containers_Admin_UsersAndRoles_gql_RoleDeleteMutation, + containers_Admin_UsersAndRoles_gql_RoleDeleteMutationVariables > -export { containers_Admin_RolesAndPolicies_gql_RoleDeleteDocument as default } +export { containers_Admin_UsersAndRoles_gql_RoleDeleteDocument as default } diff --git a/catalog/app/containers/Admin/UsersAndRoles/gql/RoleSetDefault.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/RoleSetDefault.generated.ts index 78ad026f4d9..ff194d1497a 100644 --- a/catalog/app/containers/Admin/UsersAndRoles/gql/RoleSetDefault.generated.ts +++ b/catalog/app/containers/Admin/UsersAndRoles/gql/RoleSetDefault.generated.ts @@ -2,12 +2,12 @@ import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core' import * as Types from '../../../../model/graphql/types.generated' -export type containers_Admin_RolesAndPolicies_gql_RoleSetDefaultMutationVariables = +export type containers_Admin_UsersAndRoles_gql_RoleSetDefaultMutationVariables = Types.Exact<{ id: Types.Scalars['ID'] }> -export type containers_Admin_RolesAndPolicies_gql_RoleSetDefaultMutation = { +export type containers_Admin_UsersAndRoles_gql_RoleSetDefaultMutation = { readonly __typename: 'Mutation' } & { readonly roleSetDefault: @@ -19,16 +19,13 @@ export type containers_Admin_RolesAndPolicies_gql_RoleSetDefaultMutation = { | { readonly __typename: 'RoleDoesNotExist' } } -export const containers_Admin_RolesAndPolicies_gql_RoleSetDefaultDocument = { +export const containers_Admin_UsersAndRoles_gql_RoleSetDefaultDocument = { kind: 'Document', definitions: [ { kind: 'OperationDefinition', operation: 'mutation', - name: { - kind: 'Name', - value: 'containers_Admin_RolesAndPolicies_gql_RoleSetDefault', - }, + name: { kind: 'Name', value: 'containers_Admin_UsersAndRoles_gql_RoleSetDefault' }, variableDefinitions: [ { kind: 'VariableDefinition', @@ -115,8 +112,8 @@ export const containers_Admin_RolesAndPolicies_gql_RoleSetDefaultDocument = { }, ], } as unknown as DocumentNode< - containers_Admin_RolesAndPolicies_gql_RoleSetDefaultMutation, - containers_Admin_RolesAndPolicies_gql_RoleSetDefaultMutationVariables + containers_Admin_UsersAndRoles_gql_RoleSetDefaultMutation, + containers_Admin_UsersAndRoles_gql_RoleSetDefaultMutationVariables > -export { containers_Admin_RolesAndPolicies_gql_RoleSetDefaultDocument as default } +export { containers_Admin_UsersAndRoles_gql_RoleSetDefaultDocument as default } diff --git a/catalog/app/containers/Admin/UsersAndRoles/gql/RoleUpdateManaged.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/RoleUpdateManaged.generated.ts index e7b9e3d9d54..dbedafb06c3 100644 --- a/catalog/app/containers/Admin/UsersAndRoles/gql/RoleUpdateManaged.generated.ts +++ b/catalog/app/containers/Admin/UsersAndRoles/gql/RoleUpdateManaged.generated.ts @@ -8,13 +8,13 @@ import { RoleSelectionFragmentDoc, } from './RoleSelection.generated' -export type containers_Admin_RolesAndPolicies_gql_RoleUpdateManagedMutationVariables = +export type containers_Admin_UsersAndRoles_gql_RoleUpdateManagedMutationVariables = Types.Exact<{ id: Types.Scalars['ID'] input: Types.ManagedRoleInput }> -export type containers_Admin_RolesAndPolicies_gql_RoleUpdateManagedMutation = { +export type containers_Admin_UsersAndRoles_gql_RoleUpdateManagedMutation = { readonly __typename: 'Mutation' } & { readonly roleUpdate: @@ -33,7 +33,7 @@ export type containers_Admin_RolesAndPolicies_gql_RoleUpdateManagedMutation = { | { readonly __typename: 'RoleHasTooManyPoliciesToAttach' } } -export const containers_Admin_RolesAndPolicies_gql_RoleUpdateManagedDocument = { +export const containers_Admin_UsersAndRoles_gql_RoleUpdateManagedDocument = { kind: 'Document', definitions: [ { @@ -41,7 +41,7 @@ export const containers_Admin_RolesAndPolicies_gql_RoleUpdateManagedDocument = { operation: 'mutation', name: { kind: 'Name', - value: 'containers_Admin_RolesAndPolicies_gql_RoleUpdateManaged', + value: 'containers_Admin_UsersAndRoles_gql_RoleUpdateManaged', }, variableDefinitions: [ { @@ -120,8 +120,8 @@ export const containers_Admin_RolesAndPolicies_gql_RoleUpdateManagedDocument = { ...RoleSelectionFragmentDoc.definitions, ], } as unknown as DocumentNode< - containers_Admin_RolesAndPolicies_gql_RoleUpdateManagedMutation, - containers_Admin_RolesAndPolicies_gql_RoleUpdateManagedMutationVariables + containers_Admin_UsersAndRoles_gql_RoleUpdateManagedMutation, + containers_Admin_UsersAndRoles_gql_RoleUpdateManagedMutationVariables > -export { containers_Admin_RolesAndPolicies_gql_RoleUpdateManagedDocument as default } +export { containers_Admin_UsersAndRoles_gql_RoleUpdateManagedDocument as default } diff --git a/catalog/app/containers/Admin/UsersAndRoles/gql/RoleUpdateUnmanaged.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/RoleUpdateUnmanaged.generated.ts index b050a7eaba3..7a8c7a65311 100644 --- a/catalog/app/containers/Admin/UsersAndRoles/gql/RoleUpdateUnmanaged.generated.ts +++ b/catalog/app/containers/Admin/UsersAndRoles/gql/RoleUpdateUnmanaged.generated.ts @@ -8,13 +8,13 @@ import { RoleSelectionFragmentDoc, } from './RoleSelection.generated' -export type containers_Admin_RolesAndPolicies_gql_RoleUpdateUnmanagedMutationVariables = +export type containers_Admin_UsersAndRoles_gql_RoleUpdateUnmanagedMutationVariables = Types.Exact<{ id: Types.Scalars['ID'] input: Types.UnmanagedRoleInput }> -export type containers_Admin_RolesAndPolicies_gql_RoleUpdateUnmanagedMutation = { +export type containers_Admin_UsersAndRoles_gql_RoleUpdateUnmanagedMutation = { readonly __typename: 'Mutation' } & { readonly roleUpdate: @@ -33,7 +33,7 @@ export type containers_Admin_RolesAndPolicies_gql_RoleUpdateUnmanagedMutation = | { readonly __typename: 'RoleHasTooManyPoliciesToAttach' } } -export const containers_Admin_RolesAndPolicies_gql_RoleUpdateUnmanagedDocument = { +export const containers_Admin_UsersAndRoles_gql_RoleUpdateUnmanagedDocument = { kind: 'Document', definitions: [ { @@ -41,7 +41,7 @@ export const containers_Admin_RolesAndPolicies_gql_RoleUpdateUnmanagedDocument = operation: 'mutation', name: { kind: 'Name', - value: 'containers_Admin_RolesAndPolicies_gql_RoleUpdateUnmanaged', + value: 'containers_Admin_UsersAndRoles_gql_RoleUpdateUnmanaged', }, variableDefinitions: [ { @@ -120,8 +120,8 @@ export const containers_Admin_RolesAndPolicies_gql_RoleUpdateUnmanagedDocument = ...RoleSelectionFragmentDoc.definitions, ], } as unknown as DocumentNode< - containers_Admin_RolesAndPolicies_gql_RoleUpdateUnmanagedMutation, - containers_Admin_RolesAndPolicies_gql_RoleUpdateUnmanagedMutationVariables + containers_Admin_UsersAndRoles_gql_RoleUpdateUnmanagedMutation, + containers_Admin_UsersAndRoles_gql_RoleUpdateUnmanagedMutationVariables > -export { containers_Admin_RolesAndPolicies_gql_RoleUpdateUnmanagedDocument as default } +export { containers_Admin_UsersAndRoles_gql_RoleUpdateUnmanagedDocument as default } diff --git a/catalog/app/containers/Admin/UsersAndRoles/gql/Roles.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/Roles.generated.ts index 33b7bf5b63e..c08e73c7020 100644 --- a/catalog/app/containers/Admin/UsersAndRoles/gql/Roles.generated.ts +++ b/catalog/app/containers/Admin/UsersAndRoles/gql/Roles.generated.ts @@ -8,11 +8,11 @@ import { RoleSelectionFragmentDoc, } from './RoleSelection.generated' -export type containers_Admin_RolesAndPolicies_gql_RolesQueryVariables = Types.Exact<{ +export type containers_Admin_UsersAndRoles_gql_RolesQueryVariables = Types.Exact<{ [key: string]: never }> -export type containers_Admin_RolesAndPolicies_gql_RolesQuery = { +export type containers_Admin_UsersAndRoles_gql_RolesQuery = { readonly __typename: 'Query' } & { readonly roles: ReadonlyArray< @@ -25,13 +25,13 @@ export type containers_Admin_RolesAndPolicies_gql_RolesQuery = { > } -export const containers_Admin_RolesAndPolicies_gql_RolesDocument = { +export const containers_Admin_UsersAndRoles_gql_RolesDocument = { kind: 'Document', definitions: [ { kind: 'OperationDefinition', operation: 'query', - name: { kind: 'Name', value: 'containers_Admin_RolesAndPolicies_gql_Roles' }, + name: { kind: 'Name', value: 'containers_Admin_UsersAndRoles_gql_Roles' }, selectionSet: { kind: 'SelectionSet', selections: [ @@ -86,8 +86,8 @@ export const containers_Admin_RolesAndPolicies_gql_RolesDocument = { ...RoleSelectionFragmentDoc.definitions, ], } as unknown as DocumentNode< - containers_Admin_RolesAndPolicies_gql_RolesQuery, - containers_Admin_RolesAndPolicies_gql_RolesQueryVariables + containers_Admin_UsersAndRoles_gql_RolesQuery, + containers_Admin_UsersAndRoles_gql_RolesQueryVariables > -export { containers_Admin_RolesAndPolicies_gql_RolesDocument as default } +export { containers_Admin_UsersAndRoles_gql_RolesDocument as default } diff --git a/catalog/app/containers/Admin/UsersAndRoles/gql/UserCreate.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/UserCreate.generated.ts index ca678da00a1..e14cbbd7c6f 100644 --- a/catalog/app/containers/Admin/UsersAndRoles/gql/UserCreate.generated.ts +++ b/catalog/app/containers/Admin/UsersAndRoles/gql/UserCreate.generated.ts @@ -9,11 +9,11 @@ import { UserResultSelectionFragmentDoc, } from './UserResultSelection.generated' -export type containers_Admin_Users_gql_UserCreateMutationVariables = Types.Exact<{ +export type containers_Admin_UsersAndRoles_gql_UserCreateMutationVariables = Types.Exact<{ input: Types.UserInput }> -export type containers_Admin_Users_gql_UserCreateMutation = { +export type containers_Admin_UsersAndRoles_gql_UserCreateMutation = { readonly __typename: 'Mutation' } & { readonly admin: { readonly __typename: 'AdminMutations' } & { @@ -30,13 +30,13 @@ export type containers_Admin_Users_gql_UserCreateMutation = { } } -export const containers_Admin_Users_gql_UserCreateDocument = { +export const containers_Admin_UsersAndRoles_gql_UserCreateDocument = { kind: 'Document', definitions: [ { kind: 'OperationDefinition', operation: 'mutation', - name: { kind: 'Name', value: 'containers_Admin_Users_gql_UserCreate' }, + name: { kind: 'Name', value: 'containers_Admin_UsersAndRoles_gql_UserCreate' }, variableDefinitions: [ { kind: 'VariableDefinition', @@ -97,8 +97,8 @@ export const containers_Admin_Users_gql_UserCreateDocument = { ...UserResultSelectionFragmentDoc.definitions, ], } as unknown as DocumentNode< - containers_Admin_Users_gql_UserCreateMutation, - containers_Admin_Users_gql_UserCreateMutationVariables + containers_Admin_UsersAndRoles_gql_UserCreateMutation, + containers_Admin_UsersAndRoles_gql_UserCreateMutationVariables > -export { containers_Admin_Users_gql_UserCreateDocument as default } +export { containers_Admin_UsersAndRoles_gql_UserCreateDocument as default } diff --git a/catalog/app/containers/Admin/UsersAndRoles/gql/UserDelete.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/UserDelete.generated.ts index 7b8a68057d2..cf196c146ad 100644 --- a/catalog/app/containers/Admin/UsersAndRoles/gql/UserDelete.generated.ts +++ b/catalog/app/containers/Admin/UsersAndRoles/gql/UserDelete.generated.ts @@ -2,11 +2,11 @@ import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core' import * as Types from '../../../../model/graphql/types.generated' -export type containers_Admin_Users_gql_UserDeleteMutationVariables = Types.Exact<{ +export type containers_Admin_UsersAndRoles_gql_UserDeleteMutationVariables = Types.Exact<{ name: Types.Scalars['String'] }> -export type containers_Admin_Users_gql_UserDeleteMutation = { +export type containers_Admin_UsersAndRoles_gql_UserDeleteMutation = { readonly __typename: 'Mutation' } & { readonly admin: { readonly __typename: 'AdminMutations' } & { @@ -33,13 +33,13 @@ export type containers_Admin_Users_gql_UserDeleteMutation = { } } -export const containers_Admin_Users_gql_UserDeleteDocument = { +export const containers_Admin_UsersAndRoles_gql_UserDeleteDocument = { kind: 'Document', definitions: [ { kind: 'OperationDefinition', operation: 'mutation', - name: { kind: 'Name', value: 'containers_Admin_Users_gql_UserDelete' }, + name: { kind: 'Name', value: 'containers_Admin_UsersAndRoles_gql_UserDelete' }, variableDefinitions: [ { kind: 'VariableDefinition', @@ -169,8 +169,8 @@ export const containers_Admin_Users_gql_UserDeleteDocument = { }, ], } as unknown as DocumentNode< - containers_Admin_Users_gql_UserDeleteMutation, - containers_Admin_Users_gql_UserDeleteMutationVariables + containers_Admin_UsersAndRoles_gql_UserDeleteMutation, + containers_Admin_UsersAndRoles_gql_UserDeleteMutationVariables > -export { containers_Admin_Users_gql_UserDeleteDocument as default } +export { containers_Admin_UsersAndRoles_gql_UserDeleteDocument as default } diff --git a/catalog/app/containers/Admin/UsersAndRoles/gql/UserSetActive.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/UserSetActive.generated.ts index 343a4cff8fd..16f0c65a372 100644 --- a/catalog/app/containers/Admin/UsersAndRoles/gql/UserSetActive.generated.ts +++ b/catalog/app/containers/Admin/UsersAndRoles/gql/UserSetActive.generated.ts @@ -9,12 +9,13 @@ import { UserResultSelectionFragmentDoc, } from './UserResultSelection.generated' -export type containers_Admin_Users_gql_UserSetActiveMutationVariables = Types.Exact<{ - name: Types.Scalars['String'] - active: Types.Scalars['Boolean'] -}> +export type containers_Admin_UsersAndRoles_gql_UserSetActiveMutationVariables = + Types.Exact<{ + name: Types.Scalars['String'] + active: Types.Scalars['Boolean'] + }> -export type containers_Admin_Users_gql_UserSetActiveMutation = { +export type containers_Admin_UsersAndRoles_gql_UserSetActiveMutation = { readonly __typename: 'Mutation' } & { readonly admin: { readonly __typename: 'AdminMutations' } & { @@ -35,13 +36,13 @@ export type containers_Admin_Users_gql_UserSetActiveMutation = { } } -export const containers_Admin_Users_gql_UserSetActiveDocument = { +export const containers_Admin_UsersAndRoles_gql_UserSetActiveDocument = { kind: 'Document', definitions: [ { kind: 'OperationDefinition', operation: 'mutation', - name: { kind: 'Name', value: 'containers_Admin_Users_gql_UserSetActive' }, + name: { kind: 'Name', value: 'containers_Admin_UsersAndRoles_gql_UserSetActive' }, variableDefinitions: [ { kind: 'VariableDefinition', @@ -129,8 +130,8 @@ export const containers_Admin_Users_gql_UserSetActiveDocument = { ...UserResultSelectionFragmentDoc.definitions, ], } as unknown as DocumentNode< - containers_Admin_Users_gql_UserSetActiveMutation, - containers_Admin_Users_gql_UserSetActiveMutationVariables + containers_Admin_UsersAndRoles_gql_UserSetActiveMutation, + containers_Admin_UsersAndRoles_gql_UserSetActiveMutationVariables > -export { containers_Admin_Users_gql_UserSetActiveDocument as default } +export { containers_Admin_UsersAndRoles_gql_UserSetActiveDocument as default } diff --git a/catalog/app/containers/Admin/UsersAndRoles/gql/UserSetAdmin.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/UserSetAdmin.generated.ts index 87c7a5c85ac..71d6e44ce30 100644 --- a/catalog/app/containers/Admin/UsersAndRoles/gql/UserSetAdmin.generated.ts +++ b/catalog/app/containers/Admin/UsersAndRoles/gql/UserSetAdmin.generated.ts @@ -9,12 +9,13 @@ import { UserResultSelectionFragmentDoc, } from './UserResultSelection.generated' -export type containers_Admin_Users_gql_UserSetAdminMutationVariables = Types.Exact<{ - name: Types.Scalars['String'] - admin: Types.Scalars['Boolean'] -}> +export type containers_Admin_UsersAndRoles_gql_UserSetAdminMutationVariables = + Types.Exact<{ + name: Types.Scalars['String'] + admin: Types.Scalars['Boolean'] + }> -export type containers_Admin_Users_gql_UserSetAdminMutation = { +export type containers_Admin_UsersAndRoles_gql_UserSetAdminMutation = { readonly __typename: 'Mutation' } & { readonly admin: { readonly __typename: 'AdminMutations' } & { @@ -35,13 +36,13 @@ export type containers_Admin_Users_gql_UserSetAdminMutation = { } } -export const containers_Admin_Users_gql_UserSetAdminDocument = { +export const containers_Admin_UsersAndRoles_gql_UserSetAdminDocument = { kind: 'Document', definitions: [ { kind: 'OperationDefinition', operation: 'mutation', - name: { kind: 'Name', value: 'containers_Admin_Users_gql_UserSetAdmin' }, + name: { kind: 'Name', value: 'containers_Admin_UsersAndRoles_gql_UserSetAdmin' }, variableDefinitions: [ { kind: 'VariableDefinition', @@ -129,8 +130,8 @@ export const containers_Admin_Users_gql_UserSetAdminDocument = { ...UserResultSelectionFragmentDoc.definitions, ], } as unknown as DocumentNode< - containers_Admin_Users_gql_UserSetAdminMutation, - containers_Admin_Users_gql_UserSetAdminMutationVariables + containers_Admin_UsersAndRoles_gql_UserSetAdminMutation, + containers_Admin_UsersAndRoles_gql_UserSetAdminMutationVariables > -export { containers_Admin_Users_gql_UserSetAdminDocument as default } +export { containers_Admin_UsersAndRoles_gql_UserSetAdminDocument as default } diff --git a/catalog/app/containers/Admin/UsersAndRoles/gql/UserSetEmail.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/UserSetEmail.generated.ts index 8776fb06f11..8d46325fe0c 100644 --- a/catalog/app/containers/Admin/UsersAndRoles/gql/UserSetEmail.generated.ts +++ b/catalog/app/containers/Admin/UsersAndRoles/gql/UserSetEmail.generated.ts @@ -9,12 +9,13 @@ import { UserResultSelectionFragmentDoc, } from './UserResultSelection.generated' -export type containers_Admin_Users_gql_UserSetEmailMutationVariables = Types.Exact<{ - name: Types.Scalars['String'] - email: Types.Scalars['String'] -}> +export type containers_Admin_UsersAndRoles_gql_UserSetEmailMutationVariables = + Types.Exact<{ + name: Types.Scalars['String'] + email: Types.Scalars['String'] + }> -export type containers_Admin_Users_gql_UserSetEmailMutation = { +export type containers_Admin_UsersAndRoles_gql_UserSetEmailMutation = { readonly __typename: 'Mutation' } & { readonly admin: { readonly __typename: 'AdminMutations' } & { @@ -35,13 +36,13 @@ export type containers_Admin_Users_gql_UserSetEmailMutation = { } } -export const containers_Admin_Users_gql_UserSetEmailDocument = { +export const containers_Admin_UsersAndRoles_gql_UserSetEmailDocument = { kind: 'Document', definitions: [ { kind: 'OperationDefinition', operation: 'mutation', - name: { kind: 'Name', value: 'containers_Admin_Users_gql_UserSetEmail' }, + name: { kind: 'Name', value: 'containers_Admin_UsersAndRoles_gql_UserSetEmail' }, variableDefinitions: [ { kind: 'VariableDefinition', @@ -129,8 +130,8 @@ export const containers_Admin_Users_gql_UserSetEmailDocument = { ...UserResultSelectionFragmentDoc.definitions, ], } as unknown as DocumentNode< - containers_Admin_Users_gql_UserSetEmailMutation, - containers_Admin_Users_gql_UserSetEmailMutationVariables + containers_Admin_UsersAndRoles_gql_UserSetEmailMutation, + containers_Admin_UsersAndRoles_gql_UserSetEmailMutationVariables > -export { containers_Admin_Users_gql_UserSetEmailDocument as default } +export { containers_Admin_UsersAndRoles_gql_UserSetEmailDocument as default } diff --git a/catalog/app/containers/Admin/UsersAndRoles/gql/UserSetRole.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/UserSetRole.generated.ts index 65b3d88258d..2a262dd7f42 100644 --- a/catalog/app/containers/Admin/UsersAndRoles/gql/UserSetRole.generated.ts +++ b/catalog/app/containers/Admin/UsersAndRoles/gql/UserSetRole.generated.ts @@ -9,13 +9,14 @@ import { UserResultSelectionFragmentDoc, } from './UserResultSelection.generated' -export type containers_Admin_Users_gql_UserSetRoleMutationVariables = Types.Exact<{ - name: Types.Scalars['String'] - role: Types.Scalars['String'] - extraRoles: ReadonlyArray -}> +export type containers_Admin_UsersAndRoles_gql_UserSetRoleMutationVariables = + Types.Exact<{ + name: Types.Scalars['String'] + role: Types.Scalars['String'] + extraRoles: ReadonlyArray + }> -export type containers_Admin_Users_gql_UserSetRoleMutation = { +export type containers_Admin_UsersAndRoles_gql_UserSetRoleMutation = { readonly __typename: 'Mutation' } & { readonly admin: { readonly __typename: 'AdminMutations' } & { @@ -36,13 +37,13 @@ export type containers_Admin_Users_gql_UserSetRoleMutation = { } } -export const containers_Admin_Users_gql_UserSetRoleDocument = { +export const containers_Admin_UsersAndRoles_gql_UserSetRoleDocument = { kind: 'Document', definitions: [ { kind: 'OperationDefinition', operation: 'mutation', - name: { kind: 'Name', value: 'containers_Admin_Users_gql_UserSetRole' }, + name: { kind: 'Name', value: 'containers_Admin_UsersAndRoles_gql_UserSetRole' }, variableDefinitions: [ { kind: 'VariableDefinition', @@ -152,8 +153,8 @@ export const containers_Admin_Users_gql_UserSetRoleDocument = { ...UserResultSelectionFragmentDoc.definitions, ], } as unknown as DocumentNode< - containers_Admin_Users_gql_UserSetRoleMutation, - containers_Admin_Users_gql_UserSetRoleMutationVariables + containers_Admin_UsersAndRoles_gql_UserSetRoleMutation, + containers_Admin_UsersAndRoles_gql_UserSetRoleMutationVariables > -export { containers_Admin_Users_gql_UserSetRoleDocument as default } +export { containers_Admin_UsersAndRoles_gql_UserSetRoleDocument as default } diff --git a/catalog/app/containers/Admin/UsersAndRoles/gql/Users.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/Users.generated.ts index 80f5a711c7c..c8f6dc5bfd8 100644 --- a/catalog/app/containers/Admin/UsersAndRoles/gql/Users.generated.ts +++ b/catalog/app/containers/Admin/UsersAndRoles/gql/Users.generated.ts @@ -7,11 +7,13 @@ import { UserSelectionFragmentDoc, } from './UserSelection.generated' -export type containers_Admin_Users_gql_UsersQueryVariables = Types.Exact<{ +export type containers_Admin_UsersAndRoles_gql_UsersQueryVariables = Types.Exact<{ [key: string]: never }> -export type containers_Admin_Users_gql_UsersQuery = { readonly __typename: 'Query' } & { +export type containers_Admin_UsersAndRoles_gql_UsersQuery = { + readonly __typename: 'Query' +} & { readonly admin: { readonly __typename: 'AdminQueries' } & { readonly user: { readonly __typename: 'UserAdminQueries' } & { readonly list: ReadonlyArray< @@ -35,13 +37,13 @@ export type containers_Admin_Users_gql_UsersQuery = { readonly __typename: 'Quer > } -export const containers_Admin_Users_gql_UsersDocument = { +export const containers_Admin_UsersAndRoles_gql_UsersDocument = { kind: 'Document', definitions: [ { kind: 'OperationDefinition', operation: 'query', - name: { kind: 'Name', value: 'containers_Admin_Users_gql_Users' }, + name: { kind: 'Name', value: 'containers_Admin_UsersAndRoles_gql_Users' }, selectionSet: { kind: 'SelectionSet', selections: [ @@ -156,8 +158,8 @@ export const containers_Admin_Users_gql_UsersDocument = { ...UserSelectionFragmentDoc.definitions, ], } as unknown as DocumentNode< - containers_Admin_Users_gql_UsersQuery, - containers_Admin_Users_gql_UsersQueryVariables + containers_Admin_UsersAndRoles_gql_UsersQuery, + containers_Admin_UsersAndRoles_gql_UsersQueryVariables > -export { containers_Admin_Users_gql_UsersDocument as default } +export { containers_Admin_UsersAndRoles_gql_UsersDocument as default } From a6815e3f7eb606cef757b1642a862f2746c31fbb Mon Sep 17 00:00:00 2001 From: nl_0 Date: Thu, 6 Jun 2024 15:44:35 +0200 Subject: [PATCH 49/69] nav menu polish --- catalog/app/containers/NavBar/NavMenu.tsx | 252 +++++++++++------- .../NavBar/gql/SwitchRole.generated.ts | 42 +-- .../containers/NavBar/gql/SwitchRole.graphql | 11 - 3 files changed, 155 insertions(+), 150 deletions(-) diff --git a/catalog/app/containers/NavBar/NavMenu.tsx b/catalog/app/containers/NavBar/NavMenu.tsx index 7e8cb8c9887..89acfba5b8d 100644 --- a/catalog/app/containers/NavBar/NavMenu.tsx +++ b/catalog/app/containers/NavBar/NavMenu.tsx @@ -1,3 +1,4 @@ +import cx from 'classnames' import * as R from 'ramda' import * as React from 'react' import * as redux from 'react-redux' @@ -76,10 +77,11 @@ interface DropdownMenuProps trigger: ( open: React.EventHandler>, ) => React.ReactNode + onClose?: () => void items: (ItemDescriptor | false)[] } -function DropdownMenu({ trigger, items, ...rest }: DropdownMenuProps) { +function DropdownMenu({ trigger, items, onClose, ...rest }: DropdownMenuProps) { const [anchor, setAnchor] = React.useState(null) const open = React.useCallback( @@ -91,7 +93,8 @@ function DropdownMenu({ trigger, items, ...rest }: DropdownMenuProps) { const close = React.useCallback(() => { setAnchor(null) - }, [setAnchor]) + if (onClose) onClose() + }, [setAnchor, onClose]) const filtered = items.filter(Boolean) as ItemDescriptor[] const children = filtered.map( @@ -137,7 +140,7 @@ function DropdownMenu({ trigger, items, ...rest }: DropdownMenuProps) { anchorOrigin={{ horizontal: 'right', vertical: 'top' }} open={!!anchor} onClose={close} - MenuListProps={{ component: 'nav' } as M.MenuListProps} + MenuListProps={{ component: 'nav', disablePadding: true } as M.MenuListProps} {...rest} > {children} @@ -177,28 +180,6 @@ function useLinks(): ItemDescriptor[] { return links } -const useUserDisplayStyles = M.makeStyles({ - role: { - opacity: 0.5, - fontWeight: 'lighter', - }, -}) - -interface UserDisplayProps { - user: Me -} - -function UserDisplay({ user }: UserDisplayProps) { - const classes = useUserDisplayStyles() - return withIcon( - user.isAdmin ? 'security' : 'person', - <> - {user.name} -  ({user.role.name}) - , - ) -} - const useBadgeStyles = M.makeStyles({ root: { alignItems: 'inherit', @@ -225,59 +206,151 @@ function Badge({ children, color, invisible, ...props }: BadgeProps) { ) } -const withIcon = (icon: string, children: React.ReactNode) => ( - <> - {icon} {children} - -) +const useItemStyles = M.makeStyles({ + icon: { + minWidth: '36px', + }, + hasAction: {}, + text: { + paddingRight: '8px', -interface DesktopUserDropdownProps { - user: Me + '&$hasAction': { + paddingRight: '36px', + }, + }, +}) + +interface ItemContentsProps { + icon?: React.ReactNode + primary: React.ReactNode + secondary?: React.ReactNode + action?: React.ReactNode } -function DesktopUserDropdown({ user }: DesktopUserDropdownProps) { +function ItemContents({ icon, primary, secondary, action }: ItemContentsProps) { + const classes = useItemStyles() + const iconEl = typeof icon === 'string' ? {icon} : icon + return ( + <> + {!!iconEl && {iconEl}} + + {!!action && {action}} + + ) +} + +function useGetAuthItems() { const { urls } = NamedRoutes.use() const switchRole = useRoleSwitcher() const bookmarks = Bookmarks.use() - // XXX: reset bookmarks updates state on close? - const hasBookmarksUpdates = bookmarks?.hasUpdates ?? false - - const items = [ - cfg.mode === 'OPEN' && - ItemDescriptor.To(urls.profile(), withIcon('person', 'Profile')), - user.roles.length > 1 && - ItemDescriptor.Click(() => switchRole(user), withIcon('loop', 'Switch role')), - !!bookmarks && - ItemDescriptor.Click( - () => bookmarks.show(), - <> - - bookmarks_outlined - -  Bookmarks - , + + return function getAuthLinks(user: Me) { + const items: ItemDescriptor[] = [] + + const extraRoles = user.roles.length - 1 + const userItem = ( + + {user.role.name} + {extraRoles > 0 && ( + +  +{extraRoles} + + )} + + } + action={ + extraRoles > 0 && ( + + switchRole(user)}> + loop + + + ) + } + /> + ) + items.push( + cfg.mode === 'OPEN' // currently only OPEN has profile page + ? ItemDescriptor.To(urls.profile(), userItem) + : ItemDescriptor.Text(userItem), + ) + + items.push(ItemDescriptor.Divider()) + + if (bookmarks) { + items.push( + ItemDescriptor.Click( + bookmarks.show, + + bookmarks_outlined +
+ } + primary="Bookmarks" + />, + ), + ) + } + + if (user.isAdmin) { + items.push( + ItemDescriptor.To( + urls.admin(), + , + ), + ) + } + + items.push( + ItemDescriptor.To( + urls.signOut(), + , ), - user.isAdmin && - ItemDescriptor.To(urls.admin(), withIcon('security', 'Admin settings')), - ItemDescriptor.To(urls.signOut(), withIcon('meeting_room', 'Sign Out')), - ] + ) + + return items + } +} + +interface DesktopUserDropdownProps { + user: Me +} + +function DesktopUserDropdown({ user }: DesktopUserDropdownProps) { + const bookmarks = Bookmarks.use() + const getAuthItems = useGetAuthItems() return ( ( + // XXX: badge here or around the button? - - - {' '} - expand_more + + {user.isAdmin ? 'security' : 'person'} + +   + {user.name} +   + expand_more )} - items={items} + items={getAuthItems(user)} + // XXX: reset bookmarks updates state on close? + // onClose={() => bookmarks?.hide()} /> ) } @@ -346,10 +419,8 @@ interface MobileMenuProps { function MobileMenu({ auth }: MobileMenuProps) { const { urls } = NamedRoutes.use() const links = useLinks() - const switchRole = useRoleSwitcher() + const getAuthItems = useGetAuthItems() const bookmarks = Bookmarks.use() - // XXX: reset bookmarks updates state on close? - const hasBookmarksUpdates = bookmarks?.hasUpdates ?? false const authItems = cfg.disableNavigator || cfg.mode === 'LOCAL' @@ -357,45 +428,27 @@ function MobileMenu({ auth }: MobileMenuProps) { : [ ...AuthState.match<(ItemDescriptor | false)[]>( { - Loading: () => [ItemDescriptor.Text(withIcon('person', 'Loading...'))], + Loading: () => [ + ItemDescriptor.Text( + } + primary="Loading..." + />, + ), + ], Error: () => [ - ItemDescriptor.To(urls.signIn(), withIcon('error_outline', 'Sign In')), + ItemDescriptor.To( + urls.signIn(), + , + ), ], Ready: ({ user }) => user - ? [ - cfg.mode === 'OPEN' - ? ItemDescriptor.To(urls.profile(), ) - : ItemDescriptor.Text(), - user.roles.length > 1 && - ItemDescriptor.Click( - () => switchRole(user), - withIcon('loop', 'Switch role'), - ), - !!bookmarks && - ItemDescriptor.Click( - () => bookmarks.show(), - <> - - bookmarks_outlined - -  Bookmarks - , - ), - user.isAdmin && - ItemDescriptor.To( - urls.admin(), - withIcon('security', 'Admin settings'), - ), - ItemDescriptor.To( - urls.signOut(), - withIcon('meeting_room', 'Sign Out'), - ), - ] + ? getAuthItems(user) : [ ItemDescriptor.To( urls.signIn(), - withIcon('exit_to_app', 'Sign In'), + , ), ], }, @@ -404,16 +457,19 @@ function MobileMenu({ auth }: MobileMenuProps) { ItemDescriptor.Divider(), ] - const items = [...authItems, ...links] - return ( + // XXX: badge here or around the button? ( - menu + + menu + )} - items={items} + items={[...authItems, ...links]} + // XXX: reset bookmarks updates state on close? + // onClose={() => bookmarks?.hide()} /> ) } diff --git a/catalog/app/containers/NavBar/gql/SwitchRole.generated.ts b/catalog/app/containers/NavBar/gql/SwitchRole.generated.ts index 0945ad5b11e..43e48592422 100644 --- a/catalog/app/containers/NavBar/gql/SwitchRole.generated.ts +++ b/catalog/app/containers/NavBar/gql/SwitchRole.generated.ts @@ -10,12 +10,7 @@ export type containers_NavBar_gql_SwitchRoleMutation = { readonly __typename: 'Mutation' } & { readonly switchRole: - | ({ readonly __typename: 'Me' } & Pick & { - readonly role: { readonly __typename: 'MyRole' } & Pick - readonly roles: ReadonlyArray< - { readonly __typename: 'MyRole' } & Pick - > - }) + | { readonly __typename: 'Me' } | ({ readonly __typename: 'InvalidInput' } & { readonly errors: ReadonlyArray< { readonly __typename: 'InputError' } & Pick< @@ -64,41 +59,6 @@ export const containers_NavBar_gql_SwitchRoleDocument = { kind: 'SelectionSet', selections: [ { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, - { - kind: 'InlineFragment', - typeCondition: { - kind: 'NamedType', - name: { kind: 'Name', value: 'Me' }, - }, - selectionSet: { - kind: 'SelectionSet', - selections: [ - { kind: 'Field', name: { kind: 'Name', value: 'name' } }, - { kind: 'Field', name: { kind: 'Name', value: 'email' } }, - { kind: 'Field', name: { kind: 'Name', value: 'isAdmin' } }, - { - kind: 'Field', - name: { kind: 'Name', value: 'role' }, - selectionSet: { - kind: 'SelectionSet', - selections: [ - { kind: 'Field', name: { kind: 'Name', value: 'name' } }, - ], - }, - }, - { - kind: 'Field', - name: { kind: 'Name', value: 'roles' }, - selectionSet: { - kind: 'SelectionSet', - selections: [ - { kind: 'Field', name: { kind: 'Name', value: 'name' } }, - ], - }, - }, - ], - }, - }, { kind: 'InlineFragment', typeCondition: { diff --git a/catalog/app/containers/NavBar/gql/SwitchRole.graphql b/catalog/app/containers/NavBar/gql/SwitchRole.graphql index 944dc8a2fda..eaf35baae02 100644 --- a/catalog/app/containers/NavBar/gql/SwitchRole.graphql +++ b/catalog/app/containers/NavBar/gql/SwitchRole.graphql @@ -1,17 +1,6 @@ mutation ($roleName: String!) { switchRole(roleName: $roleName) { __typename - ... on Me { - name - email - isAdmin - role { - name - } - roles { - name - } - } ... on OperationError { message name From 453f992a4a1f1625574f287140c4f89c9952364f Mon Sep 17 00:00:00 2001 From: nl_0 Date: Fri, 7 Jun 2024 08:32:22 +0200 Subject: [PATCH 50/69] CHECKPOINT From 7635228ce150547ff0ee9b6400a2f7f09f9f316c Mon Sep 17 00:00:00 2001 From: nl_0 Date: Fri, 7 Jun 2024 09:23:21 +0200 Subject: [PATCH 51/69] adjust the menu --- catalog/app/containers/NavBar/NavMenu.tsx | 75 +++++++++++------------ 1 file changed, 36 insertions(+), 39 deletions(-) diff --git a/catalog/app/containers/NavBar/NavMenu.tsx b/catalog/app/containers/NavBar/NavMenu.tsx index 89acfba5b8d..bdf78a6195b 100644 --- a/catalog/app/containers/NavBar/NavMenu.tsx +++ b/catalog/app/containers/NavBar/NavMenu.tsx @@ -1,4 +1,3 @@ -import cx from 'classnames' import * as R from 'ramda' import * as React from 'react' import * as redux from 'react-redux' @@ -126,7 +125,9 @@ function DropdownMenu({ trigger, items, onClose, ...rest }: DropdownMenuProps) { {...props} /> ), - Text: (props, i) => , + Text: (props, i) => ( + + ), Divider: (_, i) => , }), ) @@ -140,7 +141,7 @@ function DropdownMenu({ trigger, items, onClose, ...rest }: DropdownMenuProps) { anchorOrigin={{ horizontal: 'right', vertical: 'top' }} open={!!anchor} onClose={close} - MenuListProps={{ component: 'nav', disablePadding: true } as M.MenuListProps} + MenuListProps={{ component: 'nav' } as M.MenuListProps} {...rest} > {children} @@ -206,39 +207,45 @@ function Badge({ children, color, invisible, ...props }: BadgeProps) { ) } -const useItemStyles = M.makeStyles({ +const useItemStyles = M.makeStyles((t) => ({ icon: { minWidth: '36px', }, - hasAction: {}, text: { paddingRight: '8px', + }, + secondary: { + fontSize: t.typography.body2.fontSize, + fontWeight: 'lighter', + opacity: 0.6, - '&$hasAction': { - paddingRight: '36px', + '&::before': { + content: '" – "', }, }, -}) +})) interface ItemContentsProps { icon?: React.ReactNode primary: React.ReactNode secondary?: React.ReactNode - action?: React.ReactNode } -function ItemContents({ icon, primary, secondary, action }: ItemContentsProps) { +function ItemContents({ icon, primary, secondary }: ItemContentsProps) { const classes = useItemStyles() const iconEl = typeof icon === 'string' ? {icon} : icon return ( <> {!!iconEl && {iconEl}} + {primary} + {!!secondary && {secondary}} + + } + className={classes.text} /> - {!!action && {action}} ) } @@ -251,31 +258,8 @@ function useGetAuthItems() { return function getAuthLinks(user: Me) { const items: ItemDescriptor[] = [] - const extraRoles = user.roles.length - 1 const userItem = ( - - {user.role.name} - {extraRoles > 0 && ( - -  +{extraRoles} - - )} - - } - action={ - extraRoles > 0 && ( - - switchRole(user)}> - loop - - - ) - } - /> + ) items.push( cfg.mode === 'OPEN' // currently only OPEN has profile page @@ -283,6 +267,19 @@ function useGetAuthItems() { : ItemDescriptor.Text(userItem), ) + if (user.roles.length > 1) { + items.push( + ItemDescriptor.Click( + () => switchRole(user), + {user.roles.length} available} + />, + ), + ) + } + items.push(ItemDescriptor.Divider()) if (bookmarks) { @@ -340,7 +337,7 @@ function DesktopUserDropdown({ user }: DesktopUserDropdownProps) { style={{ textTransform: 'none' }} > - {user.isAdmin ? 'security' : 'person'} + person   {user.name} From 859b236f4ca9bddee1cb3c6b0c8d027ced33c2db Mon Sep 17 00:00:00 2001 From: nl_0 Date: Fri, 7 Jun 2024 10:22:34 +0200 Subject: [PATCH 52/69] icons for nav links on mobile --- catalog/app/containers/NavBar/NavMenu.tsx | 178 ++++++++++++---------- 1 file changed, 100 insertions(+), 78 deletions(-) diff --git a/catalog/app/containers/NavBar/NavMenu.tsx b/catalog/app/containers/NavBar/NavMenu.tsx index bdf78a6195b..49946f3984e 100644 --- a/catalog/app/containers/NavBar/NavMenu.tsx +++ b/catalog/app/containers/NavBar/NavMenu.tsx @@ -16,6 +16,7 @@ import * as authSelectors from 'containers/Auth/selectors' import * as CatalogSettings from 'utils/CatalogSettings' import * as GQL from 'utils/GraphQL' import * as NamedRoutes from 'utils/NamedRoutes' +import assertNever from 'utils/assertNever' import * as tagged from 'utils/taggedV2' import useRoleSwitcher from './RoleSwitcher' @@ -151,34 +152,37 @@ function DropdownMenu({ trigger, items, onClose, ...rest }: DropdownMenuProps) { ) } -function useLinks(): ItemDescriptor[] { +const mkNavItem = ( + kind: 'to' | 'href', + target: string, + label: string, + icon: string = 'open_in_new', +) => ({ kind, target, label, icon }) + +type NavItem = ReturnType + +function useNavItems(): NavItem[] { const { urls } = NamedRoutes.use() const settings = CatalogSettings.use() - const customNavLink: ItemDescriptor | false = React.useMemo(() => { + const customNavLink: NavItem | false = React.useMemo(() => { if (!settings?.customNavLink) return false const href = sanitizeUrl(settings.customNavLink.url) if (href === 'about:blank') return false - return ItemDescriptor.Href(href, settings.customNavLink.label) + return mkNavItem('href', href, settings.customNavLink.label) }, [settings?.customNavLink]) - const links: ItemDescriptor[] = [] - - if (process.env.NODE_ENV === 'development') { - links.push(ItemDescriptor.To(urls.example(), 'Example')) - } - if (customNavLink) links.push(customNavLink) - if (cfg.mode !== 'MARKETING') { - links.push(ItemDescriptor.To(urls.uriResolver(), 'URI')) - } - links.push(ItemDescriptor.Href(URLS.docs, 'Docs')) - if (cfg.mode === 'MARKETING' || cfg.mode === 'OPEN') { - links.push(ItemDescriptor.Href(URLS.jobs, 'Jobs')) - } - if (cfg.mode !== 'PRODUCT') links.push(ItemDescriptor.Href(URLS.blog, 'Blog')) - if (cfg.mode === 'MARKETING') links.push(ItemDescriptor.To(urls.about(), 'About')) - - return links + return [ + process.env.NODE_ENV === 'development' && + mkNavItem('to', urls.example(), 'Example', 'account_tree'), + customNavLink, + cfg.mode !== 'MARKETING' && mkNavItem('to', urls.uriResolver(), 'URI', 'public'), + mkNavItem('href', URLS.docs, 'Docs', 'menu_book'), + (cfg.mode === 'MARKETING' || cfg.mode === 'OPEN') && + mkNavItem('href', URLS.jobs, 'Jobs', 'work'), + cfg.mode !== 'PRODUCT' && mkNavItem('href', URLS.blog, 'Blog', 'chat'), + cfg.mode === 'MARKETING' && mkNavItem('to', urls.about(), 'About', 'help'), + ].filter(Boolean) as NavItem[] } const useBadgeStyles = M.makeStyles({ @@ -415,44 +419,54 @@ interface MobileMenuProps { function MobileMenu({ auth }: MobileMenuProps) { const { urls } = NamedRoutes.use() - const links = useLinks() + const navItems = useNavItems() const getAuthItems = useGetAuthItems() const bookmarks = Bookmarks.use() - const authItems = - cfg.disableNavigator || cfg.mode === 'LOCAL' - ? [] - : [ - ...AuthState.match<(ItemDescriptor | false)[]>( - { - Loading: () => [ - ItemDescriptor.Text( - } - primary="Loading..." - />, - ), - ], - Error: () => [ + const authItems = React.useMemo(() => { + if (cfg.disableNavigator || cfg.mode === 'LOCAL') return [] + return AuthState.match<(ItemDescriptor | false)[]>( + { + Loading: () => [ + ItemDescriptor.Text( + } primary="Loading..." />, + ), + ], + Error: () => [ + ItemDescriptor.To( + urls.signIn(), + , + ), + ], + Ready: ({ user }) => + user + ? getAuthItems(user) + : [ ItemDescriptor.To( urls.signIn(), - , + , ), ], - Ready: ({ user }) => - user - ? getAuthItems(user) - : [ - ItemDescriptor.To( - urls.signIn(), - , - ), - ], - }, - auth, - ), - ItemDescriptor.Divider(), - ] + }, + auth, + ).concat(ItemDescriptor.Divider()) + }, [auth, getAuthItems, urls]) + + const links = React.useMemo( + () => + navItems.map((n) => { + const children = + switch (n.kind) { + case 'to': + return ItemDescriptor.To(n.target, children) + case 'href': + return ItemDescriptor.Href(n.target, children) + default: + assertNever(n.kind) + } + }), + [navItems], + ) return ( // XXX: badge here or around the button? @@ -503,36 +517,44 @@ const useNavStyles = M.makeStyles((t) => ({ export function Links() { const classes = useNavStyles() const intercom = Intercom.use() - const links = useLinks() - - const mkTitle = (children: React.ReactNode) => - typeof children === 'string' && children.length > 10 ? children : undefined + const navItems = useNavItems() + + const mkTitle = (label: string) => (label.length > 10 ? label : undefined) + + const links = navItems.map((n, i) => { + switch (n.kind) { + case 'to': + return ( + + {n.label} + + ) + case 'href': + return ( +
+ {n.label} + + ) + default: + assertNever(n.kind) + } + }) return (