Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
canifa_note
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Vũ Hoàng Anh
canifa_note
Commits
c54fcf7a
Unverified
Commit
c54fcf7a
authored
Nov 08, 2025
by
Johnny
Committed by
GitHub
Nov 08, 2025
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
refactor(web): redesign Settings components (#5237)
Co-authored-by:
Claude
<
noreply@anthropic.com
>
parent
805bb4e7
Changes
14
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
829 additions
and
680 deletions
+829
-680
AccessTokenSection.tsx
web/src/components/Settings/AccessTokenSection.tsx
+57
-71
InstanceSection.tsx
web/src/components/Settings/InstanceSection.tsx
+97
-95
MemberSection.tsx
web/src/components/Settings/MemberSection.tsx
+70
-73
MemoRelatedSettings.tsx
web/src/components/Settings/MemoRelatedSettings.tsx
+99
-93
MyAccountSection.tsx
web/src/components/Settings/MyAccountSection.tsx
+38
-30
PreferencesSection.tsx
web/src/components/Settings/PreferencesSection.tsx
+37
-38
SSOSection.tsx
web/src/components/Settings/SSOSection.tsx
+54
-41
SettingGroup.tsx
web/src/components/Settings/SettingGroup.tsx
+34
-0
SettingRow.tsx
web/src/components/Settings/SettingRow.tsx
+45
-0
SettingSection.tsx
web/src/components/Settings/SettingSection.tsx
+35
-0
SettingTable.tsx
web/src/components/Settings/SettingTable.tsx
+69
-0
StorageSection.tsx
web/src/components/Settings/StorageSection.tsx
+68
-85
UserSessionsSection.tsx
web/src/components/Settings/UserSessionsSection.tsx
+81
-97
WebhookSection.tsx
web/src/components/Settings/WebhookSection.tsx
+45
-57
No files found.
web/src/components/Settings/AccessTokenSection.tsx
View file @
c54fcf7a
import
copy
from
"copy-to-clipboard"
;
import
copy
from
"copy-to-clipboard"
;
import
{
ClipboardIcon
,
TrashIcon
}
from
"lucide-react"
;
import
{
ClipboardIcon
,
PlusIcon
,
TrashIcon
}
from
"lucide-react"
;
import
{
useEffect
,
useState
}
from
"react"
;
import
{
useEffect
,
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
{
toast
}
from
"react-hot-toast"
;
import
ConfirmDialog
from
"@/components/ConfirmDialog"
;
import
ConfirmDialog
from
"@/components/ConfirmDialog"
;
...
@@ -10,6 +10,7 @@ import { useDialog } from "@/hooks/useDialog";
...
@@ -10,6 +10,7 @@ import { useDialog } from "@/hooks/useDialog";
import
{
UserAccessToken
}
from
"@/types/proto/api/v1/user_service"
;
import
{
UserAccessToken
}
from
"@/types/proto/api/v1/user_service"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
CreateAccessTokenDialog
from
"../CreateAccessTokenDialog"
;
import
CreateAccessTokenDialog
from
"../CreateAccessTokenDialog"
;
import
SettingTable
from
"./SettingTable"
;
const
listAccessTokens
=
async
(
parent
:
string
)
=>
{
const
listAccessTokens
=
async
(
parent
:
string
)
=>
{
const
{
accessTokens
}
=
await
userServiceClient
.
listUserAccessTokens
({
parent
});
const
{
accessTokens
}
=
await
userServiceClient
.
listUserAccessTokens
({
parent
});
...
@@ -63,79 +64,64 @@ const AccessTokenSection = () => {
...
@@ -63,79 +64,64 @@ const AccessTokenSection = () => {
};
};
return
(
return
(
<
div
className=
"mt-6 w-full flex flex-col justify-start items-start space-y-4"
>
<
div
className=
"w-full flex flex-col gap-2"
>
<
div
className=
"w-full"
>
<
div
className=
"flex flex-col sm:flex-row sm:items-start sm:justify-between gap-2"
>
<
div
className=
"sm:flex sm:items-center sm:justify-between"
>
<
div
className=
"flex flex-col gap-1"
>
<
div
className=
"sm:flex-auto space-y-1"
>
<
h4
className=
"text-sm font-medium text-muted-foreground"
>
{
t
(
"setting.access-token-section.title"
)
}
</
h4
>
<
p
className=
"flex flex-row justify-start items-center font-medium text-muted-foreground"
>
<
p
className=
"text-xs text-muted-foreground"
>
{
t
(
"setting.access-token-section.description"
)
}
</
p
>
{
t
(
"setting.access-token-section.title"
)
}
</
p
>
<
p
className=
"text-sm text-muted-foreground"
>
{
t
(
"setting.access-token-section.description"
)
}
</
p
>
</
div
>
<
div
className=
"mt-4 sm:mt-0"
>
<
Button
color=
"primary"
onClick=
{
handleCreateToken
}
>
{
t
(
"common.create"
)
}
</
Button
>
</
div
>
</
div
>
<
div
className=
"w-full mt-2 flow-root"
>
<
div
className=
"overflow-x-auto"
>
<
div
className=
"inline-block min-w-full border border-border rounded-lg align-middle"
>
<
table
className=
"min-w-full divide-y divide-border"
>
<
thead
>
<
tr
>
<
th
scope=
"col"
className=
"px-3 py-2 text-left text-sm font-semibold text-foreground"
>
{
t
(
"setting.access-token-section.token"
)
}
</
th
>
<
th
scope=
"col"
className=
"py-2 pl-4 pr-3 text-left text-sm font-semibold text-foreground"
>
{
t
(
"common.description"
)
}
</
th
>
<
th
scope=
"col"
className=
"px-3 py-2 text-left text-sm font-semibold text-foreground"
>
{
t
(
"setting.access-token-section.create-dialog.created-at"
)
}
</
th
>
<
th
scope=
"col"
className=
"px-3 py-2 text-left text-sm font-semibold text-foreground"
>
{
t
(
"setting.access-token-section.create-dialog.expires-at"
)
}
</
th
>
<
th
scope=
"col"
className=
"relative py-3.5 pl-3 pr-4"
>
<
span
className=
"sr-only"
>
{
t
(
"common.delete"
)
}
</
span
>
</
th
>
</
tr
>
</
thead
>
<
tbody
className=
"divide-y divide-border"
>
{
userAccessTokens
.
map
((
userAccessToken
)
=>
(
<
tr
key=
{
userAccessToken
.
accessToken
}
>
<
td
className=
"whitespace-nowrap px-3 py-2 text-sm text-foreground flex flex-row justify-start items-center gap-x-1"
>
<
span
className=
"font-mono"
>
{
getFormatedAccessToken
(
userAccessToken
.
accessToken
)
}
</
span
>
<
Button
variant=
"ghost"
onClick=
{
()
=>
copyAccessToken
(
userAccessToken
.
accessToken
)
}
>
<
ClipboardIcon
className=
"w-4 h-auto text-muted-foreground"
/>
</
Button
>
</
td
>
<
td
className=
"whitespace-nowrap py-2 pl-4 pr-3 text-sm text-foreground"
>
{
userAccessToken
.
description
}
</
td
>
<
td
className=
"whitespace-nowrap px-3 py-2 text-sm text-muted-foreground"
>
{
userAccessToken
.
issuedAt
?.
toLocaleString
()
}
</
td
>
<
td
className=
"whitespace-nowrap px-3 py-2 text-sm text-muted-foreground"
>
{
userAccessToken
.
expiresAt
?.
toLocaleString
()
??
t
(
"setting.access-token-section.create-dialog.duration-never"
)
}
</
td
>
<
td
className=
"relative whitespace-nowrap py-2 pl-3 pr-4 text-right text-sm"
>
<
Button
variant=
"ghost"
onClick=
{
()
=>
{
handleDeleteAccessToken
(
userAccessToken
);
}
}
>
<
TrashIcon
className=
"text-destructive w-4 h-auto"
/>
</
Button
>
</
td
>
</
tr
>
))
}
</
tbody
>
</
table
>
</
div
>
</
div
>
</
div
>
</
div
>
<
Button
onClick=
{
handleCreateToken
}
size=
"sm"
>
<
PlusIcon
className=
"w-4 h-4 mr-1.5"
/>
{
t
(
"common.create"
)
}
</
Button
>
</
div
>
</
div
>
<
SettingTable
columns=
{
[
{
key
:
"accessToken"
,
header
:
t
(
"setting.access-token-section.token"
),
render
:
(
_
,
token
:
UserAccessToken
)
=>
(
<
div
className=
"flex items-center gap-1"
>
<
span
className=
"font-mono text-foreground"
>
{
getFormatedAccessToken
(
token
.
accessToken
)
}
</
span
>
<
Button
variant=
"ghost"
size=
"sm"
onClick=
{
()
=>
copyAccessToken
(
token
.
accessToken
)
}
>
<
ClipboardIcon
className=
"w-4 h-auto text-muted-foreground"
/>
</
Button
>
</
div
>
),
},
{
key
:
"description"
,
header
:
t
(
"common.description"
),
render
:
(
_
,
token
:
UserAccessToken
)
=>
<
span
className=
"text-foreground"
>
{
token
.
description
}
</
span
>,
},
{
key
:
"issuedAt"
,
header
:
t
(
"setting.access-token-section.create-dialog.created-at"
),
render
:
(
_
,
token
:
UserAccessToken
)
=>
token
.
issuedAt
?.
toLocaleString
(),
},
{
key
:
"expiresAt"
,
header
:
t
(
"setting.access-token-section.create-dialog.expires-at"
),
render
:
(
_
,
token
:
UserAccessToken
)
=>
token
.
expiresAt
?.
toLocaleString
()
??
t
(
"setting.access-token-section.create-dialog.duration-never"
),
},
{
key
:
"actions"
,
header
:
""
,
className
:
"text-right"
,
render
:
(
_
,
token
:
UserAccessToken
)
=>
(
<
Button
variant=
"ghost"
size=
"sm"
onClick=
{
()
=>
handleDeleteAccessToken
(
token
)
}
>
<
TrashIcon
className=
"text-destructive w-4 h-auto"
/>
</
Button
>
),
},
]
}
data=
{
userAccessTokens
}
emptyMessage=
"No access tokens found"
getRowKey=
{
(
token
)
=>
token
.
name
}
/>
{
/* Create Access Token Dialog */
}
{
/* Create Access Token Dialog */
}
<
CreateAccessTokenDialog
<
CreateAccessTokenDialog
open=
{
createTokenDialog
.
isOpen
}
open=
{
createTokenDialog
.
isOpen
}
...
...
web/src/components/Settings/InstanceSection.tsx
View file @
c54fcf7a
...
@@ -4,7 +4,6 @@ import { useEffect, useState } from "react";
...
@@ -4,7 +4,6 @@ import { useEffect, useState } from "react";
import
{
toast
}
from
"react-hot-toast"
;
import
{
toast
}
from
"react-hot-toast"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Select
,
SelectContent
,
SelectItem
,
SelectTrigger
,
SelectValue
}
from
"@/components/ui/select"
;
import
{
Select
,
SelectContent
,
SelectItem
,
SelectTrigger
,
SelectValue
}
from
"@/components/ui/select"
;
import
{
Separator
}
from
"@/components/ui/separator"
;
import
{
Switch
}
from
"@/components/ui/switch"
;
import
{
Switch
}
from
"@/components/ui/switch"
;
import
{
Textarea
}
from
"@/components/ui/textarea"
;
import
{
Textarea
}
from
"@/components/ui/textarea"
;
import
{
identityProviderServiceClient
}
from
"@/grpcweb"
;
import
{
identityProviderServiceClient
}
from
"@/grpcweb"
;
...
@@ -16,6 +15,9 @@ import { InstanceSetting_GeneralSetting, InstanceSetting_Key } from "@/types/pro
...
@@ -16,6 +15,9 @@ import { InstanceSetting_GeneralSetting, InstanceSetting_Key } from "@/types/pro
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
ThemeSelect
from
"../ThemeSelect"
;
import
ThemeSelect
from
"../ThemeSelect"
;
import
UpdateCustomizedProfileDialog
from
"../UpdateCustomizedProfileDialog"
;
import
UpdateCustomizedProfileDialog
from
"../UpdateCustomizedProfileDialog"
;
import
SettingGroup
from
"./SettingGroup"
;
import
SettingRow
from
"./SettingRow"
;
import
SettingSection
from
"./SettingSection"
;
const
InstanceSection
=
observer
(()
=>
{
const
InstanceSection
=
observer
(()
=>
{
const
t
=
useTranslate
();
const
t
=
useTranslate
();
...
@@ -67,99 +69,99 @@ const InstanceSection = observer(() => {
...
@@ -67,99 +69,99 @@ const InstanceSection = observer(() => {
};
};
return
(
return
(
<
div
className=
"w-full flex flex-col gap-2 pt-2 pb-4"
>
<
SettingSection
>
<
p
className=
"font-medium text-foreground"
>
{
t
(
"common.basic"
)
}
</
p
>
<
SettingGroup
title=
{
t
(
"common.basic"
)
}
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
SettingRow
label=
{
t
(
"setting.system-section.server-name"
)
}
description=
{
instanceGeneralSetting
.
customProfile
?.
title
||
"Memos"
}
>
<
div
>
<
Button
variant=
"outline"
onClick=
{
handleUpdateCustomizedProfileButtonClick
}
>
{
t
(
"setting.system-section.server-name"
)
}
:
{
" "
}
{
t
(
"common.edit"
)
}
<
span
className=
"font-mono font-bold"
>
{
instanceGeneralSetting
.
customProfile
?.
title
||
"Memos"
}
</
spa
n
>
<
/
Butto
n
>
</
div
>
</
SettingRow
>
<
Button
variant=
"outline"
onClick=
{
handleUpdateCustomizedProfileButtonClick
}
>
</
SettingGroup
>
{
t
(
"common.edit"
)
}
</
Button
>
<
SettingGroup
title=
{
t
(
"setting.system-section.title"
)
}
showSeparator
>
</
div
>
<
SettingRow
label=
"Theme"
>
<
Separator
/>
<
ThemeSelect
<
p
className=
"font-medium text-foreground"
>
{
t
(
"setting.system-section.title"
)
}
</
p
>
value=
{
instanceGeneralSetting
.
theme
||
"default"
}
<
div
className=
"w-full flex flex-row justify-between items-center"
>
onValueChange=
{
(
value
:
string
)
=>
updatePartialSetting
({
theme
:
value
})
}
<
span
>
Theme
</
span
>
className=
"min-w-fit"
<
ThemeSelect
/>
value=
{
instanceGeneralSetting
.
theme
||
"default"
}
</
SettingRow
>
onValueChange=
{
(
value
:
string
)
=>
updatePartialSetting
({
theme
:
value
})
}
className=
"min-w-fit"
<
SettingRow
label=
{
t
(
"setting.system-section.additional-style"
)
}
vertical
>
/>
<
Textarea
</
div
>
className=
"font-mono w-full"
<
div
className=
"w-full flex flex-row justify-between items-center"
>
rows=
{
3
}
<
span
>
{
t
(
"setting.system-section.additional-style"
)
}
</
span
>
placeholder=
{
t
(
"setting.system-section.additional-style-placeholder"
)
}
</
div
>
value=
{
instanceGeneralSetting
.
additionalStyle
}
<
Textarea
onChange=
{
(
event
)
=>
updatePartialSetting
({
additionalStyle
:
event
.
target
.
value
})
}
className=
"font-mono w-full"
/>
rows=
{
3
}
</
SettingRow
>
placeholder=
{
t
(
"setting.system-section.additional-style-placeholder"
)
}
value=
{
instanceGeneralSetting
.
additionalStyle
}
<
SettingRow
label=
{
t
(
"setting.system-section.additional-script"
)
}
vertical
>
onChange=
{
(
event
)
=>
updatePartialSetting
({
additionalStyle
:
event
.
target
.
value
})
}
<
Textarea
/>
className=
"font-mono w-full"
<
div
className=
"w-full flex flex-row justify-between items-center"
>
rows=
{
3
}
<
span
>
{
t
(
"setting.system-section.additional-script"
)
}
</
span
>
placeholder=
{
t
(
"setting.system-section.additional-script-placeholder"
)
}
</
div
>
value=
{
instanceGeneralSetting
.
additionalScript
}
<
Textarea
onChange=
{
(
event
)
=>
updatePartialSetting
({
additionalScript
:
event
.
target
.
value
})
}
className=
"font-mono w-full"
/>
rows=
{
3
}
</
SettingRow
>
placeholder=
{
t
(
"setting.system-section.additional-script-placeholder"
)
}
</
SettingGroup
>
value=
{
instanceGeneralSetting
.
additionalScript
}
onChange=
{
(
event
)
=>
updatePartialSetting
({
additionalScript
:
event
.
target
.
value
})
}
<
SettingGroup
title=
{
t
(
"setting.instance-section.disallow-user-registration"
)
}
showSeparator
>
/
>
<
SettingRow
label=
{
t
(
"setting.instance-section.disallow-user-registration"
)
}
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
Switch
<
span
>
{
t
(
"setting.instance-section.disallow-user-registration"
)
}
</
span
>
disabled=
{
instanceStore
.
state
.
profile
.
mode
===
"demo"
}
<
Switch
checked=
{
instanceGeneralSetting
.
disallowUserRegistration
}
disabled=
{
instanceStore
.
state
.
profile
.
mode
===
"demo"
}
onCheckedChange=
{
(
checked
)
=>
updatePartialSetting
({
disallowUserRegistration
:
checked
})
}
checked=
{
instanceGeneralSetting
.
disallowUserRegistration
}
/>
onCheckedChange=
{
(
checked
)
=>
updatePartialSetting
({
disallowUserRegistration
:
checked
})
}
</
SettingRow
>
/>
</
div
>
<
SettingRow
label=
{
t
(
"setting.instance-section.disallow-password-auth"
)
}
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
Switch
<
span
>
{
t
(
"setting.instance-section.disallow-password-auth"
)
}
</
span
>
disabled=
{
<
Switch
instanceStore
.
state
.
profile
.
mode
===
"demo"
||
disabled=
{
(
identityProviderList
.
length
===
0
&&
!
instanceGeneralSetting
.
disallowPasswordAuth
)
instanceStore
.
state
.
profile
.
mode
===
"demo"
||
}
(
identityProviderList
.
length
===
0
&&
!
instanceGeneralSetting
.
disallowPasswordAuth
)
checked=
{
instanceGeneralSetting
.
disallowPasswordAuth
}
}
onCheckedChange=
{
(
checked
)
=>
updatePartialSetting
({
disallowPasswordAuth
:
checked
})
}
checked=
{
instanceGeneralSetting
.
disallowPasswordAuth
}
/>
onCheckedChange=
{
(
checked
)
=>
updatePartialSetting
({
disallowPasswordAuth
:
checked
})
}
</
SettingRow
>
/>
</
div
>
<
SettingRow
label=
{
t
(
"setting.instance-section.disallow-change-username"
)
}
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
Switch
<
span
>
{
t
(
"setting.instance-section.disallow-change-username"
)
}
</
span
>
checked=
{
instanceGeneralSetting
.
disallowChangeUsername
}
<
Switch
onCheckedChange=
{
(
checked
)
=>
updatePartialSetting
({
disallowChangeUsername
:
checked
})
}
checked=
{
instanceGeneralSetting
.
disallowChangeUsername
}
/>
onCheckedChange=
{
(
checked
)
=>
updatePartialSetting
({
disallowChangeUsername
:
checked
})
}
</
SettingRow
>
/>
</
div
>
<
SettingRow
label=
{
t
(
"setting.instance-section.disallow-change-nickname"
)
}
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
Switch
<
span
>
{
t
(
"setting.instance-section.disallow-change-nickname"
)
}
</
span
>
checked=
{
instanceGeneralSetting
.
disallowChangeNickname
}
<
Switch
onCheckedChange=
{
(
checked
)
=>
updatePartialSetting
({
disallowChangeNickname
:
checked
})
}
checked=
{
instanceGeneralSetting
.
disallowChangeNickname
}
/>
onCheckedChange=
{
(
checked
)
=>
updatePartialSetting
({
disallowChangeNickname
:
checked
})
}
</
SettingRow
>
/>
</
div
>
<
SettingRow
label=
{
t
(
"setting.instance-section.week-start-day"
)
}
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
Select
<
span
className=
"truncate"
>
{
t
(
"setting.instance-section.week-start-day"
)
}
</
span
>
value=
{
instanceGeneralSetting
.
weekStartDayOffset
.
toString
()
}
<
Select
onValueChange=
{
(
value
)
=>
{
value=
{
instanceGeneralSetting
.
weekStartDayOffset
.
toString
()
}
updatePartialSetting
({
weekStartDayOffset
:
parseInt
(
value
)
||
0
});
onValueChange=
{
(
value
)
=>
{
}
}
updatePartialSetting
({
weekStartDayOffset
:
parseInt
(
value
)
||
0
});
>
}
}
<
SelectTrigger
className=
"min-w-fit"
>
>
<
SelectValue
/
>
<
SelectTrigger
className=
"min-w-fit"
>
</
SelectTrigger
>
<
Select
Value
/
>
<
Select
Content
>
</
SelectTrigger
>
<
SelectItem
value=
"-1"
>
{
t
(
"setting.instance-section.saturday"
)
}
</
SelectItem
>
<
SelectContent
>
<
SelectItem
value=
"0"
>
{
t
(
"setting.instance-section.sunday"
)
}
</
SelectItem
>
<
SelectItem
value=
"-1"
>
{
t
(
"setting.instance-section.satur
day"
)
}
</
SelectItem
>
<
SelectItem
value=
"1"
>
{
t
(
"setting.instance-section.mon
day"
)
}
</
SelectItem
>
<
SelectItem
value=
"0"
>
{
t
(
"setting.instance-section.sunday"
)
}
</
SelectItem
>
<
/
SelectContent
>
<
SelectItem
value=
"1"
>
{
t
(
"setting.instance-section.monday"
)
}
</
SelectItem
>
</
Select
>
</
SelectContent
>
</
SettingRow
>
</
Select
>
</
SettingGroup
>
</
div
>
<
div
className=
"
mt-2
w-full flex justify-end"
>
<
div
className=
"w-full flex justify-end"
>
<
Button
disabled=
{
isEqual
(
instanceGeneralSetting
,
originalSetting
)
}
onClick=
{
handleSaveGeneralSetting
}
>
<
Button
disabled=
{
isEqual
(
instanceGeneralSetting
,
originalSetting
)
}
onClick=
{
handleSaveGeneralSetting
}
>
{
t
(
"common.save"
)
}
{
t
(
"common.save"
)
}
</
Button
>
</
Button
>
...
@@ -173,7 +175,7 @@ const InstanceSection = observer(() => {
...
@@ -173,7 +175,7 @@ const InstanceSection = observer(() => {
toast
.
success
(
"Profile updated successfully!"
);
toast
.
success
(
"Profile updated successfully!"
);
}
}
}
}
/>
/>
</
div
>
</
SettingSection
>
);
);
});
});
...
...
web/src/components/Settings/MemberSection.tsx
View file @
c54fcf7a
...
@@ -14,6 +14,8 @@ import { User, User_Role } from "@/types/proto/api/v1/user_service";
...
@@ -14,6 +14,8 @@ import { User, User_Role } from "@/types/proto/api/v1/user_service";
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
CreateUserDialog
from
"../CreateUserDialog"
;
import
CreateUserDialog
from
"../CreateUserDialog"
;
import
{
DropdownMenu
,
DropdownMenuContent
,
DropdownMenuItem
,
DropdownMenuTrigger
}
from
"../ui/dropdown-menu"
;
import
{
DropdownMenu
,
DropdownMenuContent
,
DropdownMenuItem
,
DropdownMenuTrigger
}
from
"../ui/dropdown-menu"
;
import
SettingSection
from
"./SettingSection"
;
import
SettingTable
from
"./SettingTable"
;
const
MemberSection
=
observer
(()
=>
{
const
MemberSection
=
observer
(()
=>
{
const
t
=
useTranslate
();
const
t
=
useTranslate
();
...
@@ -101,84 +103,79 @@ const MemberSection = observer(() => {
...
@@ -101,84 +103,79 @@ const MemberSection = observer(() => {
};
};
return
(
return
(
<
div
className=
"w-full flex flex-col gap-2 pt-2 pb-4"
>
<
SettingSection
<
div
className=
"w-full flex flex-row justify-between items-center"
>
title=
{
t
(
"setting.member-list"
)
}
<
p
className=
"font-medium text-muted-foreground"
>
{
t
(
"setting.member-section.create-a-member"
)
}
</
p
>
actions=
{
<
Button
onClick=
{
handleCreateUser
}
>
<
Button
onClick=
{
handleCreateUser
}
>
<
PlusIcon
className=
"w-4 h-4 mr-2"
/>
<
PlusIcon
className=
"w-4 h-4 mr-2"
/>
{
t
(
"common.create"
)
}
{
t
(
"common.create"
)
}
</
Button
>
</
Button
>
</
div
>
}
<
div
className=
"w-full flex flex-row justify-between items-center mt-6"
>
>
<
div
className=
"title-text"
>
{
t
(
"setting.member-list"
)
}
</
div
>
<
SettingTable
</
div
>
columns=
{
[
<
div
className=
"w-full overflow-x-auto"
>
{
<
div
className=
"inline-block min-w-full align-middle border border-border rounded-lg"
>
key
:
"username"
,
<
table
className=
"min-w-full divide-y divide-border"
>
header
:
t
(
"common.username"
),
<
thead
>
render
:
(
_
,
user
:
User
)
=>
(
<
tr
className=
"text-sm font-semibold text-left text-foreground"
>
<
span
className=
"text-foreground"
>
<
th
scope=
"col"
className=
"px-3 py-2"
>
{
user
.
username
}
{
t
(
"common.username"
)
}
{
user
.
state
===
State
.
ARCHIVED
&&
<
span
className=
"ml-2 italic text-muted-foreground"
>
(Archived)
</
span
>
}
</
th
>
</
span
>
<
th
scope=
"col"
className=
"px-3 py-2"
>
),
{
t
(
"common.role"
)
}
},
</
th
>
{
<
th
scope=
"col"
className=
"px-3 py-2"
>
key
:
"role"
,
{
t
(
"common.nickname"
)
}
header
:
t
(
"common.role"
),
</
th
>
render
:
(
_
,
user
:
User
)
=>
stringifyUserRole
(
user
.
role
),
<
th
scope=
"col"
className=
"px-3 py-2"
>
},
{
t
(
"common.email"
)
}
{
</
th
>
key
:
"displayName"
,
<
th
scope=
"col"
className=
"relative py-2 pl-3 pr-4"
></
th
>
header
:
t
(
"common.nickname"
),
</
tr
>
render
:
(
_
,
user
:
User
)
=>
user
.
displayName
,
</
thead
>
},
<
tbody
className=
"divide-y divide-border"
>
{
{
sortedUsers
.
map
((
user
)
=>
(
key
:
"email"
,
<
tr
key=
{
user
.
name
}
>
header
:
t
(
"common.email"
),
<
td
className=
"whitespace-nowrap px-3 py-2 text-sm text-muted-foreground"
>
render
:
(
_
,
user
:
User
)
=>
user
.
email
,
{
user
.
username
}
},
<
span
className=
"ml-1 italic"
>
{
user
.
state
===
State
.
ARCHIVED
&&
"(Archived)"
}
</
span
>
{
</
td
>
key
:
"actions"
,
<
td
className=
"whitespace-nowrap px-3 py-2 text-sm text-muted-foreground"
>
{
stringifyUserRole
(
user
.
role
)
}
</
td
>
header
:
""
,
<
td
className=
"whitespace-nowrap px-3 py-2 text-sm text-muted-foreground"
>
{
user
.
displayName
}
</
td
>
className
:
"text-right"
,
<
td
className=
"whitespace-nowrap px-3 py-2 text-sm text-muted-foreground"
>
{
user
.
email
}
</
td
>
render
:
(
_
,
user
:
User
)
=>
<
td
className=
"relative whitespace-nowrap py-2 pl-3 pr-4 text-right text-sm font-medium flex justify-end"
>
currentUser
?.
name
===
user
.
name
?
(
{
currentUser
?.
name
===
user
.
name
?
(
<
span
className=
"text-muted-foreground"
>
{
t
(
"common.yourself"
)
}
</
span
>
<
span
>
{
t
(
"common.yourself"
)
}
</
span
>
)
:
(
<
DropdownMenu
>
<
DropdownMenuTrigger
asChild
>
<
Button
variant=
"outline"
size=
"sm"
>
<
MoreVerticalIcon
className=
"w-4 h-auto"
/>
</
Button
>
</
DropdownMenuTrigger
>
<
DropdownMenuContent
align=
"end"
sideOffset=
{
2
}
>
<
DropdownMenuItem
onClick=
{
()
=>
handleEditUser
(
user
)
}
>
{
t
(
"common.update"
)
}
</
DropdownMenuItem
>
{
user
.
state
===
State
.
NORMAL
?
(
<
DropdownMenuItem
onClick=
{
()
=>
handleArchiveUserClick
(
user
)
}
>
{
t
(
"setting.member-section.archive-member"
)
}
</
DropdownMenuItem
>
)
:
(
)
:
(
<
DropdownMenu
>
<>
<
DropdownMenuTrigger
asChild
>
<
DropdownMenuItem
onClick=
{
()
=>
handleRestoreUserClick
(
user
)
}
>
{
t
(
"common.restore"
)
}
</
DropdownMenuItem
>
<
Button
variant=
"outline"
>
<
DropdownMenuItem
onClick=
{
()
=>
handleDeleteUserClick
(
user
)
}
className=
"text-destructive focus:text-destructive"
>
<
MoreVerticalIcon
className=
"w-4 h-auto"
/>
{
t
(
"setting.member-section.delete-member"
)
}
</
Button
>
</
DropdownMenuItem
>
</
DropdownMenuTrigger
>
</>
<
DropdownMenuContent
align=
"end"
sideOffset=
{
2
}
>
<
DropdownMenuItem
onClick=
{
()
=>
handleEditUser
(
user
)
}
>
{
t
(
"common.update"
)
}
</
DropdownMenuItem
>
{
user
.
state
===
State
.
NORMAL
?
(
<
DropdownMenuItem
onClick=
{
()
=>
handleArchiveUserClick
(
user
)
}
>
{
t
(
"setting.member-section.archive-member"
)
}
</
DropdownMenuItem
>
)
:
(
<>
<
DropdownMenuItem
onClick=
{
()
=>
handleRestoreUserClick
(
user
)
}
>
{
t
(
"common.restore"
)
}
</
DropdownMenuItem
>
<
DropdownMenuItem
onClick=
{
()
=>
handleDeleteUserClick
(
user
)
}
className=
"text-destructive focus:text-destructive"
>
{
t
(
"setting.member-section.delete-member"
)
}
</
DropdownMenuItem
>
</>
)
}
</
DropdownMenuContent
>
</
DropdownMenu
>
)
}
)
}
</
td
>
</
DropdownMenuContent
>
</
tr
>
</
DropdownMenu
>
))
}
),
</
tbody
>
},
</
table
>
]
}
</
div
>
data=
{
sortedUsers
}
</
div
>
emptyMessage=
"No members found"
getRowKey=
{
(
user
)
=>
user
.
name
}
/>
{
/* Create User Dialog */
}
{
/* Create User Dialog */
}
<
CreateUserDialog
open=
{
createDialog
.
isOpen
}
onOpenChange=
{
createDialog
.
setOpen
}
onSuccess=
{
fetchUsers
}
/>
<
CreateUserDialog
open=
{
createDialog
.
isOpen
}
onOpenChange=
{
createDialog
.
setOpen
}
onSuccess=
{
fetchUsers
}
/>
...
@@ -207,7 +204,7 @@ const MemberSection = observer(() => {
...
@@ -207,7 +204,7 @@ const MemberSection = observer(() => {
onConfirm=
{
confirmDeleteUser
}
onConfirm=
{
confirmDeleteUser
}
confirmVariant=
"destructive"
confirmVariant=
"destructive"
/>
/>
</
div
>
</
SettingSection
>
);
);
});
});
...
...
web/src/components/Settings/MemoRelatedSettings.tsx
View file @
c54fcf7a
This diff is collapsed.
Click to expand it.
web/src/components/Settings/MyAccountSection.tsx
View file @
c54fcf7a
...
@@ -8,6 +8,8 @@ import UpdateAccountDialog from "../UpdateAccountDialog";
...
@@ -8,6 +8,8 @@ import UpdateAccountDialog from "../UpdateAccountDialog";
import
UserAvatar
from
"../UserAvatar"
;
import
UserAvatar
from
"../UserAvatar"
;
import
{
DropdownMenu
,
DropdownMenuContent
,
DropdownMenuItem
,
DropdownMenuTrigger
}
from
"../ui/dropdown-menu"
;
import
{
DropdownMenu
,
DropdownMenuContent
,
DropdownMenuItem
,
DropdownMenuTrigger
}
from
"../ui/dropdown-menu"
;
import
AccessTokenSection
from
"./AccessTokenSection"
;
import
AccessTokenSection
from
"./AccessTokenSection"
;
import
SettingGroup
from
"./SettingGroup"
;
import
SettingSection
from
"./SettingSection"
;
import
UserSessionsSection
from
"./UserSessionsSection"
;
import
UserSessionsSection
from
"./UserSessionsSection"
;
const
MyAccountSection
=
()
=>
{
const
MyAccountSection
=
()
=>
{
...
@@ -25,44 +27,50 @@ const MyAccountSection = () => {
...
@@ -25,44 +27,50 @@ const MyAccountSection = () => {
};
};
return
(
return
(
<
div
className=
"w-full gap-2 pt-2 pb-4"
>
<
SettingSection
>
<
p
className=
"font-medium text-muted-foreground"
>
{
t
(
"setting.account-section.title"
)
}
</
p
>
<
SettingGroup
title=
{
t
(
"setting.account-section.title"
)
}
>
<
div
className=
"w-full mt-2 flex flex-row justify-start items-center"
>
<
div
className=
"w-full flex flex-row justify-start items-center gap-3"
>
<
UserAvatar
className=
"mr-2 shrink-0 w-10 h-10"
avatarUrl=
{
user
.
avatarUrl
}
/>
<
UserAvatar
className=
"shrink-0 w-12 h-12"
avatarUrl=
{
user
.
avatarUrl
}
/>
<
div
className=
"max-w-[calc(100%-3rem)] flex flex-col justify-center items-start"
>
<
div
className=
"flex-1 min-w-0 flex flex-col justify-center items-start gap-1"
>
<
p
className=
"w-full"
>
<
div
className=
"w-full"
>
<
span
className=
"text-xl leading-tight font-medium"
>
{
user
.
displayName
}
</
span
>
<
span
className=
"text-lg font-semibold"
>
{
user
.
displayName
}
</
span
>
<
span
className=
"ml-1 text-base leading-tight text-muted-foreground"
>
(
{
user
.
username
}
)
</
span
>
<
span
className=
"ml-2 text-sm text-muted-foreground"
>
@
{
user
.
username
}
</
span
>
</
p
>
</
div
>
<
p
className=
"w-4/5 leading-tight text-sm truncate"
>
{
user
.
description
}
</
p
>
{
user
.
description
&&
<
p
className=
"w-full text-sm text-muted-foreground truncate"
>
{
user
.
description
}
</
p
>
}
</
div
>
</
div
>
</
div
>
<
div
className=
"flex items-center gap-2 shrink-0"
>
<
div
className=
"w-full flex flex-row justify-start items-center mt-2 space-x-2"
>
<
Button
variant=
"outline"
size=
"sm"
onClick=
{
handleEditAccount
}
>
<
Button
variant=
"outline"
onClick=
{
handleEditAccount
}
>
<
PenLineIcon
className=
"w-4 h-4 mr-1.5"
/>
<
PenLineIcon
className=
"w-4 h-4 mx-auto mr-1"
/>
{
t
(
"common.edit"
)
}
{
t
(
"common.edit"
)
}
</
Button
>
<
DropdownMenu
>
<
DropdownMenuTrigger
asChild
>
<
Button
variant=
"outline"
>
<
MoreVerticalIcon
className=
"w-4 h-4 mx-auto"
/>
</
Button
>
</
Button
>
</
DropdownMenuTrigger
>
<
DropdownMenu
>
<
DropdownMenuContent
align=
"start"
>
<
DropdownMenuTrigger
asChild
>
<
DropdownMenuItem
onClick=
{
handleChangePassword
}
>
{
t
(
"setting.account-section.change-password"
)
}
</
DropdownMenuItem
>
<
Button
variant=
"outline"
size=
"sm"
>
</
DropdownMenuContent
>
<
MoreVerticalIcon
className=
"w-4 h-4"
/>
</
DropdownMenu
>
</
Button
>
</
div
>
</
DropdownMenuTrigger
>
<
DropdownMenuContent
align=
"end"
>
<
DropdownMenuItem
onClick=
{
handleChangePassword
}
>
{
t
(
"setting.account-section.change-password"
)
}
</
DropdownMenuItem
>
</
DropdownMenuContent
>
</
DropdownMenu
>
</
div
>
</
div
>
</
SettingGroup
>
<
SettingGroup
showSeparator
>
<
UserSessionsSection
/>
</
SettingGroup
>
<
UserSessionsSection
/>
<
SettingGroup
showSeparator
>
<
AccessTokenSection
/>
<
AccessTokenSection
/>
</
SettingGroup
>
{
/* Update Account Dialog */
}
{
/* Update Account Dialog */
}
<
UpdateAccountDialog
open=
{
accountDialog
.
isOpen
}
onOpenChange=
{
accountDialog
.
setOpen
}
/>
<
UpdateAccountDialog
open=
{
accountDialog
.
isOpen
}
onOpenChange=
{
accountDialog
.
setOpen
}
/>
{
/* Change Password Dialog */
}
{
/* Change Password Dialog */
}
<
ChangeMemberPasswordDialog
open=
{
passwordDialog
.
isOpen
}
onOpenChange=
{
passwordDialog
.
setOpen
}
user=
{
user
}
/>
<
ChangeMemberPasswordDialog
open=
{
passwordDialog
.
isOpen
}
onOpenChange=
{
passwordDialog
.
setOpen
}
user=
{
user
}
/>
</
div
>
</
SettingSection
>
);
);
};
};
...
...
web/src/components/Settings/PreferencesSection.tsx
View file @
c54fcf7a
import
{
observer
}
from
"mobx-react-lite"
;
import
{
observer
}
from
"mobx-react-lite"
;
import
{
Select
,
SelectContent
,
SelectItem
,
SelectTrigger
,
SelectValue
}
from
"@/components/ui/select"
;
import
{
Select
,
SelectContent
,
SelectItem
,
SelectTrigger
,
SelectValue
}
from
"@/components/ui/select"
;
import
{
Separator
}
from
"@/components/ui/separator"
;
import
{
userStore
,
instanceStore
}
from
"@/store"
;
import
{
userStore
,
instanceStore
}
from
"@/store"
;
import
{
Visibility
}
from
"@/types/proto/api/v1/memo_service"
;
import
{
Visibility
}
from
"@/types/proto/api/v1/memo_service"
;
import
{
UserSetting_GeneralSetting
}
from
"@/types/proto/api/v1/user_service"
;
import
{
UserSetting_GeneralSetting
}
from
"@/types/proto/api/v1/user_service"
;
...
@@ -9,6 +8,9 @@ import { convertVisibilityFromString, convertVisibilityToString } from "@/utils/
...
@@ -9,6 +8,9 @@ import { convertVisibilityFromString, convertVisibilityToString } from "@/utils/
import
LocaleSelect
from
"../LocaleSelect"
;
import
LocaleSelect
from
"../LocaleSelect"
;
import
ThemeSelect
from
"../ThemeSelect"
;
import
ThemeSelect
from
"../ThemeSelect"
;
import
VisibilityIcon
from
"../VisibilityIcon"
;
import
VisibilityIcon
from
"../VisibilityIcon"
;
import
SettingGroup
from
"./SettingGroup"
;
import
SettingRow
from
"./SettingRow"
;
import
SettingSection
from
"./SettingSection"
;
import
WebhookSection
from
"./WebhookSection"
;
import
WebhookSection
from
"./WebhookSection"
;
const
PreferencesSection
=
observer
(()
=>
{
const
PreferencesSection
=
observer
(()
=>
{
...
@@ -41,46 +43,43 @@ const PreferencesSection = observer(() => {
...
@@ -41,46 +43,43 @@ const PreferencesSection = observer(() => {
};
};
return
(
return
(
<
div
className=
"w-full flex flex-col gap-2 pt-2 pb-4"
>
<
SettingSection
>
<
p
className=
"font-medium text-muted-foreground"
>
{
t
(
"common.basic"
)
}
</
p
>
<
SettingGroup
title=
{
t
(
"common.basic"
)
}
>
<
SettingRow
label=
{
t
(
"common.language"
)
}
>
<
LocaleSelect
value=
{
setting
.
locale
}
onChange=
{
handleLocaleSelectChange
}
/>
</
SettingRow
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
SettingRow
label=
{
t
(
"setting.preference-section.theme"
)
}
>
<
span
>
{
t
(
"common.language"
)
}
</
span
>
<
ThemeSelect
value=
{
setting
.
theme
}
onValueChange=
{
handleThemeChange
}
/
>
<
LocaleSelect
value=
{
setting
.
locale
}
onChange=
{
handleLocaleSelectChange
}
/
>
<
/
SettingRow
>
</
div
>
</
SettingGroup
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
SettingGroup
title=
{
t
(
"setting.preference"
)
}
showSeparator
>
<
span
>
{
t
(
"setting.preference-section.theme"
)
}
</
span
>
<
SettingRow
label=
{
t
(
"setting.preference-section.default-memo-visibility"
)
}
>
<
ThemeSelect
value=
{
setting
.
theme
}
onValueChange=
{
handleThemeChange
}
/>
<
Select
value=
{
setting
.
memoVisibility
}
onValueChange=
{
handleDefaultMemoVisibilityChanged
}
>
</
div
>
<
SelectTrigger
className=
"min-w-fit"
>
<
div
className=
"flex items-center gap-2"
>
<
VisibilityIcon
visibility=
{
convertVisibilityFromString
(
setting
.
memoVisibility
)
}
/>
<
SelectValue
/>
</
div
>
</
SelectTrigger
>
<
SelectContent
>
{
[
Visibility
.
PRIVATE
,
Visibility
.
PROTECTED
,
Visibility
.
PUBLIC
]
.
map
((
v
)
=>
convertVisibilityToString
(
v
))
.
map
((
item
)
=>
(
<
SelectItem
key=
{
item
}
value=
{
item
}
className=
"whitespace-nowrap"
>
{
t
(
`memo.visibility.${item.toLowerCase() as Lowercase<typeof item>}`
)
}
</
SelectItem
>
))
}
</
SelectContent
>
</
Select
>
</
SettingRow
>
</
SettingGroup
>
<
p
className=
"font-medium text-muted-foreground"
>
{
t
(
"setting.preference"
)
}
</
p
>
<
SettingGroup
showSeparator
>
<
WebhookSection
/>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
</
SettingGroup
>
<
span
className=
"truncate"
>
{
t
(
"setting.preference-section.default-memo-visibility"
)
}
</
span
>
</
SettingSection
>
<
Select
value=
{
setting
.
memoVisibility
}
onValueChange=
{
handleDefaultMemoVisibilityChanged
}
>
<
SelectTrigger
className=
"min-w-fit"
>
<
div
className=
"flex items-center gap-2"
>
<
VisibilityIcon
visibility=
{
convertVisibilityFromString
(
setting
.
memoVisibility
)
}
/>
<
SelectValue
/>
</
div
>
</
SelectTrigger
>
<
SelectContent
>
{
[
Visibility
.
PRIVATE
,
Visibility
.
PROTECTED
,
Visibility
.
PUBLIC
]
.
map
((
v
)
=>
convertVisibilityToString
(
v
))
.
map
((
item
)
=>
(
<
SelectItem
key=
{
item
}
value=
{
item
}
className=
"whitespace-nowrap"
>
{
t
(
`memo.visibility.${item.toLowerCase() as Lowercase<typeof item>}`
)
}
</
SelectItem
>
))
}
</
SelectContent
>
</
Select
>
</
div
>
<
Separator
className=
"my-3"
/>
<
WebhookSection
/>
</
div
>
);
);
});
});
...
...
web/src/components/Settings/SSOSection.tsx
View file @
c54fcf7a
import
{
MoreVerticalIcon
}
from
"lucide-react"
;
import
{
MoreVerticalIcon
,
PlusIcon
}
from
"lucide-react"
;
import
{
useEffect
,
useState
}
from
"react"
;
import
{
useEffect
,
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
{
toast
}
from
"react-hot-toast"
;
import
ConfirmDialog
from
"@/components/ConfirmDialog"
;
import
ConfirmDialog
from
"@/components/ConfirmDialog"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
DropdownMenu
,
DropdownMenuContent
,
DropdownMenuItem
,
DropdownMenuTrigger
}
from
"@/components/ui/dropdown-menu"
;
import
{
DropdownMenu
,
DropdownMenuContent
,
DropdownMenuItem
,
DropdownMenuTrigger
}
from
"@/components/ui/dropdown-menu"
;
import
{
Separator
}
from
"@/components/ui/separator"
;
import
{
identityProviderServiceClient
}
from
"@/grpcweb"
;
import
{
identityProviderServiceClient
}
from
"@/grpcweb"
;
import
{
IdentityProvider
}
from
"@/types/proto/api/v1/idp_service"
;
import
{
IdentityProvider
}
from
"@/types/proto/api/v1/idp_service"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
CreateIdentityProviderDialog
from
"../CreateIdentityProviderDialog"
;
import
CreateIdentityProviderDialog
from
"../CreateIdentityProviderDialog"
;
import
LearnMore
from
"../LearnMore"
;
import
LearnMore
from
"../LearnMore"
;
import
SettingSection
from
"./SettingSection"
;
import
SettingTable
from
"./SettingTable"
;
const
SSOSection
=
()
=>
{
const
SSOSection
=
()
=>
{
const
t
=
useTranslate
();
const
t
=
useTranslate
();
...
@@ -68,48 +69,60 @@ const SSOSection = () => {
...
@@ -68,48 +69,60 @@ const SSOSection = () => {
};
};
return
(
return
(
<
div
className=
"w-full flex flex-col gap-2 pt-2 pb-4"
>
<
SettingSection
<
div
className=
"w-full flex flex-row justify-between items-center gap-1"
>
title=
{
<
div
className=
"flex
flex-row items-center gap-1
"
>
<
div
className=
"flex
items-center gap-2
"
>
<
span
className=
"font-mono text-muted-foreground"
>
{
t
(
"setting.sso-section.sso-list"
)
}
</
span
>
<
span
>
{
t
(
"setting.sso-section.sso-list"
)
}
</
span
>
<
LearnMore
url=
"https://www.usememos.com/docs/configuration/authentication"
/>
<
LearnMore
url=
"https://www.usememos.com/docs/configuration/authentication"
/>
</
div
>
</
div
>
<
Button
color=
"primary"
onClick=
{
handleCreateIdentityProvider
}
>
}
actions=
{
<
Button
onClick=
{
handleCreateIdentityProvider
}
>
<
PlusIcon
className=
"w-4 h-4 mr-2"
/>
{
t
(
"common.create"
)
}
{
t
(
"common.create"
)
}
</
Button
>
</
Button
>
</
div
>
}
<
Separator
/>
>
{
identityProviderList
.
map
((
identityProvider
)
=>
(
<
SettingTable
<
div
columns=
{
[
key=
{
identityProvider
.
name
}
{
className=
"py-2 w-full border-b last:border-b border-border flex flex-row items-center justify-between"
key
:
"title"
,
>
header
:
t
(
"common.name"
),
<
div
className=
"flex flex-row items-center"
>
render
:
(
_
,
provider
:
IdentityProvider
)
=>
(
<
p
className=
"ml-2"
>
<
span
className=
"text-foreground"
>
{
identityProvider
.
title
}
{
provider
.
title
}
<
span
className=
"text-sm ml-1 text-muted-foreground"
>
(
{
identityProvider
.
type
}
)
</
span
>
<
span
className=
"ml-2 text-sm text-muted-foreground"
>
(
{
provider
.
type
}
)
</
span
>
</
p
>
</
span
>
</
div
>
),
<
div
className=
"flex flex-row items-center"
>
},
<
DropdownMenu
>
{
<
DropdownMenuTrigger
asChild
>
key
:
"actions"
,
<
Button
variant=
"outline"
>
header
:
""
,
<
MoreVerticalIcon
className=
"w-4 h-auto"
/>
className
:
"text-right"
,
</
Button
>
render
:
(
_
,
provider
:
IdentityProvider
)
=>
(
</
DropdownMenuTrigger
>
<
DropdownMenu
>
<
DropdownMenuContent
align=
"end"
sideOffset=
{
2
}
>
<
DropdownMenuTrigger
asChild
>
<
DropdownMenuItem
onClick=
{
()
=>
handleEditIdentityProvider
(
identityProvider
)
}
>
{
t
(
"common.edit"
)
}
</
DropdownMenuItem
>
<
Button
variant=
"outline"
size=
"sm"
>
<
DropdownMenuItem
onClick=
{
()
=>
handleDeleteIdentityProvider
(
identityProvider
)
}
>
{
t
(
"common.delete"
)
}
</
DropdownMenuItem
>
<
MoreVerticalIcon
className=
"w-4 h-auto"
/>
</
DropdownMenuContent
>
</
Button
>
</
DropdownMenu
>
</
DropdownMenuTrigger
>
</
div
>
<
DropdownMenuContent
align=
"end"
sideOffset=
{
2
}
>
</
div
>
<
DropdownMenuItem
onClick=
{
()
=>
handleEditIdentityProvider
(
provider
)
}
>
{
t
(
"common.edit"
)
}
</
DropdownMenuItem
>
))
}
<
DropdownMenuItem
{
identityProviderList
.
length
===
0
&&
(
onClick=
{
()
=>
handleDeleteIdentityProvider
(
provider
)
}
<
div
className=
"w-full mt-2 text-sm border-border text-muted-foreground flex flex-row items-center justify-between"
>
className=
"text-destructive focus:text-destructive"
<
p
className=
""
>
{
t
(
"setting.sso-section.no-sso-found"
)
}
</
p
>
>
</
div
>
{
t
(
"common.delete"
)
}
)
}
</
DropdownMenuItem
>
</
DropdownMenuContent
>
</
DropdownMenu
>
),
},
]
}
data=
{
identityProviderList
}
emptyMessage=
{
t
(
"setting.sso-section.no-sso-found"
)
}
getRowKey=
{
(
provider
)
=>
provider
.
name
}
/>
<
CreateIdentityProviderDialog
<
CreateIdentityProviderDialog
open=
{
isCreateDialogOpen
}
open=
{
isCreateDialogOpen
}
...
@@ -127,7 +140,7 @@ const SSOSection = () => {
...
@@ -127,7 +140,7 @@ const SSOSection = () => {
onConfirm=
{
confirmDeleteIdentityProvider
}
onConfirm=
{
confirmDeleteIdentityProvider
}
confirmVariant=
"destructive"
confirmVariant=
"destructive"
/>
/>
</
div
>
</
SettingSection
>
);
);
};
};
...
...
web/src/components/Settings/SettingGroup.tsx
0 → 100644
View file @
c54fcf7a
import
React
from
"react"
;
import
{
Separator
}
from
"@/components/ui/separator"
;
import
{
cn
}
from
"@/lib/utils"
;
interface
SettingGroupProps
{
title
?:
string
;
description
?:
string
;
children
:
React
.
ReactNode
;
className
?:
string
;
showSeparator
?:
boolean
;
}
/**
* Groups related settings together with optional title and separator
* Use this to organize multiple SettingRows under a common category
*/
const
SettingGroup
:
React
.
FC
<
SettingGroupProps
>
=
({
title
,
description
,
children
,
className
,
showSeparator
=
false
})
=>
{
return
(
<>
{
showSeparator
&&
<
Separator
className=
"my-2"
/>
}
<
div
className=
{
cn
(
"flex flex-col gap-3"
,
className
)
}
>
{
(
title
||
description
)
&&
(
<
div
className=
"flex flex-col gap-1"
>
{
title
&&
<
h4
className=
"text-sm font-medium text-muted-foreground"
>
{
title
}
</
h4
>
}
{
description
&&
<
p
className=
"text-xs text-muted-foreground"
>
{
description
}
</
p
>
}
</
div
>
)
}
<
div
className=
"flex flex-col gap-3"
>
{
children
}
</
div
>
</
div
>
</>
);
};
export
default
SettingGroup
;
web/src/components/Settings/SettingRow.tsx
0 → 100644
View file @
c54fcf7a
import
{
HelpCircleIcon
}
from
"lucide-react"
;
import
React
from
"react"
;
import
{
Tooltip
,
TooltipContent
,
TooltipProvider
,
TooltipTrigger
}
from
"@/components/ui/tooltip"
;
import
{
cn
}
from
"@/lib/utils"
;
interface
SettingRowProps
{
label
:
string
;
description
?:
string
;
tooltip
?:
string
;
children
:
React
.
ReactNode
;
className
?:
string
;
vertical
?:
boolean
;
}
/**
* Standardized row component for individual settings
* Provides consistent label/control layout with optional tooltip
*/
const
SettingRow
:
React
.
FC
<
SettingRowProps
>
=
({
label
,
description
,
tooltip
,
children
,
className
,
vertical
=
false
})
=>
{
return
(
<
div
className=
{
cn
(
"w-full flex gap-3"
,
vertical
?
"flex-col"
:
"flex-row justify-between items-center"
,
className
)
}
>
<
div
className=
{
cn
(
"flex flex-col gap-1"
,
vertical
?
"w-full"
:
"flex-1 min-w-0"
)
}
>
<
div
className=
"flex items-center gap-1.5"
>
<
span
className=
{
cn
(
"text-sm"
,
vertical
?
"font-medium"
:
""
)
}
>
{
label
}
</
span
>
{
tooltip
&&
(
<
TooltipProvider
>
<
Tooltip
>
<
TooltipTrigger
asChild
>
<
HelpCircleIcon
className=
"w-4 h-4 text-muted-foreground cursor-help"
/>
</
TooltipTrigger
>
<
TooltipContent
>
<
p
className=
"max-w-xs"
>
{
tooltip
}
</
p
>
</
TooltipContent
>
</
Tooltip
>
</
TooltipProvider
>
)
}
</
div
>
{
description
&&
<
p
className=
"text-xs text-muted-foreground"
>
{
description
}
</
p
>
}
</
div
>
<
div
className=
{
cn
(
"flex items-center"
,
vertical
?
"w-full"
:
"shrink-0"
)
}
>
{
children
}
</
div
>
</
div
>
);
};
export
default
SettingRow
;
web/src/components/Settings/SettingSection.tsx
0 → 100644
View file @
c54fcf7a
import
React
from
"react"
;
import
{
cn
}
from
"@/lib/utils"
;
interface
SettingSectionProps
{
title
?:
React
.
ReactNode
;
description
?:
string
;
children
:
React
.
ReactNode
;
className
?:
string
;
actions
?:
React
.
ReactNode
;
}
/**
* Wrapper component for consistent section layout in settings pages
* Provides standardized spacing, titles, and descriptions
*/
const
SettingSection
:
React
.
FC
<
SettingSectionProps
>
=
({
title
,
description
,
children
,
className
,
actions
})
=>
{
return
(
<
div
className=
{
cn
(
"w-full flex flex-col gap-4 pt-2 pb-4"
,
className
)
}
>
{
(
title
||
description
||
actions
)
&&
(
<
div
className=
"flex flex-col sm:flex-row sm:items-start sm:justify-between gap-2"
>
<
div
className=
"flex-1"
>
{
title
&&
(
<
div
className=
"text-base font-semibold text-foreground mb-1"
>
{
typeof
title
===
"string"
?
<
h3
>
{
title
}
</
h3
>
:
title
}
</
div
>
)
}
{
description
&&
<
p
className=
"text-sm text-muted-foreground"
>
{
description
}
</
p
>
}
</
div
>
{
actions
&&
<
div
className=
"flex items-center gap-2"
>
{
actions
}
</
div
>
}
</
div
>
)
}
<
div
className=
"flex flex-col gap-4"
>
{
children
}
</
div
>
</
div
>
);
};
export
default
SettingSection
;
web/src/components/Settings/SettingTable.tsx
0 → 100644
View file @
c54fcf7a
import
React
from
"react"
;
import
{
cn
}
from
"@/lib/utils"
;
interface
SettingTableColumn
{
key
:
string
;
header
:
string
;
className
?:
string
;
render
?:
(
value
:
any
,
row
:
any
)
=>
React
.
ReactNode
;
}
interface
SettingTableProps
{
columns
:
SettingTableColumn
[];
data
:
any
[];
emptyMessage
?:
string
;
className
?:
string
;
getRowKey
?:
(
row
:
any
,
index
:
number
)
=>
string
;
}
/**
* Standardized table component for settings data lists
* Provides consistent styling for tables in settings pages
*/
const
SettingTable
:
React
.
FC
<
SettingTableProps
>
=
({
columns
,
data
,
emptyMessage
=
"No data"
,
className
,
getRowKey
})
=>
{
return
(
<
div
className=
{
cn
(
"w-full overflow-x-auto"
,
className
)
}
>
<
div
className=
"inline-block min-w-full align-middle border border-border rounded-lg"
>
<
table
className=
"min-w-full divide-y divide-border"
>
<
thead
>
<
tr
className=
"text-sm font-semibold text-left text-foreground"
>
{
columns
.
map
((
column
)
=>
(
<
th
key=
{
column
.
key
}
scope=
"col"
className=
{
cn
(
"px-3 py-2"
,
column
.
className
)
}
>
{
column
.
header
}
</
th
>
))
}
</
tr
>
</
thead
>
<
tbody
className=
"divide-y divide-border"
>
{
data
.
length
===
0
?
(
<
tr
>
<
td
colSpan=
{
columns
.
length
}
className=
"px-3 py-4 text-center text-sm text-muted-foreground"
>
{
emptyMessage
}
</
td
>
</
tr
>
)
:
(
data
.
map
((
row
,
rowIndex
)
=>
{
const
rowKey
=
getRowKey
?
getRowKey
(
row
,
rowIndex
)
:
rowIndex
.
toString
();
return
(
<
tr
key=
{
rowKey
}
>
{
columns
.
map
((
column
)
=>
{
const
value
=
row
[
column
.
key
];
const
content
=
column
.
render
?
column
.
render
(
value
,
row
)
:
value
;
return
(
<
td
key=
{
column
.
key
}
className=
{
cn
(
"whitespace-nowrap px-3 py-2 text-sm text-muted-foreground"
,
column
.
className
)
}
>
{
content
}
</
td
>
);
})
}
</
tr
>
);
})
)
}
</
tbody
>
</
table
>
</
div
>
</
div
>
);
};
export
default
SettingTable
;
web/src/components/Settings/StorageSection.tsx
View file @
c54fcf7a
import
{
isEqual
}
from
"lodash-es"
;
import
{
isEqual
}
from
"lodash-es"
;
import
{
HelpCircleIcon
}
from
"lucide-react"
;
import
{
observer
}
from
"mobx-react-lite"
;
import
{
observer
}
from
"mobx-react-lite"
;
import
React
,
{
useEffect
,
useMemo
,
useState
}
from
"react"
;
import
React
,
{
useEffect
,
useMemo
,
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
{
toast
}
from
"react-hot-toast"
;
...
@@ -8,7 +7,6 @@ import { Input } from "@/components/ui/input";
...
@@ -8,7 +7,6 @@ import { Input } from "@/components/ui/input";
import
{
Label
}
from
"@/components/ui/label"
;
import
{
Label
}
from
"@/components/ui/label"
;
import
{
RadioGroup
,
RadioGroupItem
}
from
"@/components/ui/radio-group"
;
import
{
RadioGroup
,
RadioGroupItem
}
from
"@/components/ui/radio-group"
;
import
{
Switch
}
from
"@/components/ui/switch"
;
import
{
Switch
}
from
"@/components/ui/switch"
;
import
{
Tooltip
,
TooltipContent
,
TooltipProvider
,
TooltipTrigger
}
from
"@/components/ui/tooltip"
;
import
{
instanceStore
}
from
"@/store"
;
import
{
instanceStore
}
from
"@/store"
;
import
{
instanceSettingNamePrefix
}
from
"@/store/common"
;
import
{
instanceSettingNamePrefix
}
from
"@/store/common"
;
import
{
import
{
...
@@ -18,6 +16,9 @@ import {
...
@@ -18,6 +16,9 @@ import {
InstanceSetting_StorageSetting_StorageType
,
InstanceSetting_StorageSetting_StorageType
,
}
from
"@/types/proto/api/v1/instance_service"
;
}
from
"@/types/proto/api/v1/instance_service"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
SettingGroup
from
"./SettingGroup"
;
import
SettingRow
from
"./SettingRow"
;
import
SettingSection
from
"./SettingSection"
;
const
StorageSection
=
observer
(()
=>
{
const
StorageSection
=
observer
(()
=>
{
const
t
=
useTranslate
();
const
t
=
useTranslate
();
...
@@ -131,107 +132,89 @@ const StorageSection = observer(() => {
...
@@ -131,107 +132,89 @@ const StorageSection = observer(() => {
};
};
return
(
return
(
<
div
className=
"w-full flex flex-col gap-2 pt-2 pb-4"
>
<
SettingSection
>
<
div
className=
"font-medium text-muted-foreground"
>
{
t
(
"setting.storage-section.current-storage"
)
}
</
div
>
<
SettingGroup
title=
{
t
(
"setting.storage-section.current-storage"
)
}
>
<
RadioGroup
<
div
className=
"w-full"
>
value=
{
instanceStorageSetting
.
storageType
}
<
RadioGroup
onValueChange=
{
(
value
)
=>
{
value=
{
instanceStorageSetting
.
storageType
}
handleStorageTypeChanged
(
value
as
InstanceSetting_StorageSetting_StorageType
);
onValueChange=
{
(
value
)
=>
{
}
}
handleStorageTypeChanged
(
value
as
InstanceSetting_StorageSetting_StorageType
);
className=
"flex flex-row gap-4"
}
}
>
className=
"flex flex-row gap-4"
<
div
className=
"flex items-center space-x-2"
>
>
<
RadioGroupItem
value=
{
InstanceSetting_StorageSetting_StorageType
.
DATABASE
}
id=
"database"
/>
<
div
className=
"flex items-center space-x-2"
>
<
Label
htmlFor=
"database"
>
{
t
(
"setting.storage-section.type-database"
)
}
</
Label
>
<
RadioGroupItem
value=
{
InstanceSetting_StorageSetting_StorageType
.
DATABASE
}
id=
"database"
/>
<
Label
htmlFor=
"database"
>
{
t
(
"setting.storage-section.type-database"
)
}
</
Label
>
</
div
>
<
div
className=
"flex items-center space-x-2"
>
<
RadioGroupItem
value=
{
InstanceSetting_StorageSetting_StorageType
.
LOCAL
}
id=
"local"
/>
<
Label
htmlFor=
"local"
>
{
t
(
"setting.storage-section.type-local"
)
}
</
Label
>
</
div
>
<
div
className=
"flex items-center space-x-2"
>
<
RadioGroupItem
value=
{
InstanceSetting_StorageSetting_StorageType
.
S3
}
id=
"s3"
/>
<
Label
htmlFor=
"s3"
>
S3
</
Label
>
</
div
>
</
RadioGroup
>
</
div
>
</
div
>
<
div
className=
"flex items-center space-x-2"
>
<
RadioGroupItem
value=
{
InstanceSetting_StorageSetting_StorageType
.
LOCAL
}
id=
"local"
/>
<
SettingRow
label=
{
t
(
"setting.system-section.max-upload-size"
)
}
tooltip=
{
t
(
"setting.system-section.max-upload-size-hint"
)
}
>
<
Label
htmlFor=
"local"
>
{
t
(
"setting.storage-section.type-local"
)
}
</
Label
>
<
Input
className=
"w-24 font-mono"
value=
{
instanceStorageSetting
.
uploadSizeLimitMb
}
onChange=
{
handleMaxUploadSizeChanged
}
/>
</
div
>
</
SettingRow
>
<
div
className=
"flex items-center space-x-2"
>
<
RadioGroupItem
value=
{
InstanceSetting_StorageSetting_StorageType
.
S3
}
id=
"s3"
/>
{
instanceStorageSetting
.
storageType
!==
InstanceSetting_StorageSetting_StorageType
.
DATABASE
&&
(
<
Label
htmlFor=
"s3"
>
S3
</
Label
>
<
SettingRow
label=
{
t
(
"setting.storage-section.filepath-template"
)
}
>
</
div
>
</
RadioGroup
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
div
className=
"flex flex-row items-center"
>
<
span
className=
"text-muted-foreground mr-1"
>
{
t
(
"setting.system-section.max-upload-size"
)
}
</
span
>
<
TooltipProvider
>
<
Tooltip
>
<
TooltipTrigger
>
<
HelpCircleIcon
className=
"w-4 h-auto"
/>
</
TooltipTrigger
>
<
TooltipContent
>
<
p
>
{
t
(
"setting.system-section.max-upload-size-hint"
)
}
</
p
>
</
TooltipContent
>
</
Tooltip
>
</
TooltipProvider
>
</
div
>
<
Input
className=
"w-16 font-mono"
value=
{
instanceStorageSetting
.
uploadSizeLimitMb
}
onChange=
{
handleMaxUploadSizeChanged
}
/>
</
div
>
{
instanceStorageSetting
.
storageType
!==
InstanceSetting_StorageSetting_StorageType
.
DATABASE
&&
(
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
span
className=
"text-muted-foreground mr-1"
>
{
t
(
"setting.storage-section.filepath-template"
)
}
</
span
>
<
Input
className=
"w-64"
value=
{
instanceStorageSetting
.
filepathTemplate
}
placeholder=
"assets/
{
timestamp
}
_
{
filename
}"
onChange=
{
handleFilepathTemplateChanged
}
/>
</
div
>
)
}
{
instanceStorageSetting
.
storageType
===
InstanceSetting_StorageSetting_StorageType
.
S3
&&
(
<>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
span
className=
"text-muted-foreground mr-1"
>
Access key id
</
span
>
<
Input
<
Input
className=
"w-64"
className=
"w-64"
value=
{
instanceStorageSetting
.
s3Config
?.
accessKeyId
}
value=
{
instanceStorageSetting
.
filepathTemplate
}
placeholder=
""
placeholder=
"
assets/
{
timestamp
}
_
{
filename
}
"
onChange=
{
handle
S3ConfigAccessKeyId
Changed
}
onChange=
{
handle
FilepathTemplate
Changed
}
/>
/>
</
div
>
</
SettingRow
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
)
}
<
span
className=
"text-muted-foreground mr-1"
>
Access key secret
</
span
>
</
SettingGroup
>
{
instanceStorageSetting
.
storageType
===
InstanceSetting_StorageSetting_StorageType
.
S3
&&
(
<
SettingGroup
title=
"S3 Configuration"
showSeparator
>
<
SettingRow
label=
"Access key id"
>
<
Input
className=
"w-64"
value=
{
instanceStorageSetting
.
s3Config
?.
accessKeyId
}
onChange=
{
handleS3ConfigAccessKeyIdChanged
}
/>
</
SettingRow
>
<
SettingRow
label=
"Access key secret"
>
<
Input
<
Input
className=
"w-64"
className=
"w-64"
type=
"password"
value=
{
instanceStorageSetting
.
s3Config
?.
accessKeySecret
}
value=
{
instanceStorageSetting
.
s3Config
?.
accessKeySecret
}
placeholder=
""
onChange=
{
handleS3ConfigAccessKeySecretChanged
}
onChange=
{
handleS3ConfigAccessKeySecretChanged
}
/>
/>
</
div
>
</
SettingRow
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
span
className=
"text-muted-foreground mr-1"
>
Endpoint
</
span
>
<
SettingRow
label=
"Endpoint"
>
<
Input
<
Input
className=
"w-64"
value=
{
instanceStorageSetting
.
s3Config
?.
endpoint
}
onChange=
{
handleS3ConfigEndpointChanged
}
/>
className=
"w-64"
</
SettingRow
>
value=
{
instanceStorageSetting
.
s3Config
?.
endpoint
}
placeholder=
""
<
SettingRow
label=
"Region"
>
onChange=
{
handleS3ConfigEndpointChanged
}
<
Input
className=
"w-64"
value=
{
instanceStorageSetting
.
s3Config
?.
region
}
onChange=
{
handleS3ConfigRegionChanged
}
/>
/>
</
SettingRow
>
</
div
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
SettingRow
label=
"Bucket"
>
<
span
className=
"text-muted-foreground mr-1"
>
Region
</
span
>
<
Input
className=
"w-64"
value=
{
instanceStorageSetting
.
s3Config
?.
bucket
}
onChange=
{
handleS3ConfigBucketChanged
}
/>
<
Input
className=
"w-64"
value=
{
instanceStorageSetting
.
s3Config
?.
region
}
placeholder=
""
onChange=
{
handleS3ConfigRegionChanged
}
/>
</
SettingRow
>
</
div
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
SettingRow
label=
"Use Path Style"
>
<
span
className=
"text-muted-foreground mr-1"
>
Bucket
</
span
>
<
Input
className=
"w-64"
value=
{
instanceStorageSetting
.
s3Config
?.
bucket
}
placeholder=
""
onChange=
{
handleS3ConfigBucketChanged
}
/>
</
div
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
span
className=
"text-muted-foreground mr-1"
>
Use Path Style
</
span
>
<
Switch
<
Switch
checked=
{
instanceStorageSetting
.
s3Config
?.
usePathStyle
}
checked=
{
instanceStorageSetting
.
s3Config
?.
usePathStyle
}
onCheckedChange=
{
(
checked
)
=>
handleS3ConfigUsePathStyleChanged
({
target
:
{
checked
}
}
as
any
)
}
onCheckedChange=
{
(
checked
)
=>
handleS3ConfigUsePathStyleChanged
({
target
:
{
checked
}
}
as
any
)
}
/>
/>
</
div
>
</
SettingRow
>
</>
</
SettingGroup
>
)
}
)
}
<
div
>
<
div
className=
"w-full flex justify-end"
>
<
Button
disabled=
{
!
allowSaveStorageSetting
}
onClick=
{
saveInstanceStorageSetting
}
>
<
Button
disabled=
{
!
allowSaveStorageSetting
}
onClick=
{
saveInstanceStorageSetting
}
>
{
t
(
"common.save"
)
}
{
t
(
"common.save"
)
}
</
Button
>
</
Button
>
</
div
>
</
div
>
</
div
>
</
SettingSection
>
);
);
});
});
...
...
web/src/components/Settings/UserSessionsSection.tsx
View file @
c54fcf7a
...
@@ -7,6 +7,7 @@ import { userServiceClient } from "@/grpcweb";
...
@@ -7,6 +7,7 @@ import { userServiceClient } from "@/grpcweb";
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
{
UserSession
}
from
"@/types/proto/api/v1/user_service"
;
import
{
UserSession
}
from
"@/types/proto/api/v1/user_service"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
SettingTable
from
"./SettingTable"
;
const
listUserSessions
=
async
(
parent
:
string
)
=>
{
const
listUserSessions
=
async
(
parent
:
string
)
=>
{
const
{
sessions
}
=
await
userServiceClient
.
listUserSessions
({
parent
});
const
{
sessions
}
=
await
userServiceClient
.
listUserSessions
({
parent
});
...
@@ -71,104 +72,87 @@ const UserSessionsSection = () => {
...
@@ -71,104 +72,87 @@ const UserSessionsSection = () => {
};
};
return
(
return
(
<
div
className=
"mt-6 w-full flex flex-col justify-start items-start space-y-4"
>
<
div
className=
"w-full flex flex-col gap-2"
>
<
div
className=
"w-full"
>
<
div
className=
"flex flex-col gap-1"
>
<
div
className=
"sm:flex sm:items-center sm:justify-between"
>
<
h4
className=
"text-sm font-medium text-muted-foreground"
>
{
t
(
"setting.user-sessions-section.title"
)
}
</
h4
>
<
div
className=
"sm:flex-auto space-y-1"
>
<
p
className=
"text-xs text-muted-foreground"
>
{
t
(
"setting.user-sessions-section.description"
)
}
</
p
>
<
p
className=
"flex flex-row justify-start items-center font-medium text-muted-foreground"
>
{
t
(
"setting.user-sessions-section.title"
)
}
</
p
>
<
p
className=
"text-sm text-muted-foreground"
>
{
t
(
"setting.user-sessions-section.description"
)
}
</
p
>
</
div
>
</
div
>
<
div
className=
"w-full mt-2 flow-root"
>
<
div
className=
"overflow-x-auto"
>
<
div
className=
"inline-block min-w-full border border-border rounded-lg align-middle"
>
<
table
className=
"min-w-full divide-y divide-border"
>
<
thead
>
<
tr
>
<
th
scope=
"col"
className=
"px-3 py-2 text-left text-sm font-semibold text-foreground"
>
{
t
(
"setting.user-sessions-section.device"
)
}
</
th
>
<
th
scope=
"col"
className=
"px-3 py-2 text-left text-sm font-semibold text-foreground"
>
{
t
(
"setting.user-sessions-section.last-active"
)
}
</
th
>
<
th
scope=
"col"
className=
"relative py-3.5 pl-3 pr-4"
>
<
span
className=
"sr-only"
>
{
t
(
"common.delete"
)
}
</
span
>
</
th
>
</
tr
>
</
thead
>
<
tbody
className=
"divide-y divide-border"
>
{
userSessions
.
map
((
userSession
)
=>
(
<
tr
key=
{
userSession
.
sessionId
}
>
<
td
className=
"whitespace-nowrap px-3 py-2 text-sm text-foreground"
>
<
div
className=
"flex items-center space-x-3"
>
{
getDeviceIcon
(
userSession
.
clientInfo
?.
deviceType
||
""
)
}
<
div
className=
"flex flex-col"
>
<
span
className=
"font-medium"
>
{
formatDeviceInfo
(
userSession
.
clientInfo
)
}
{
isCurrentSession
(
userSession
)
&&
(
<
span
className=
"ml-2 inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-primary/20 text-primary"
>
<
WifiIcon
className=
"w-3 h-3 mr-1"
/>
{
t
(
"setting.user-sessions-section.current"
)
}
</
span
>
)
}
</
span
>
<
span
className=
"text-xs text-muted-foreground font-mono"
>
{
getFormattedSessionId
(
userSession
.
sessionId
)
}
</
span
>
</
div
>
</
div
>
</
td
>
<
td
className=
"whitespace-nowrap px-3 py-2 text-sm text-muted-foreground"
>
<
div
className=
"flex items-center space-x-1"
>
<
ClockIcon
className=
"w-4 h-4"
/>
<
span
>
{
userSession
.
lastAccessedTime
?.
toLocaleString
()
}
</
span
>
</
div
>
</
td
>
<
td
className=
"relative whitespace-nowrap py-2 pl-3 pr-4 text-right text-sm"
>
<
Button
variant=
"ghost"
disabled=
{
isCurrentSession
(
userSession
)
}
onClick=
{
()
=>
{
handleRevokeSession
(
userSession
);
}
}
title=
{
isCurrentSession
(
userSession
)
?
t
(
"setting.user-sessions-section.cannot-revoke-current"
)
:
t
(
"setting.user-sessions-section.revoke-session"
)
}
>
<
TrashIcon
className=
{
`w-4 h-auto ${isCurrentSession(userSession) ? "text-muted-foreground" : "text-destructive"}`
}
/>
</
Button
>
</
td
>
</
tr
>
))
}
</
tbody
>
</
table
>
{
userSessions
.
length
===
0
&&
(
<
div
className=
"text-center py-8 text-muted-foreground"
>
{
t
(
"setting.user-sessions-section.no-sessions"
)
}
</
div
>
)
}
</
div
>
</
div
>
</
div
>
<
ConfirmDialog
open=
{
!!
revokeTarget
}
onOpenChange=
{
(
open
)
=>
!
open
&&
setRevokeTarget
(
undefined
)
}
title=
{
revokeTarget
?
t
(
"setting.user-sessions-section.session-revocation"
,
{
sessionId
:
getFormattedSessionId
(
revokeTarget
.
sessionId
),
})
:
""
}
description=
{
revokeTarget
?
t
(
"setting.user-sessions-section.session-revocation-description"
)
:
""
}
confirmLabel=
{
t
(
"setting.user-sessions-section.revoke-session-button"
)
}
cancelLabel=
{
t
(
"common.cancel"
)
}
onConfirm=
{
confirmRevokeSession
}
confirmVariant=
"destructive"
/>
</
div
>
</
div
>
<
SettingTable
columns=
{
[
{
key
:
"device"
,
header
:
t
(
"setting.user-sessions-section.device"
),
render
:
(
_
,
session
:
UserSession
)
=>
(
<
div
className=
"flex items-center space-x-3"
>
{
getDeviceIcon
(
session
.
clientInfo
?.
deviceType
||
""
)
}
<
div
className=
"flex flex-col"
>
<
span
className=
"font-medium text-foreground"
>
{
formatDeviceInfo
(
session
.
clientInfo
)
}
{
isCurrentSession
(
session
)
&&
(
<
span
className=
"ml-2 inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-primary/20 text-primary"
>
<
WifiIcon
className=
"w-3 h-3 mr-1"
/>
{
t
(
"setting.user-sessions-section.current"
)
}
</
span
>
)
}
</
span
>
<
span
className=
"text-xs text-muted-foreground font-mono"
>
{
getFormattedSessionId
(
session
.
sessionId
)
}
</
span
>
</
div
>
</
div
>
),
},
{
key
:
"lastAccessedTime"
,
header
:
t
(
"setting.user-sessions-section.last-active"
),
render
:
(
_
,
session
:
UserSession
)
=>
(
<
div
className=
"flex items-center space-x-1"
>
<
ClockIcon
className=
"w-4 h-4"
/>
<
span
>
{
session
.
lastAccessedTime
?.
toLocaleString
()
}
</
span
>
</
div
>
),
},
{
key
:
"actions"
,
header
:
""
,
className
:
"text-right"
,
render
:
(
_
,
session
:
UserSession
)
=>
(
<
Button
variant=
"ghost"
size=
"sm"
disabled=
{
isCurrentSession
(
session
)
}
onClick=
{
()
=>
handleRevokeSession
(
session
)
}
title=
{
isCurrentSession
(
session
)
?
t
(
"setting.user-sessions-section.cannot-revoke-current"
)
:
t
(
"setting.user-sessions-section.revoke-session"
)
}
>
<
TrashIcon
className=
{
`w-4 h-auto ${isCurrentSession(session) ? "text-muted-foreground" : "text-destructive"}`
}
/>
</
Button
>
),
},
]
}
data=
{
userSessions
}
emptyMessage=
{
t
(
"setting.user-sessions-section.no-sessions"
)
}
getRowKey=
{
(
session
)
=>
session
.
sessionId
}
/>
<
ConfirmDialog
open=
{
!!
revokeTarget
}
onOpenChange=
{
(
open
)
=>
!
open
&&
setRevokeTarget
(
undefined
)
}
title=
{
revokeTarget
?
t
(
"setting.user-sessions-section.session-revocation"
,
{
sessionId
:
getFormattedSessionId
(
revokeTarget
.
sessionId
),
})
:
""
}
description=
{
revokeTarget
?
t
(
"setting.user-sessions-section.session-revocation-description"
)
:
""
}
confirmLabel=
{
t
(
"setting.user-sessions-section.revoke-session-button"
)
}
cancelLabel=
{
t
(
"common.cancel"
)
}
onConfirm=
{
confirmRevokeSession
}
confirmVariant=
"destructive"
/>
</
div
>
</
div
>
);
);
};
};
...
...
web/src/components/Settings/WebhookSection.tsx
View file @
c54fcf7a
import
{
ExternalLinkIcon
,
TrashIcon
}
from
"lucide-react"
;
import
{
ExternalLinkIcon
,
PlusIcon
,
TrashIcon
}
from
"lucide-react"
;
import
{
useEffect
,
useState
}
from
"react"
;
import
{
useEffect
,
useState
}
from
"react"
;
import
toast
from
"react-hot-toast"
;
import
toast
from
"react-hot-toast"
;
import
{
Link
}
from
"react-router-dom"
;
import
{
Link
}
from
"react-router-dom"
;
...
@@ -9,6 +9,7 @@ import useCurrentUser from "@/hooks/useCurrentUser";
...
@@ -9,6 +9,7 @@ import useCurrentUser from "@/hooks/useCurrentUser";
import
{
UserWebhook
}
from
"@/types/proto/api/v1/user_service"
;
import
{
UserWebhook
}
from
"@/types/proto/api/v1/user_service"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
CreateWebhookDialog
from
"../CreateWebhookDialog"
;
import
CreateWebhookDialog
from
"../CreateWebhookDialog"
;
import
SettingTable
from
"./SettingTable"
;
const
WebhookSection
=
()
=>
{
const
WebhookSection
=
()
=>
{
const
t
=
useTranslate
();
const
t
=
useTranslate
();
...
@@ -52,71 +53,58 @@ const WebhookSection = () => {
...
@@ -52,71 +53,58 @@ const WebhookSection = () => {
};
};
return
(
return
(
<
div
className=
"w-full flex flex-col justify-start items-start"
>
<
div
className=
"w-full flex flex-col gap-2"
>
<
div
className=
"w-full flex justify-between items-center"
>
<
div
className=
"flex flex-col sm:flex-row sm:items-start sm:justify-between gap-2"
>
<
div
className=
"flex-auto space-y-1"
>
<
h4
className=
"text-sm font-medium text-muted-foreground"
>
{
t
(
"setting.webhook-section.title"
)
}
</
h4
>
<
p
className=
"flex flex-row justify-start items-center font-medium text-muted-foreground"
>
{
t
(
"setting.webhook-section.title"
)
}
</
p
>
<
Button
onClick=
{
()
=>
setIsCreateWebhookDialogOpen
(
true
)
}
size=
"sm"
>
</
div
>
<
PlusIcon
className=
"w-4 h-4 mr-1.5"
/>
<
div
>
{
t
(
"common.create"
)
}
<
Button
color=
"primary"
onClick=
{
()
=>
setIsCreateWebhookDialogOpen
(
true
)
}
>
</
Button
>
{
t
(
"common.create"
)
}
</
Button
>
</
div
>
</
div
>
</
div
>
<
div
className=
"w-full mt-2 flow-root"
>
<
div
className=
"overflow-x-auto"
>
<
div
className=
"inline-block min-w-full border border-border rounded-lg align-middle"
>
<
table
className=
"min-w-full divide-y divide-border"
>
<
thead
>
<
tr
>
<
th
scope=
"col"
className=
"px-3 py-2 text-left text-sm font-semibold text-foreground"
>
{
t
(
"common.name"
)
}
</
th
>
<
th
scope=
"col"
className=
"px-3 py-2 text-left text-sm font-semibold text-foreground"
>
{
t
(
"setting.webhook-section.url"
)
}
</
th
>
<
th
scope=
"col"
className=
"relative px-3 py-2 pr-4"
>
<
span
className=
"sr-only"
>
{
t
(
"common.delete"
)
}
</
span
>
</
th
>
</
tr
>
</
thead
>
<
tbody
className=
"divide-y divide-border"
>
{
webhooks
.
map
((
webhook
)
=>
(
<
tr
key=
{
webhook
.
name
}
>
<
td
className=
"whitespace-nowrap px-3 py-2 text-sm text-foreground"
>
{
webhook
.
displayName
}
</
td
>
<
td
className=
"max-w-[200px] px-3 py-2 text-sm text-foreground truncate"
title=
{
webhook
.
url
}
>
{
webhook
.
url
}
</
td
>
<
td
className=
"relative whitespace-nowrap px-3 py-2 text-right text-sm"
>
<
Button
variant=
"ghost"
onClick=
{
()
=>
handleDeleteWebhook
(
webhook
)
}
>
<
TrashIcon
className=
"text-destructive w-4 h-auto"
/>
</
Button
>
</
td
>
</
tr
>
))
}
{
webhooks
.
length
===
0
&&
(
<
SettingTable
<
tr
>
columns=
{
[
<
td
className=
"whitespace-nowrap px-3 py-2 text-sm text-foreground"
colSpan=
{
3
}
>
{
{
t
(
"setting.webhook-section.no-webhooks-found"
)
}
key
:
"displayName"
,
</
td
>
header
:
t
(
"common.name"
),
</
tr
>
render
:
(
_
,
webhook
:
UserWebhook
)
=>
<
span
className=
"text-foreground"
>
{
webhook
.
displayName
}
</
span
>,
)
}
},
</
tbody
>
{
</
table
>
key
:
"url"
,
</
div
>
header
:
t
(
"setting.webhook-section.url"
),
</
div
>
render
:
(
_
,
webhook
:
UserWebhook
)
=>
(
</
div
>
<
span
className=
"max-w-[300px] inline-block truncate text-foreground"
title=
{
webhook
.
url
}
>
<
div
className=
"w-full mt-2"
>
{
webhook
.
url
}
</
span
>
),
},
{
key
:
"actions"
,
header
:
""
,
className
:
"text-right"
,
render
:
(
_
,
webhook
:
UserWebhook
)
=>
(
<
Button
variant=
"ghost"
size=
"sm"
onClick=
{
()
=>
handleDeleteWebhook
(
webhook
)
}
>
<
TrashIcon
className=
"text-destructive w-4 h-auto"
/>
</
Button
>
),
},
]
}
data=
{
webhooks
}
emptyMessage=
{
t
(
"setting.webhook-section.no-webhooks-found"
)
}
getRowKey=
{
(
webhook
)
=>
webhook
.
name
}
/>
<
div
className=
"w-full"
>
<
Link
<
Link
className=
"text-muted-foreground text-sm inline-flex
flex-row justify-start
items-center hover:underline hover:text-primary"
className=
"text-muted-foreground text-sm inline-flex items-center hover:underline hover:text-primary"
to=
"https://www.usememos.com/docs/integrations/webhooks"
to=
"https://www.usememos.com/docs/integrations/webhooks"
target=
"_blank"
target=
"_blank"
>
>
{
t
(
"common.learn-more"
)
}
{
t
(
"common.learn-more"
)
}
<
ExternalLinkIcon
className=
"
inline w-4 h-auto
ml-1"
/>
<
ExternalLinkIcon
className=
"
w-4 h-4
ml-1"
/>
</
Link
>
</
Link
>
</
div
>
</
div
>
<
CreateWebhookDialog
<
CreateWebhookDialog
open=
{
isCreateWebhookDialogOpen
}
open=
{
isCreateWebhookDialogOpen
}
onOpenChange=
{
setIsCreateWebhookDialogOpen
}
onOpenChange=
{
setIsCreateWebhookDialogOpen
}
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment