From 84d5aff73fb5aaf501fba425f9464202d2346e16 Mon Sep 17 00:00:00 2001 From: Yume <2839681263@qq.com> Date: Sun, 10 Aug 2025 18:36:53 +0800 Subject: [PATCH 1/3] feat(UI): add SSH_keys setting --- moon/apps/web/components/Setting/SSHKeys.tsx | 180 +++++++++++++++++++ moon/apps/web/hooks/useDeleteSSHKeyById.ts | 9 + moon/apps/web/hooks/useGetSSHList.ts | 32 ++++ moon/apps/web/hooks/usePostSSHKey.ts | 9 + moon/apps/web/pages/me/settings/index.tsx | 45 +++-- 5 files changed, 250 insertions(+), 25 deletions(-) create mode 100644 moon/apps/web/components/Setting/SSHKeys.tsx create mode 100644 moon/apps/web/hooks/useDeleteSSHKeyById.ts create mode 100644 moon/apps/web/hooks/useGetSSHList.ts create mode 100644 moon/apps/web/hooks/usePostSSHKey.ts diff --git a/moon/apps/web/components/Setting/SSHKeys.tsx b/moon/apps/web/components/Setting/SSHKeys.tsx new file mode 100644 index 000000000..0f28eda44 --- /dev/null +++ b/moon/apps/web/components/Setting/SSHKeys.tsx @@ -0,0 +1,180 @@ +import React, {useState} from 'react'; +import {LoadingSpinner, LockIcon, Button, TextField, PlusIcon} from '@gitmono/ui' +import * as Dialog from '@gitmono/ui/src/Dialog' +import {ListSSHKey} from "@gitmono/types"; +import {useGetSSHList} from '@/hooks/useGetSSHList' +import {usePostSSHKey} from '@/hooks/usePostSSHKey' +import {useDeleteSSHKeyById} from '@/hooks/useDeleteSSHKeyById' +import {legacyApiClient} from "@/utils/queryClient"; +import {useQueryClient} from "@tanstack/react-query"; +import HandleTime from "@/components/MrView/components/HandleTime"; + +const SshKeyItem = ({keyData}: { keyData: ListSSHKey }) => { + const {mutate: deleteSSHKey} = useDeleteSSHKeyById() + const fetchSSHList = legacyApiClient.v1.getApiUserSshList() + const queryClient = useQueryClient() + + return ( +
+
+
+ +
+ ) +} + +const NewSSHKeyDialog = ({open, setOpen}) => { + const {mutate: postSSHKey, isPending} = usePostSSHKey() + const [title, setTitle] = useState('') + const [sshKey, setSshKey] = useState('') + const [errors, setErrors] = useState<{ title?: string; sshKey?: string }>({}) + + const fetchSSHList = legacyApiClient.v1.getApiUserSshList() + const queryClient = useQueryClient() + + const handleSubmit = (e?: React.FormEvent | React.MouseEvent) => { + if (e) e.preventDefault() + const nextErrors: { title?: string; sshKey?: string } = {} + + if (!title.trim()) nextErrors.title = 'Title is required' + if (!sshKey.trim()) nextErrors.sshKey = 'SSH key is required' + setErrors(nextErrors) + if (Object.keys(nextErrors).length > 0) return + postSSHKey( + {data: {title: title.trim(), ssh_key: sshKey}}, + { + onSuccess: () => { + setOpen(false) + setTitle('') + setSshKey('') + setErrors({}) + + queryClient.invalidateQueries({queryKey: fetchSSHList.requestKey()}) + } + } + ) + } + + return ( + + + Add SSH key + + +
+ + {errors.title && {errors.title}} +
+ +
+ + {errors.sshKey && {errors.sshKey}} +
+
+ + + + + + + +
+ ) +} + +const SSHKeys = () => { + const {sshKeys, isLoading} = useGetSSHList() + const [open, setOpen] = useState(false) + + return ( + <> +
+
+

SSH keys

+ +
+ +

+ This is a list of SSH keys associated with your account. Remove any keys that you do not recognize. +

+ +
+

+ Authentication keys +

+ {isLoading ? ( +
+ +
+ ) : ( +
+ {sshKeys?.map((key) => ( + + ))} +
+ )} +
+
+ + + ); +}; + +export default SSHKeys; \ No newline at end of file diff --git a/moon/apps/web/hooks/useDeleteSSHKeyById.ts b/moon/apps/web/hooks/useDeleteSSHKeyById.ts new file mode 100644 index 000000000..c74ffa8ff --- /dev/null +++ b/moon/apps/web/hooks/useDeleteSSHKeyById.ts @@ -0,0 +1,9 @@ +import { useMutation } from '@tanstack/react-query' +import { DeleteApiUserSshByKeyIdData } from '@gitmono/types' +import { legacyApiClient } from '@/utils/queryClient' + +export function useDeleteSSHKeyById() { + return useMutation({ + mutationFn: ({ keyId }) => legacyApiClient.v1.deleteApiUserSshByKeyId().request(keyId) + }) +} \ No newline at end of file diff --git a/moon/apps/web/hooks/useGetSSHList.ts b/moon/apps/web/hooks/useGetSSHList.ts new file mode 100644 index 000000000..ba3d2b20d --- /dev/null +++ b/moon/apps/web/hooks/useGetSSHList.ts @@ -0,0 +1,32 @@ +import {legacyApiClient} from "@/utils/queryClient"; +import {atomFamily} from "jotai/utils"; +import {atomWithWebStorage} from "@/utils/atomWithWebStorage"; +import {ListSSHKey} from "@gitmono/types"; +import {useAtom} from "jotai"; +import { useQuery } from '@tanstack/react-query' +import { useMemo } from 'react' + +const fetchSSHList = legacyApiClient.v1.getApiUserSshList() +const getSSHListAtom = atomFamily(() => + atomWithWebStorage(`ssh-key`, []) +) + +export const useGetSSHList = () => { + const [, setSSHList] = useAtom(getSSHListAtom()) + + const { data, isLoading, isPending, isFetching } = useQuery({ + queryKey: fetchSSHList.requestKey(), + queryFn: async () => { + const result = await fetchSSHList.request() + + return result.data + }, + }); + + const sshKeys = useMemo(() => { + setSSHList(data); + return data ?? [] + }, [data, setSSHList]); + + return { sshKeys, isLoading, isPending, isFetching } +} \ No newline at end of file diff --git a/moon/apps/web/hooks/usePostSSHKey.ts b/moon/apps/web/hooks/usePostSSHKey.ts new file mode 100644 index 000000000..7bd1461b3 --- /dev/null +++ b/moon/apps/web/hooks/usePostSSHKey.ts @@ -0,0 +1,9 @@ +import { useMutation } from '@tanstack/react-query'; +import { AddSSHKey, PostApiUserSshData, RequestParams } from '@gitmono/types' +import { legacyApiClient } from '@/utils/queryClient' + +export function usePostSSHKey() { + return useMutation({ + mutationFn: ({ data, params }) => legacyApiClient.v1.postApiUserSsh().request(data, params) + }) +} \ No newline at end of file diff --git a/moon/apps/web/pages/me/settings/index.tsx b/moon/apps/web/pages/me/settings/index.tsx index 9a1c1d30f..191e60f0d 100644 --- a/moon/apps/web/pages/me/settings/index.tsx +++ b/moon/apps/web/pages/me/settings/index.tsx @@ -1,25 +1,22 @@ -import { useEffect } from 'react' -import { ProfileDisplay } from 'components/UserSettings/ProfileDisplay' -import { ProfileSecurity } from 'components/UserSettings/ProfileSecurity' -import { TwoFactorAuthentication } from 'components/UserSettings/TwoFactorAuthentication' -import Head from 'next/head' - -import { CopyCurrentUrl } from '@/components/CopyCurrentUrl' -import { DesktopAppUpsell } from '@/components/DesktopAppUpsell' -import AuthAppProviders from '@/components/Providers/AuthAppProviders' -import { PWAUpsell } from '@/components/PWAUpsell' -import { ThemePicker } from '@/components/ThemePicker' -import { Behaviors } from '@/components/UserSettings/Behaviors' -import { CalDotComIntegration } from '@/components/UserSettings/CalDotComIntegration' -import { FigmaIntegration } from '@/components/UserSettings/FigmaIntegration' -import { NotificationSettings } from '@/components/UserSettings/Notifications/NotificationSettings' -import { PushNotificationSettings } from '@/components/UserSettings/Notifications/PushNotificationSettings' -import { NotificationSchedule } from '@/components/UserSettings/NotificationSchedule' -import { UserSettingsPageWrapper } from '@/components/UserSettings/PageWrapper' -import { PersonalCallLinks } from '@/components/UserSettings/PersonalCallLinks' -import { SlackNotificationSettings } from '@/components/UserSettings/SlackNotificationSettings' +import { useEffect } from 'react'; +import { ProfileDisplay } from 'components/UserSettings/ProfileDisplay'; +import { ProfileSecurity } from 'components/UserSettings/ProfileSecurity'; +import { TwoFactorAuthentication } from 'components/UserSettings/TwoFactorAuthentication'; +import Head from 'next/head'; +import { CopyCurrentUrl } from '@/components/CopyCurrentUrl'; +import AuthAppProviders from '@/components/Providers/AuthAppProviders'; +import SSHKeys from '@/components/Setting/SSHKeys'; +import PersonalToken from '@/components/Setting/PersonalToken'; +import { ThemePicker } from '@/components/ThemePicker'; +import { Behaviors } from '@/components/UserSettings/Behaviors'; +import { NotificationSettings } from '@/components/UserSettings/Notifications/NotificationSettings'; +import { PushNotificationSettings } from '@/components/UserSettings/Notifications/PushNotificationSettings'; +import { NotificationSchedule } from '@/components/UserSettings/NotificationSchedule'; +import { UserSettingsPageWrapper } from '@/components/UserSettings/PageWrapper'; +import { PersonalCallLinks } from '@/components/UserSettings/PersonalCallLinks'; +import { SlackNotificationSettings } from '@/components/UserSettings/SlackNotificationSettings'; import { Timezone } from '@/components/UserSettings/Timezone' -import { PageWithProviders } from '@/utils/types' +import { PageWithProviders } from '@/utils/types'; const UserSettingsPage: PageWithProviders = () => { useEffect(() => { @@ -45,10 +42,8 @@ const UserSettingsPage: PageWithProviders = () => { - - - - + + From b8ae0cacdc74e98ab02dad94b309472929cab36a Mon Sep 17 00:00:00 2001 From: Yume <2839681263@qq.com> Date: Sun, 10 Aug 2025 18:37:46 +0800 Subject: [PATCH 2/3] feat(UI): add Personal-Token setting --- .../web/components/Setting/PersonalToken.tsx | 146 ++++++++++++++++++ moon/apps/web/hooks/useDeleteTokenById.ts | 9 ++ moon/apps/web/hooks/useGetTokenList.ts | 37 +++++ moon/apps/web/hooks/usePostTokenGenerate.ts | 9 ++ 4 files changed, 201 insertions(+) create mode 100644 moon/apps/web/components/Setting/PersonalToken.tsx create mode 100644 moon/apps/web/hooks/useDeleteTokenById.ts create mode 100644 moon/apps/web/hooks/useGetTokenList.ts create mode 100644 moon/apps/web/hooks/usePostTokenGenerate.ts diff --git a/moon/apps/web/components/Setting/PersonalToken.tsx b/moon/apps/web/components/Setting/PersonalToken.tsx new file mode 100644 index 000000000..a5003e529 --- /dev/null +++ b/moon/apps/web/components/Setting/PersonalToken.tsx @@ -0,0 +1,146 @@ +import React, {useState} from 'react' +import {LoadingSpinner, LockIcon, Button, PlusIcon} from '@gitmono/ui' +import {useGetTokenList} from '@/hooks/useGetTokenList' +import {usePostTokenGenerate} from '@/hooks/usePostTokenGenerate' +import {useDeleteTokenById} from '@/hooks/useDeleteTokenById' +import {useQueryClient} from '@tanstack/react-query' +import {legacyApiClient} from '@/utils/queryClient' +import {ListToken} from '@gitmono/types' +import toast from "react-hot-toast"; +import HandleTime from "@/components/MrView/components/HandleTime"; + +const TokenItem = ({item}: { item: ListToken }) => { + const {mutate: deleteToken} = useDeleteTokenById() + const queryClient = useQueryClient() + const fetchTokenList = legacyApiClient.v1.getApiUserTokenList() + + return ( +
+
+
+ +
+ ) +} + +const PersonalToken = () => { + const {tokenList, isLoading} = useGetTokenList() + const {mutate: generateToken, isPending: isGenerating} = usePostTokenGenerate() + const queryClient = useQueryClient() + const fetchTokenList = legacyApiClient.v1.getApiUserTokenList() + + const [generated, setGenerated] = useState(null) + const [copied, setCopied] = useState(false) + + const handleGenerate = () => { + generateToken(undefined, { + onSuccess: (result) => { + setGenerated(result?.data ?? null) + queryClient.invalidateQueries({queryKey: fetchTokenList.requestKey()}) + } + }) + } + + const handleCopy = async () => { + if (!generated) return + if (navigator.clipboard) { + await navigator.clipboard + .writeText(generated) + .then(() => toast.success("Copied to clipboard")) + .catch(() => toast.error("Copied failed")) + } else { + const textArea = document + .createElement('textarea') + + textArea.value = generated + document.body.appendChild(textArea) + textArea.select() + try { + document.execCommand('copy') + toast.success('Copied to clipboard') + document.body.removeChild(textArea) + } catch { + toast.error("Copied failed") + } + } + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + + return ( +
+
+

Personal tokens

+ +
+ +

+ This is a list of personal access tokens associated with your account. Remove any tokens that you do not + recognize. +

+ + {generated && ( +
+

Your new token has been generated.

+
+ {generated} + +
+

Make sure to copy your new token now. You won’t be able to see it + again.

+
+ )} + +
+

Tokens

+ {isLoading ? ( +
+ +
+ ) : ( +
+ {tokenList.map((item) => ( + + ))} +
+ )} +
+
+ ) +} + +export default PersonalToken diff --git a/moon/apps/web/hooks/useDeleteTokenById.ts b/moon/apps/web/hooks/useDeleteTokenById.ts new file mode 100644 index 000000000..4621ad81c --- /dev/null +++ b/moon/apps/web/hooks/useDeleteTokenById.ts @@ -0,0 +1,9 @@ +import { useMutation } from '@tanstack/react-query' +import { DeleteApiUserTokenByKeyIdData } from '@gitmono/types' +import { legacyApiClient } from '@/utils/queryClient' + +export function useDeleteTokenById() { + return useMutation({ + mutationFn: ({ keyId }) => legacyApiClient.v1.deleteApiUserTokenByKeyId().request(keyId) + }) +} \ No newline at end of file diff --git a/moon/apps/web/hooks/useGetTokenList.ts b/moon/apps/web/hooks/useGetTokenList.ts new file mode 100644 index 000000000..1d714fdc1 --- /dev/null +++ b/moon/apps/web/hooks/useGetTokenList.ts @@ -0,0 +1,37 @@ +import { atomFamily } from 'jotai/utils' +import { atomWithWebStorage } from '@/utils/atomWithWebStorage' +import { legacyApiClient } from '@/utils/queryClient' +import { useQuery } from '@tanstack/react-query' +import { useAtom } from 'jotai' +import { useMemo } from 'react' +import { ListToken } from '@gitmono/types' + +const fetchTokenList = legacyApiClient.v1.getApiUserTokenList() +const getTokenListAtom = atomFamily(() => + atomWithWebStorage(`token`, []) +) + +export const useGetTokenList = () => { + const [, setTokenList] = useAtom(getTokenListAtom()) + + const {data, isLoading, isPending, isFetching} = useQuery({ + queryKey: fetchTokenList.requestKey(), + queryFn: async () => { + const result = await fetchTokenList.request() + + return result.data + }, + }) + + const tokenList = useMemo(() => { + setTokenList(data) + return data ?? [] + }, [data, setTokenList]) + + return { + tokenList, + isLoading, + isPending, + isFetching + } +} diff --git a/moon/apps/web/hooks/usePostTokenGenerate.ts b/moon/apps/web/hooks/usePostTokenGenerate.ts new file mode 100644 index 000000000..3511d1119 --- /dev/null +++ b/moon/apps/web/hooks/usePostTokenGenerate.ts @@ -0,0 +1,9 @@ +import { useMutation } from '@tanstack/react-query' +import { PostApiUserTokenGenerateData } from '@gitmono/types' +import { legacyApiClient } from '@/utils/queryClient' + +export function usePostTokenGenerate() { + return useMutation({ + mutationFn: () => legacyApiClient.v1.postApiUserTokenGenerate().request() + }) +} \ No newline at end of file From d06402bb2605dcf3c7a023798c29cf2416901ceb Mon Sep 17 00:00:00 2001 From: Yume <2839681263@qq.com> Date: Sun, 10 Aug 2025 20:32:09 +0800 Subject: [PATCH 3/3] fix(UI): fix missing types --- moon/apps/web/components/Setting/SSHKeys.tsx | 8 ++++++-- moon/apps/web/hooks/useGetSSHList.ts | 11 ++++++----- moon/apps/web/hooks/useGetTokenList.ts | 13 +++++++------ 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/moon/apps/web/components/Setting/SSHKeys.tsx b/moon/apps/web/components/Setting/SSHKeys.tsx index 0f28eda44..48dbe649f 100644 --- a/moon/apps/web/components/Setting/SSHKeys.tsx +++ b/moon/apps/web/components/Setting/SSHKeys.tsx @@ -43,7 +43,12 @@ const SshKeyItem = ({keyData}: { keyData: ListSSHKey }) => { ) } -const NewSSHKeyDialog = ({open, setOpen}) => { +interface NewSSHKeyDialogProps { + open: boolean; + setOpen: (open: boolean) => void; +} + +const NewSSHKeyDialog = ({open, setOpen}: NewSSHKeyDialogProps) => { const {mutate: postSSHKey, isPending} = usePostSSHKey() const [title, setTitle] = useState('') const [sshKey, setSshKey] = useState('') @@ -97,7 +102,6 @@ const NewSSHKeyDialog = ({open, setOpen}) => {
@@ -12,7 +12,7 @@ const getSSHListAtom = atomFamily(() => ) export const useGetSSHList = () => { - const [, setSSHList] = useAtom(getSSHListAtom()) + const [sshKeys, setSSHList] = useAtom(getSSHListAtom('ssh-key')) const { data, isLoading, isPending, isFetching } = useQuery({ queryKey: fetchSSHList.requestKey(), @@ -23,9 +23,10 @@ export const useGetSSHList = () => { }, }); - const sshKeys = useMemo(() => { - setSSHList(data); - return data ?? [] + useEffect(() => { + if(data){ + setSSHList(data) + } }, [data, setSSHList]); return { sshKeys, isLoading, isPending, isFetching } diff --git a/moon/apps/web/hooks/useGetTokenList.ts b/moon/apps/web/hooks/useGetTokenList.ts index 1d714fdc1..d0f7be6b3 100644 --- a/moon/apps/web/hooks/useGetTokenList.ts +++ b/moon/apps/web/hooks/useGetTokenList.ts @@ -3,7 +3,7 @@ import { atomWithWebStorage } from '@/utils/atomWithWebStorage' import { legacyApiClient } from '@/utils/queryClient' import { useQuery } from '@tanstack/react-query' import { useAtom } from 'jotai' -import { useMemo } from 'react' +import {useEffect} from 'react' import { ListToken } from '@gitmono/types' const fetchTokenList = legacyApiClient.v1.getApiUserTokenList() @@ -12,7 +12,7 @@ const getTokenListAtom = atomFamily(() => ) export const useGetTokenList = () => { - const [, setTokenList] = useAtom(getTokenListAtom()) + const [tokenList, setTokenList] = useAtom(getTokenListAtom('token')) const {data, isLoading, isPending, isFetching} = useQuery({ queryKey: fetchTokenList.requestKey(), @@ -23,10 +23,11 @@ export const useGetTokenList = () => { }, }) - const tokenList = useMemo(() => { - setTokenList(data) - return data ?? [] - }, [data, setTokenList]) + useEffect(() => { + if(data){ + setTokenList(data) + } + }, [data, setTokenList]); return { tokenList,