-
-
![]()
-
- {{ getProductName(systemDetail.data.type || '') || $t('system_detail.unknown_product') }}
-
+
+
+
![]()
+
+ {{
+ getProductName(systemDetail.data.type || '') || $t('system_detail.unknown_product')
+ }}
+
+
+
+
@@ -158,19 +177,19 @@ const truncatedNotes = computed(() => {
-
-
+
+
{{ $t('systems.notes') }}
-
-
-
- {{ truncatedNotes }}
+
+
{{
+ systemDetail.data.notes
+ }}
+
+
+ {{ $t('systems.show_notes') }}
-
- {{ systemDetail.data.notes }}
-
-
-
+
+
@@ -179,5 +198,11 @@ const truncatedNotes = computed(() => {
:notes="systemDetail.data?.notes"
@close="isNotesModalShown = false"
/>
+
+
diff --git a/frontend/src/components/systems/SystemNotesModal.vue b/frontend/src/components/systems/SystemNotesModal.vue
index 1d9432b8..1bc689c3 100644
--- a/frontend/src/components/systems/SystemNotesModal.vue
+++ b/frontend/src/components/systems/SystemNotesModal.vue
@@ -25,6 +25,6 @@ const emit = defineEmits(['close'])
@close="emit('close')"
@primary-click="emit('close')"
>
-
{{ notes }}
+
{{ notes }}
diff --git a/frontend/src/components/systems/SystemsTable.vue b/frontend/src/components/systems/SystemsTable.vue
index 1663e63b..3b13eb34 100644
--- a/frontend/src/components/systems/SystemsTable.vue
+++ b/frontend/src/components/systems/SystemsTable.vue
@@ -18,6 +18,7 @@ import {
faFilePdf,
faFileCsv,
faKey,
+ faRotateLeft,
} from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import {
@@ -47,7 +48,7 @@ import { savePageSizeToStorage } from '@/lib/tablePageSize'
import { canManageSystems } from '@/lib/permissions'
import { useSystems } from '@/queries/systems/systems'
import {
- getExport,
+ exportSystem,
getProductLogo,
getProductName,
SYSTEMS_TABLE_ID,
@@ -59,13 +60,14 @@ import DeleteSystemModal from './DeleteSystemModal.vue'
import { useProductFilter } from '@/queries/systems/productFilter'
import { useCreatedByFilter } from '@/queries/systems/createdByFilter'
import { useVersionFilter } from '@/queries/systems/versionFilter'
+import { useOrganizationFilter } from '@/queries/systems/organizationFilter'
import UserAvatar from '../UserAvatar.vue'
import { buildVersionFilterOptions } from '@/lib/systems/versionFilter'
import OrganizationIcon from '../OrganizationIcon.vue'
-import { downloadFile } from '@/lib/common'
import RegenerateSecretModal from './RegenerateSecretModal.vue'
import SecretRegeneratedModal from './SecretRegeneratedModal.vue'
import ClickToCopy from '../ClickToCopy.vue'
+import RestoreSystemModal from './RestoreSystemModal.vue'
const { isShownCreateSystemDrawer = false } = defineProps<{
isShownCreateSystemDrawer: boolean
@@ -85,6 +87,7 @@ const {
createdByFilter,
versionFilter,
statusFilter,
+ organizationFilter,
sortBy,
sortDescending,
} = useSystems()
@@ -92,10 +95,13 @@ const { state: productFilterState, asyncStatus: productFilterAsyncStatus } = use
const { state: createdByFilterState, asyncStatus: createdByFilterAsyncStatus } =
useCreatedByFilter()
const { state: versionFilterState, asyncStatus: versionFilterAsyncStatus } = useVersionFilter()
+const { state: organizationFilterState, asyncStatus: organizationFilterAsyncStatus } =
+ useOrganizationFilter()
const currentSystem = ref
()
const isShownCreateOrEditSystemDrawer = ref(false)
const isShownDeleteSystemModal = ref(false)
+const isShownRestoreSystemModal = ref(false)
const isShownRegenerateSecretModal = ref(false)
const isShownSecretRegeneratedModal = ref(false)
const newSecret = ref('')
@@ -163,6 +169,31 @@ const createdByFilterOptions = computed(() => {
}
})
+const organizationFilterOptions = computed(() => {
+ if (!organizationFilterState.value.data || !organizationFilterState.value.data.organizations) {
+ return []
+ } else {
+ return organizationFilterState.value.data.organizations.map((org) => ({
+ id: org.id,
+ label: org.name,
+ }))
+ }
+})
+
+const isNoDataEmptyStateShown = computed(() => {
+ return (
+ !systemsPage.value?.length && !debouncedTextFilter.value && state.value.status === 'success'
+ )
+})
+
+const isNoMatchEmptyStateShown = computed(() => {
+ return !systemsPage.value?.length && !!debouncedTextFilter.value
+})
+
+const noEmptyStateShown = computed(() => {
+ return !isNoDataEmptyStateShown.value && !isNoMatchEmptyStateShown.value
+})
+
watch(
() => isShownCreateSystemDrawer,
() => {
@@ -204,6 +235,11 @@ function showDeleteSystemModal(system: System) {
isShownDeleteSystemModal.value = true
}
+function showRestoreSystemModal(system: System) {
+ currentSystem.value = system
+ isShownRestoreSystemModal.value = true
+}
+
function showRegenerateSecretModal(system: System) {
currentSystem.value = system
isShownRegenerateSecretModal.value = true
@@ -217,7 +253,7 @@ function onCloseDrawer() {
function getKebabMenuItems(system: System) {
let items: NeDropdownItem[] = []
- if (canManageSystems()) {
+ if (canManageSystems() && system.status !== 'deleted') {
items.push({
id: 'editSystem',
label: t('common.edit'),
@@ -234,18 +270,18 @@ function getKebabMenuItems(system: System) {
label: t('systems.export_to_pdf'),
icon: faFilePdf,
action: () => exportSystem(system, 'pdf'),
- disabled: !state.value.data?.systems,
+ disabled: asyncStatus.value === 'loading',
},
{
id: 'exportToCsv',
label: t('systems.export_to_csv'),
icon: faFileCsv,
action: () => exportSystem(system, 'csv'),
- disabled: !state.value.data?.systems,
+ disabled: asyncStatus.value === 'loading',
},
]
- if (canManageSystems()) {
+ if (canManageSystems() && system.status !== 'deleted') {
items = [
...items,
{
@@ -265,6 +301,17 @@ function getKebabMenuItems(system: System) {
},
]
}
+
+ if (canManageSystems() && system.status === 'deleted') {
+ items.push({
+ id: 'restoreSystem',
+ label: t('common.restore'),
+ icon: faRotateLeft,
+ action: () => showRestoreSystemModal(system),
+ disabled: asyncStatus.value === 'loading',
+ })
+ }
+
return items
}
@@ -277,17 +324,6 @@ const goToSystemDetails = (system: System) => {
router.push({ name: 'system_detail', params: { systemId: system.id } })
}
-async function exportSystem(system: System, format: 'pdf' | 'csv') {
- try {
- const exportData = await getExport(format, system.system_key)
- const fileName = `${system.name}.${format}`
- downloadFile(exportData, fileName, format)
- } catch (error) {
- console.error('Cannot export system to pdf:', error)
- throw error
- }
-}
-
function onSecretRegenerated(secret: string) {
newSecret.value = secret
isShownSecretRegeneratedModal.value = true
@@ -309,113 +345,9 @@ function onCloseSecretRegeneratedModal() {
:description="state.error.message"
class="mb-6"
/>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ t('systems.reset_filters') }}
-
-
-
-
-
-
- {{ $t('common.updating') }}
-
-
-
-
-
-
-
- {{ $t('systems.reset_filters') }}
-
-
-
-
- {{
- $t('systems.name')
- }}
- {{
- $t('systems.version')
- }}
- {{
- $t('systems.fqdn_ip_address')
- }}
- {{
- $t('systems.organization')
- }}
- {{
- $t('systems.created_by')
- }}
- {{
- $t('systems.status')
- }}
-
-
-
-
-
-
-
-
-
-
-
![]()
-
- {{ item.name || '-' }}
-
-
-
-
-
-
-
- {{ item.version || '-' }}
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('systems.reset_filters') }}
+
+
+
+
-
-
-
-
- {{ item.ipv6_address }}
-
-
-
+
+
+ {{ $t('common.updating') }}
-
-
-
-
-
-
-
-
-
- {{ t(`organizations.${item.organization.type}`) }}
-
-
- {{ item.organization.name || '-' }}
+
+
+
+
+
+
+ {{ $t('systems.reset_filters') }}
+
+
+
+
+ {{
+ $t('systems.name')
+ }}
+ {{
+ $t('systems.version')
+ }}
+ {{
+ $t('systems.fqdn_ip_address')
+ }}
+ {{
+ $t('systems.organization')
+ }}
+ {{
+ $t('systems.created_by')
+ }}
+ {{
+ $t('systems.status')
+ }}
+
+
+
+
+
+
+
+
+
+
+
![]()
+
+ {{ item.name || '-' }}
+
+
+
-
-
-
-
-
+
+
+
+ {{ item.version || '-' }}
+
+
+
+
+
+
+
+ {{ item.ipv6_address }}
+
+
-
+
+
+
+
-
-
-
{{ item.created_by.name || '-' }}
-
- {{ item.created_by.organization_name }}
+
+
+
+
+
+ {{ t(`organizations.${item.organization.type}`) }}
+
+
+ {{ item.organization.name || '-' }}
+
+
+
+
+
+
+
+
+
+
{{ item.created_by.name || '-' }}
+
+ {{ item.created_by.organization_name }}
+
-
-
- -
-
-
-
-
-
-
-
-
-
- {{ t(`systems.status_${item.status}`) }}
-
- -
-
-
-
-
-
-
-
- {{ $t('common.view_details') }}
-
-
-
-
-
-
-
-
- {
- pageNum = page
- }
- "
- @select-page-size="
- (size: number) => {
- pageSize = size
- savePageSizeToStorage(SYSTEMS_TABLE_ID, size)
- }
- "
- />
-
-
+
-
+
+
+
+
+
+
+
+
+
+ {{ t(`systems.status_${item.status}`) }}
+
+ -
+
+
+
+
+
+
+
+
+ {{ $t('common.view') }}
+
+
+
+
+
+
+
+
+ {
+ pageNum = page
+ }
+ "
+ @select-page-size="
+ (size: number) => {
+ pageSize = size
+ savePageSizeToStorage(SYSTEMS_TABLE_ID, size)
+ }
+ "
+ />
+
+
+
+
+
{
queryCache.invalidateQueries({ key: [USERS_KEY] })
queryCache.invalidateQueries({ key: [USERS_TOTAL_KEY] })
+ queryCache.invalidateQueries({ key: [SYSTEM_ORGANIZATION_FILTER_KEY] })
},
})
@@ -120,7 +124,10 @@ const {
console.error('Error editing user:', error)
validationIssues.value = getValidationIssues(error as AxiosError, 'users')
},
- onSettled: () => queryCache.invalidateQueries({ key: [USERS_KEY] }),
+ onSettled: () => {
+ queryCache.invalidateQueries({ key: [USERS_KEY] })
+ // queryCache.invalidateQueries({ key: [ORGANIZATION_FILTER_KEY] }) ////
+ },
})
const email = ref('')
@@ -153,7 +160,7 @@ const organizationOptions = computed(() => {
}
return organizations.value.data?.map((org) => ({
- id: org.id,
+ id: org.logto_id,
label: org.name,
description: t(`organizations.${org.type}`),
}))
diff --git a/frontend/src/components/users/ReactivateUserModal.vue b/frontend/src/components/users/ReactivateUserModal.vue
new file mode 100644
index 00000000..ffeac132
--- /dev/null
+++ b/frontend/src/components/users/ReactivateUserModal.vue
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+ {{ t('users.reactivate_user_confirmation', { name: user?.name }) }}
+
+
+
+
diff --git a/frontend/src/components/users/SuspendUserModal.vue b/frontend/src/components/users/SuspendUserModal.vue
new file mode 100644
index 00000000..82b34449
--- /dev/null
+++ b/frontend/src/components/users/SuspendUserModal.vue
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+ {{ t('users.suspend_user_confirmation', { name: user?.name }) }}
+
+
+
+
diff --git a/frontend/src/components/users/UsersTable.vue b/frontend/src/components/users/UsersTable.vue
index adcd6daa..d65fa348 100644
--- a/frontend/src/components/users/UsersTable.vue
+++ b/frontend/src/components/users/UsersTable.vue
@@ -13,6 +13,9 @@ import {
faTrash,
faKey,
faUserSecret,
+ faCirclePause,
+ faCirclePlay,
+ faCircleCheck,
} from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import {
@@ -31,8 +34,9 @@ import {
NeDropdown,
type SortEvent,
NeSortDropdown,
- NeBadge,
sortByProperty,
+ type NeDropdownItem,
+ NeTooltip,
} from '@nethesis/vue-components'
import { computed, ref, watch } from 'vue'
import CreateOrEditUserDrawer from './CreateOrEditUserDrawer.vue'
@@ -45,7 +49,11 @@ import { useUsers } from '@/queries/users'
import { canManageUsers, canImpersonateUsers } from '@/lib/permissions'
import { useLoginStore } from '@/stores/login'
import ImpersonateUserModal from './ImpersonateUserModal.vue'
-import { normalize } from '@/lib/common'
+import SuspendUserModal from './SuspendUserModal.vue'
+import ReactivateUserModal from './ReactivateUserModal.vue'
+import OrganizationIcon from '../OrganizationIcon.vue'
+import UserRoleBadge from '../UserRoleBadge.vue'
+// import { useOrganizationFilter } from '@/queries/systems/organizationFilter' ////
const { isShownCreateUserDrawer = false } = defineProps<{
isShownCreateUserDrawer: boolean
@@ -64,8 +72,10 @@ const {
sortBy,
sortDescending,
} = useUsers()
-
const loginStore = useLoginStore()
+// const { state: organizationFilterState } = useOrganizationFilter() ////
+// const { state: userRoleFilterState, asyncStatus: userRoleFilterAsyncStatus } =
+// useUserRoleFilter() ////
const currentUser = ref()
const isShownCreateOrEditUserDrawer = ref(false)
@@ -73,6 +83,8 @@ const isShownDeleteUserModal = ref(false)
const isShownResetPasswordModal = ref(false)
const isShownPasswordChangedModal = ref(false)
const isShownImpersonateUserModal = ref(false)
+const isShownSuspendUserModal = ref(false)
+const isShownReactivateUserModal = ref(false)
const newPassword = ref('')
const isImpersonating = ref(false)
@@ -84,6 +96,43 @@ const pagination = computed(() => {
return state.value.data?.pagination
})
+const isNoDataEmptyStateShown = computed(() => {
+ return !usersPage.value?.length && !debouncedTextFilter.value && state.value.status === 'success'
+})
+
+const isNoMatchEmptyStateShown = computed(() => {
+ return !usersPage.value?.length && !!debouncedTextFilter.value
+})
+
+const noEmptyStateShown = computed(() => {
+ return !isNoDataEmptyStateShown.value && !isNoMatchEmptyStateShown.value
+})
+
+////
+// const organizationFilterOptions = computed(() => {
+// if (!organizationFilterState.value.data || !organizationFilterState.value.data.organizations) {
+// return []
+// } else {
+// return organizationFilterState.value.data.organizations.map((org) => ({
+// id: org.id,
+// label: org.name,
+// }))
+// }
+// })
+
+////
+// const userRoleOptions = computed(() => {
+// if (!allUserRoles.value.data) {
+// return []
+// }
+
+// return allUserRoles.value.data?.map((role) => ({
+// id: role.id,
+// label: t(`user_roles.${normalize(role.name)}`),
+// description: t(`user_roles.${normalize(role.name)}_description`),
+// }))
+// })
+
watch(
() => isShownCreateUserDrawer,
() => {
@@ -118,6 +167,16 @@ function showResetPasswordModal(user: User) {
isShownResetPasswordModal.value = true
}
+function showSuspendUserModal(user: User) {
+ currentUser.value = user
+ isShownSuspendUserModal.value = true
+}
+
+function showReactivateUserModal(user: User) {
+ currentUser.value = user
+ isShownReactivateUserModal.value = true
+}
+
function showImpersonateUserModal(user: User) {
currentUser.value = user
isShownImpersonateUserModal.value = true
@@ -134,36 +193,67 @@ function onCloseDrawer() {
}
function getKebabMenuItems(user: User) {
- const items = [
- {
- id: 'resetPassword',
- label: t('users.reset_password'),
- icon: faKey,
- action: () => showResetPasswordModal(user),
- disabled: asyncStatus.value === 'loading',
- },
- {
- id: 'deleteAccount',
- label: t('common.delete'),
- icon: faTrash,
- danger: true,
- action: () => showDeleteUserModal(user),
- disabled: asyncStatus.value === 'loading',
- },
- ]
+ let items: NeDropdownItem[] = []
// Add impersonate option for owners, but not for self
if (canImpersonateUsers() && user.id !== loginStore.userInfo?.id) {
- items.unshift({
- id: 'impersonate',
- label: t('users.impersonate_user'),
- icon: faUserSecret,
- action: () => showImpersonateUserModal(user),
- disabled:
- asyncStatus.value === 'loading' || isImpersonating.value || !user.can_be_impersonated,
- })
+ items = [
+ ...items,
+ {
+ id: 'impersonate',
+ label: t('users.impersonate_user'),
+ icon: faUserSecret,
+ action: () => showImpersonateUserModal(user),
+ disabled:
+ asyncStatus.value === 'loading' || isImpersonating.value || !user.can_be_impersonated,
+ },
+ ]
}
+ if (canManageUsers()) {
+ if (user.suspended_at) {
+ items = [
+ ...items,
+ {
+ id: 'reactivateUser',
+ label: t('users.reactivate'),
+ icon: faCirclePlay,
+ action: () => showReactivateUserModal(user),
+ disabled: asyncStatus.value === 'loading',
+ },
+ ]
+ } else {
+ items = [
+ ...items,
+ {
+ id: 'suspendUser',
+ label: t('users.suspend'),
+ icon: faCirclePause,
+ action: () => showSuspendUserModal(user),
+ disabled: asyncStatus.value === 'loading',
+ },
+ ]
+ }
+
+ items = [
+ ...items,
+ {
+ id: 'resetPassword',
+ label: t('users.reset_password'),
+ icon: faKey,
+ action: () => showResetPasswordModal(user),
+ disabled: asyncStatus.value === 'loading',
+ },
+ {
+ id: 'deleteAccount',
+ label: t('common.delete'),
+ icon: faTrash,
+ danger: true,
+ action: () => showDeleteUserModal(user),
+ disabled: asyncStatus.value === 'loading',
+ },
+ ]
+ }
return items
}
@@ -188,177 +278,228 @@ const onClosePasswordChangedModal = () => {
:description="state.error.message"
class="mb-6"
/>
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ $t('common.updating') }}
+
+
+
+
+
+
+
+ {{ $t('users.create_user') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('common.updating') }}
+
-
-
-
- {{
- $t('users.name')
- }}
- {{
- $t('users.email')
- }}
- {{
- $t('users.organization')
- }}
- {{ $t('users.roles') }}
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+ {{ $t('common.clear_filters') }}
+
+
+
+ {{
+ $t('users.name')
+ }}
+ {{
+ $t('users.email')
+ }}
+ {{
+ $t('users.organization')
+ }}
+ {{ $t('users.roles') }}
+ {{
+ $t('common.status')
+ }}
+
+
+
+
+
+
+
+ {{ item.name }}
+
+
+ {{ item.email }}
+
+
+
+
+
+
+
+
+ {{ t(`organizations.${item.organization.type}`) }}
+
+
+ {{ item.organization.name || '-' }}
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+ {{ t('users.suspended') }}
+
- {{ $t('users.create_user') }}
-
-
-
-
-
-
-
-
-
- {{ $t('common.clear_filters') }}
-
-
-
-
-
- {{ item.name }}
-
-
- {{ item.email }}
-
-
- {{ item.organization?.name || '-' }}
-
-
- -
-
-
-
-
-
-
-
-
-
+
+
+
+ {{ t('common.enabled') }}
+
- {{ $t('common.edit') }}
-
-
-
-
-
-
-
-
- {
- pageNum = page
- }
- "
- @select-page-size="
- (size: number) => {
- pageSize = size
- savePageSizeToStorage(USERS_TABLE_ID, size)
- }
- "
- />
-
-
+
+
+
+
+
+
+
+
+ {{ $t('common.edit') }}
+
+
+
+
+
+
+
+
+ {
+ pageNum = page
+ }
+ "
+ @select-page-size="
+ (size: number) => {
+ pageSize = size
+ savePageSizeToStorage(USERS_TABLE_ID, size)
+ }
+ "
+ />
+
+
+
{
:user="currentUser"
@close="isShownDeleteUserModal = false"
/>
+
+
+
+
+
+interface ApplicationsResponse {
+ code: number
+ message: string
+ data: {
+ applications: Application[]
+ pagination: Pagination
+ }
+}
+
+interface ApplicationsTotalResponse {
+ code: number
+ message: string
+ data: {
+ total: number
+ unassigned: number
+ assigned: number
+ with_errors: number
+ by_type: {
+ mail: number
+ webtop: number
+ nethvoice: number
+ nextcloud: number
+ }
+ by_status: {
+ assigned: number
+ unassigned: number
+ }
+ }
+}
+
+export const getQueryStringParams = (
+ pageNum: number,
+ pageSize: number,
+ textFilter: string | null,
+ typeFilter: string[],
+ versionFilter: string[],
+ systemFilter: string[],
+ organizationFilter: string[],
+ sortBy: string | null,
+ sortDescending: boolean,
+) => {
+ const searchParams = new URLSearchParams({
+ page: pageNum.toString(),
+ page_size: pageSize.toString(),
+ sort_by: sortBy || '',
+ sort_direction: sortDescending ? 'desc' : 'asc',
+ })
+
+ if (textFilter?.trim()) {
+ searchParams.append('search', textFilter)
+ }
+
+ typeFilter.forEach((product) => {
+ searchParams.append('type', product)
+ })
+
+ versionFilter.forEach((version) => {
+ searchParams.append('version', version)
+ })
+
+ systemFilter.forEach((systemId) => {
+ searchParams.append('system_id', systemId)
+ })
+
+ organizationFilter.forEach((orgId) => {
+ searchParams.append('organization_id', orgId)
+ })
+ return searchParams.toString()
+}
+
+export const getDisplayName = (app: Application) => {
+ if (app.display_name) {
+ return `${app.display_name} (${app.module_id})`
+ } else {
+ return app.module_id
+ }
+}
+
+export const getApplicationLogo = (appId: string) => {
+ try {
+ return new URL(`../../assets/application_logos/${appId}.svg`, import.meta.url).href
+ } catch {
+ return undefined
+ }
+}
+
+export const saveShowUnassignedAppsNotificationToStorage = (show: boolean) => {
+ const loginStore = useLoginStore()
+ const username = loginStore.userInfo?.email
+
+ if (username) {
+ savePreference(SHOW_UNASSIGNED_APPS_NOTIFICATION, show, username)
+ }
+}
+
+export const getApplications = (
+ pageNum: number,
+ pageSize: number,
+ textFilter: string,
+ typeFilter: string[],
+ versionFilter: string[],
+ systemFilter: string[],
+ organizationFilter: string[],
+ sortBy: string,
+ sortDescending: boolean,
+) => {
+ const loginStore = useLoginStore()
+ const params = getQueryStringParams(
+ pageNum,
+ pageSize,
+ textFilter,
+ typeFilter,
+ versionFilter,
+ systemFilter,
+ organizationFilter,
+ sortBy,
+ sortDescending,
+ )
+
+ return axios
+ .get(`${API_URL}/applications?${params}`, {
+ headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
+ })
+ .then((res) => res.data.data)
+}
+
+export const assignOrganization = (organizationId: string, applicationId: string) => {
+ const loginStore = useLoginStore()
+
+ return axios.patch(
+ `${API_URL}/applications/${applicationId}/assign`,
+ { organization_id: organizationId },
+ {
+ headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
+ },
+ )
+}
+
+export const putApplication = (application: Application) => {
+ const loginStore = useLoginStore()
+
+ return axios.put(`${API_URL}/applications/${application.id}`, application, {
+ headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
+ })
+}
+
+export const getApplicationsTotal = () => {
+ const loginStore = useLoginStore()
+
+ return axios
+ .get(`${API_URL}/applications/totals`, {
+ headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
+ })
+ .then((res) => res.data.data)
+}
diff --git a/frontend/src/lib/applications/organizationFilter.ts b/frontend/src/lib/applications/organizationFilter.ts
new file mode 100644
index 00000000..3898deae
--- /dev/null
+++ b/frontend/src/lib/applications/organizationFilter.ts
@@ -0,0 +1,34 @@
+// Copyright (C) 2026 Nethesis S.r.l.
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import axios from 'axios'
+import { API_URL } from '../config'
+import { useLoginStore } from '@/stores/login'
+
+export const APPLICATION_ORGANIZATION_FILTER_KEY = 'applicationOrganizationFilter'
+
+const APPLICATION_ORGANIZATION_FILTER_PATH = 'filters/applications/organizations'
+
+interface OrganizationFilterResponse {
+ code: number
+ message: string
+ data: ApplicationOrganization[]
+}
+
+interface ApplicationOrganization {
+ id: string
+ logto_id: string
+ name: string
+ description: string
+ type: string
+}
+
+export const getOrganizationFilter = () => {
+ const loginStore = useLoginStore()
+
+ return axios
+ .get(`${API_URL}/${APPLICATION_ORGANIZATION_FILTER_PATH}`, {
+ headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
+ })
+ .then((res) => res.data.data)
+}
diff --git a/frontend/src/lib/applications/systemFilter.ts b/frontend/src/lib/applications/systemFilter.ts
new file mode 100644
index 00000000..0ec40b19
--- /dev/null
+++ b/frontend/src/lib/applications/systemFilter.ts
@@ -0,0 +1,31 @@
+// Copyright (C) 2026 Nethesis S.r.l.
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import axios from 'axios'
+import { API_URL } from '../config'
+import { useLoginStore } from '@/stores/login'
+
+export const APPLICATION_SYSTEM_FILTER_KEY = 'applicationSystemFilter'
+
+const APPLICATION_SYSTEM_FILTER_PATH = 'filters/applications/systems'
+
+interface SystemFilterResponse {
+ code: number
+ message: string
+ data: ApplicationSystem[]
+}
+
+interface ApplicationSystem {
+ id: string
+ name: string
+}
+
+export const getSystemFilter = () => {
+ const loginStore = useLoginStore()
+
+ return axios
+ .get(`${API_URL}/${APPLICATION_SYSTEM_FILTER_PATH}`, {
+ headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
+ })
+ .then((res) => res.data.data)
+}
diff --git a/frontend/src/lib/applications/typeFilter.ts b/frontend/src/lib/applications/typeFilter.ts
new file mode 100644
index 00000000..371b2eec
--- /dev/null
+++ b/frontend/src/lib/applications/typeFilter.ts
@@ -0,0 +1,32 @@
+// Copyright (C) 2026 Nethesis S.r.l.
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import axios from 'axios'
+import { API_URL } from '../config'
+import { useLoginStore } from '@/stores/login'
+
+export const APPLICATION_TYPE_FILTER_KEY = 'applicationTypeFilter'
+
+const APPLICATION_TYPE_FILTER_PATH = 'filters/applications/types'
+
+interface TypeFilterResponse {
+ code: number
+ message: string
+ data: ApplicationType[]
+}
+
+interface ApplicationType {
+ instance_of: string
+ is_user_facing: boolean
+ count: number
+}
+
+export const getTypeFilter = () => {
+ const loginStore = useLoginStore()
+
+ return axios
+ .get(`${API_URL}/${APPLICATION_TYPE_FILTER_PATH}`, {
+ headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
+ })
+ .then((res) => res.data.data)
+}
diff --git a/frontend/src/lib/applications/versionFilter.ts b/frontend/src/lib/applications/versionFilter.ts
new file mode 100644
index 00000000..6f1ac3b4
--- /dev/null
+++ b/frontend/src/lib/applications/versionFilter.ts
@@ -0,0 +1,56 @@
+// Copyright (C) 2026 Nethesis S.r.l.
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import axios from 'axios'
+import { API_URL } from '../config'
+import { useLoginStore } from '@/stores/login'
+import type { FilterOption } from '@nethesis/vue-components'
+
+export const APPLICATION_VERSION_FILTER_KEY = 'applicationVersionFilter'
+
+const APPLICATION_VERSION_FILTER_PATH = 'filters/applications/versions'
+
+export interface ApplicationVersions {
+ application: string
+ name: string
+ versions: string[]
+}
+
+interface VersionFilterResponse {
+ code: number
+ message: string
+ data: {
+ versions: ApplicationVersions[]
+ }
+}
+
+export const getVersionFilter = () => {
+ const loginStore = useLoginStore()
+
+ return axios
+ .get(`${API_URL}/${APPLICATION_VERSION_FILTER_PATH}`, {
+ headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
+ })
+ .then((res) => res.data.data)
+}
+
+export const buildVersionFilterOptions = (applicationVersions: ApplicationVersions[]) => {
+ const options: FilterOption[] = []
+
+ applicationVersions.forEach((av) => {
+ const appName = av.name
+
+ av.versions.forEach((appAndVersion) => {
+ // split application and version
+ const [, version] = appAndVersion.split(':')
+
+ if (appName && version) {
+ options.push({
+ id: appAndVersion,
+ label: `${appName} ${version}`,
+ })
+ }
+ })
+ })
+ return options
+}
diff --git a/frontend/src/lib/customers.ts b/frontend/src/lib/customers.ts
index d74830b7..1a78a2e0 100644
--- a/frontend/src/lib/customers.ts
+++ b/frontend/src/lib/customers.ts
@@ -21,7 +21,7 @@ export const CreateCustomerSchema = v.object({
export const CustomerSchema = v.object({
...CreateCustomerSchema.entries,
- id: v.string(),
+ logto_id: v.string(),
})
export type CreateCustomer = v.InferOutput
@@ -64,7 +64,7 @@ export const postCustomer = (customer: CreateCustomer) => {
export const putCustomer = (customer: Customer) => {
const loginStore = useLoginStore()
- return axios.put(`${API_URL}/customers/${customer.id}`, customer, {
+ return axios.put(`${API_URL}/customers/${customer.logto_id}`, customer, {
headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
})
}
@@ -72,7 +72,7 @@ export const putCustomer = (customer: Customer) => {
export const deleteCustomer = (customer: Customer) => {
const loginStore = useLoginStore()
- return axios.delete(`${API_URL}/customers/${customer.id}`, {
+ return axios.delete(`${API_URL}/customers/${customer.logto_id}`, {
headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
})
}
diff --git a/frontend/src/lib/distributors.ts b/frontend/src/lib/distributors.ts
index bae80542..d068fbe5 100644
--- a/frontend/src/lib/distributors.ts
+++ b/frontend/src/lib/distributors.ts
@@ -21,7 +21,7 @@ export const CreateDistributorSchema = v.object({
export const DistributorSchema = v.object({
...CreateDistributorSchema.entries,
- id: v.string(),
+ logto_id: v.string(),
})
export type CreateDistributor = v.InferOutput
@@ -64,7 +64,7 @@ export const postDistributor = (distributor: CreateDistributor) => {
export const putDistributor = (distributor: Distributor) => {
const loginStore = useLoginStore()
- return axios.put(`${API_URL}/distributors/${distributor.id}`, distributor, {
+ return axios.put(`${API_URL}/distributors/${distributor.logto_id}`, distributor, {
headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
})
}
@@ -72,7 +72,7 @@ export const putDistributor = (distributor: Distributor) => {
export const deleteDistributor = (distributor: Distributor) => {
const loginStore = useLoginStore()
- return axios.delete(`${API_URL}/distributors/${distributor.id}`, {
+ return axios.delete(`${API_URL}/distributors/${distributor.logto_id}`, {
headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
})
}
diff --git a/frontend/src/lib/organizations.ts b/frontend/src/lib/organizations.ts
index 94071c5a..ae5bb603 100644
--- a/frontend/src/lib/organizations.ts
+++ b/frontend/src/lib/organizations.ts
@@ -5,16 +5,19 @@ import axios from 'axios'
import { API_URL } from './config'
import { useLoginStore } from '@/stores/login'
import { faBuilding, faCity, faCrown, faGlobe, faQuestion } from '@fortawesome/free-solid-svg-icons'
-
-export type Organization = {
- id: string
- name: string
- description: string
- type: string
-}
+import * as v from 'valibot'
export const ORGANIZATIONS_KEY = 'organizations'
+export const OrganizationSchema = v.object({
+ logto_id: v.string(),
+ name: v.string(),
+ description: v.string(),
+ type: v.string(),
+})
+
+export type Organization = v.InferOutput
+
export const getOrganizations = () => {
const loginStore = useLoginStore()
diff --git a/frontend/src/lib/permissions.ts b/frontend/src/lib/permissions.ts
index 96a0fc73..7a228a5d 100644
--- a/frontend/src/lib/permissions.ts
+++ b/frontend/src/lib/permissions.ts
@@ -14,6 +14,8 @@ const MANAGE_USERS = 'manage:users'
const IMPERSONATE_USERS = 'impersonate:users'
const READ_SYSTEMS = 'read:systems'
const MANAGE_SYSTEMS = 'manage:systems'
+const READ_APPLICATIONS = 'read:applications'
+const MANAGE_APPLICATIONS = 'manage:applications'
export const canReadDistributors = () => {
const loginStore = useLoginStore()
@@ -69,3 +71,13 @@ export const canManageSystems = () => {
const loginStore = useLoginStore()
return loginStore.permissions.includes(MANAGE_SYSTEMS)
}
+
+export const canReadApplications = () => {
+ const loginStore = useLoginStore()
+ return loginStore.permissions.includes(READ_APPLICATIONS)
+}
+
+export const canManageApplications = () => {
+ const loginStore = useLoginStore()
+ return loginStore.permissions.includes(MANAGE_APPLICATIONS)
+}
diff --git a/frontend/src/lib/resellers.ts b/frontend/src/lib/resellers.ts
index 28a2e3b6..3637db83 100644
--- a/frontend/src/lib/resellers.ts
+++ b/frontend/src/lib/resellers.ts
@@ -21,7 +21,7 @@ export const CreateResellerSchema = v.object({
export const ResellerSchema = v.object({
...CreateResellerSchema.entries,
- id: v.string(),
+ logto_id: v.string(),
})
export type CreateReseller = v.InferOutput
@@ -64,7 +64,7 @@ export const postReseller = (reseller: CreateReseller) => {
export const putReseller = (reseller: Reseller) => {
const loginStore = useLoginStore()
- return axios.put(`${API_URL}/resellers/${reseller.id}`, reseller, {
+ return axios.put(`${API_URL}/resellers/${reseller.logto_id}`, reseller, {
headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
})
}
@@ -72,7 +72,7 @@ export const putReseller = (reseller: Reseller) => {
export const deleteReseller = (reseller: Reseller) => {
const loginStore = useLoginStore()
- return axios.delete(`${API_URL}/resellers/${reseller.id}`, {
+ return axios.delete(`${API_URL}/resellers/${reseller.logto_id}`, {
headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
})
}
diff --git a/frontend/src/lib/systems/createdByFilter.ts b/frontend/src/lib/systems/createdByFilter.ts
index 8d8b1031..854f4df6 100644
--- a/frontend/src/lib/systems/createdByFilter.ts
+++ b/frontend/src/lib/systems/createdByFilter.ts
@@ -5,8 +5,9 @@ import axios from 'axios'
import { API_URL } from '../config'
import { useLoginStore } from '@/stores/login'
-export const CREATED_BY_FILTER_KEY = 'createdByFilter'
-export const CREATED_BY_FILTER_PATH = 'filters/created-by'
+export const SYSTEM_CREATED_BY_FILTER_KEY = 'systemCreatedByFilter'
+
+const SYSTEM_CREATED_BY_FILTER_PATH = 'filters/systems/created-by'
interface CreatedByFilterResponse {
code: number
@@ -25,7 +26,7 @@ export const getCreatedByFilter = () => {
const loginStore = useLoginStore()
return axios
- .get(`${API_URL}/${CREATED_BY_FILTER_PATH}`, {
+ .get(`${API_URL}/${SYSTEM_CREATED_BY_FILTER_PATH}`, {
headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
})
.then((res) => res.data.data)
diff --git a/frontend/src/lib/systems/organizationFilter.ts b/frontend/src/lib/systems/organizationFilter.ts
new file mode 100644
index 00000000..eab5f366
--- /dev/null
+++ b/frontend/src/lib/systems/organizationFilter.ts
@@ -0,0 +1,33 @@
+// Copyright (C) 2026 Nethesis S.r.l.
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import axios from 'axios'
+import { API_URL } from '../config'
+import { useLoginStore } from '@/stores/login'
+
+export const SYSTEM_ORGANIZATION_FILTER_KEY = 'systemOrganizationFilter'
+
+const SYSTEM_ORGANIZATION_FILTER_PATH = 'filters/systems/organizations'
+
+interface OrganizationFilterResponse {
+ code: number
+ message: string
+ data: {
+ organizations: OrganizationItem[]
+ }
+}
+
+interface OrganizationItem {
+ id: string // logto_id
+ name: string
+}
+
+export const getOrganizationFilter = () => {
+ const loginStore = useLoginStore()
+
+ return axios
+ .get(`${API_URL}/${SYSTEM_ORGANIZATION_FILTER_PATH}`, {
+ headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
+ })
+ .then((res) => res.data.data)
+}
diff --git a/frontend/src/lib/systems/productFilter.ts b/frontend/src/lib/systems/productFilter.ts
index f66ae5d3..09237f50 100644
--- a/frontend/src/lib/systems/productFilter.ts
+++ b/frontend/src/lib/systems/productFilter.ts
@@ -6,7 +6,7 @@ import { API_URL } from '../config'
import { useLoginStore } from '@/stores/login'
export const PRODUCT_FILTER_KEY = 'productFilter'
-export const PRODUCT_FILTER_PATH = 'filters/products'
+export const PRODUCT_FILTER_PATH = 'filters/systems/products'
interface ProductFilterResponse {
code: number
diff --git a/frontend/src/lib/systems/systems.ts b/frontend/src/lib/systems/systems.ts
index 3005352b..7b104eb8 100644
--- a/frontend/src/lib/systems/systems.ts
+++ b/frontend/src/lib/systems/systems.ts
@@ -5,9 +5,9 @@ import axios from 'axios'
import { API_URL } from '../config'
import { useLoginStore } from '@/stores/login'
import * as v from 'valibot'
-import { type Pagination } from '../common'
-import Ns8Logo from '@/assets/ns8_logo.svg'
-import NsecLogo from '@/assets/nsec_logo.svg'
+import { downloadFile, type Pagination } from '../common'
+import Ns8Logo from '@/assets/system_logos/nethserver.svg'
+import NsecLogo from '@/assets/system_logos/nethsecurity.svg'
export const SYSTEMS_KEY = 'systems'
export const SYSTEMS_TOTAL_KEY = 'systemsTotal'
@@ -98,6 +98,7 @@ export const getQueryStringParams = (
createdByFilter: string[],
versionFilter: string[],
statusFilter: SystemStatus[],
+ organizationFilter: string[],
sortBy: string | null,
sortDescending: boolean,
) => {
@@ -127,6 +128,10 @@ export const getQueryStringParams = (
statusFilter.forEach((status) => {
searchParams.append('status', status)
})
+
+ organizationFilter.forEach((orgId) => {
+ searchParams.append('organization_id', orgId)
+ })
return searchParams.toString()
}
@@ -196,6 +201,7 @@ export const getSystems = (
createdByFilter: string[],
versionFilter: string[],
statusFilter: SystemStatus[],
+ organizationFilter: string[],
sortBy: string,
sortDescending: boolean,
) => {
@@ -208,6 +214,7 @@ export const getSystems = (
createdByFilter,
versionFilter,
statusFilter,
+ organizationFilter,
sortBy,
sortDescending,
)
@@ -245,6 +252,18 @@ export const deleteSystem = (system: System) => {
})
}
+export const restoreSystem = (system: System) => {
+ const loginStore = useLoginStore()
+
+ return axios.patch(
+ `${API_URL}/systems/${system.id}/restore`,
+ {},
+ {
+ headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
+ },
+ )
+}
+
export const regenerateSystemSecret = (systemId: string) => {
const loginStore = useLoginStore()
@@ -290,6 +309,17 @@ export const getProductLogo = (systemType: string) => {
}
}
+export async function exportSystem(system: System, format: 'pdf' | 'csv') {
+ try {
+ const exportData = await getExport(format, system.system_key)
+ const fileName = `${system.name}.${format}`
+ downloadFile(exportData, fileName, format)
+ } catch (error) {
+ console.error(`Cannot export system to ${format}:`, error)
+ throw error
+ }
+}
+
export const getExport = (
format: 'csv' | 'pdf',
systemKey: string | undefined = undefined,
diff --git a/frontend/src/lib/systems/versionFilter.ts b/frontend/src/lib/systems/versionFilter.ts
index 3360ba7f..c7a9db1d 100644
--- a/frontend/src/lib/systems/versionFilter.ts
+++ b/frontend/src/lib/systems/versionFilter.ts
@@ -7,8 +7,9 @@ import { useLoginStore } from '@/stores/login'
import type { FilterOption } from '@nethesis/vue-components'
import { getProductName } from './systems'
-export const VERSION_FILTER_KEY = 'versionFilter'
-export const VERSION_FILTER_PATH = 'filters/versions'
+export const SYSTEM_VERSION_FILTER_KEY = 'systemVersionFilter'
+
+const SYSTEM_VERSION_FILTER_PATH = 'filters/systems/versions'
export interface ProductVersions {
product: string
@@ -27,7 +28,7 @@ export const getVersionFilter = () => {
const loginStore = useLoginStore()
return axios
- .get(`${API_URL}/${VERSION_FILTER_PATH}`, {
+ .get(`${API_URL}/${SYSTEM_VERSION_FILTER_PATH}`, {
headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
})
.then((res) => res.data.data)
diff --git a/frontend/src/lib/thirdPartyApps.ts b/frontend/src/lib/thirdPartyApps.ts
index ec7d3377..e9e6160d 100644
--- a/frontend/src/lib/thirdPartyApps.ts
+++ b/frontend/src/lib/thirdPartyApps.ts
@@ -38,7 +38,7 @@ export const getThirdPartyApps = () => {
const loginStore = useLoginStore()
return axios
- .get(`${API_URL}/applications`, {
+ .get(`${API_URL}/third-party-applications`, {
headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
})
.then((res) => res.data.data.sort(sortThirdPartyApps) as ThirdPartyApp[])
diff --git a/frontend/src/lib/userRoles.ts b/frontend/src/lib/userRoles.ts
index db712951..567f61df 100644
--- a/frontend/src/lib/userRoles.ts
+++ b/frontend/src/lib/userRoles.ts
@@ -5,6 +5,8 @@ import axios from 'axios'
import { API_URL } from './config'
import { useLoginStore } from '@/stores/login'
+//// is this used?
+
export type UserRole = {
id: string
name: string
@@ -12,6 +14,7 @@ export type UserRole = {
}
export const USER_ROLES_KEY = 'userRoles'
+export const USER_ROLE_FILTER_KEY = 'userRoleFilter' //// used?
export const getUserRoles = () => {
const loginStore = useLoginStore()
diff --git a/frontend/src/lib/users.ts b/frontend/src/lib/users.ts
index c26766d9..b15ed067 100644
--- a/frontend/src/lib/users.ts
+++ b/frontend/src/lib/users.ts
@@ -5,7 +5,7 @@ import axios from 'axios'
import { API_URL } from './config'
import { useLoginStore } from '@/stores/login'
import * as v from 'valibot'
-import { getQueryStringParams, type Pagination } from './common'
+import { type Pagination } from './common'
export const USERS_KEY = 'users'
export const USERS_TOTAL_KEY = 'usersTotal'
@@ -37,13 +37,13 @@ export const UserSchema = v.object({
logto_id: v.optional(v.string()),
can_be_impersonated: v.boolean(),
logto_synced_at: v.optional(v.string()),
- organization: v.optional(
- v.object({
- id: v.string(),
- logto_id: v.optional(v.string()),
- name: v.string(),
- }),
- ),
+ suspended_at: v.optional(v.string()),
+ organization: v.object({
+ id: v.string(),
+ logto_id: v.optional(v.string()),
+ name: v.string(),
+ type: v.string(),
+ }),
roles: v.optional(
v.array(
v.object({
@@ -67,15 +67,48 @@ interface UsersResponse {
}
}
+export const getQueryStringParams = (
+ pageNum: number,
+ pageSize: number,
+ textFilter: string | null,
+ organizationFilter: string[],
+ sortBy: string | null,
+ sortDescending: boolean,
+) => {
+ const searchParams = new URLSearchParams({
+ page: pageNum.toString(),
+ page_size: pageSize.toString(),
+ sort_by: sortBy || '',
+ sort_direction: sortDescending ? 'desc' : 'asc',
+ })
+
+ if (textFilter?.trim()) {
+ searchParams.append('search', textFilter)
+ }
+
+ organizationFilter.forEach((orgId) => {
+ searchParams.append('organization_id', orgId)
+ })
+ return searchParams.toString()
+}
+
export const getUsers = (
pageNum: number,
pageSize: number,
textFilter: string,
+ organizationFilter: string[],
sortBy: string,
sortDescending: boolean,
) => {
const loginStore = useLoginStore()
- const params = getQueryStringParams(pageNum, pageSize, textFilter, sortBy, sortDescending)
+ const params = getQueryStringParams(
+ pageNum,
+ pageSize,
+ textFilter,
+ organizationFilter,
+ sortBy,
+ sortDescending,
+ )
return axios
.get(`${API_URL}/users?${params}`, {
@@ -130,3 +163,55 @@ export const resetPassword = (user: User, newPassword: string) => {
},
)
}
+
+export const suspendUser = (user: User) => {
+ const loginStore = useLoginStore()
+
+ return axios.patch(
+ `${API_URL}/users/${user.id}/suspend`,
+ {},
+ {
+ headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
+ },
+ )
+}
+
+export const reactivateUser = (user: User) => {
+ const loginStore = useLoginStore()
+
+ return axios.patch(
+ `${API_URL}/users/${user.id}/reactivate`,
+ {},
+ {
+ headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
+ },
+ )
+}
+
+//// TODO wait for backend fix
+// export const getExport = (
+// format: 'csv' | 'pdf',
+// textFilter: string | undefined = undefined,
+// roleFilter: string[] | undefined = undefined,
+// organizationFilter: string[] | undefined = undefined,
+// statusFilter: SystemStatus[] | undefined = undefined,
+// sortBy: string | undefined = undefined,
+// sortDescending: boolean | undefined = undefined,
+// ) => {
+// const loginStore = useLoginStore()
+// const params = getQueryStringParamsForExport(
+// format,
+// textFilter,
+// roleFilter,
+// organizationFilter,
+// statusFilter,
+// sortBy,
+// sortDescending,
+// )
+
+// return axios
+// .get(`${API_URL}/systems/export?${params}`, {
+// headers: { Authorization: `Bearer ${loginStore.jwtToken}` },
+// })
+// .then((res) => res.data)
+// }
diff --git a/frontend/src/queries/applications.ts b/frontend/src/queries/applications.ts
new file mode 100644
index 00000000..06ff3547
--- /dev/null
+++ b/frontend/src/queries/applications.ts
@@ -0,0 +1,110 @@
+// Copyright (C) 2025 Nethesis S.r.l.
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import { MIN_SEARCH_LENGTH } from '@/lib/common'
+import { canReadApplications } from '@/lib/permissions'
+import { DEFAULT_PAGE_SIZE, loadPageSizeFromStorage } from '@/lib/tablePageSize'
+import { useLoginStore } from '@/stores/login'
+import { defineQuery, useQuery } from '@pinia/colada'
+import { useDebounceFn } from '@vueuse/core'
+import { ref, watch } from 'vue'
+import {
+ APPLICATIONS_KEY,
+ APPLICATIONS_TABLE_ID,
+ getApplications,
+ type Application,
+} from '@/lib/applications/applications'
+
+//// review (search "system")
+
+export const useApplications = defineQuery(() => {
+ const loginStore = useLoginStore()
+ const pageNum = ref(1)
+ const pageSize = ref(DEFAULT_PAGE_SIZE)
+ const textFilter = ref('')
+ const debouncedTextFilter = ref('')
+ const typeFilter = ref([])
+ const versionFilter = ref([])
+ const systemFilter = ref([])
+ const organizationFilter = ref([])
+ const sortBy = ref('display_name')
+ const sortDescending = ref(false)
+
+ const { state, asyncStatus, ...rest } = useQuery({
+ key: () => [
+ APPLICATIONS_KEY,
+ {
+ pageNum: pageNum.value,
+ pageSize: pageSize.value,
+ textFilter: debouncedTextFilter.value,
+ typeFilter: typeFilter.value,
+ versionFilter: versionFilter.value,
+ systemFilter: systemFilter.value,
+ organizationFilter: organizationFilter.value,
+ sortBy: sortBy.value,
+ sortDirection: sortDescending.value,
+ },
+ ],
+ enabled: () => !!loginStore.jwtToken && canReadApplications(),
+ query: () =>
+ getApplications(
+ pageNum.value,
+ pageSize.value,
+ debouncedTextFilter.value,
+ typeFilter.value,
+ versionFilter.value,
+ systemFilter.value,
+ organizationFilter.value,
+ sortBy.value,
+ sortDescending.value,
+ ),
+ })
+
+ // load table page size from storage
+ watch(
+ () => loginStore.userInfo?.email,
+ (email) => {
+ if (email) {
+ pageSize.value = loadPageSizeFromStorage(APPLICATIONS_TABLE_ID)
+ }
+ },
+ { immediate: true },
+ )
+
+ watch(
+ () => textFilter.value,
+ useDebounceFn(() => {
+ // debounce and ignore if text filter is too short
+ if (textFilter.value.length === 0 || textFilter.value.length >= MIN_SEARCH_LENGTH) {
+ debouncedTextFilter.value = textFilter.value
+
+ // reset to first page when filter changes
+ pageNum.value = 1
+ }
+ }, 500),
+ )
+
+ // reset to first page when page size changes
+ watch(
+ () => pageSize.value,
+ () => {
+ pageNum.value = 1
+ },
+ )
+
+ return {
+ ...rest,
+ state,
+ asyncStatus,
+ pageNum,
+ pageSize,
+ textFilter,
+ typeFilter,
+ versionFilter,
+ systemFilter,
+ organizationFilter,
+ debouncedTextFilter,
+ sortBy,
+ sortDescending,
+ }
+})
diff --git a/frontend/src/queries/applications/organizationFilter.ts b/frontend/src/queries/applications/organizationFilter.ts
new file mode 100644
index 00000000..1f34a5be
--- /dev/null
+++ b/frontend/src/queries/applications/organizationFilter.ts
@@ -0,0 +1,25 @@
+// Copyright (C) 2026 Nethesis S.r.l.
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import {
+ APPLICATION_ORGANIZATION_FILTER_KEY,
+ getOrganizationFilter,
+} from '@/lib/applications/organizationFilter'
+import { useLoginStore } from '@/stores/login'
+import { defineQuery, useQuery } from '@pinia/colada'
+
+export const useOrganizationFilter = defineQuery(() => {
+ const loginStore = useLoginStore()
+
+ const { state, asyncStatus, ...rest } = useQuery({
+ key: () => [APPLICATION_ORGANIZATION_FILTER_KEY],
+ enabled: () => !!loginStore.jwtToken,
+ query: () => getOrganizationFilter(),
+ })
+
+ return {
+ ...rest,
+ state,
+ asyncStatus,
+ }
+})
diff --git a/frontend/src/queries/applications/systemFilter.ts b/frontend/src/queries/applications/systemFilter.ts
new file mode 100644
index 00000000..3ff4571a
--- /dev/null
+++ b/frontend/src/queries/applications/systemFilter.ts
@@ -0,0 +1,22 @@
+// Copyright (C) 2026 Nethesis S.r.l.
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import { APPLICATION_SYSTEM_FILTER_KEY, getSystemFilter } from '@/lib/applications/systemFilter'
+import { useLoginStore } from '@/stores/login'
+import { defineQuery, useQuery } from '@pinia/colada'
+
+export const useSystemFilter = defineQuery(() => {
+ const loginStore = useLoginStore()
+
+ const { state, asyncStatus, ...rest } = useQuery({
+ key: () => [APPLICATION_SYSTEM_FILTER_KEY],
+ enabled: () => !!loginStore.jwtToken,
+ query: () => getSystemFilter(),
+ })
+
+ return {
+ ...rest,
+ state,
+ asyncStatus,
+ }
+})
diff --git a/frontend/src/queries/applications/typeFilter.ts b/frontend/src/queries/applications/typeFilter.ts
new file mode 100644
index 00000000..a1c08deb
--- /dev/null
+++ b/frontend/src/queries/applications/typeFilter.ts
@@ -0,0 +1,22 @@
+// Copyright (C) 2026 Nethesis S.r.l.
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import { APPLICATION_TYPE_FILTER_KEY, getTypeFilter } from '@/lib/applications/typeFilter'
+import { useLoginStore } from '@/stores/login'
+import { defineQuery, useQuery } from '@pinia/colada'
+
+export const useTypeFilter = defineQuery(() => {
+ const loginStore = useLoginStore()
+
+ const { state, asyncStatus, ...rest } = useQuery({
+ key: () => [APPLICATION_TYPE_FILTER_KEY],
+ enabled: () => !!loginStore.jwtToken,
+ query: () => getTypeFilter(),
+ })
+
+ return {
+ ...rest,
+ state,
+ asyncStatus,
+ }
+})
diff --git a/frontend/src/queries/applications/versionFilter.ts b/frontend/src/queries/applications/versionFilter.ts
new file mode 100644
index 00000000..2c2ba734
--- /dev/null
+++ b/frontend/src/queries/applications/versionFilter.ts
@@ -0,0 +1,22 @@
+// Copyright (C) 2026 Nethesis S.r.l.
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import { APPLICATION_VERSION_FILTER_KEY, getVersionFilter } from '@/lib/applications/versionFilter'
+import { useLoginStore } from '@/stores/login'
+import { defineQuery, useQuery } from '@pinia/colada'
+
+export const useVersionFilter = defineQuery(() => {
+ const loginStore = useLoginStore()
+
+ const { state, asyncStatus, ...rest } = useQuery({
+ key: () => [APPLICATION_VERSION_FILTER_KEY],
+ enabled: () => !!loginStore.jwtToken,
+ query: () => getVersionFilter(),
+ })
+
+ return {
+ ...rest,
+ state,
+ asyncStatus,
+ }
+})
diff --git a/frontend/src/queries/systems/createdByFilter.ts b/frontend/src/queries/systems/createdByFilter.ts
index 0d77bd61..e90ad34e 100644
--- a/frontend/src/queries/systems/createdByFilter.ts
+++ b/frontend/src/queries/systems/createdByFilter.ts
@@ -1,7 +1,7 @@
// Copyright (C) 2025 Nethesis S.r.l.
// SPDX-License-Identifier: GPL-3.0-or-later
-import { CREATED_BY_FILTER_KEY, getCreatedByFilter } from '@/lib/systems/createdByFilter'
+import { SYSTEM_CREATED_BY_FILTER_KEY, getCreatedByFilter } from '@/lib/systems/createdByFilter'
import { useLoginStore } from '@/stores/login'
import { defineQuery, useQuery } from '@pinia/colada'
@@ -9,7 +9,7 @@ export const useCreatedByFilter = defineQuery(() => {
const loginStore = useLoginStore()
const { state, asyncStatus, ...rest } = useQuery({
- key: () => [CREATED_BY_FILTER_KEY],
+ key: () => [SYSTEM_CREATED_BY_FILTER_KEY],
enabled: () => !!loginStore.jwtToken,
query: () => getCreatedByFilter(),
})
diff --git a/frontend/src/queries/systems/organizationFilter.ts b/frontend/src/queries/systems/organizationFilter.ts
new file mode 100644
index 00000000..3872c52f
--- /dev/null
+++ b/frontend/src/queries/systems/organizationFilter.ts
@@ -0,0 +1,25 @@
+// Copyright (C) 2025 Nethesis S.r.l.
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import {
+ getOrganizationFilter,
+ SYSTEM_ORGANIZATION_FILTER_KEY,
+} from '@/lib/systems/organizationFilter'
+import { useLoginStore } from '@/stores/login'
+import { defineQuery, useQuery } from '@pinia/colada'
+
+export const useOrganizationFilter = defineQuery(() => {
+ const loginStore = useLoginStore()
+
+ const { state, asyncStatus, ...rest } = useQuery({
+ key: () => [SYSTEM_ORGANIZATION_FILTER_KEY],
+ enabled: () => !!loginStore.jwtToken,
+ query: () => getOrganizationFilter(),
+ })
+
+ return {
+ ...rest,
+ state,
+ asyncStatus,
+ }
+})
diff --git a/frontend/src/queries/systems/systems.ts b/frontend/src/queries/systems/systems.ts
index 332eef6b..aee23cfe 100644
--- a/frontend/src/queries/systems/systems.ts
+++ b/frontend/src/queries/systems/systems.ts
@@ -26,6 +26,7 @@ export const useSystems = defineQuery(() => {
const createdByFilter = ref([])
const versionFilter = ref([])
const statusFilter = ref(['online', 'offline', 'unknown'])
+ const organizationFilter = ref([])
const sortBy = ref('name')
const sortDescending = ref(false)
@@ -40,6 +41,7 @@ export const useSystems = defineQuery(() => {
createdByFilter: createdByFilter.value,
versionFilter: versionFilter.value,
statusFilter: statusFilter.value,
+ organizationFilter: organizationFilter.value,
sortBy: sortBy.value,
sortDirection: sortDescending.value,
},
@@ -54,6 +56,7 @@ export const useSystems = defineQuery(() => {
createdByFilter.value,
versionFilter.value,
statusFilter.value,
+ organizationFilter.value,
sortBy.value,
sortDescending.value,
),
@@ -102,6 +105,7 @@ export const useSystems = defineQuery(() => {
createdByFilter,
versionFilter,
statusFilter,
+ organizationFilter,
debouncedTextFilter,
sortBy,
sortDescending,
diff --git a/frontend/src/queries/systems/userRoleFilter.ts b/frontend/src/queries/systems/userRoleFilter.ts
new file mode 100644
index 00000000..19e3c3d2
--- /dev/null
+++ b/frontend/src/queries/systems/userRoleFilter.ts
@@ -0,0 +1,21 @@
+// Copyright (C) 2025 Nethesis S.r.l.
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+//// is this used?
+
+////
+// export const useUserRoleFilter = defineQuery(() => {
+// const loginStore = useLoginStore()
+
+// const { state, asyncStatus, ...rest } = useQuery({
+// key: () => [USER_ROLE_FILTER_KEY],
+// enabled: () => !!loginStore.jwtToken,
+// query: () => getUserRoleFilter(),
+// })
+
+// return {
+// ...rest,
+// state,
+// asyncStatus,
+// }
+// })
diff --git a/frontend/src/queries/systems/versionFilter.ts b/frontend/src/queries/systems/versionFilter.ts
index ee65bb2a..655b60c9 100644
--- a/frontend/src/queries/systems/versionFilter.ts
+++ b/frontend/src/queries/systems/versionFilter.ts
@@ -1,7 +1,7 @@
// Copyright (C) 2025 Nethesis S.r.l.
// SPDX-License-Identifier: GPL-3.0-or-later
-import { VERSION_FILTER_KEY, getVersionFilter } from '@/lib/systems/versionFilter'
+import { SYSTEM_VERSION_FILTER_KEY, getVersionFilter } from '@/lib/systems/versionFilter'
import { useLoginStore } from '@/stores/login'
import { defineQuery, useQuery } from '@pinia/colada'
@@ -9,7 +9,7 @@ export const useVersionFilter = defineQuery(() => {
const loginStore = useLoginStore()
const { state, asyncStatus, ...rest } = useQuery({
- key: () => [VERSION_FILTER_KEY],
+ key: () => [SYSTEM_VERSION_FILTER_KEY],
enabled: () => !!loginStore.jwtToken,
query: () => getVersionFilter(),
})
diff --git a/frontend/src/queries/userRoles.ts b/frontend/src/queries/userRoles.ts
new file mode 100644
index 00000000..15550e64
--- /dev/null
+++ b/frontend/src/queries/userRoles.ts
@@ -0,0 +1,25 @@
+// Copyright (C) 2026 Nethesis S.r.l.
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+// import { getUserRoles, USER_ROLES_KEY } from '@/lib/userRoles' ////
+// import { useLoginStore } from '@/stores/login'
+// import { defineQuery, useQuery } from '@pinia/colada'
+
+//// is this used?
+
+////
+// export const useUserRoles = defineQuery(() => {
+// const loginStore = useLoginStore()
+
+// const { state, asyncStatus, ...rest } = useQuery({
+// key: () => [USER_ROLES_KEY],
+// enabled: () => !!loginStore.jwtToken,
+// query: () => getUserRoles(),
+// })
+
+// return {
+// ...rest,
+// state,
+// asyncStatus,
+// }
+// })
diff --git a/frontend/src/queries/users.ts b/frontend/src/queries/users.ts
index b3bfef85..738e6b1d 100644
--- a/frontend/src/queries/users.ts
+++ b/frontend/src/queries/users.ts
@@ -16,6 +16,7 @@ export const useUsers = defineQuery(() => {
const pageSize = ref(DEFAULT_PAGE_SIZE)
const textFilter = ref('')
const debouncedTextFilter = ref('')
+ const organizationFilter = ref([])
const sortBy = ref('name')
const sortDescending = ref(false)
@@ -26,6 +27,7 @@ export const useUsers = defineQuery(() => {
pageNum: pageNum.value,
pageSize: pageSize.value,
textFilter: debouncedTextFilter.value,
+ organizationFilter: organizationFilter.value,
sortBy: sortBy.value,
sortDirection: sortDescending.value,
},
@@ -36,6 +38,7 @@ export const useUsers = defineQuery(() => {
pageNum.value,
pageSize.value,
debouncedTextFilter.value,
+ organizationFilter.value,
sortBy.value,
sortDescending.value,
),
@@ -81,6 +84,7 @@ export const useUsers = defineQuery(() => {
pageSize,
textFilter,
debouncedTextFilter,
+ organizationFilter,
sortBy,
sortDescending,
}
diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts
index ed534434..8efc1c76 100644
--- a/frontend/src/router/index.ts
+++ b/frontend/src/router/index.ts
@@ -65,6 +65,16 @@ const router = createRouter({
name: 'system_detail',
component: () => import('../views/SystemDetailView.vue'),
},
+ {
+ path: '/applications',
+ name: 'applications',
+ component: () => import('../views/ApplicationsView.vue'),
+ },
+ // { ////
+ // path: '/applications/:applicationId',
+ // name: 'application_detail',
+ // component: () => import('../views/ApplicationDetailView.vue'),
+ // },
],
})
diff --git a/frontend/src/views/ApplicationsView.vue b/frontend/src/views/ApplicationsView.vue
new file mode 100644
index 00000000..1e3574af
--- /dev/null
+++ b/frontend/src/views/ApplicationsView.vue
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
{{ $t('applications.title') }}
+
+ {{ $t('applications.page_description') }}
+
+
+
+
+
+
+
diff --git a/frontend/src/views/CustomersView.vue b/frontend/src/views/CustomersView.vue
index 63ad6797..43bf9b35 100644
--- a/frontend/src/views/CustomersView.vue
+++ b/frontend/src/views/CustomersView.vue
@@ -6,12 +6,19 @@
@@ -23,7 +30,7 @@ const isShownCreateCustomerDrawer = ref(false)
@@ -21,19 +77,44 @@ const isShownCreateDistributorDrawer = ref(false)
{{ $t('distributors.page_description') }}
-
-
+
-
-
-
- {{ $t('distributors.create_distributor') }}
-
+
+ >
+
+
+
+ {{ $t('common.actions') }}
+
+
+
+
+
+
+
+ {{ $t('distributors.create_distributor') }}
+
+
import { NeButton, NeHeading } from '@nethesis/vue-components'
import ResellersTable from '@/components/resellers/ResellersTable.vue'
-import { ref } from 'vue'
+import { computed, ref } from 'vue'
import { faCirclePlus } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { canManageResellers } from '@/lib/permissions'
+import { useResellers } from '@/queries/resellers'
const isShownCreateResellerDrawer = ref(false)
+
+const { state, debouncedTextFilter } = useResellers()
+
+const resellersPage = computed(() => {
+ return state.value.data?.resellers
+})
@@ -23,7 +30,7 @@ const isShownCreateResellerDrawer = ref(false)
{
- return ['ns8', 'nsec'].includes(systemDetail.value.data?.type || '')
-}
-
-const getSystemUrl = () => {
+const systemUrl = computed(() => {
if (!systemDetail.value.data?.fqdn) {
return ''
}
+ if (!['ns8', 'nsec'].includes(systemDetail.value.data?.type || '')) {
+ return ''
+ }
+
const fqdn = systemDetail.value.data.fqdn
let port = ''
let path = ''
@@ -45,13 +46,11 @@ const getSystemUrl = () => {
}
const url = `https://${fqdn}${port}${path}`
return url
-}
+})
const openSystem = () => {
- const url = getSystemUrl()
-
- if (url) {
- window.open(url, '_blank')
+ if (systemUrl.value) {
+ window.open(systemUrl.value, '_blank')
}
}
@@ -82,15 +81,19 @@ const openSystem = () => {
-
+
- {{ $t('system_detail.open_system') }}
+ {{ $t('system_detail.go_to_system') }}
- {{ $t('system_detail.open_system_tooltip') }}
+ {{
+ systemUrl
+ ? $t('system_detail.go_to_system_tooltip')
+ : $t('system_detail.cannot_determine_system_url_description')
+ }}
diff --git a/frontend/src/views/SystemsView.vue b/frontend/src/views/SystemsView.vue
index 467e4599..183ff9e7 100644
--- a/frontend/src/views/SystemsView.vue
+++ b/frontend/src/views/SystemsView.vue
@@ -74,7 +74,7 @@ async function exportSystems(format: 'pdf' | 'csv') {
const fileName = `${t('systems.title')}.${format}`
downloadFile(exportData, fileName, format)
} catch (error) {
- console.error('Cannot export systems to pdf:', error)
+ console.error(`Cannot export systems to ${format}:`, error)
throw error
}
}
@@ -113,7 +113,7 @@ async function exportSystems(format: 'pdf' | 'csv') {
import { NeButton, NeHeading } from '@nethesis/vue-components'
import UsersTable from '@/components/users/UsersTable.vue'
-import { ref } from 'vue'
+import { computed, ref } from 'vue'
import { faCirclePlus } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { PRODUCT_NAME } from '@/lib/config'
import { canManageUsers } from '@/lib/permissions'
+import { useUsers } from '@/queries/users'
+// import { useI18n } from 'vue-i18n' ////
+// import { getExport } from '@/lib/users' ////
+
+// const { t } = useI18n() ////
+const {
+ state,
+ // asyncStatus, ////
+ // pageNum,
+ // pageSize,
+ // textFilter,
+ debouncedTextFilter,
+ // sortBy, ////
+ // sortDescending,
+} = useUsers()
const isShownCreateUserDrawer = ref(false)
+
+const usersPage = computed(() => {
+ return state.value.data?.users
+})
+
+//// TODO wait for backend fix
+// function getBulkActionsMenuItems() {
+// return [
+// {
+// id: 'exportFilteredToPdf',
+// label: t('users.export_users_to_pdf'),
+// icon: faFilePdf,
+// // action: () => exportUsers('pdf'), ////
+// disabled: !state.value.data?.users,
+// },
+// {
+// id: 'exportFilteredToCsv',
+// label: t('users.export_users_to_csv'),
+// icon: faFileCsv,
+// // action: () => exportUsers('csv'), ////
+// disabled: !state.value.data?.users,
+// },
+// ]
+// }
+
+//// TODO wait for backend fix
+// async function exportUsers(format: 'pdf' | 'csv') {
+// try {
+// const exportData = await getExport(
+// format,
+// debouncedTextFilter.value,
+// productFilter.value,
+// createdByFilter.value,
+// versionFilter.value,
+// statusFilter.value,
+// sortBy.value,
+// sortDescending.value,
+// )
+// const fileName = `${t('users.title')}.${format}`
+// downloadFile(exportData, fileName, format)
+// } catch (error) {
+// console.error(`Cannot export users to ${format}:`, error)
+// throw error
+// }
+// }
@@ -24,7 +84,7 @@ const isShownCreateUserDrawer = ref(false)