Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions spx-gui/src/apps/xbuilder/pages/admin/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { RouterLink } from 'vue-router'
import { useMessageHandle } from '@/utils/exception'
import { useI18n } from '@/utils/i18n'
import { useQuery } from '@/utils/query'
import { usePageTitle } from '@/utils/utils'
import { useSignedInStateQuery } from '@/stores/user'
import { UIButton, UIError, UILoading, UISwitch, UITextInput } from '@/components/ui'
import CopyButton from '@/components/common/CopyButton.vue'
Expand Down Expand Up @@ -39,6 +40,14 @@ const appQuery = useQuery(
}
)
const app = computed(() => appQuery.data.value)
usePageTitle(() =>
app.value == null
? { en: 'Account admin OAuth app', zh: '账号管理 OAuth 应用' }
: [
{ en: app.value.displayName, zh: app.value.displayName },
{ en: 'Account admin OAuth app', zh: '账号管理 OAuth 应用' }
]
)

const displayName = ref('')
const status = ref<accountAdminApis.AccountApp['status']>('active')
Expand Down
18 changes: 12 additions & 6 deletions spx-gui/src/apps/xbuilder/pages/admin/apps.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
<script setup lang="ts">
import { computed, reactive, ref, watch } from 'vue'
import { computed, reactive, ref } from 'vue'
import { RouterLink, useRouter } from 'vue-router'

import { useMessageHandle } from '@/utils/exception'
import { useQuery } from '@/utils/query'
import { useRouteQueryParamInt, useRouteQueryParamStrEnum } from '@/utils/route'
import { usePageTitle } from '@/utils/utils'
import { useSignedInStateQuery } from '@/stores/user'
import { UIButton, UIError, UILoading, UIPagination, UISelect, UISelectOption, UITextInput } from '@/components/ui'
import * as accountAdminApis from '@/apis/admin/account'
Expand All @@ -16,13 +18,19 @@ import {
parseLines
} from './common'

const SortOrder = {
Asc: 'asc',
Desc: 'desc'
} as const

const signedInStateQuery = useSignedInStateQuery()
const router = useRouter()
const canManageAccount = computed(() => signedInStateQuery.data.value?.user?.capabilities.canManageAccount === true)

const pageSize = 20
const page = ref(1)
const sortOrder = ref<'asc' | 'desc'>('desc')
const page = useRouteQueryParamInt('p', 1)
const resetPage = (query: Partial<Record<string, string | null>>) => ({ ...query, p: null })
const sortOrder = useRouteQueryParamStrEnum('order', SortOrder, SortOrder.Desc, resetPage)
const showCreateForm = ref(false)

