From c600db731099665c590cb0c47369530020f5a4c2 Mon Sep 17 00:00:00 2001 From: Sameer Poswal <106386145+poswalsameer@users.noreply.github.com> Date: Tue, 17 Dec 2024 22:04:10 +0530 Subject: [PATCH] chore(platoform): Swapped all legacy API calls with `@keyshade/api-client` (#584) Co-authored-by: rajdip-b --- apps/platform/.eslintrc.cjs | 11 +- apps/platform/Dockerfile | 4 +- apps/platform/src/app/(main)/page.tsx | 28 +- .../(main)/project/[project]/@secret/page.tsx | 63 +- .../app/(main)/project/[project]/layout.tsx | 24 +- .../src/app/(main)/settings/@profile/page.tsx | 79 +-- .../src/components/shared/navbar/index.tsx | 2 +- .../src/components/shared/sidebar/index.tsx | 2 +- apps/platform/src/components/ui/combobox.tsx | 105 ++- apps/platform/src/lib/api-client.ts | 93 --- apps/platform/src/lib/controller-instance.ts | 84 ++- apps/platform/src/lib/workspace-storage.ts | 1 - erDetails(userData) | 664 ++++++++++++++++++ packages/api-client/src/index.ts | 4 +- packages/schema/src/secret/index.ts | 20 +- packages/schema/tests/secret.spec.ts | 21 +- t updateSelf = useCallback(async () => { | 664 ++++++++++++++++++ ...ccess('User details updated successfully') | 258 +++++++ 18 files changed, 1833 insertions(+), 294 deletions(-) delete mode 100644 apps/platform/src/lib/api-client.ts create mode 100644 erDetails(userData) create mode 100644 t updateSelf = useCallback(async () => { create mode 100644 t.success('User details updated successfully') diff --git a/apps/platform/.eslintrc.cjs b/apps/platform/.eslintrc.cjs index 28a8b27a..e67976a0 100644 --- a/apps/platform/.eslintrc.cjs +++ b/apps/platform/.eslintrc.cjs @@ -7,24 +7,17 @@ module.exports = { }, rules: { 'import/no-extraneous-dependencies': 0, - '@typescript-eslint/interface-name-prefix': 'off', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-unused-vars': ['warn'], - '@typescript-eslint/no-unsafe-call': 'off', - '@typescript-eslint/no-unsafe-return': 'off', '@typescript-eslint/space-before-function-paren': 'off', '@typescript-eslint/strict-boolean-expressions': 'off', - '@typescript-eslint/prefer-nullish-coalescing': 'off', - 'space-before-function-paren': 'off', - '@typescript-eslint/member-delimiter-style': 'off', '@typescript-eslint/no-confusing-void-expression': 'off', '@typescript-eslint/no-floating-promises': 'off', '@typescript-eslint/no-misused-promises': 'off', '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-call': 'off', '@typescript-eslint/no-unsafe-member-access': 'off', - '@typescript-eslint/no-unsafe-argument': 'off', - '@typescript-eslint/no-unnecessary-condition': 'off' + '@typescript-eslint/no-unsafe-return': 'off' } } diff --git a/apps/platform/Dockerfile b/apps/platform/Dockerfile index 4dee7895..69f9c239 100644 --- a/apps/platform/Dockerfile +++ b/apps/platform/Dockerfile @@ -36,13 +36,11 @@ RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs USER nextjs -COPY --from=installer /app/apps/platform/next.config.mjs . COPY --from=installer /app/apps/platform/package.json . # Automatically leverage output traces to reduce image size # https://nextjs.org/docs/advanced-features/output-file-tracing -COPY --from=installer --chown=nextjs:nodejs /app/apps/platform/.next/standalone ./ -COPY --from=installer --chown=nextjs:nodejs /app/apps/platform/.next/static ./apps/platform/.next/static +COPY --from=installer --chown=nextjs:nodejs /app/apps/platform/.next ./apps/platform/.next COPY --from=installer --chown=nextjs:nodejs /app/apps/platform/public ./apps/platform/public diff --git a/apps/platform/src/app/(main)/page.tsx b/apps/platform/src/app/(main)/page.tsx index fe8fa2aa..9a9cb2ac 100644 --- a/apps/platform/src/app/(main)/page.tsx +++ b/apps/platform/src/app/(main)/page.tsx @@ -6,7 +6,6 @@ import type { ProjectWithCount, Workspace } from '@keyshade/schema' -import { ProjectController } from '@keyshade/api-client' import { AddSVG } from '@public/svg/shared' import { FolderSVG } from '@public/svg/dashboard' import ProjectCard from '@/components/dashboard/projectCard' @@ -38,6 +37,7 @@ import { DialogHeader, DialogTrigger } from '@/components/ui/dialog' +import ControllerInstance from '@/lib/controller-instance' export default function Index(): JSX.Element { const [isSheetOpen, setIsSheetOpen] = useState(false) @@ -76,16 +76,13 @@ export default function Index(): JSX.Element { // If a workspace is selected, we want to fetch all the projects // under that workspace and display it in the dashboard. useEffect(() => { - const projectController = new ProjectController( - process.env.NEXT_PUBLIC_BACKEND_URL - ) - async function getAllProjects() { if (currentWorkspace) { - const { success, error, data } = await projectController.getAllProjects( - { workspaceSlug: currentWorkspace.slug }, - {} - ) + const { success, error, data } = + await ControllerInstance.getInstance().projectController.getAllProjects( + { workspaceSlug: currentWorkspace.slug }, + {} + ) if (success && data) { setProjects(data.items) @@ -105,16 +102,13 @@ export default function Index(): JSX.Element { // Function to create a new project const createNewProject = useCallback(async () => { if (currentWorkspace) { - const projectController = new ProjectController( - process.env.NEXT_PUBLIC_BACKEND_URL - ) - newProjectData.workspaceSlug = currentWorkspace.slug - const { data, error, success } = await projectController.createProject( - newProjectData, - {} - ) + const { data, error, success } = + await ControllerInstance.getInstance().projectController.createProject( + newProjectData, + {} + ) if (success && data) { setProjects([ diff --git a/apps/platform/src/app/(main)/project/[project]/@secret/page.tsx b/apps/platform/src/app/(main)/project/[project]/@secret/page.tsx index 5061bf8e..e1bd8e63 100644 --- a/apps/platform/src/app/(main)/project/[project]/@secret/page.tsx +++ b/apps/platform/src/app/(main)/project/[project]/@secret/page.tsx @@ -5,6 +5,7 @@ import { usePathname } from 'next/navigation' import dayjs, { extend } from 'dayjs' import relativeTime from 'dayjs/plugin/relativeTime' import { NoteIconSVG } from '@public/svg/secret' +import type { GetAllSecretsOfProjectResponse } from '@keyshade/schema' import { Accordion, AccordionContent, @@ -19,8 +20,6 @@ import { TableHeader, TableRow } from '@/components/ui/table' -import { Secrets } from '@/lib/api-functions/secrets' -import type { Secret } from '@/types' import { ScrollArea } from '@/components/ui/scroll-area' import { Tooltip, @@ -29,35 +28,45 @@ import { TooltipTrigger } from '@/components/ui/tooltip' import { Skeleton } from '@/components/ui/skeleton' +import ControllerInstance from '@/lib/controller-instance' extend(relativeTime) function SecretPage(): React.JSX.Element { - const [allSecrets, setAllSecrets] = useState() + const [allSecrets, setAllSecrets] = + useState() const [isLoading, setIsLoading] = useState(true) const pathname = usePathname() useEffect(() => { setIsLoading(true) - Secrets.getAllSecretbyProjectId(pathname.split('/')[2]) - .then((data) => { - setAllSecrets(data) - }) - .catch((error) => { + + async function getAllSecretsByProjectSlug() { + const { success, error, data } = + await ControllerInstance.getInstance().secretController.getAllSecretsOfProject( + { projectSlug: pathname.split('/')[2] }, + {} + ) + + if (success && data) { + setAllSecrets(data.items) + } else { // eslint-disable-next-line no-console -- we need to log the error console.error(error) - }) - .finally(() => { - setIsLoading(false) - }) + } + } + + getAllSecretsByProjectSlug() + + setIsLoading(false) }, [pathname]) if (isLoading) { return (
- - - + + +
) } @@ -69,37 +78,35 @@ function SecretPage(): React.JSX.Element { collapsible type="single" > - {allSecrets?.map((secret) => { + {allSecrets?.map(({ secret, values }) => { return ( - {dayjs(secret.secret.updatedAt).toNow(true)} ago by{' '} - - {secret.secret.lastUpdatedBy.name} - + {dayjs(secret.updatedAt).toNow(true)} ago by{' '} + {secret.lastUpdatedById} } >
{/* */} - {secret.secret.name} + {secret.name}
- {secret.secret.note ? ( + {secret.note ? ( -

{secret.secret.note}

+

{secret.note}

@@ -115,10 +122,10 @@ function SecretPage(): React.JSX.Element { - {secret.values.map((value) => { + {values.map((value) => { return ( - {value.environment.name} + {value.environment.slug} {value.value} @@ -136,7 +143,7 @@ function SecretPage(): React.JSX.Element { ) } -function SerectLoader(): React.JSX.Element { +function SecretLoader(): React.JSX.Element { return (
diff --git a/apps/platform/src/app/(main)/project/[project]/layout.tsx b/apps/platform/src/app/(main)/project/[project]/layout.tsx index 8a747ac4..9edfb0bb 100644 --- a/apps/platform/src/app/(main)/project/[project]/layout.tsx +++ b/apps/platform/src/app/(main)/project/[project]/layout.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react' import { useSearchParams } from 'next/navigation' import { AddSVG } from '@public/svg/shared' +import type { Project } from '@keyshade/schema' import { Button } from '@/components/ui/button' import { Dialog, @@ -13,8 +14,7 @@ import { } from '@/components/ui/dialog' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' -import type { Project } from '@/types' -import { Projects } from '@/lib/api-functions/projects' +import ControllerInstance from '@/lib/controller-instance' interface DetailedProjectPageProps { params: { project: string } @@ -38,14 +38,22 @@ function DetailedProjectPage({ const tab = searchParams.get('tab') ?? 'rollup-details' useEffect(() => { - Projects.getProjectbyID(params.project) - .then((project) => { - setCurrentProject(project) - }) - .catch((error) => { + async function getProjectBySlug() { + const { success, error, data } = + await ControllerInstance.getInstance().projectController.getProject( + { projectSlug: params.project }, + {} + ) + + if (success && data) { + setCurrentProject(data) + } else { // eslint-disable-next-line no-console -- we need to log the error console.error(error) - }) + } + } + + getProjectBySlug() }, [params.project]) return ( diff --git a/apps/platform/src/app/(main)/settings/@profile/page.tsx b/apps/platform/src/app/(main)/settings/@profile/page.tsx index 21de60b3..ac8060c0 100644 --- a/apps/platform/src/app/(main)/settings/@profile/page.tsx +++ b/apps/platform/src/app/(main)/settings/@profile/page.tsx @@ -1,54 +1,52 @@ 'use client' -import React, { useEffect, useState } from 'react' +import React, { useCallback, useEffect, useState } from 'react' import { toast } from 'sonner' -import type { User } from '@keyshade/schema' import InputLoading from './loading' import { Input } from '@/components/ui/input' import { Separator } from '@/components/ui/separator' +import ControllerInstance from '@/lib/controller-instance' import { Button } from '@/components/ui/button' -import { apiClient } from '@/lib/api-client' - -type UserData = Omit< - User, - 'id' | 'isActive' | 'isOnboardingFinished' | 'isAdmin' | 'authProvider' -> -async function getUserDetails(): Promise { - try { - return await apiClient.get('/user') - } catch (error) { - // eslint-disable-next-line no-console -- we need to log the error - console.error(error) - } -} - -async function updateUserDetails(userData: UserData): Promise { - try { - await apiClient.put('/user', userData) - } catch (error) { - // eslint-disable-next-line no-console -- we need to log the error - console.error(error) - } -} function ProfilePage(): React.JSX.Element { const [isLoading, setIsLoading] = useState(true) - const [userData, setUserData] = useState({ + const [userData, setUserData] = useState({ email: '', name: '', profilePictureUrl: '' }) const [isModified, setIsModified] = useState(false) + const updateSelf = useCallback(async () => { + try { + await ControllerInstance.getInstance().userController.updateSelf( + { + name: userData.name, + email: userData.email + }, + {} + ) + toast.success('Profile updated successfully') + } catch (error) { + // eslint-disable-next-line no-console -- we need to log the error + console.error(error) + } + setIsModified(false) + }, [userData]) + useEffect(() => { - getUserDetails() - .then((data) => { - if (data) { + ControllerInstance.getInstance() + .userController.getSelf() + .then(({ data, success, error }) => { + if (success && data) { setUserData({ email: data.email, - name: data.name ?? '', - profilePictureUrl: data.profilePictureUrl + name: data.name, + profilePictureUrl: data.profilePictureUrl || '' }) setIsLoading(false) + } else { + // eslint-disable-next-line no-console -- we need to log the error + console.error(error) } }) .catch((error) => { @@ -67,7 +65,7 @@ function ProfilePage(): React.JSX.Element { Upload a picture to change your avatar across Keyshade.
-
{' '} +
{/* //! This is will be replaced by an image tag */}
{/* Name */} @@ -87,7 +85,7 @@ function ProfilePage(): React.JSX.Element { setUserData((prev) => ({ ...prev, name: e.target.value })) }} placeholder="name" - value={userData.name ?? ''} + value={userData.name || ''} /> )}
@@ -114,20 +112,7 @@ function ProfilePage(): React.JSX.Element { )}
-
diff --git a/apps/platform/src/components/shared/navbar/index.tsx b/apps/platform/src/components/shared/navbar/index.tsx index 693aa8bd..224e0ed7 100644 --- a/apps/platform/src/components/shared/navbar/index.tsx +++ b/apps/platform/src/components/shared/navbar/index.tsx @@ -34,7 +34,7 @@ async function fetchNameImage(): Promise { ) const data: User = (await response.json()) as User return { - name: data.name?.split(' ')[0] ?? data.email.split('@')[0], + name: data.name.split(' ')[0] ?? data.email.split('@')[0], image: data.profilePictureUrl } } catch (error) { diff --git a/apps/platform/src/components/shared/sidebar/index.tsx b/apps/platform/src/components/shared/sidebar/index.tsx index ee7cd9b4..7da768b3 100644 --- a/apps/platform/src/components/shared/sidebar/index.tsx +++ b/apps/platform/src/components/shared/sidebar/index.tsx @@ -6,8 +6,8 @@ import { SettingsSVG, TeamSVG } from '@public/svg/shared' -import { Combobox } from '@/components/ui/combobox' import SidebarTab from './sidebarTab' +import { Combobox } from '@/components/ui/combobox' function Sidebar(): JSX.Element { const sidebarTabData = [ diff --git a/apps/platform/src/components/ui/combobox.tsx b/apps/platform/src/components/ui/combobox.tsx index b3107dc1..b252056c 100644 --- a/apps/platform/src/components/ui/combobox.tsx +++ b/apps/platform/src/components/ui/combobox.tsx @@ -5,27 +5,6 @@ import { useEffect, useState } from 'react' import { useRouter } from 'next/navigation' import { toast } from 'sonner' import { AddSVG } from '@public/svg/shared' -import { cn } from '@/lib/utils' -import { - Command, - CommandEmpty, - CommandInput, - CommandItem, - CommandList -} from '@/components/ui/command' -import { - Popover, - PopoverContent, - PopoverTrigger -} from '@/components/ui/popover' -import { apiClient } from '@/lib/api-client' -// import type { Workspace } from '@/types' -import { zWorkspace } from '@/types' -import { - getCurrentWorkspace, - setCurrentWorkspace, - setWorkspace -} from '@/lib/workspace-storage' import type { Workspace } from '@keyshade/schema' import { Input } from './input' import { Label } from './label' @@ -38,39 +17,43 @@ import { DialogTrigger } from './dialog' import { Button } from './button' -import { WorkspaceSchema } from '@keyshade/schema/schemas' - -interface WorkspaceResponse { - items: Workspace[] - metadata: { - page: number - perPage: number - pageCount: number - totalCount: number - links: { - self: string - first: string - previous: string | null - next: string | null - last: string - } - } -} +import { + getCurrentWorkspace, + setCurrentWorkspace, + setWorkspace +} from '@/lib/workspace-storage' +import { cn } from '@/lib/utils' +import { + Popover, + PopoverContent, + PopoverTrigger +} from '@/components/ui/popover' +import { + Command, + CommandEmpty, + CommandInput, + CommandItem, + CommandList +} from '@/components/ui/command' +import ControllerInstance from '@/lib/controller-instance' async function getAllWorkspace(): Promise { try { - const workspaceData: WorkspaceResponse = - await apiClient.get('/workspace') + const { data, success, error } = + await ControllerInstance.getInstance().workspaceController.getWorkspacesOfUser( + {}, + {} + ) - // TODO: We are getting error here from the success flag, need to see this again - // const { success, data } = WorkspaceSchema.array().safeParse(workspaceData.items) - // if (!success) { - // throw new Error('Invalid data') - // } + if (error) { + // eslint-disable-next-line no-console -- we need to log the error + console.error(error) + return undefined + } - return workspaceData.items - // return data - // return workspaceData; + if (success && data) { + return data.items + } } catch (error) { // eslint-disable-next-line no-console -- we need to log the error console.error(error) @@ -92,11 +75,25 @@ export function Combobox(): React.JSX.Element { } setIsNameEmpty(false) try { - const response = await apiClient.post('/workspace', { - name - }) - setCurrentWorkspace(response) - setOpen(false) + const { data, error, success } = + await ControllerInstance.getInstance().workspaceController.createWorkspace( + { + name + }, + {} + ) + + if (error) { + // eslint-disable-next-line no-console -- we need to log the error + console.error(error) + return + } + + if (success && data) { + toast.success('Workspace created successfully') + setCurrentWorkspace(data) + setOpen(false) + } } catch (error) { // eslint-disable-next-line no-console -- we need to log the error console.error(error) diff --git a/apps/platform/src/lib/api-client.ts b/apps/platform/src/lib/api-client.ts deleted file mode 100644 index 410f1864..00000000 --- a/apps/platform/src/lib/api-client.ts +++ /dev/null @@ -1,93 +0,0 @@ -interface ErrorWithResponse extends Error { - status: number - response: Record -} - -class APIClient { - private baseUrl: string - constructor(baseUrl: string) { - this.baseUrl = baseUrl - } - - async request(url: string, options: RequestInit): Promise { - const response = await fetch(`${this.baseUrl}${url}`, options) - if (!response.ok) { - const error = new Error(response.statusText) as ErrorWithResponse - error.status = response.status - error.response = (await response.json()) as Record // Add type annotation here - throw error - } - return response.json() as Promise - } - - /** - * Sends a GET request to the specified URL and returns a Promise that resolves to the response data. - * @param url - The URL to send the GET request to. - * @returns A Promise that resolves to the response data. - */ - get(url: string): Promise { - return this.request(url, { - method: 'GET', - headers: { - 'Content-Type': 'application/json' - }, - credentials: 'include' - }) - } - - /** - * Sends a POST request to the specified URL with the provided data. - * - * @param url - The URL to send the request to. - * @param data - The data to send in the request body. - * @returns A Promise that resolves to the response data. - */ - post(url: string, data: Record): Promise { - return this.request(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(data), - credentials: 'include' - }) - } - - /** - * Sends a PUT request to the specified URL with the provided data. - * - * @param url - The URL to send the request to. - * @param data - The data to be sent in the request body. - * @returns A Promise that resolves to the response data. - */ - put(url: string, data: Record): Promise { - return this.request(url, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(data), - credentials: 'include' - }) - } - - /** - * Sends a DELETE request to the specified URL and returns a Promise that resolves to the response data. - * - * @param url - The URL to send the DELETE request to. - * @returns A Promise that resolves to the response data. - */ - delete(url: string): Promise { - return this.request(url, { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json' - }, - credentials: 'include' - }) - } -} - -export const apiClient = new APIClient( - `${process.env.NEXT_PUBLIC_BACKEND_URL}/api` -) diff --git a/apps/platform/src/lib/controller-instance.ts b/apps/platform/src/lib/controller-instance.ts index fe432b7b..a766038f 100644 --- a/apps/platform/src/lib/controller-instance.ts +++ b/apps/platform/src/lib/controller-instance.ts @@ -1,30 +1,90 @@ -import { AuthController } from '@keyshade/api-client' +import { + AuthController, + EnvironmentController, + ProjectController, + SecretController, + UserController, + VariableController, + WorkspaceController, + WorkspaceMembershipController, + WorkspaceRoleController +} from '@keyshade/api-client' export default class ControllerInstance { private static instance: ControllerInstance | null - private _authController: AuthController | null = null + private _authController: AuthController + private _userController: UserController + private _workspaceController: WorkspaceController + private _workspaceMembershipController: WorkspaceMembershipController + private _workspaceRoleController: WorkspaceRoleController + private _projectController: ProjectController + private _environmentController: EnvironmentController + private _secretController: SecretController + private _variableController: VariableController get authController(): AuthController { - if (!this._authController) { - throw new Error('ControllerInstance not initialized') - } return this._authController } - static initialize(baseUrl: string): void { - if (!ControllerInstance.instance) { - const instance = new ControllerInstance() + get workspaceController(): WorkspaceController { + return this._workspaceController + } + + get workspaceMembershipController(): WorkspaceMembershipController { + return this._workspaceMembershipController + } - instance._authController = new AuthController(baseUrl) + get workspaceRoleController(): WorkspaceRoleController { + return this._workspaceRoleController + } - ControllerInstance.instance = instance - } + get projectController(): ProjectController { + return this._projectController + } + + get environmentController(): EnvironmentController { + return this._environmentController + } + + get secretController(): SecretController { + return this._secretController + } + + get variableController(): VariableController { + return this._variableController + } + + get userController(): UserController { + return this._userController } static getInstance(): ControllerInstance { if (!ControllerInstance.instance) { - throw new Error('ControllerInstance not initialized') + ControllerInstance.instance = new ControllerInstance() + ControllerInstance.instance._authController = new AuthController( + process.env.NEXT_PUBLIC_BACKEND_URL + ) + ControllerInstance.instance._userController = new UserController( + process.env.NEXT_PUBLIC_BACKEND_URL + ) + ControllerInstance.instance._workspaceController = + new WorkspaceController(process.env.NEXT_PUBLIC_BACKEND_URL) + ControllerInstance.instance._workspaceMembershipController = + new WorkspaceMembershipController(process.env.NEXT_PUBLIC_BACKEND_URL) + ControllerInstance.instance._workspaceRoleController = + new WorkspaceRoleController(process.env.NEXT_PUBLIC_BACKEND_URL) + ControllerInstance.instance._projectController = new ProjectController( + process.env.NEXT_PUBLIC_BACKEND_URL + ) + ControllerInstance.instance._environmentController = + new EnvironmentController(process.env.NEXT_PUBLIC_BACKEND_URL) + ControllerInstance.instance._secretController = new SecretController( + process.env.NEXT_PUBLIC_BACKEND_URL + ) + ControllerInstance.instance._variableController = new VariableController( + process.env.NEXT_PUBLIC_BACKEND_URL + ) } return ControllerInstance.instance } diff --git a/apps/platform/src/lib/workspace-storage.ts b/apps/platform/src/lib/workspace-storage.ts index 9d09dafb..7da1685e 100644 --- a/apps/platform/src/lib/workspace-storage.ts +++ b/apps/platform/src/lib/workspace-storage.ts @@ -1,4 +1,3 @@ -// import type { Workspace } from '@/types' import type { Workspace } from '@keyshade/schema' export function setWorkspace(workspaceData: Workspace[]): void { diff --git a/erDetails(userData) b/erDetails(userData) new file mode 100644 index 00000000..95c798b9 --- /dev/null +++ b/erDetails(userData) @@ -0,0 +1,664 @@ +diff --git a/apps/platform/.eslintrc.cjs b/apps/platform/.eslintrc.cjs +index 28a8b27..fa9233a 100644 +--- a/apps/platform/.eslintrc.cjs ++++ b/apps/platform/.eslintrc.cjs +@@ -7,24 +7,13 @@ module.exports = { + }, + rules: { + 'import/no-extraneous-dependencies': 0, +- '@typescript-eslint/interface-name-prefix': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', +- '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unused-vars': ['warn'], +- '@typescript-eslint/no-unsafe-call': 'off', +- '@typescript-eslint/no-unsafe-return': 'off', + '@typescript-eslint/space-before-function-paren': 'off', + '@typescript-eslint/strict-boolean-expressions': 'off', +- '@typescript-eslint/prefer-nullish-coalescing': 'off', +- 'space-before-function-paren': 'off', +- '@typescript-eslint/member-delimiter-style': 'off', + '@typescript-eslint/no-confusing-void-expression': 'off', + '@typescript-eslint/no-floating-promises': 'off', +- '@typescript-eslint/no-misused-promises': 'off', +- '@typescript-eslint/no-unsafe-assignment': 'off', +- '@typescript-eslint/no-unsafe-member-access': 'off', +- '@typescript-eslint/no-unsafe-argument': 'off', +- '@typescript-eslint/no-unnecessary-condition': 'off' ++ '@typescript-eslint/no-misused-promises': 'off' + } + } +diff --git a/apps/platform/src/app/(main)/page.tsx b/apps/platform/src/app/(main)/page.tsx +index fe8fa2a..9a9cb2a 100644 +--- a/apps/platform/src/app/(main)/page.tsx ++++ b/apps/platform/src/app/(main)/page.tsx +@@ -6,7 +6,6 @@ import type { + ProjectWithCount, + Workspace + } from '@keyshade/schema' +-import { ProjectController } from '@keyshade/api-client' + import { AddSVG } from '@public/svg/shared' + import { FolderSVG } from '@public/svg/dashboard' + import ProjectCard from '@/components/dashboard/projectCard' +@@ -38,6 +37,7 @@ import { + DialogHeader, + DialogTrigger + } from '@/components/ui/dialog' ++import ControllerInstance from '@/lib/controller-instance' +  + export default function Index(): JSX.Element { + const [isSheetOpen, setIsSheetOpen] = useState(false) +@@ -76,16 +76,13 @@ export default function Index(): JSX.Element { + // If a workspace is selected, we want to fetch all the projects + // under that workspace and display it in the dashboard. + useEffect(() => { +- const projectController = new ProjectController( +- process.env.NEXT_PUBLIC_BACKEND_URL +- ) +- + async function getAllProjects() { + if (currentWorkspace) { +- const { success, error, data } = await projectController.getAllProjects( +- { workspaceSlug: currentWorkspace.slug }, +- {} +- ) ++ const { success, error, data } = ++ await ControllerInstance.getInstance().projectController.getAllProjects( ++ { workspaceSlug: currentWorkspace.slug }, ++ {} ++ ) +  + if (success && data) { + setProjects(data.items) +@@ -105,16 +102,13 @@ export default function Index(): JSX.Element { + // Function to create a new project + const createNewProject = useCallback(async () => { + if (currentWorkspace) { +- const projectController = new ProjectController( +- process.env.NEXT_PUBLIC_BACKEND_URL +- ) +- + newProjectData.workspaceSlug = currentWorkspace.slug +  +- const { data, error, success } = await projectController.createProject( +- newProjectData, +- {} +- ) ++ const { data, error, success } = ++ await ControllerInstance.getInstance().projectController.createProject( ++ newProjectData, ++ {} ++ ) +  + if (success && data) { + setProjects([ +diff --git a/apps/platform/src/app/(main)/project/[project]/@secret/page.tsx b/apps/platform/src/app/(main)/project/[project]/@secret/page.tsx +index 18134ab..e1bd8e6 100644 +--- a/apps/platform/src/app/(main)/project/[project]/@secret/page.tsx ++++ b/apps/platform/src/app/(main)/project/[project]/@secret/page.tsx +@@ -5,6 +5,7 @@ import { usePathname } from 'next/navigation' + import dayjs, { extend } from 'dayjs' + import relativeTime from 'dayjs/plugin/relativeTime' + import { NoteIconSVG } from '@public/svg/secret' ++import type { GetAllSecretsOfProjectResponse } from '@keyshade/schema' + import { + Accordion, + AccordionContent, +@@ -19,8 +20,6 @@ import { + TableHeader, + TableRow + } from '@/components/ui/table' +-import type { Secret } from '@keyshade/schema' +-import { SecretController } from '@keyshade/api-client' + import { ScrollArea } from '@/components/ui/scroll-area' + import { + Tooltip, +@@ -29,31 +28,28 @@ import { + TooltipTrigger + } from '@/components/ui/tooltip' + import { Skeleton } from '@/components/ui/skeleton' ++import ControllerInstance from '@/lib/controller-instance' +  + extend(relativeTime) +  + function SecretPage(): React.JSX.Element { +- const [allSecrets, setAllSecrets] = useState() ++ const [allSecrets, setAllSecrets] = ++ useState() + const [isLoading, setIsLoading] = useState(true) + const pathname = usePathname() +  + useEffect(() => { + setIsLoading(true) +  +- const secretController = new SecretController( +- process.env.NEXT_PUBLIC_BACKEND_URL +- ) +- + async function getAllSecretsByProjectSlug() { + const { success, error, data } = +- await secretController.getAllSecretsOfProject( ++ await ControllerInstance.getInstance().secretController.getAllSecretsOfProject( + { projectSlug: pathname.split('/')[2] }, + {} + ) +  + if (success && data) { +- //@ts-ignore +- setAllSecrets(data) ++ setAllSecrets(data.items) + } else { + // eslint-disable-next-line no-console -- we need to log the error + console.error(error) +@@ -68,9 +64,9 @@ function SecretPage(): React.JSX.Element { + if (isLoading) { + return ( +
 +-  +-  +-  ++  ++  ++  +
 + ) + } +@@ -82,7 +78,7 @@ function SecretPage(): React.JSX.Element { + collapsible + type="single" + > +- {allSecrets?.map((secret) => { ++ {allSecrets?.map(({ secret, values }) => { + return ( +  +  +  +- {secret.versions.map((value) => { ++ {values.map((value) => { + return ( +  + {value.environment.slug} +@@ -147,7 +143,7 @@ function SecretPage(): React.JSX.Element { + ) + } +  +-function SerectLoader(): React.JSX.Element { ++function SecretLoader(): React.JSX.Element { + return ( +
 +
 +diff --git a/apps/platform/src/app/(main)/project/[project]/layout.tsx b/apps/platform/src/app/(main)/project/[project]/layout.tsx +index 1eb511b..9edfb0b 100644 +--- a/apps/platform/src/app/(main)/project/[project]/layout.tsx ++++ b/apps/platform/src/app/(main)/project/[project]/layout.tsx +@@ -2,6 +2,7 @@ + import { useEffect, useState } from 'react' + import { useSearchParams } from 'next/navigation' + import { AddSVG } from '@public/svg/shared' ++import type { Project } from '@keyshade/schema' + import { Button } from '@/components/ui/button' + import { + Dialog, +@@ -13,8 +14,7 @@ import { + } from '@/components/ui/dialog' + import { Input } from '@/components/ui/input' + import { Label } from '@/components/ui/label' +-import { ProjectController } from '@keyshade/api-client' +-import type { Project } from '@keyshade/schema' ++import ControllerInstance from '@/lib/controller-instance' +  + interface DetailedProjectPageProps { + params: { project: string } +@@ -38,29 +38,22 @@ function DetailedProjectPage({ + const tab = searchParams.get('tab') ?? 'rollup-details' +  + useEffect(() => { ++ async function getProjectBySlug() { ++ const { success, error, data } = ++ await ControllerInstance.getInstance().projectController.getProject( ++ { projectSlug: params.project }, ++ {} ++ ) +  +- const projectController = new ProjectController( +- process.env.NEXT_PUBLIC_BACKEND_URL +- ) +- +- async function getProjectBySlug(){ +- const {success, error, data} = await projectController.getProject( +- {projectSlug: params.project}, +- {} +- ) +- +- if( success && data ){ +- //@ts-ignore ++ if (success && data) { + setCurrentProject(data) +- } +- else{ ++ } else { + // eslint-disable-next-line no-console -- we need to log the error + console.error(error) + } + } +  + getProjectBySlug() +- + }, [params.project]) +  + return ( +diff --git a/apps/platform/src/app/(main)/settings/@profile/page.tsx b/apps/platform/src/app/(main)/settings/@profile/page.tsx +index 21de60b..4559ec6 100644 +--- a/apps/platform/src/app/(main)/settings/@profile/page.tsx ++++ b/apps/platform/src/app/(main)/settings/@profile/page.tsx +@@ -1,54 +1,52 @@ + 'use client' +-import React, { useEffect, useState } from 'react' ++import React, { useCallback, useEffect, useState } from 'react' + import { toast } from 'sonner' +-import type { User } from '@keyshade/schema' + import InputLoading from './loading' + import { Input } from '@/components/ui/input' + import { Separator } from '@/components/ui/separator' ++import ControllerInstance from '@/lib/controller-instance' + import { Button } from '@/components/ui/button' +-import { apiClient } from '@/lib/api-client' +- +-type UserData = Omit< +- User, +- 'id' | 'isActive' | 'isOnboardingFinished' | 'isAdmin' | 'authProvider' +-> +-async function getUserDetails(): Promise { +- try { +- return await apiClient.get('/user') +- } catch (error) { +- // eslint-disable-next-line no-console -- we need to log the error +- console.error(error) +- } +-} +- +-async function updateUserDetails(userData: UserData): Promise { +- try { +- await apiClient.put('/user', userData) +- } catch (error) { +- // eslint-disable-next-line no-console -- we need to log the error +- console.error(error) +- } +-} +  + function ProfilePage(): React.JSX.Element { + const [isLoading, setIsLoading] = useState(true) +- const [userData, setUserData] = useState({ ++ const [userData, setUserData] = useState({ + email: '', + name: '', + profilePictureUrl: '' + }) + const [isModified, setIsModified] = useState(false) +  ++ const updateSelf = useCallback(async () => { ++ try { ++ await ControllerInstance.getInstance().userController.updateSelf( ++ { ++ name: userData.name, ++ email: userData.email ++ }, ++ {} ++ ) ++ toast.success('Profile updated successfully') ++ } catch (error) { ++ // eslint-disable-next-line no-console -- we need to log the error ++ console.error(error) ++ } ++ setIsModified(false) ++ }, [userData]) ++ + useEffect(() => { +- getUserDetails() +- .then((data) => { +- if (data) { ++ ControllerInstance.getInstance() ++ .userController.getSelf() ++ .then(({ data, success, error }) => { ++ if (success && data) { + setUserData({ + email: data.email, +- name: data.name ?? '', +- profilePictureUrl: data.profilePictureUrl ++ name: data.name, ++ profilePictureUrl: data.profilePictureUrl || '' + }) + setIsLoading(false) ++ } else { ++ // eslint-disable-next-line no-console -- we need to log the error ++ console.error(error) + } + }) + .catch((error) => { +@@ -67,7 +65,7 @@ function ProfilePage(): React.JSX.Element { + Upload a picture to change your avatar across Keyshade. +  +
 +-
{' '} ++
 + {/* //! This is will be replaced by an image tag */} +
 + {/* Name */} +@@ -114,20 +112,7 @@ function ProfilePage(): React.JSX.Element { + )} +
 +
 +- { +- updateUserDetails(userData) +- .then(() => { +- toast.success('User details updated successfully') +- }) +- .catch(() => { +- toast.error('Failed to update user details') +- }) +- setIsModified(false) +- }} +- variant="secondary" +- > ++  +
 +diff --git a/apps/platform/src/components/shared/sidebar/index.tsx b/apps/platform/src/components/shared/sidebar/index.tsx +index ee7cd9b..7da768b 100644 +--- a/apps/platform/src/components/shared/sidebar/index.tsx ++++ b/apps/platform/src/components/shared/sidebar/index.tsx +@@ -6,8 +6,8 @@ import { + SettingsSVG, + TeamSVG + } from '@public/svg/shared' +-import { Combobox } from '@/components/ui/combobox' + import SidebarTab from './sidebarTab' ++import { Combobox } from '@/components/ui/combobox' +  + function Sidebar(): JSX.Element { + const sidebarTabData = [ +diff --git a/apps/platform/src/components/ui/combobox.tsx b/apps/platform/src/components/ui/combobox.tsx +index b3107dc..8414098 100644 +--- a/apps/platform/src/components/ui/combobox.tsx ++++ b/apps/platform/src/components/ui/combobox.tsx +@@ -5,27 +5,6 @@ import { useEffect, useState } from 'react' + import { useRouter } from 'next/navigation' + import { toast } from 'sonner' + import { AddSVG } from '@public/svg/shared' +-import { cn } from '@/lib/utils' +-import { +- Command, +- CommandEmpty, +- CommandInput, +- CommandItem, +- CommandList +-} from '@/components/ui/command' +-import { +- Popover, +- PopoverContent, +- PopoverTrigger +-} from '@/components/ui/popover' +-import { apiClient } from '@/lib/api-client' +-// import type { Workspace } from '@/types' +-import { zWorkspace } from '@/types' +-import { +- getCurrentWorkspace, +- setCurrentWorkspace, +- setWorkspace +-} from '@/lib/workspace-storage' + import type { Workspace } from '@keyshade/schema' + import { Input } from './input' + import { Label } from './label' +@@ -38,39 +17,43 @@ import { + DialogTrigger + } from './dialog' + import { Button } from './button' +-import { WorkspaceSchema } from '@keyshade/schema/schemas' +- +-interface WorkspaceResponse { +- items: Workspace[] +- metadata: { +- page: number +- perPage: number +- pageCount: number +- totalCount: number +- links: { +- self: string +- first: string +- previous: string | null +- next: string | null +- last: string +- } +- } +-} ++import { ++ getCurrentWorkspace, ++ setCurrentWorkspace, ++ setWorkspace ++} from '@/lib/workspace-storage' ++import { cn } from '@/lib/utils' ++import { ++ Popover, ++ PopoverContent, ++ PopoverTrigger ++} from '@/components/ui/popover' ++import { ++ Command, ++ CommandEmpty, ++ CommandInput, ++ CommandItem, ++ CommandList ++} from '@/components/ui/command' ++import ControllerInstance from '@/lib/controller-instance' +  + async function getAllWorkspace(): Promise { + try { +- const workspaceData: WorkspaceResponse = +- await apiClient.get('/workspace') ++ const { data, success, error } = ++ await ControllerInstance.getInstance().workspaceController.getWorkspacesOfUser( ++ {}, ++ {} ++ ) +  +- // TODO: We are getting error here from the success flag, need to see this again +- // const { success, data } = WorkspaceSchema.array().safeParse(workspaceData.items) +- // if (!success) { +- // throw new Error('Invalid data') +- // } ++ if (error) { ++ // eslint-disable-next-line no-console -- we need to log the error ++ console.error(error) ++ return undefined ++ } +  +- return workspaceData.items +- // return data +- // return workspaceData; ++ if (success && data) { ++ return data.items ++ } + } catch (error) { + // eslint-disable-next-line no-console -- we need to log the error + console.error(error) +@@ -92,9 +75,13 @@ export function Combobox(): React.JSX.Element { + } + setIsNameEmpty(false) + try { +- const response = await apiClient.post('/workspace', { +- name +- }) ++ const response = ++ await ControllerInstance.getInstance().workspaceController.createWorkspace( ++ { ++ name ++ }, ++ {} ++ ) + setCurrentWorkspace(response) + setOpen(false) + } catch (error) { +diff --git a/apps/platform/src/lib/controller-instance.ts b/apps/platform/src/lib/controller-instance.ts +index fe432b7..a766038 100644 +--- a/apps/platform/src/lib/controller-instance.ts ++++ b/apps/platform/src/lib/controller-instance.ts +@@ -1,30 +1,90 @@ +-import { AuthController } from '@keyshade/api-client' ++import { ++ AuthController, ++ EnvironmentController, ++ ProjectController, ++ SecretController, ++ UserController, ++ VariableController, ++ WorkspaceController, ++ WorkspaceMembershipController, ++ WorkspaceRoleController ++} from '@keyshade/api-client' +  + export default class ControllerInstance { + private static instance: ControllerInstance | null +  +- private _authController: AuthController | null = null ++ private _authController: AuthController ++ private _userController: UserController ++ private _workspaceController: WorkspaceController ++ private _workspaceMembershipController: WorkspaceMembershipController ++ private _workspaceRoleController: WorkspaceRoleController ++ private _projectController: ProjectController ++ private _environmentController: EnvironmentController ++ private _secretController: SecretController ++ private _variableController: VariableController +  + get authController(): AuthController { +- if (!this._authController) { +- throw new Error('ControllerInstance not initialized') +- } + return this._authController + } +  +- static initialize(baseUrl: string): void { +- if (!ControllerInstance.instance) { +- const instance = new ControllerInstance() ++ get workspaceController(): WorkspaceController { ++ return this._workspaceController ++ } ++ ++ get workspaceMembershipController(): WorkspaceMembershipController { ++ return this._workspaceMembershipController ++ } +  +- instance._authController = new AuthController(baseUrl) ++ get workspaceRoleController(): WorkspaceRoleController { ++ return this._workspaceRoleController ++ } +  +- ControllerInstance.instance = instance +- } ++ get projectController(): ProjectController { ++ return this._projectController ++ } ++ ++ get environmentController(): EnvironmentController { ++ return this._environmentController ++ } ++ ++ get secretController(): SecretController { ++ return this._secretController ++ } ++ ++ get variableController(): VariableController { ++ return this._variableController ++ } ++ ++ get userController(): UserController { ++ return this._userController + } +  + static getInstance(): ControllerInstance { + if (!ControllerInstance.instance) { +- throw new Error('ControllerInstance not initialized') ++ ControllerInstance.instance = new ControllerInstance() ++ ControllerInstance.instance._authController = new AuthController( ++ process.env.NEXT_PUBLIC_BACKEND_URL ++ ) ++ ControllerInstance.instance._userController = new UserController( ++ process.env.NEXT_PUBLIC_BACKEND_URL ++ ) ++ ControllerInstance.instance._workspaceController = ++ new WorkspaceController(process.env.NEXT_PUBLIC_BACKEND_URL) ++ ControllerInstance.instance._workspaceMembershipController = ++ new WorkspaceMembershipController(process.env.NEXT_PUBLIC_BACKEND_URL) ++ ControllerInstance.instance._workspaceRoleController = ++ new WorkspaceRoleController(process.env.NEXT_PUBLIC_BACKEND_URL) ++ ControllerInstance.instance._projectController = new ProjectController( ++ process.env.NEXT_PUBLIC_BACKEND_URL ++ ) ++ ControllerInstance.instance._environmentController = ++ new EnvironmentController(process.env.NEXT_PUBLIC_BACKEND_URL) ++ ControllerInstance.instance._secretController = new SecretController( ++ process.env.NEXT_PUBLIC_BACKEND_URL ++ ) ++ ControllerInstance.instance._variableController = new VariableController( ++ process.env.NEXT_PUBLIC_BACKEND_URL ++ ) + } + return ControllerInstance.instance + } +diff --git a/packages/api-client/src/index.ts b/packages/api-client/src/index.ts +index 79ab5f6..edd344f 100644 +--- a/packages/api-client/src/index.ts ++++ b/packages/api-client/src/index.ts +@@ -8,6 +8,7 @@ import WorkspaceController from '@api-client/controllers/workspace' + import WorkspaceRoleController from '@api-client/controllers/workspace-role' + import WorkspaceMembershipController from '@api-client/controllers/workspace-membership' + import AuthController from '@api-client/controllers/auth' ++import UserController from '@api-client/controllers/user' + export { + EnvironmentController, + SecretController, +@@ -18,5 +19,6 @@ export { + WorkspaceController, + WorkspaceRoleController, + WorkspaceMembershipController, +- AuthController ++ AuthController, ++ UserController + } +diff --git a/packages/schema/src/secret/index.ts b/packages/schema/src/secret/index.ts +index ff5b499..4277679 100644 +--- a/packages/schema/src/secret/index.ts ++++ b/packages/schema/src/secret/index.ts +@@ -100,15 +100,17 @@ export const GetAllSecretsOfProjectResponseSchema = PageResponseSchema( + name: z.string() + }) + }), +- values: z.object({ +- environment: z.object({ +- id: z.string(), +- name: z.string(), +- slug: z.string() +- }), +- value: z.string(), +- version: z.number() +- }) ++ values: z.array( ++ z.object({ ++ environment: z.object({ ++ id: z.string(), ++ name: z.string(), ++ slug: z.string() ++ }), ++ value: z.string(), ++ version: z.number() ++ }) ++ ) + }) + ) +  diff --git a/packages/api-client/src/index.ts b/packages/api-client/src/index.ts index 79ab5f6d..edd344fb 100644 --- a/packages/api-client/src/index.ts +++ b/packages/api-client/src/index.ts @@ -8,6 +8,7 @@ import WorkspaceController from '@api-client/controllers/workspace' import WorkspaceRoleController from '@api-client/controllers/workspace-role' import WorkspaceMembershipController from '@api-client/controllers/workspace-membership' import AuthController from '@api-client/controllers/auth' +import UserController from '@api-client/controllers/user' export { EnvironmentController, SecretController, @@ -18,5 +19,6 @@ export { WorkspaceController, WorkspaceRoleController, WorkspaceMembershipController, - AuthController + AuthController, + UserController } diff --git a/packages/schema/src/secret/index.ts b/packages/schema/src/secret/index.ts index ff5b499e..4277679f 100644 --- a/packages/schema/src/secret/index.ts +++ b/packages/schema/src/secret/index.ts @@ -100,15 +100,17 @@ export const GetAllSecretsOfProjectResponseSchema = PageResponseSchema( name: z.string() }) }), - values: z.object({ - environment: z.object({ - id: z.string(), - name: z.string(), - slug: z.string() - }), - value: z.string(), - version: z.number() - }) + values: z.array( + z.object({ + environment: z.object({ + id: z.string(), + name: z.string(), + slug: z.string() + }), + value: z.string(), + version: z.number() + }) + ) }) ) diff --git a/packages/schema/tests/secret.spec.ts b/packages/schema/tests/secret.spec.ts index 823d471d..6d30c099 100644 --- a/packages/schema/tests/secret.spec.ts +++ b/packages/schema/tests/secret.spec.ts @@ -16,7 +16,6 @@ import { GetRevisionsOfSecretResponseSchema } from '@/secret' import { rotateAfterEnum } from '@/enums' -import { env, versions } from 'process' describe('Secret Schema Tests', () => { describe('SecretSchema Tests', () => { @@ -350,15 +349,17 @@ describe('Secret Schema Tests', () => { name: 'John Doe' } }, - values: { - environment: { - id: 'env123', - name: 'Development', - slug: 'development' - }, - value: 'secret-value', - version: 1 - } + values: [ + { + environment: { + id: 'env123', + name: 'Development', + slug: 'development' + }, + value: 'secret-value', + version: 1 + } + ] } ], metadata: { diff --git a/t updateSelf = useCallback(async () => { b/t updateSelf = useCallback(async () => { new file mode 100644 index 00000000..95c798b9 --- /dev/null +++ b/t updateSelf = useCallback(async () => { @@ -0,0 +1,664 @@ +diff --git a/apps/platform/.eslintrc.cjs b/apps/platform/.eslintrc.cjs +index 28a8b27..fa9233a 100644 +--- a/apps/platform/.eslintrc.cjs ++++ b/apps/platform/.eslintrc.cjs +@@ -7,24 +7,13 @@ module.exports = { + }, + rules: { + 'import/no-extraneous-dependencies': 0, +- '@typescript-eslint/interface-name-prefix': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', +- '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unused-vars': ['warn'], +- '@typescript-eslint/no-unsafe-call': 'off', +- '@typescript-eslint/no-unsafe-return': 'off', + '@typescript-eslint/space-before-function-paren': 'off', + '@typescript-eslint/strict-boolean-expressions': 'off', +- '@typescript-eslint/prefer-nullish-coalescing': 'off', +- 'space-before-function-paren': 'off', +- '@typescript-eslint/member-delimiter-style': 'off', + '@typescript-eslint/no-confusing-void-expression': 'off', + '@typescript-eslint/no-floating-promises': 'off', +- '@typescript-eslint/no-misused-promises': 'off', +- '@typescript-eslint/no-unsafe-assignment': 'off', +- '@typescript-eslint/no-unsafe-member-access': 'off', +- '@typescript-eslint/no-unsafe-argument': 'off', +- '@typescript-eslint/no-unnecessary-condition': 'off' ++ '@typescript-eslint/no-misused-promises': 'off' + } + } +diff --git a/apps/platform/src/app/(main)/page.tsx b/apps/platform/src/app/(main)/page.tsx +index fe8fa2a..9a9cb2a 100644 +--- a/apps/platform/src/app/(main)/page.tsx ++++ b/apps/platform/src/app/(main)/page.tsx +@@ -6,7 +6,6 @@ import type { + ProjectWithCount, + Workspace + } from '@keyshade/schema' +-import { ProjectController } from '@keyshade/api-client' + import { AddSVG } from '@public/svg/shared' + import { FolderSVG } from '@public/svg/dashboard' + import ProjectCard from '@/components/dashboard/projectCard' +@@ -38,6 +37,7 @@ import { + DialogHeader, + DialogTrigger + } from '@/components/ui/dialog' ++import ControllerInstance from '@/lib/controller-instance' +  + export default function Index(): JSX.Element { + const [isSheetOpen, setIsSheetOpen] = useState(false) +@@ -76,16 +76,13 @@ export default function Index(): JSX.Element { + // If a workspace is selected, we want to fetch all the projects + // under that workspace and display it in the dashboard. + useEffect(() => { +- const projectController = new ProjectController( +- process.env.NEXT_PUBLIC_BACKEND_URL +- ) +- + async function getAllProjects() { + if (currentWorkspace) { +- const { success, error, data } = await projectController.getAllProjects( +- { workspaceSlug: currentWorkspace.slug }, +- {} +- ) ++ const { success, error, data } = ++ await ControllerInstance.getInstance().projectController.getAllProjects( ++ { workspaceSlug: currentWorkspace.slug }, ++ {} ++ ) +  + if (success && data) { + setProjects(data.items) +@@ -105,16 +102,13 @@ export default function Index(): JSX.Element { + // Function to create a new project + const createNewProject = useCallback(async () => { + if (currentWorkspace) { +- const projectController = new ProjectController( +- process.env.NEXT_PUBLIC_BACKEND_URL +- ) +- + newProjectData.workspaceSlug = currentWorkspace.slug +  +- const { data, error, success } = await projectController.createProject( +- newProjectData, +- {} +- ) ++ const { data, error, success } = ++ await ControllerInstance.getInstance().projectController.createProject( ++ newProjectData, ++ {} ++ ) +  + if (success && data) { + setProjects([ +diff --git a/apps/platform/src/app/(main)/project/[project]/@secret/page.tsx b/apps/platform/src/app/(main)/project/[project]/@secret/page.tsx +index 18134ab..e1bd8e6 100644 +--- a/apps/platform/src/app/(main)/project/[project]/@secret/page.tsx ++++ b/apps/platform/src/app/(main)/project/[project]/@secret/page.tsx +@@ -5,6 +5,7 @@ import { usePathname } from 'next/navigation' + import dayjs, { extend } from 'dayjs' + import relativeTime from 'dayjs/plugin/relativeTime' + import { NoteIconSVG } from '@public/svg/secret' ++import type { GetAllSecretsOfProjectResponse } from '@keyshade/schema' + import { + Accordion, + AccordionContent, +@@ -19,8 +20,6 @@ import { + TableHeader, + TableRow + } from '@/components/ui/table' +-import type { Secret } from '@keyshade/schema' +-import { SecretController } from '@keyshade/api-client' + import { ScrollArea } from '@/components/ui/scroll-area' + import { + Tooltip, +@@ -29,31 +28,28 @@ import { + TooltipTrigger + } from '@/components/ui/tooltip' + import { Skeleton } from '@/components/ui/skeleton' ++import ControllerInstance from '@/lib/controller-instance' +  + extend(relativeTime) +  + function SecretPage(): React.JSX.Element { +- const [allSecrets, setAllSecrets] = useState() ++ const [allSecrets, setAllSecrets] = ++ useState() + const [isLoading, setIsLoading] = useState(true) + const pathname = usePathname() +  + useEffect(() => { + setIsLoading(true) +  +- const secretController = new SecretController( +- process.env.NEXT_PUBLIC_BACKEND_URL +- ) +- + async function getAllSecretsByProjectSlug() { + const { success, error, data } = +- await secretController.getAllSecretsOfProject( ++ await ControllerInstance.getInstance().secretController.getAllSecretsOfProject( + { projectSlug: pathname.split('/')[2] }, + {} + ) +  + if (success && data) { +- //@ts-ignore +- setAllSecrets(data) ++ setAllSecrets(data.items) + } else { + // eslint-disable-next-line no-console -- we need to log the error + console.error(error) +@@ -68,9 +64,9 @@ function SecretPage(): React.JSX.Element { + if (isLoading) { + return ( +
 +-  +-  +-  ++  ++  ++  +
 + ) + } +@@ -82,7 +78,7 @@ function SecretPage(): React.JSX.Element { + collapsible + type="single" + > +- {allSecrets?.map((secret) => { ++ {allSecrets?.map(({ secret, values }) => { + return ( +  +  +  +- {secret.versions.map((value) => { ++ {values.map((value) => { + return ( +  + {value.environment.slug} +@@ -147,7 +143,7 @@ function SecretPage(): React.JSX.Element { + ) + } +  +-function SerectLoader(): React.JSX.Element { ++function SecretLoader(): React.JSX.Element { + return ( +
 +
 +diff --git a/apps/platform/src/app/(main)/project/[project]/layout.tsx b/apps/platform/src/app/(main)/project/[project]/layout.tsx +index 1eb511b..9edfb0b 100644 +--- a/apps/platform/src/app/(main)/project/[project]/layout.tsx ++++ b/apps/platform/src/app/(main)/project/[project]/layout.tsx +@@ -2,6 +2,7 @@ + import { useEffect, useState } from 'react' + import { useSearchParams } from 'next/navigation' + import { AddSVG } from '@public/svg/shared' ++import type { Project } from '@keyshade/schema' + import { Button } from '@/components/ui/button' + import { + Dialog, +@@ -13,8 +14,7 @@ import { + } from '@/components/ui/dialog' + import { Input } from '@/components/ui/input' + import { Label } from '@/components/ui/label' +-import { ProjectController } from '@keyshade/api-client' +-import type { Project } from '@keyshade/schema' ++import ControllerInstance from '@/lib/controller-instance' +  + interface DetailedProjectPageProps { + params: { project: string } +@@ -38,29 +38,22 @@ function DetailedProjectPage({ + const tab = searchParams.get('tab') ?? 'rollup-details' +  + useEffect(() => { ++ async function getProjectBySlug() { ++ const { success, error, data } = ++ await ControllerInstance.getInstance().projectController.getProject( ++ { projectSlug: params.project }, ++ {} ++ ) +  +- const projectController = new ProjectController( +- process.env.NEXT_PUBLIC_BACKEND_URL +- ) +- +- async function getProjectBySlug(){ +- const {success, error, data} = await projectController.getProject( +- {projectSlug: params.project}, +- {} +- ) +- +- if( success && data ){ +- //@ts-ignore ++ if (success && data) { + setCurrentProject(data) +- } +- else{ ++ } else { + // eslint-disable-next-line no-console -- we need to log the error + console.error(error) + } + } +  + getProjectBySlug() +- + }, [params.project]) +  + return ( +diff --git a/apps/platform/src/app/(main)/settings/@profile/page.tsx b/apps/platform/src/app/(main)/settings/@profile/page.tsx +index 21de60b..4559ec6 100644 +--- a/apps/platform/src/app/(main)/settings/@profile/page.tsx ++++ b/apps/platform/src/app/(main)/settings/@profile/page.tsx +@@ -1,54 +1,52 @@ + 'use client' +-import React, { useEffect, useState } from 'react' ++import React, { useCallback, useEffect, useState } from 'react' + import { toast } from 'sonner' +-import type { User } from '@keyshade/schema' + import InputLoading from './loading' + import { Input } from '@/components/ui/input' + import { Separator } from '@/components/ui/separator' ++import ControllerInstance from '@/lib/controller-instance' + import { Button } from '@/components/ui/button' +-import { apiClient } from '@/lib/api-client' +- +-type UserData = Omit< +- User, +- 'id' | 'isActive' | 'isOnboardingFinished' | 'isAdmin' | 'authProvider' +-> +-async function getUserDetails(): Promise { +- try { +- return await apiClient.get('/user') +- } catch (error) { +- // eslint-disable-next-line no-console -- we need to log the error +- console.error(error) +- } +-} +- +-async function updateUserDetails(userData: UserData): Promise { +- try { +- await apiClient.put('/user', userData) +- } catch (error) { +- // eslint-disable-next-line no-console -- we need to log the error +- console.error(error) +- } +-} +  + function ProfilePage(): React.JSX.Element { + const [isLoading, setIsLoading] = useState(true) +- const [userData, setUserData] = useState({ ++ const [userData, setUserData] = useState({ + email: '', + name: '', + profilePictureUrl: '' + }) + const [isModified, setIsModified] = useState(false) +  ++ const updateSelf = useCallback(async () => { ++ try { ++ await ControllerInstance.getInstance().userController.updateSelf( ++ { ++ name: userData.name, ++ email: userData.email ++ }, ++ {} ++ ) ++ toast.success('Profile updated successfully') ++ } catch (error) { ++ // eslint-disable-next-line no-console -- we need to log the error ++ console.error(error) ++ } ++ setIsModified(false) ++ }, [userData]) ++ + useEffect(() => { +- getUserDetails() +- .then((data) => { +- if (data) { ++ ControllerInstance.getInstance() ++ .userController.getSelf() ++ .then(({ data, success, error }) => { ++ if (success && data) { + setUserData({ + email: data.email, +- name: data.name ?? '', +- profilePictureUrl: data.profilePictureUrl ++ name: data.name, ++ profilePictureUrl: data.profilePictureUrl || '' + }) + setIsLoading(false) ++ } else { ++ // eslint-disable-next-line no-console -- we need to log the error ++ console.error(error) + } + }) + .catch((error) => { +@@ -67,7 +65,7 @@ function ProfilePage(): React.JSX.Element { + Upload a picture to change your avatar across Keyshade. +  +
 +-
{' '} ++
 + {/* //! This is will be replaced by an image tag */} +
 + {/* Name */} +@@ -114,20 +112,7 @@ function ProfilePage(): React.JSX.Element { + )} +
 +
 +- { +- updateUserDetails(userData) +- .then(() => { +- toast.success('User details updated successfully') +- }) +- .catch(() => { +- toast.error('Failed to update user details') +- }) +- setIsModified(false) +- }} +- variant="secondary" +- > ++  +
 +diff --git a/apps/platform/src/components/shared/sidebar/index.tsx b/apps/platform/src/components/shared/sidebar/index.tsx +index ee7cd9b..7da768b 100644 +--- a/apps/platform/src/components/shared/sidebar/index.tsx ++++ b/apps/platform/src/components/shared/sidebar/index.tsx +@@ -6,8 +6,8 @@ import { + SettingsSVG, + TeamSVG + } from '@public/svg/shared' +-import { Combobox } from '@/components/ui/combobox' + import SidebarTab from './sidebarTab' ++import { Combobox } from '@/components/ui/combobox' +  + function Sidebar(): JSX.Element { + const sidebarTabData = [ +diff --git a/apps/platform/src/components/ui/combobox.tsx b/apps/platform/src/components/ui/combobox.tsx +index b3107dc..8414098 100644 +--- a/apps/platform/src/components/ui/combobox.tsx ++++ b/apps/platform/src/components/ui/combobox.tsx +@@ -5,27 +5,6 @@ import { useEffect, useState } from 'react' + import { useRouter } from 'next/navigation' + import { toast } from 'sonner' + import { AddSVG } from '@public/svg/shared' +-import { cn } from '@/lib/utils' +-import { +- Command, +- CommandEmpty, +- CommandInput, +- CommandItem, +- CommandList +-} from '@/components/ui/command' +-import { +- Popover, +- PopoverContent, +- PopoverTrigger +-} from '@/components/ui/popover' +-import { apiClient } from '@/lib/api-client' +-// import type { Workspace } from '@/types' +-import { zWorkspace } from '@/types' +-import { +- getCurrentWorkspace, +- setCurrentWorkspace, +- setWorkspace +-} from '@/lib/workspace-storage' + import type { Workspace } from '@keyshade/schema' + import { Input } from './input' + import { Label } from './label' +@@ -38,39 +17,43 @@ import { + DialogTrigger + } from './dialog' + import { Button } from './button' +-import { WorkspaceSchema } from '@keyshade/schema/schemas' +- +-interface WorkspaceResponse { +- items: Workspace[] +- metadata: { +- page: number +- perPage: number +- pageCount: number +- totalCount: number +- links: { +- self: string +- first: string +- previous: string | null +- next: string | null +- last: string +- } +- } +-} ++import { ++ getCurrentWorkspace, ++ setCurrentWorkspace, ++ setWorkspace ++} from '@/lib/workspace-storage' ++import { cn } from '@/lib/utils' ++import { ++ Popover, ++ PopoverContent, ++ PopoverTrigger ++} from '@/components/ui/popover' ++import { ++ Command, ++ CommandEmpty, ++ CommandInput, ++ CommandItem, ++ CommandList ++} from '@/components/ui/command' ++import ControllerInstance from '@/lib/controller-instance' +  + async function getAllWorkspace(): Promise { + try { +- const workspaceData: WorkspaceResponse = +- await apiClient.get('/workspace') ++ const { data, success, error } = ++ await ControllerInstance.getInstance().workspaceController.getWorkspacesOfUser( ++ {}, ++ {} ++ ) +  +- // TODO: We are getting error here from the success flag, need to see this again +- // const { success, data } = WorkspaceSchema.array().safeParse(workspaceData.items) +- // if (!success) { +- // throw new Error('Invalid data') +- // } ++ if (error) { ++ // eslint-disable-next-line no-console -- we need to log the error ++ console.error(error) ++ return undefined ++ } +  +- return workspaceData.items +- // return data +- // return workspaceData; ++ if (success && data) { ++ return data.items ++ } + } catch (error) { + // eslint-disable-next-line no-console -- we need to log the error + console.error(error) +@@ -92,9 +75,13 @@ export function Combobox(): React.JSX.Element { + } + setIsNameEmpty(false) + try { +- const response = await apiClient.post('/workspace', { +- name +- }) ++ const response = ++ await ControllerInstance.getInstance().workspaceController.createWorkspace( ++ { ++ name ++ }, ++ {} ++ ) + setCurrentWorkspace(response) + setOpen(false) + } catch (error) { +diff --git a/apps/platform/src/lib/controller-instance.ts b/apps/platform/src/lib/controller-instance.ts +index fe432b7..a766038 100644 +--- a/apps/platform/src/lib/controller-instance.ts ++++ b/apps/platform/src/lib/controller-instance.ts +@@ -1,30 +1,90 @@ +-import { AuthController } from '@keyshade/api-client' ++import { ++ AuthController, ++ EnvironmentController, ++ ProjectController, ++ SecretController, ++ UserController, ++ VariableController, ++ WorkspaceController, ++ WorkspaceMembershipController, ++ WorkspaceRoleController ++} from '@keyshade/api-client' +  + export default class ControllerInstance { + private static instance: ControllerInstance | null +  +- private _authController: AuthController | null = null ++ private _authController: AuthController ++ private _userController: UserController ++ private _workspaceController: WorkspaceController ++ private _workspaceMembershipController: WorkspaceMembershipController ++ private _workspaceRoleController: WorkspaceRoleController ++ private _projectController: ProjectController ++ private _environmentController: EnvironmentController ++ private _secretController: SecretController ++ private _variableController: VariableController +  + get authController(): AuthController { +- if (!this._authController) { +- throw new Error('ControllerInstance not initialized') +- } + return this._authController + } +  +- static initialize(baseUrl: string): void { +- if (!ControllerInstance.instance) { +- const instance = new ControllerInstance() ++ get workspaceController(): WorkspaceController { ++ return this._workspaceController ++ } ++ ++ get workspaceMembershipController(): WorkspaceMembershipController { ++ return this._workspaceMembershipController ++ } +  +- instance._authController = new AuthController(baseUrl) ++ get workspaceRoleController(): WorkspaceRoleController { ++ return this._workspaceRoleController ++ } +  +- ControllerInstance.instance = instance +- } ++ get projectController(): ProjectController { ++ return this._projectController ++ } ++ ++ get environmentController(): EnvironmentController { ++ return this._environmentController ++ } ++ ++ get secretController(): SecretController { ++ return this._secretController ++ } ++ ++ get variableController(): VariableController { ++ return this._variableController ++ } ++ ++ get userController(): UserController { ++ return this._userController + } +  + static getInstance(): ControllerInstance { + if (!ControllerInstance.instance) { +- throw new Error('ControllerInstance not initialized') ++ ControllerInstance.instance = new ControllerInstance() ++ ControllerInstance.instance._authController = new AuthController( ++ process.env.NEXT_PUBLIC_BACKEND_URL ++ ) ++ ControllerInstance.instance._userController = new UserController( ++ process.env.NEXT_PUBLIC_BACKEND_URL ++ ) ++ ControllerInstance.instance._workspaceController = ++ new WorkspaceController(process.env.NEXT_PUBLIC_BACKEND_URL) ++ ControllerInstance.instance._workspaceMembershipController = ++ new WorkspaceMembershipController(process.env.NEXT_PUBLIC_BACKEND_URL) ++ ControllerInstance.instance._workspaceRoleController = ++ new WorkspaceRoleController(process.env.NEXT_PUBLIC_BACKEND_URL) ++ ControllerInstance.instance._projectController = new ProjectController( ++ process.env.NEXT_PUBLIC_BACKEND_URL ++ ) ++ ControllerInstance.instance._environmentController = ++ new EnvironmentController(process.env.NEXT_PUBLIC_BACKEND_URL) ++ ControllerInstance.instance._secretController = new SecretController( ++ process.env.NEXT_PUBLIC_BACKEND_URL ++ ) ++ ControllerInstance.instance._variableController = new VariableController( ++ process.env.NEXT_PUBLIC_BACKEND_URL ++ ) + } + return ControllerInstance.instance + } +diff --git a/packages/api-client/src/index.ts b/packages/api-client/src/index.ts +index 79ab5f6..edd344f 100644 +--- a/packages/api-client/src/index.ts ++++ b/packages/api-client/src/index.ts +@@ -8,6 +8,7 @@ import WorkspaceController from '@api-client/controllers/workspace' + import WorkspaceRoleController from '@api-client/controllers/workspace-role' + import WorkspaceMembershipController from '@api-client/controllers/workspace-membership' + import AuthController from '@api-client/controllers/auth' ++import UserController from '@api-client/controllers/user' + export { + EnvironmentController, + SecretController, +@@ -18,5 +19,6 @@ export { + WorkspaceController, + WorkspaceRoleController, + WorkspaceMembershipController, +- AuthController ++ AuthController, ++ UserController + } +diff --git a/packages/schema/src/secret/index.ts b/packages/schema/src/secret/index.ts +index ff5b499..4277679 100644 +--- a/packages/schema/src/secret/index.ts ++++ b/packages/schema/src/secret/index.ts +@@ -100,15 +100,17 @@ export const GetAllSecretsOfProjectResponseSchema = PageResponseSchema( + name: z.string() + }) + }), +- values: z.object({ +- environment: z.object({ +- id: z.string(), +- name: z.string(), +- slug: z.string() +- }), +- value: z.string(), +- version: z.number() +- }) ++ values: z.array( ++ z.object({ ++ environment: z.object({ ++ id: z.string(), ++ name: z.string(), ++ slug: z.string() ++ }), ++ value: z.string(), ++ version: z.number() ++ }) ++ ) + }) + ) +  diff --git a/t.success('User details updated successfully') b/t.success('User details updated successfully') new file mode 100644 index 00000000..333a0b57 --- /dev/null +++ b/t.success('User details updated successfully') @@ -0,0 +1,258 @@ + + SSUUMMMMAARRYY OOFF LLEESSSS CCOOMMMMAANNDDSS + + Commands marked with * may be preceded by a number, _N. + Notes in parentheses indicate the behavior if _N is given. + A key preceded by a caret indicates the Ctrl key; thus ^K is ctrl-K. + + h H Display this help. + q :q Q :Q ZZ Exit. + --------------------------------------------------------------------------- + + MMOOVVIINNGG + + e ^E j ^N CR * Forward one line (or _N lines). + y ^Y k ^K ^P * Backward one line (or _N lines). + f ^F ^V SPACE * Forward one window (or _N lines). + b ^B ESC-v * Backward one window (or _N lines). + z * Forward one window (and set window to _N). + w * Backward one window (and set window to _N). + ESC-SPACE * Forward one window, but don't stop at end-of-file. + d ^D * Forward one half-window (and set half-window to _N). + u ^U * Backward one half-window (and set half-window to _N). + ESC-) RightArrow * Right one half screen width (or _N positions). + ESC-( LeftArrow * Left one half screen width (or _N positions). + ESC-} ^RightArrow Right to last column displayed. + ESC-{ ^LeftArrow Left to first column. + F Forward forever; like "tail -f". + ESC-F Like F but stop when search pattern is found. + r ^R ^L Repaint screen. + R Repaint screen, discarding buffered input. + --------------------------------------------------- + Default "window" is the screen height. + Default "half-window" is half of the screen height. + --------------------------------------------------------------------------- + + SSEEAARRCCHHIINNGG + + /_p_a_t_t_e_r_n * Search forward for (_N-th) matching line. + ?_p_a_t_t_e_r_n * Search backward for (_N-th) matching line. + n * Repeat previous search (for _N-th occurrence). + N * Repeat previous search in reverse direction. + ESC-n * Repeat previous search, spanning files. + ESC-N * Repeat previous search, reverse dir. & spanning files. + ESC-u Undo (toggle) search highlighting. + ESC-U Clear search highlighting. + &_p_a_t_t_e_r_n * Display only matching lines. + --------------------------------------------------- + A search pattern may begin with one or more of: + ^N or ! Search for NON-matching lines. + ^E or * Search multiple files (pass thru END OF FILE). + ^F or @ Start search at FIRST file (for /) or last file (for ?). + ^K Highlight matches, but don't move (KEEP position). + ^R Don't use REGULAR EXPRESSIONS. + ^W WRAP search if no match found. + --------------------------------------------------------------------------- + + JJUUMMPPIINNGG + + g < ESC-< * Go to first line in file (or line _N). + G > ESC-> * Go to last line in file (or line _N). + p % * Go to beginning of file (or _N percent into file). + t * Go to the (_N-th) next tag. + T * Go to the (_N-th) previous tag. + { ( [ * Find close bracket } ) ]. + } ) ] * Find open bracket { ( [. + ESC-^F _<_c_1_> _<_c_2_> * Find close bracket _<_c_2_>. + ESC-^B _<_c_1_> _<_c_2_> * Find open bracket _<_c_1_>. + --------------------------------------------------- + Each "find close bracket" command goes forward to the close bracket + matching the (_N-th) open bracket in the top line. + Each "find open bracket" command goes backward to the open bracket + matching the (_N-th) close bracket in the bottom line. + + m_<_l_e_t_t_e_r_> Mark the current top line with . + M_<_l_e_t_t_e_r_> Mark the current bottom line with . + '_<_l_e_t_t_e_r_> Go to a previously marked position. + '' Go to the previous position. + ^X^X Same as '. + ESC-M_<_l_e_t_t_e_r_> Clear a mark. + --------------------------------------------------- + A mark is any upper-case or lower-case letter. + Certain marks are predefined: + ^ means beginning of the file + $ means end of the file + --------------------------------------------------------------------------- + + CCHHAANNGGIINNGG FFIILLEESS + + :e [_f_i_l_e] Examine a new file. + ^X^V Same as :e. + :n * Examine the (_N-th) next file from the command line. + :p * Examine the (_N-th) previous file from the command line. + :x * Examine the first (or _N-th) file from the command line. + :d Delete the current file from the command line list. + = ^G :f Print current file name. + --------------------------------------------------------------------------- + + MMIISSCCEELLLLAANNEEOOUUSS CCOOMMMMAANNDDSS + + -_<_f_l_a_g_> Toggle a command line option [see OPTIONS below]. + --_<_n_a_m_e_> Toggle a command line option, by name. + __<_f_l_a_g_> Display the setting of a command line option. + ___<_n_a_m_e_> Display the setting of an option, by name. + +_c_m_d Execute the less cmd each time a new file is examined. + + !_c_o_m_m_a_n_d Execute the shell command with $SHELL. + |XX_c_o_m_m_a_n_d Pipe file between current pos & mark XX to shell command. + s _f_i_l_e Save input to a file. + v Edit the current file with $VISUAL or $EDITOR. + V Print version number of "less". + --------------------------------------------------------------------------- + + OOPPTTIIOONNSS + + Most options may be changed either on the command line, + or from within less by using the - or -- command. + Options may be given in one of two forms: either a single + character preceded by a -, or a name preceded by --. + + -? ........ --help + Display help (from command line). + -a ........ --search-skip-screen + Search skips current screen. + -A ........ --SEARCH-SKIP-SCREEN + Search starts just after target line. + -b [_N] .... --buffers=[_N] + Number of buffers. + -B ........ --auto-buffers + Don't automatically allocate buffers for pipes. + -c ........ --clear-screen + Repaint by clearing rather than scrolling. + -d ........ --dumb + Dumb terminal. + -D xx_c_o_l_o_r . --color=xx_c_o_l_o_r + Set screen colors. + -e -E .... --quit-at-eof --QUIT-AT-EOF + Quit at end of file. + -f ........ --force + Force open non-regular files. + -F ........ --quit-if-one-screen + Quit if entire file fits on first screen. + -g ........ --hilite-search + Highlight only last match for searches. + -G ........ --HILITE-SEARCH + Don't highlight any matches for searches. + -h [_N] .... --max-back-scroll=[_N] + Backward scroll limit. + -i ........ --ignore-case + Ignore case in searches that do not contain uppercase. + -I ........ --IGNORE-CASE + Ignore case in all searches. + -j [_N] .... --jump-target=[_N] + Screen position of target lines. + -J ........ --status-column + Display a status column at left edge of screen. + -k [_f_i_l_e] . --lesskey-file=[_f_i_l_e] + Use a lesskey file. + -K ........ --quit-on-intr + Exit less in response to ctrl-C. + -L ........ --no-lessopen + Ignore the LESSOPEN environment variable. + -m -M .... --long-prompt --LONG-PROMPT + Set prompt style. + -n -N .... --line-numbers --LINE-NUMBERS + Don't use line numbers. + -o [_f_i_l_e] . --log-file=[_f_i_l_e] + Copy to log file (standard input only). + -O [_f_i_l_e] . --LOG-FILE=[_f_i_l_e] + Copy to log file (unconditionally overwrite). + -p [_p_a_t_t_e_r_n] --pattern=[_p_a_t_t_e_r_n] + Start at pattern (from command line). + -P [_p_r_o_m_p_t] --prompt=[_p_r_o_m_p_t] + Define new prompt. + -q -Q .... --quiet --QUIET --silent --SILENT + Quiet the terminal bell. + -r -R .... --raw-control-chars --RAW-CONTROL-CHARS + Output "raw" control characters. + -s ........ --squeeze-blank-lines + Squeeze multiple blank lines. + -S ........ --chop-long-lines + Chop (truncate) long lines rather than wrapping. + -t [_t_a_g] .. --tag=[_t_a_g] + Find a tag. + -T [_t_a_g_s_f_i_l_e] --tag-file=[_t_a_g_s_f_i_l_e] + Use an alternate tags file. + -u -U .... --underline-special --UNDERLINE-SPECIAL + Change handling of backspaces. + -V ........ --version + Display the version number of "less". + -w ........ --hilite-unread + Highlight first new line after forward-screen. + -W ........ --HILITE-UNREAD + Highlight first new line after any forward movement. + -x [_N[,...]] --tabs=[_N[,...]] + Set tab stops. + -X ........ --no-init + Don't use termcap init/deinit strings. + -y [_N] .... --max-forw-scroll=[_N] + Forward scroll limit. + -z [_N] .... --window=[_N] + Set size of window. + -" [_c[_c]] . --quotes=[_c[_c]] + Set shell quote characters. + -~ ........ --tilde + Don't display tildes after end of file. + -# [_N] .... --shift=[_N] + Set horizontal scroll amount (0 = one half screen width). + --file-size + Automatically determine the size of the input file. + --follow-name + The F command changes files if the input file is renamed. + --incsearch + Search file as each pattern character is typed in. + --line-num-width=N + Set the width of the -N line number field to N characters. + --mouse + Enable mouse input. + --no-keypad + Don't send termcap keypad init/deinit strings. + --no-histdups + Remove duplicates from command history. + --rscroll=C + Set the character used to mark truncated lines. + --save-marks + Retain marks across invocations of less. + --status-col-width=N + Set the width of the -J status column to N characters. + --use-backslash + Subsequent options use backslash as escape char. + --use-color + Enables colored text. + --wheel-lines=N + Each click of the mouse wheel moves N lines. + + + --------------------------------------------------------------------------- + + LLIINNEE EEDDIITTIINNGG + + These keys can be used to edit text being entered + on the "command line" at the bottom of the screen. + + RightArrow ..................... ESC-l ... Move cursor right one character. + LeftArrow ...................... ESC-h ... Move cursor left one character. + ctrl-RightArrow ESC-RightArrow ESC-w ... Move cursor right one word. + ctrl-LeftArrow ESC-LeftArrow ESC-b ... Move cursor left one word. + HOME ........................... ESC-0 ... Move cursor to start of line. + END ............................ ESC-$ ... Move cursor to end of line. + BACKSPACE ................................ Delete char to left of cursor. + DELETE ......................... ESC-x ... Delete char under cursor. + ctrl-BACKSPACE ESC-BACKSPACE ........... Delete word to left of cursor. + ctrl-DELETE .... ESC-DELETE .... ESC-X ... Delete word under cursor. + ctrl-U ......... ESC (MS-DOS only) ....... Delete entire line. + UpArrow ........................ ESC-k ... Retrieve previous command line. + DownArrow ...................... ESC-j ... Retrieve next command line. + TAB ...................................... Complete filename & cycle. + SHIFT-TAB ...................... ESC-TAB Complete filename & reverse cycle. + ctrl-L ................................... Complete filename, list all.