From ec31ac634e23822fe440b4bb5bc405e4b7760280 Mon Sep 17 00:00:00 2001 From: Mrkazik99 Date: Sat, 31 Aug 2024 13:18:25 +0200 Subject: [PATCH] feat: Initial user will be admin, Make console panel searchable --- idp/package-lock.json | 5 +- idp/package.json | 2 +- idp/src/account/service.ts | 18 ++- idp/src/infra/admin-requests.ts | 23 +++- idp/src/infra/error.ts | 9 +- idp/src/user/repo.ts | 20 ++- idp/src/user/router.ts | 17 +-- idp/src/user/service.ts | 91 +++++++++++-- ui/src/client/console/console.ts | 128 ++++-------------- ui/src/client/idp/user.ts | 10 +- ui/src/components/app-bar/app-bar-search.tsx | 115 +++++++++++++++- .../console/console-highlightable-tr.tsx | 9 ++ ui/src/components/layout/layout-console.tsx | 10 +- .../console-panel-database-indexes.tsx | 2 +- ui/src/pages/console/console-panel-groups.tsx | 25 +++- .../console/console-panel-invitations.tsx | 4 +- .../console/console-panel-organizations.tsx | 29 ++-- .../pages/console/console-panel-overview.tsx | 2 +- ui/src/pages/console/console-panel-users.tsx | 19 ++- .../console/console-panel-workspaces.tsx | 25 +++- 20 files changed, 376 insertions(+), 187 deletions(-) diff --git a/idp/package-lock.json b/idp/package-lock.json index c917ede73..d6dc65fd1 100644 --- a/idp/package-lock.json +++ b/idp/package-lock.json @@ -19,7 +19,7 @@ "hashids": "2.3.0", "jose": "5.3.0", "js-yaml": "4.1.0", - "meilisearch": "0.40.0", + "meilisearch": "^0.40.0", "mime-types": "2.1.35", "minio": "8.0.0", "morgan": "1.10.0", @@ -2618,7 +2618,8 @@ }, "node_modules/meilisearch": { "version": "0.40.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/meilisearch/-/meilisearch-0.40.0.tgz", + "integrity": "sha512-BoRhQMr2mBFLEeCfsvPluksGb01IaOiWvV3Deu3iEY+yYJ4jdGTu+IQi5FCjKlNQ7/TMWSN2XUToSgvH1tj0BQ==", "dependencies": { "cross-fetch": "^3.1.6" } diff --git a/idp/package.json b/idp/package.json index 868b28ca4..060b4c279 100644 --- a/idp/package.json +++ b/idp/package.json @@ -25,7 +25,7 @@ "hashids": "2.3.0", "jose": "5.3.0", "js-yaml": "4.1.0", - "meilisearch": "0.40.0", + "meilisearch": "^0.40.0", "mime-types": "2.1.35", "minio": "8.0.0", "morgan": "1.10.0", diff --git a/idp/src/account/service.ts b/idp/src/account/service.ts index b2f38692b..e6d7473cb 100644 --- a/idp/src/account/service.ts +++ b/idp/src/account/service.ts @@ -16,13 +16,14 @@ import { hashPassword } from '@/infra/password' import search, { USER_SEARCH_INDEX } from '@/infra/search' import { User } from '@/user/model' import userRepo from '@/user/repo' -import { UserDTO, mapEntity } from '@/user/service' +import {UserDTO, mapEntity, getUserCount} from '@/user/service' export type AccountCreateOptions = { email: string password: string fullName: string picture?: string + isAdmin?: boolean } export type AccountResetPasswordOptions = { @@ -53,6 +54,9 @@ export async function createUser( if (!(await userRepo.isUsernameAvailable(options.email))) { throw newError({ code: ErrorCode.UsernameUnavailable }) } + if (await getUserCount() === 0) { + options.isAdmin = true + } try { const emailConfirmationToken = newHyphenlessUuid() const user = await userRepo.insert({ @@ -64,6 +68,7 @@ export async function createUser( passwordHash: hashPassword(options.password), emailConfirmationToken, createTime: newDateTime(), + isAdmin: options.isAdmin, }) await search.index(USER_SEARCH_INDEX).addDocuments([ { @@ -72,9 +77,8 @@ export async function createUser( email: user.email, fullName: user.fullName, isEmailConfirmed: user.isEmailConfirmed, - isAdmin: user.isAdmin, - isActive: user.isActive, createTime: user.createTime, + updateTime: user.updateTime }, ]) await sendTemplateMail('email-confirmation', options.email, { @@ -106,8 +110,14 @@ export async function confirmEmail(options: AccountConfirmEmailOptions) { }) await search.index(USER_SEARCH_INDEX).updateDocuments([ { - ...user, + id: user.id, + username: user.username, + email: user.email, + fullName: user.fullName, isEmailConfirmed: user.isEmailConfirmed, + createTime: user.createTime, + updateTime: user.updateTime, + picture: user.picture, }, ]) } diff --git a/idp/src/infra/admin-requests.ts b/idp/src/infra/admin-requests.ts index 3f88923fd..d781cb00f 100755 --- a/idp/src/infra/admin-requests.ts +++ b/idp/src/infra/admin-requests.ts @@ -8,11 +8,18 @@ // by the GNU Affero General Public License v3.0 only, included in the file // licenses/AGPL.txt. import { Request } from 'express' +import {User} from "@/user/model"; export interface UserIdPostRequest extends Request { id: string } +export type SearchRequest = { + page: string + size: string + query: string +} + export interface UserSuspendPostRequest extends UserIdPostRequest { suspend: boolean } @@ -21,19 +28,21 @@ export interface UserAdminPostRequest extends UserIdPostRequest { makeAdmin: boolean } -export type Pagination = { - page: string - size: string -} - export type UserId = { id: string } -export interface PaginatedRequest extends Request { - query: Pagination +export interface SearchPaginatedRequest extends Request { + query: SearchRequest } export interface UserIdRequest extends Request { query: UserId } + +export interface UserSearchResponse { + data: User[] + page: number + size: number + totalElements: number +} \ No newline at end of file diff --git a/idp/src/infra/error.ts b/idp/src/infra/error.ts index a68744acf..2f99ddce1 100644 --- a/idp/src/infra/error.ts +++ b/idp/src/infra/error.ts @@ -25,7 +25,8 @@ export enum ErrorCode { UserSuspended = 'user_suspended', MissingPermission = 'missing_permission', UserNotFound = 'user_not_found', - OrphanError = 'orphan_error' + OrphanError = 'orphan_error', + SearchError = 'search_error' } const statuses: { [key: string]: number } = { @@ -44,7 +45,8 @@ const statuses: { [key: string]: number } = { [ErrorCode.UserSuspended]: 403, [ErrorCode.MissingPermission]: 403, [ErrorCode.UserNotFound]: 404, - [ErrorCode.OrphanError]: 400 + [ErrorCode.OrphanError]: 400, + [ErrorCode.SearchError]: 500 } const userMessages: { [key: string]: string } = { @@ -54,7 +56,8 @@ const userMessages: { [key: string]: string } = { [ErrorCode.InvalidUsernameOrPassword]: 'Invalid username or password.', [ErrorCode.UserSuspended]: 'User suspended.', [ErrorCode.MissingPermission]: 'You are not an console', - [ErrorCode.OrphanError]: 'You cannot suspend last console' + [ErrorCode.OrphanError]: 'You cannot suspend last console', + [ErrorCode.SearchError]: 'Search engine encountered error or is not available' } export type ErrorData = { diff --git a/idp/src/user/repo.ts b/idp/src/user/repo.ts index e60a5b77c..8d1414369 100644 --- a/idp/src/user/repo.ts +++ b/idp/src/user/repo.ts @@ -144,6 +144,24 @@ class UserRepoImpl { return this.mapList(rows) } + async listAllByIds(idList: number[]): Promise { + const { rowCount, rows } = await client.query( + `SELECT * + FROM "user" + WHERE id = ANY ($1) + ORDER BY create_time`, + [idList], + ) + if (rowCount < 1) { + throw newError({ + code: ErrorCode.ResourceNotFound, + error: `User list is empty`, + userMessage: `Not found users` + }) + } + return this.mapList(rows) + } + async getUserCount(): Promise { const { rowCount, rows } = await client.query( `SELECT COUNT(id) as count FROM "user"`, @@ -154,7 +172,7 @@ class UserRepoImpl { error: `Fatal database error (no users present in database)`, }) } - return rows[0].count + return parseInt(rows[0].count) } async isUsernameAvailable(username: string): Promise { diff --git a/idp/src/user/router.ts b/idp/src/user/router.ts index 3b5684ca2..5ec816d9d 100644 --- a/idp/src/user/router.ts +++ b/idp/src/user/router.ts @@ -14,7 +14,7 @@ import multer from 'multer' import os from 'os' import passport from 'passport' import { - PaginatedRequest, + SearchPaginatedRequest, UserAdminPostRequest, UserIdRequest, UserSuspendPostRequest @@ -36,8 +36,7 @@ import { UserUpdateEmailConfirmationOptions, updateEmailRequest, updateEmailConfirmation, - getUserListPaginated, - getUserCount, suspendUser, makeAdminUser, getUserByAdmin, + suspendUser, makeAdminUser, getUserByAdmin, searchUserListPaginated } from './service' const router = Router() @@ -222,18 +221,10 @@ router.get( router.get( '/all', passport.authenticate('jwt', { session: false }), - async (req: PaginatedRequest, res: Response, next: NextFunction) => { + async (req: SearchPaginatedRequest, res: Response, next: NextFunction) => { try { checkAdmin(req.header('Authorization')) - res.json({ - data: await getUserListPaginated( - parseInt(req.query.page), - parseInt(req.query.size), - ), - totalElements: await getUserCount(), - page: req.query.page, - size: req.query.size, - }) + res.json(await searchUserListPaginated(req.query.query, parseInt(req.query.size), parseInt(req.query.page))) } catch (err) { next(err) } diff --git a/idp/src/user/service.ts b/idp/src/user/service.ts index 360de8085..0f9f4140a 100644 --- a/idp/src/user/service.ts +++ b/idp/src/user/service.ts @@ -16,7 +16,7 @@ import {hashPassword, verifyPassword} from '@/infra/password' import search, {USER_SEARCH_INDEX} from '@/infra/search' import {User} from '@/user/model' import userRepo from '@/user/repo' -import {UserAdminPostRequest, UserSuspendPostRequest} from "@/infra/admin-requests"; +import {UserAdminPostRequest, UserSearchResponse, UserSuspendPostRequest} from "@/infra/admin-requests"; export type UserDTO = { id: string @@ -65,11 +65,19 @@ export async function getUserByAdmin(id: string): Promise { return adminMapEntity(await userRepo.findByID(id)) } -export async function getUserListPaginated( - page: number, - size: number, -): Promise { - return await userRepo.listAllPaginated(page, size) +export async function searchUserListPaginated(query: string, size: number, page: number +): Promise { + if (query && query.length >= 3) { + const users = await search.index(USER_SEARCH_INDEX).search(query, {page: page, hitsPerPage: size}).then((value) =>{ + return { + data: value.hits, + totalElements: value.totalHits + } + }) + return {data: await userRepo.listAllByIds(users.data.map((value) => {return value.id})), totalElements: users.totalElements, size: size, page: page} + } else { + return {data: await userRepo.listAllPaginated(page, size), totalElements: await userRepo.getUserCount(), size: size, page: page} + } } export async function getUserCount(): Promise { @@ -88,13 +96,23 @@ export async function updateFullName( user = await userRepo.update({ id: user.id, fullName: options.fullName }) await search.index(USER_SEARCH_INDEX).updateDocuments([ { - ...user, + id: user.id, + username: user.username, + email: user.email, fullName: user.fullName, + isEmailConfirmed: user.isEmailConfirmed, + createTime: user.createTime, + updateTime: user.updateTime, + picture: user.picture, }, ]) return mapEntity(user) } +export const raiseSearchError = () => { + throw newError({code: ErrorCode.SearchError}) +} + export async function updateEmailRequest( id: string, options: UserUpdateEmailRequestOptions, @@ -154,11 +172,14 @@ export async function updateEmailConfirmation( }) await search.index(USER_SEARCH_INDEX).updateDocuments([ { - ...user, + id: user.id, + username: user.username, email: user.email, - username: user.email, - emailUpdateToken: null, - emailUpdateValue: null, + fullName: user.fullName, + isEmailConfirmed: user.isEmailConfirmed, + createTime: user.createTime, + updateTime: user.updateTime, + picture: user.picture, }, ]) return mapEntity(user) @@ -191,12 +212,36 @@ export async function updatePicture( id: userId, picture: `data:${contentType};base64,${picture}`, }) + await search.index(USER_SEARCH_INDEX).updateDocuments([ + { + id: user.id, + username: user.username, + email: user.email, + fullName: user.fullName, + isEmailConfirmed: user.isEmailConfirmed, + createTime: user.createTime, + updateTime: user.updateTime, + picture: user.picture, + }, + ]) return mapEntity(user) } export async function deletePicture(id: string): Promise { let user = await userRepo.findByID(id) user = await userRepo.update({ id: user.id, picture: null }) + await search.index(USER_SEARCH_INDEX).updateDocuments([ + { + id: user.id, + username: user.username, + email: user.email, + fullName: user.fullName, + isEmailConfirmed: user.isEmailConfirmed, + createTime: user.createTime, + updateTime: user.updateTime, + picture: user.picture, + }, + ]) return mapEntity(user) } @@ -217,6 +262,18 @@ export async function suspendUser(options: UserSuspendPostRequest) { } if (user) { await userRepo.suspend(user.id, options.suspend) + await search.index(USER_SEARCH_INDEX).updateDocuments([ + { + id: user.id, + username: user.username, + email: user.email, + fullName: user.fullName, + isEmailConfirmed: user.isEmailConfirmed, + createTime: user.createTime, + updateTime: user.updateTime, + picture: user.picture, + }, + ]) } else { throw newError({ code: ErrorCode.UserNotFound }) } @@ -229,6 +286,18 @@ export async function makeAdminUser(options: UserAdminPostRequest) { } if (user) { await userRepo.makeAdmin(user.id, options.makeAdmin) + await search.index(USER_SEARCH_INDEX).updateDocuments([ + { + id: user.id, + username: user.username, + email: user.email, + fullName: user.fullName, + isEmailConfirmed: user.isEmailConfirmed, + createTime: user.createTime, + updateTime: user.updateTime, + picture: user.picture, + }, + ]) } else { throw newError({ code: ErrorCode.UserNotFound }) } diff --git a/ui/src/client/console/console.ts b/ui/src/client/console/console.ts index 027cae65a..f2af641f2 100644 --- a/ui/src/client/console/console.ts +++ b/ui/src/client/console/console.ts @@ -125,12 +125,14 @@ export interface UserOrganizationManagementList extends ListResponse { export type ListOptions = { id?: string + query?: string size?: number page?: number } type ListQueryParams = { id?: string + query?: string page?: string size?: string } @@ -159,44 +161,6 @@ export interface ComponentVersion { location: string } -export interface ComponentVersionList extends ListResponse { - data: ComponentVersion[] -} - -export interface DockerHubVersion { - creator: number - id: number - images: never - last_updated: Date - last_updater: number - last_updater_username: string - name: string - repository: number - full_size: number - v2: boolean - tag_status: string - tag_last_pulled: Date - tag_last_pushed: Date - media_type: string - content_type: string - digest: string -} - -export interface DockerHubVersionListResponse { - count: number - next: never - previous: never - results: DockerHubVersion[] -} - -export const emptyListResponseValue: emptyListResponse = { - totalPages: 0, - totalElements: 0, - page: 0, - size: 0, - data: [], -} - export default class ConsoleApi { static async checkIndexesAvailability() { const response = await fetch(`${getConfig().consoleURL}/index/all`, { @@ -329,72 +293,38 @@ export default class ConsoleApi { }) as Promise } - // static async getUiVersion() { - // const currentVersion = '2.1.0' - // return await baseFetcher( - // 'https://hub.docker.com/v2/repositories/voltaserve/ui/tags?page_size=50&page=1&ordering=last_updated&name=', - // {}, - // false, - // true, - // ) - // .then(async (resp) => { - // if (resp && resp.ok) { - // const tags: DockerHubVersionListResponse = await resp.json() - // // const latestDigest = tags.results.find((l) => { - // // l.name === 'latest' - // // }).digest - // if (tags.results) { - // const latestDigest = tags.results - // .filter((tag) => { - // tag.name === 'latest' - // }) - // .map((tag) => { - // return tag.digest - // })[0] - // const latestVersion = semver.maxSatisfying( - // tags.results - // .filter((tag) => { - // tag.name != 'latest' && tag.digest === latestDigest - // }) - // .map((tag) => { - // return tag.name - // }), - // '*', - // ) - // return { latestDigest: latestDigest, latestVersion: latestVersion } - // } - // } - // }) - // .then((value) => { - // if (value && value.latestVersion) { - // return { - // name: 'ui', - // currentVersion: currentVersion, - // location: `https://hub.docker.com/layers/voltaserve/$ui/${value.latestVersion}/images/${value.latestDigest}`, - // latestVersion: value.latestVersion, - // updateAvailable: semver.gt(value.latestVersion, currentVersion), - // } - // } else { - // return { - // name: 'ui', - // currentVersion: currentVersion, - // location: ``, - // latestVersion: '', - // updateAvailable: false, - // } - // } - // }) - // } - - static paramsFromListOptions = (options?: ListOptions): URLSearchParams => { + static async listObject(object: string, options: ListOptions) { + return consoleFetcher({ + url: `/${object}/all?${this.paramsFromListOptions(options)}`, + method: 'GET', + }) as Promise + } + + static async searchObject(object: string, options: ListOptions) { + return consoleFetcher({ + url: `/${object}/search?${this.paramsFromListOptions(options)}`, + method: 'GET', + }) as Promise + } + + static paramsFromListOptions = (options: ListOptions): URLSearchParams => { const params: ListQueryParams = {} - if (options?.id) { + if (options.id) { params.id = options.id.toString() } - if (options?.page) { + if (options.page) { + params.page = options.page.toString() + } + if (options.size) { + params.size = options.size.toString() + } + if (options.query) { + params.query = options.query.toString() + } + if (options.page) { params.page = options.page.toString() } - if (options?.size) { + if (options.size) { params.size = options.size.toString() } return new URLSearchParams(params) diff --git a/ui/src/client/idp/user.ts b/ui/src/client/idp/user.ts index de0e43bcc..cda7ca198 100644 --- a/ui/src/client/idp/user.ts +++ b/ui/src/client/idp/user.ts @@ -68,12 +68,14 @@ export type DeleteOptions = { } type ListOptions = { + query?: string id?: string size?: number page?: number } type ListQueryParams = { + query?: string id?: string page?: string size?: string @@ -179,17 +181,23 @@ export default class UserAPI { }) as Promise } - static paramsFromListOptions(options?: ListOptions): URLSearchParams { + static paramsFromListOptions(options: ListOptions): URLSearchParams { const params: ListQueryParams = {} if (options?.id) { params.id = options.id.toString() } + if (options.query) { + params.query = options.query.toString() + } if (options?.page) { params.page = options.page.toString() } if (options?.size) { params.size = options.size.toString() } + if (options?.query) { + params.query = options.query.toString() + } return new URLSearchParams(params) } } diff --git a/ui/src/components/app-bar/app-bar-search.tsx b/ui/src/components/app-bar/app-bar-search.tsx index e31f586bb..9a0858d47 100644 --- a/ui/src/components/app-bar/app-bar-search.tsx +++ b/ui/src/components/app-bar/app-bar-search.tsx @@ -34,7 +34,9 @@ import { encodeFileQuery, encodeQuery, } from '@/lib/helpers/query' +import store from '@/store/configure-store' import { useAppDispatch } from '@/store/hook' +import { errorOccurred } from '@/store/ui/error' import { modalDidOpen as searchFilterModalDidOpen } from '@/store/ui/search-filter' const AppBarSearch = () => { @@ -70,6 +72,30 @@ const AppBarSearch = () => { location.pathname.includes('/member'), [location], ) + const isConsoleUsers = useMemo( + () => + location.pathname.includes('/console/') && + location.pathname.includes('/users'), + [location], + ) + const isConsoleGroups = useMemo( + () => + location.pathname.includes('/console/') && + location.pathname.includes('/groups'), + [location], + ) + const isConsoleWorkspaces = useMemo( + () => + location.pathname.includes('/console/') && + location.pathname.includes('/workspaces'), + [location], + ) + const isConsoleOrganizations = useMemo( + () => + location.pathname.includes('/console/') && + location.pathname.includes('/organizations'), + [location], + ) const query: string | FileQuery | undefined = isFiles ? decodeFileQuery(searchParams.get('q') as string) : decodeQuery(searchParams.get('q') as string) @@ -85,8 +111,23 @@ const AppBarSearch = () => { isGroups || isOrgs || isOrgMembers || + isGroupMembers || + isConsoleUsers || + isConsoleGroups || + isConsoleOrganizations || + isConsoleWorkspaces, + [ + isWorkspaces, + isFiles, + isGroups, + isOrgs, + isOrgMembers, isGroupMembers, - [isWorkspaces, isFiles, isGroups, isOrgs, isOrgMembers, isGroupMembers], + isConsoleUsers, + isConsoleGroups, + isConsoleWorkspaces, + isConsoleOrganizations, + ], ) const hasFileQuery = useMemo(() => { const fileQuery = query as FileQuery @@ -101,20 +142,33 @@ const AppBarSearch = () => { : false }, [isFiles, query]) const placeholder = useMemo(() => { - if (isWorkspaces) { + if (isWorkspaces || isConsoleWorkspaces) { return 'Search Workspaces' } else if (isFiles) { return 'Search Files' - } else if (isGroups) { + } else if (isGroups || isConsoleGroups) { return 'Search Groups' - } else if (isOrgs) { + } else if (isOrgs || isConsoleOrganizations) { return 'Search Organizations' } else if (isOrgMembers) { return 'Search Organization Members' } else if (isGroupMembers) { return 'Search Group Members' + } else if (isConsoleUsers) { + return 'Search Users' } - }, [isWorkspaces, isFiles, isGroups, isOrgs, isOrgMembers, isGroupMembers]) + }, [ + isWorkspaces, + isFiles, + isGroups, + isOrgs, + isOrgMembers, + isGroupMembers, + isConsoleUsers, + isConsoleGroups, + isConsoleWorkspaces, + isConsoleOrganizations, + ]) const [buffer, setBuffer] = useState(parsedQuery) const [isFocused, setIsFocused] = useState(false) @@ -170,6 +224,30 @@ const AppBarSearch = () => { } else { navigate(`/group/${workspaceId}/member`) } + } else if (isConsoleUsers) { + if (value) { + navigate(`/console/users?q=${encodeQuery(value)}`) + } else { + navigate(`/console/users`) + } + } else if (isConsoleWorkspaces) { + if (value) { + navigate(`/console/workspaces?q=${encodeQuery(value)}`) + } else { + navigate(`/console/workspaces`) + } + } else if (isConsoleOrganizations) { + if (value) { + navigate(`/console/organizations?q=${encodeQuery(value)}`) + } else { + navigate(`/console/organizations`) + } + } else if (isConsoleGroups) { + if (value) { + navigate(`/console/groups?q=${encodeQuery(value)}`) + } else { + navigate(`/console/groups`) + } } }, [ @@ -181,6 +259,10 @@ const AppBarSearch = () => { isOrgs, isOrgMembers, isGroupMembers, + isConsoleUsers, + isConsoleOrganizations, + isConsoleGroups, + isConsoleWorkspaces, navigate, ], ) @@ -199,6 +281,14 @@ const AppBarSearch = () => { navigate(`/organization/${workspaceId}/member`) } else if (isGroupMembers) { navigate(`/group/${workspaceId}/member`) + } else if (isConsoleUsers) { + navigate(`/console/users`) + } else if (isConsoleOrganizations) { + navigate(`/console/organization`) + } else if (isConsoleGroups) { + navigate(`/console/groups`) + } else if (isConsoleWorkspaces) { + navigate(`/console/workspaces`) } }, [ workspaceId, @@ -214,8 +304,19 @@ const AppBarSearch = () => { const handleKeyDown = useCallback( (event: KeyboardEvent) => { - if (event.key === 'Enter') { - handleSearch(buffer) + if (event.key === 'Enter' && buffer.length >= 3) { + handleSearch(buffer.trim()) + } else if ( + event.key === 'Enter' && + buffer.trim().length > 0 && + buffer.length < 3 && + !isFiles + ) { + store.dispatch( + errorOccurred('Search query needs at least 3 characters'), + ) + } else if (event.key === 'Enter' && buffer.trim().length === 0) { + handleClear() } }, [buffer, handleSearch], diff --git a/ui/src/components/console/console-highlightable-tr.tsx b/ui/src/components/console/console-highlightable-tr.tsx index 30679f4a3..8eb096e85 100644 --- a/ui/src/components/console/console-highlightable-tr.tsx +++ b/ui/src/components/console/console-highlightable-tr.tsx @@ -1,3 +1,12 @@ +// Copyright 2024 Mateusz Kaźmierczak. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the GNU Affero General Public License v3.0 only, included in the file +// licenses/AGPL.txt. import { CSSProperties, MouseEvent, ReactNode } from 'react' import { Tr } from '@chakra-ui/react' import { useColorModeValue } from '@chakra-ui/system' diff --git a/ui/src/components/layout/layout-console.tsx b/ui/src/components/layout/layout-console.tsx index 99e87a078..679169001 100644 --- a/ui/src/components/layout/layout-console.tsx +++ b/ui/src/components/layout/layout-console.tsx @@ -69,31 +69,31 @@ const LayoutConsole = () => { { href: '/console/users', icon: , - primaryText: 'Users management', + primaryText: 'User management', secondaryText: 'Manage users of your cloud instance', }, { href: '/console/groups', icon: , - primaryText: 'Groups management', + primaryText: 'Group management', secondaryText: 'Manage groups of your cloud instance', }, { href: '/console/workspaces', icon: , - primaryText: 'Workspaces management', + primaryText: 'Workspace management', secondaryText: 'Manage workspaces of your cloud instance', }, { href: '/console/organizations', icon: , - primaryText: 'Organizations management', + primaryText: 'Organization management', secondaryText: 'Manage workspaces of your cloud instance', }, { href: '/console/invitations', icon: , - primaryText: 'Invitations management', + primaryText: 'Invitation management', secondaryText: 'Manage invitations of your cloud instance', }, { diff --git a/ui/src/pages/console/console-panel-database-indexes.tsx b/ui/src/pages/console/console-panel-database-indexes.tsx index 7f244423b..523127395 100644 --- a/ui/src/pages/console/console-panel-database-indexes.tsx +++ b/ui/src/pages/console/console-panel-database-indexes.tsx @@ -141,7 +141,7 @@ const ConsolePanelDatabaseIndexes = () => { - Indexes management + Index management
{list && list.data.length > 0 ? ( diff --git a/ui/src/pages/console/console-panel-groups.tsx b/ui/src/pages/console/console-panel-groups.tsx index 080ee670a..84fd2963d 100644 --- a/ui/src/pages/console/console-panel-groups.tsx +++ b/ui/src/pages/console/console-panel-groups.tsx @@ -8,7 +8,7 @@ // by the GNU Affero General Public License v3.0 only, included in the file // licenses/AGPL.txt. import { useEffect, useState } from 'react' -import { useLocation, useNavigate } from 'react-router-dom' +import { useLocation, useNavigate, useSearchParams } from 'react-router-dom' import { Button, Heading, @@ -29,11 +29,14 @@ import ConsoleRenameModal from '@/components/console/console-rename-modal' import { consoleGroupsPaginationStorage } from '@/infra/pagination' import PagePagination from '@/lib/components/page-pagination' import SectionSpinner from '@/lib/components/section-spinner' +import { decodeQuery } from '@/lib/helpers/query' import usePagePagination from '@/lib/hooks/page-pagination' const ConsolePanelGroups = () => { + const [searchParams] = useSearchParams() const navigate = useNavigate() const location = useLocation() + const query = decodeQuery(searchParams.get('q') as string) const [list, setList] = useState(undefined) const { page, size, steps, setPage, setSize } = usePagePagination({ navigate, @@ -76,10 +79,18 @@ const ConsolePanelGroups = () => { } useEffect(() => { - ConsoleApi.listGroups({ page: page, size: size }).then((value) => - setList(value), - ) - }, [page, size, isSubmitting]) + if (query && query.length >= 3) { + ConsoleApi.searchObject('group', { + page: page, + size: size, + query: query, + }).then((value) => setList(value)) + } else { + ConsoleApi.listGroups({ page: page, size: size }).then((value) => + setList(value), + ) + } + }, [page, size, isSubmitting, query]) if (!list) { return @@ -97,10 +108,10 @@ const ConsolePanelGroups = () => { request={renameGroup} /> - Groups management + Group management
- Groups management + Group management {list && list.data.length > 0 ? ( diff --git a/ui/src/pages/console/console-panel-invitations.tsx b/ui/src/pages/console/console-panel-invitations.tsx index aa0b834d2..e2b68c3d0 100644 --- a/ui/src/pages/console/console-panel-invitations.tsx +++ b/ui/src/pages/console/console-panel-invitations.tsx @@ -115,10 +115,10 @@ const ConsolePanelInvitations = () => { request={changeInvitationStatus} /> - Invitations management + Invitation management
- Invitations management + Invitation management {list && list.data.length > 0 ? (
diff --git a/ui/src/pages/console/console-panel-organizations.tsx b/ui/src/pages/console/console-panel-organizations.tsx index f402418dd..58d917f28 100644 --- a/ui/src/pages/console/console-panel-organizations.tsx +++ b/ui/src/pages/console/console-panel-organizations.tsx @@ -8,7 +8,7 @@ // by the GNU Affero General Public License v3.0 only, included in the file // licenses/AGPL.txt. import { useEffect, useState } from 'react' -import { useLocation, useNavigate } from 'react-router-dom' +import { useLocation, useNavigate, useSearchParams } from 'react-router-dom' import { Button, Heading, @@ -24,17 +24,22 @@ import { import * as Yup from 'yup' import cx from 'classnames' import { Helmet } from 'react-helmet-async' -import ConsoleApi, { OrganizationManagementList } from '@/client/console/console' +import ConsoleApi, { + OrganizationManagementList, +} from '@/client/console/console' import ConsoleHighlightableTr from '@/components/console/console-highlightable-tr' import ConsoleRenameModal from '@/components/console/console-rename-modal' import { consoleOrganizationsPaginationStorage } from '@/infra/pagination' import PagePagination from '@/lib/components/page-pagination' import SectionSpinner from '@/lib/components/section-spinner' +import { decodeQuery } from '@/lib/helpers/query' import usePagePagination from '@/lib/hooks/page-pagination' const ConsolePanelOrganizations = () => { + const [searchParams] = useSearchParams() const navigate = useNavigate() const location = useLocation() + const query = decodeQuery(searchParams.get('q') as string) const [list, setList] = useState( undefined, ) @@ -84,10 +89,18 @@ const ConsolePanelOrganizations = () => { } useEffect(() => { - ConsoleApi.listOrganizations({ page: page, size: size }).then((value) => - setList(value), - ) - }, [page, size, isSubmitting]) + if (query && query.length >= 3) { + ConsoleApi.searchObject('organization', { + page: page, + size: size, + query: query, + }).then((value) => setList(value)) + } else { + ConsoleApi.listOrganizations({ page: page, size: size }).then((value) => + setList(value), + ) + } + }, [page, size, isSubmitting, query]) if (!list) { return @@ -105,11 +118,11 @@ const ConsolePanelOrganizations = () => { request={renameOrganization} /> - Organizations management + Organization management
- Organizations management + Organization management {list && list.data.length > 0 ? ( diff --git a/ui/src/pages/console/console-panel-overview.tsx b/ui/src/pages/console/console-panel-overview.tsx index 477a2424e..2e3b0e408 100644 --- a/ui/src/pages/console/console-panel-overview.tsx +++ b/ui/src/pages/console/console-panel-overview.tsx @@ -101,7 +101,7 @@ const ConsolePanelOverview = () => { Console Panel
- Cloud Panel + Cloud Console diff --git a/ui/src/pages/console/console-panel-users.tsx b/ui/src/pages/console/console-panel-users.tsx index 06bb4509d..15541d643 100644 --- a/ui/src/pages/console/console-panel-users.tsx +++ b/ui/src/pages/console/console-panel-users.tsx @@ -8,7 +8,7 @@ // by the GNU Affero General Public License v3.0 only, included in the file // licenses/AGPL.txt. import { useEffect, useState } from 'react' -import { useLocation, useNavigate } from 'react-router-dom' +import { useLocation, useNavigate, useSearchParams } from 'react-router-dom' import { Badge, Button, @@ -37,9 +37,12 @@ import { getUserId } from '@/infra/token' import { IconChevronDown, IconChevronUp } from '@/lib/components/icons' import PagePagination from '@/lib/components/page-pagination' import SectionSpinner from '@/lib/components/section-spinner' +import { decodeQuery } from '@/lib/helpers/query' import usePagePagination from '@/lib/hooks/page-pagination' const ConsolePanelUsers = () => { + const [searchParams] = useSearchParams() + const query = decodeQuery(searchParams.get('q') as string) const navigate = useNavigate() const location = useLocation() const [list, setList] = useState(undefined) @@ -111,10 +114,12 @@ const ConsolePanelUsers = () => { } useEffect(() => { - UserAPI.getAllUsers({ page: page, size: size }).then((value) => { - setList(value) - }) - }, [page, size, isSubmitting]) + UserAPI.getAllUsers({ page: page, size: size, query: query }).then( + (value) => { + setList(value) + }, + ) + }, [page, size, isSubmitting, query]) if (!list) { return @@ -139,10 +144,10 @@ const ConsolePanelUsers = () => { request={makeAdminUser} /> - Users management + User management
- Users management + User management {list && list.data.length > 0 ? (
diff --git a/ui/src/pages/console/console-panel-workspaces.tsx b/ui/src/pages/console/console-panel-workspaces.tsx index 54537d155..8a1668b50 100644 --- a/ui/src/pages/console/console-panel-workspaces.tsx +++ b/ui/src/pages/console/console-panel-workspaces.tsx @@ -8,7 +8,7 @@ // by the GNU Affero General Public License v3.0 only, included in the file // licenses/AGPL.txt. import { useEffect, useState } from 'react' -import { useLocation, useNavigate } from 'react-router-dom' +import { useLocation, useNavigate, useSearchParams } from 'react-router-dom' import { Button, Heading, @@ -35,11 +35,14 @@ import { IconChevronDown, IconChevronUp } from '@/lib/components/icons' import PagePagination from '@/lib/components/page-pagination' import SectionSpinner from '@/lib/components/section-spinner' import prettyBytes from '@/lib/helpers/pretty-bytes' +import { decodeQuery } from '@/lib/helpers/query' import usePagePagination from '@/lib/hooks/page-pagination' const ConsolePanelWorkspaces = () => { + const [searchParams] = useSearchParams() const navigate = useNavigate() const location = useLocation() + const query = decodeQuery(searchParams.get('q') as string) const [list, setList] = useState( undefined, ) @@ -87,10 +90,18 @@ const ConsolePanelWorkspaces = () => { } useEffect(() => { - ConsoleApi.listWorkspaces({ page: page, size: size }).then((value) => - setList(value), - ) - }, [page, size, isSubmitting]) + if (query && query.length >= 3) { + ConsoleApi.searchObject('workspace', { + page: page, + size: size, + query: query, + }).then((value) => setList(value)) + } else { + ConsoleApi.listWorkspaces({ page: page, size: size, query: query }).then( + (value) => setList(value), + ) + } + }, [page, size, isSubmitting, query]) if (!list) { return @@ -108,10 +119,10 @@ const ConsolePanelWorkspaces = () => { request={renameWorkspace} /> - Workspaces management + Workspace management
- Workspaces management + Workspace management {list && list.data.length > 0 ? (