const createForm = reactive({
Expand All @@ -48,9 +56,7 @@ const appsQuery = useQuery(

const pageTotal = computed(() => Math.ceil((appsQuery.data.value?.total ?? 0) / pageSize))

watch(sortOrder, () => {
page.value = 1
})
usePageTitle({ en: 'Account admin OAuth apps', zh: '账号管理 OAuth 应用' })

const handleCreateApp = useMessageHandle(
async () => {
Expand Down
24 changes: 15 additions & 9 deletions spx-gui/src/apps/xbuilder/pages/admin/audit-logs.vue
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { computed } from 'vue'

import { useQuery } from '@/utils/query'
import { useRouteQueryParamInt, useRouteQueryParamStr, useRouteQueryParamStrEnum } from '@/utils/route'
import { usePageTitle } from '@/utils/utils'
Comment on lines +2 to +6

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Import useRouter from vue-router to allow batch updating the route query parameters in clearFilters and avoid race conditions.

import { computed } from 'vue'
import { useRouter } from 'vue-router'

import { useQuery } from '@/utils/query'
import { useRouteQueryParamInt, useRouteQueryParamStr, useRouteQueryParamStrEnum } from '@/utils/route'
import { usePageTitle } from '@/utils/utils'

import { UIButton, UIError, UILoading, UIPagination, UISelect, UISelectOption } from '@/components/ui'
import * as auditApis from '@/apis/admin/audit'
import { formatJSON, formatTime } from './common'

const pageSize = 20
const page = ref(1)
const sortOrder = ref<'asc' | 'desc'>('desc')
const createdAfter = ref('')
const createdBefore = ref('')
const SortOrder = {
Asc: 'asc',
Desc: 'desc'
} as const

watch([sortOrder, createdAfter, createdBefore], () => {
page.value = 1
})
const pageSize = 20
const page = useRouteQueryParamInt('p', 1)
const resetPage = (query: Partial<Record<string, string | null>>) => ({ ...query, p: null })
const sortOrder = useRouteQueryParamStrEnum('order', SortOrder, SortOrder.Desc, resetPage)
const createdAfter = useRouteQueryParamStr('from', '', resetPage)
const createdBefore = useRouteQueryParamStr('to', '', resetPage)
Comment on lines +16 to +21

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Synchronously setting both createdAfter.value = '' and createdBefore.value = '' inside clearFilters() triggers two consecutive, asynchronous router pushes. Because Vue Router updates the route query asynchronously, the second push will read the stale route query (which still contains the old from value) and overwrite the first push. As a result, only one of the filters will actually be cleared.

To fix this, define router here and update clearFilters() to perform a single router push to clear both query parameters at once:

function clearFilters() {
  router.push({
    query: {
      ...router.currentRoute.value.query,
      from: undefined,
      to: undefined,
      p: undefined
    }
  })
}
const router = useRouter()

const pageSize = 20
const page = useRouteQueryParamInt('p', 1)
const resetPage = (query: Partial<Record<string, string | null>>) => ({ ...query, p: null })
const sortOrder = useRouteQueryParamStrEnum('order', SortOrder, SortOrder.Desc, resetPage)
const createdAfter = useRouteQueryParamStr('from', '', resetPage)
const createdBefore = useRouteQueryParamStr('to', '', resetPage)


function clearFilters() {
createdAfter.value = ''
Expand Down Expand Up @@ -44,6 +48,8 @@ const auditLogsQuery = useQuery(
)

const pageTotal = computed(() => Math.ceil((auditLogsQuery.data.value?.total ?? 0) / pageSize))

usePageTitle({ en: 'Account admin audit logs', zh: '账号管理审计日志' })
</script>

<template>
Expand Down
12 changes: 12 additions & 0 deletions spx-gui/src/apps/xbuilder/pages/admin/grant.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { RouterLink } from 'vue-router'
import { useMessageHandle } from '@/utils/exception'
import { useI18n } from '@/utils/i18n'
import { useQuery } from '@/utils/query'
import { usePageTitle } from '@/utils/utils'
import { useSignedInStateQuery } from '@/stores/user'
import {
UIButton,
Expand Down Expand Up @@ -51,6 +52,17 @@ const grantQuery = useQuery(
const grant = computed(() => grantQuery.data.value)
const app = computed(() => grant.value?.app ?? null)
const appFallbackText = computed(() => app.value?.displayName.trim().charAt(0).toUpperCase() || '?')
usePageTitle(() =>
app.value == null || user.value == null
? { en: 'Account admin app grant', zh: '账号管理应用授权' }
: [
{
en: `${user.value.displayName} -> ${app.value.displayName}`,
zh: `${user.value.displayName} -> ${app.value.displayName}`
},
{ en: 'Account admin app grant', zh: '账号管理应用授权' }
]
)

const tokensPageSize = 20
const tokensPage = ref(1)
Expand Down
3 changes: 3 additions & 0 deletions spx-gui/src/apps/xbuilder/pages/admin/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@
import { computed } from 'vue'
import { RouterView, useRoute, useRouter } from 'vue-router'

import { usePageTitle } from '@/utils/utils'
import { useSignedInStateQuery } from '@/stores/user'
import { UIError, UILoading, UIMenu, UIMenuGroup, UIMenuItem } from '@/components/ui'
import CenteredWrapper from '@/components/common/CenteredWrapper.vue'
import NavbarDropdown from '@/components/navbar/NavbarDropdown.vue'
import NavbarWrapper from '@/components/navbar/NavbarWrapper.vue'

usePageTitle({ en: 'Account admin', zh: '账号管理' })

const route = useRoute()
const router = useRouter()
const signedInStateQuery = useSignedInStateQuery()
Expand Down
9 changes: 9 additions & 0 deletions spx-gui/src/apps/xbuilder/pages/admin/user.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { RouterLink } from 'vue-router'
import { useMessageHandle } from '@/utils/exception'
import { useI18n } from '@/utils/i18n'
import { useQuery } from '@/utils/query'
import { usePageTitle } from '@/utils/utils'
import { useSignedInStateQuery } from '@/stores/user'
import {
UIButton,
Expand Down Expand Up @@ -71,6 +72,14 @@ const userQuery = useQuery(
)
const user = computed(() => userQuery.data.value)
const avatarFallbackText = computed(() => user.value?.displayName.trim().charAt(0).toUpperCase() || '?')
usePageTitle(() =>
user.value == null
? { en: 'Account admin user', zh: '账号管理用户' }
: [
{ en: user.value.displayName, zh: user.value.displayName },
{ en: 'Account admin user', zh: '账号管理用户' }
]
)

const displayName = ref('')
watch(
Expand Down
26 changes: 17 additions & 9 deletions spx-gui/src/apps/xbuilder/pages/admin/users.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,28 @@ import { RouterLink, useRouter } from 'vue-router'

import { useMessageHandle } from '@/utils/exception'
import { useQuery } from '@/utils/query'
import { useRouteQueryParamInt, useRouteQueryParamStr, useRouteQueryParamStrEnum } from '@/utils/route'
import { usePageTitle } from '@/utils/utils'
import { useSignedInStateQuery } from '@/stores/user'
import { UIButton, UIError, UILoading, UIPagination, UISelect, UISelectOption, UITextInput } from '@/components/ui'
import * as accountAdminApis from '@/apis/admin/account'
import { formatTime } from './common'

const SortOrder = {
Asc: 'asc',
Desc: 'desc'
} as const

const signedInStateQuery = useSignedInStateQuery()
const router = useRouter()
const canManageAccount = computed(() => signedInStateQuery.data.value?.user?.capabilities.canManageAccount === true)

const pageSize = 20
const page = ref(1)
const sortOrder = ref<'asc' | 'desc'>('desc')
const keywordInput = ref('')
const keyword = ref('')
const page = useRouteQueryParamInt('p', 1)
const resetPage = (query: Partial<Record<string, string | null>>) => ({ ...query, p: null })
const sortOrder = useRouteQueryParamStrEnum('order', SortOrder, SortOrder.Desc, resetPage)
const keyword = useRouteQueryParamStr('q', '', resetPage)
const keywordInput = ref(keyword.value)
const showCreateForm = ref(false)

const createForm = reactive({
Expand Down Expand Up @@ -54,21 +62,21 @@ const usersQuery = useQuery(

const pageTotal = computed(() => Math.ceil((usersQuery.data.value?.total ?? 0) / pageSize))

watch(sortOrder, () => {
page.value = 1
})

const updateKeyword = debounce(() => {
const nextKeyword = keywordInput.value.trim()
if (keyword.value === nextKeyword) return
keyword.value = nextKeyword
page.value = 1
}, 300)

watch(keyword, (value) => {
if (keywordInput.value !== value) keywordInput.value = value
})
watch(keywordInput, updateKeyword)

onUnmounted(() => updateKeyword.cancel())

usePageTitle({ en: 'Account admin users', zh: '账号管理用户' })

const handleCreateUser = useMessageHandle(
async () => {
const user = await accountAdminApis.createAccountUser({
Expand Down