From feb18375060dba43466b22ba93dfb5c8d7ae6394 Mon Sep 17 00:00:00 2001 From: Peter Hegman Date: Wed, 3 Dec 2025 08:08:09 -0800 Subject: [PATCH 1/2] Use paths from Rails in admin group and project dashboards For base route of Vue router, avatar and edit links. Makes the code more flexible and reduces risk for URL related bugs. --- .../admin/groups/index/constants.js | 8 ++---- .../fragments/admin_group.fragment.graphql | 2 ++ .../javascripts/admin/groups/index/index.js | 13 +++++++-- .../javascripts/admin/groups/index/routes.js | 6 ++-- .../admin/projects/index/constants.js | 8 ++---- .../fragments/admin_project.fragment.graphql | 2 ++ .../javascripts/admin/projects/index/index.js | 8 +++--- .../admin/projects/index/routes.js | 6 ++-- .../fragments/admin_group.fragment.graphql | 2 ++ .../admin/groups/index/components/app_spec.js | 28 ++++++++++++++++--- .../projects/index/components/app_spec.js | 28 ++++++++++++++++--- 11 files changed, 78 insertions(+), 33 deletions(-) diff --git a/app/assets/javascripts/admin/groups/index/constants.js b/app/assets/javascripts/admin/groups/index/constants.js index bda8c6faa3b7d7..b04633f3743008 100644 --- a/app/assets/javascripts/admin/groups/index/constants.js +++ b/app/assets/javascripts/admin/groups/index/constants.js @@ -1,7 +1,6 @@ import { get } from 'lodash'; import groupsEmptyStateIllustration from '@gitlab/svgs/dist/illustrations/empty-state/empty-groups-md.svg?url'; import { s__, __ } from '~/locale'; -import { joinPaths } from '~/lib/utils/url_utility'; import { SORT_LABEL_NAME, SORT_LABEL_CREATED, @@ -44,12 +43,11 @@ export const SORT_OPTIONS = [ const baseTab = { formatter: (groups) => formatGraphQLGroups(groups, (group) => { - const adminPath = joinPaths('/', gon.relative_url_root, '/admin/groups/', group.fullPath); const canAdminAllResources = get(group.userPermissions, 'adminAllResources', true); return { - avatarLabelLink: adminPath, - editPath: `${adminPath}/edit`, + avatarLabelLink: group.adminShowPath, + editPath: group.adminEditPath, availableActions: canAdminAllResources ? group.availableActions : [], }; }), @@ -97,8 +95,6 @@ export const INACTIVE_TAB = { export const ADMIN_GROUPS_TABS = [ACTIVE_TAB, INACTIVE_TAB]; -export const BASE_ROUTE = '/admin/groups'; - export const ADMIN_GROUPS_ROUTE_NAME = 'admin-groups'; export const FIRST_TAB_ROUTE_NAMES = [ADMIN_GROUPS_ROUTE_NAME]; diff --git a/app/assets/javascripts/admin/groups/index/graphql/fragments/admin_group.fragment.graphql b/app/assets/javascripts/admin/groups/index/graphql/fragments/admin_group.fragment.graphql index 2110cc12e45d9d..424f013367836b 100644 --- a/app/assets/javascripts/admin/groups/index/graphql/fragments/admin_group.fragment.graphql +++ b/app/assets/javascripts/admin/groups/index/graphql/fragments/admin_group.fragment.graphql @@ -1,6 +1,8 @@ # id field is requested by ~/graphql_shared/fragments/group.fragment.graphql # eslint-disable-next-line @graphql-eslint/require-selections fragment AdminGroup on Group { + adminShowPath + adminEditPath projectStatistics { storageSize } diff --git a/app/assets/javascripts/admin/groups/index/index.js b/app/assets/javascripts/admin/groups/index/index.js index 6aae1b0221f29c..b36631ae8ea513 100644 --- a/app/assets/javascripts/admin/groups/index/index.js +++ b/app/assets/javascripts/admin/groups/index/index.js @@ -2,16 +2,17 @@ import Vue from 'vue'; import VueRouter from 'vue-router'; import VueApollo from 'vue-apollo'; import createDefaultClient from '~/lib/graphql'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import routes from './routes'; import AdminGroupsApp from './components/app.vue'; Vue.use(VueRouter); -export const createRouter = () => { +export const createRouter = (basePath) => { const router = new VueRouter({ routes, mode: 'history', - base: gon.relative_url_root || '/', + base: basePath, }); return router; @@ -22,13 +23,19 @@ export const initAdminGroups = () => { if (!el) return false; + const { + dataset: { appData }, + } = el; + + const { basePath } = convertObjectPropsToCamelCase(JSON.parse(appData)); + const apolloProvider = new VueApollo({ defaultClient: createDefaultClient(), }); return new Vue({ el, - router: createRouter(), + router: createRouter(basePath), apolloProvider, name: 'AdminGroupsRoot', render(createElement) { diff --git a/app/assets/javascripts/admin/groups/index/routes.js b/app/assets/javascripts/admin/groups/index/routes.js index bf85baf95f9f45..8cc4565cc0fb0e 100644 --- a/app/assets/javascripts/admin/groups/index/routes.js +++ b/app/assets/javascripts/admin/groups/index/routes.js @@ -1,16 +1,16 @@ -import { BASE_ROUTE, ADMIN_GROUPS_ROUTE_NAME, ADMIN_GROUPS_TABS } from './constants'; +import { ADMIN_GROUPS_ROUTE_NAME, ADMIN_GROUPS_TABS } from './constants'; import AdminGroupsApp from './components/app.vue'; export default [ { name: ADMIN_GROUPS_ROUTE_NAME, - path: BASE_ROUTE, + path: '/', component: AdminGroupsApp, }, ...ADMIN_GROUPS_TABS.map(({ value }) => ({ name: value, - path: `${BASE_ROUTE}/${value}`, + path: `/${value}`, component: AdminGroupsApp, })), ]; diff --git a/app/assets/javascripts/admin/projects/index/constants.js b/app/assets/javascripts/admin/projects/index/constants.js index 80fb1ce7436fdf..0f575c49a754fc 100644 --- a/app/assets/javascripts/admin/projects/index/constants.js +++ b/app/assets/javascripts/admin/projects/index/constants.js @@ -6,7 +6,6 @@ import ResourceListsEmptyState, { TYPES, } from '~/vue_shared/components/resource_lists/empty_state.vue'; import { formatGraphQLProjects } from '~/vue_shared/components/projects_list/formatter'; -import { joinPaths } from '~/lib/utils/url_utility'; import { SORT_LABEL_CREATED, SORT_LABEL_NAME, @@ -67,12 +66,11 @@ const baseTab = { paginationType: PAGINATION_TYPE_KEYSET, formatter: (projects) => formatGraphQLProjects(projects, (project) => { - const adminPath = joinPaths('/', gon.relative_url_root, '/admin/projects', project.fullPath); const canAdminAllResources = get(project.userPermissions, 'adminAllResources', true); return { - editPath: `${adminPath}/edit`, - avatarLabelLink: adminPath, + editPath: project.adminEditPath, + avatarLabelLink: project.adminShowPath, availableActions: canAdminAllResources ? project.availableActions : [], }; }), @@ -107,8 +105,6 @@ export const INACTIVE_TAB = { export const ADMIN_PROJECTS_TABS = [ACTIVE_TAB, INACTIVE_TAB]; -export const BASE_ROUTE = '/admin/projects'; - export const ADMIN_PROJECTS_ROUTE_NAME = 'admin-projects'; export const FIRST_TAB_ROUTE_NAMES = [ADMIN_PROJECTS_ROUTE_NAME]; diff --git a/app/assets/javascripts/admin/projects/index/graphql/fragments/admin_project.fragment.graphql b/app/assets/javascripts/admin/projects/index/graphql/fragments/admin_project.fragment.graphql index dde94ae89f83bf..b9032c7ea47abb 100644 --- a/app/assets/javascripts/admin/projects/index/graphql/fragments/admin_project.fragment.graphql +++ b/app/assets/javascripts/admin/projects/index/graphql/fragments/admin_project.fragment.graphql @@ -5,6 +5,8 @@ fragment AdminProject on Project { adminAllResources } ... on Project { + adminShowPath + adminEditPath statistics { storageSize } diff --git a/app/assets/javascripts/admin/projects/index/index.js b/app/assets/javascripts/admin/projects/index/index.js index bd74043d50feb3..3f4a816046bf5a 100644 --- a/app/assets/javascripts/admin/projects/index/index.js +++ b/app/assets/javascripts/admin/projects/index/index.js @@ -8,11 +8,11 @@ import routes from './routes'; Vue.use(VueRouter); -export const createRouter = () => { +export const createRouter = (basePath) => { const router = new VueRouter({ routes, mode: 'history', - base: gon.relative_url_root || '/', + base: basePath, }); return router; @@ -27,7 +27,7 @@ export const initAdminProjects = () => { dataset: { appData }, } = el; - const { programmingLanguages } = convertObjectPropsToCamelCase(JSON.parse(appData)); + const { programmingLanguages, basePath } = convertObjectPropsToCamelCase(JSON.parse(appData)); const apolloProvider = new VueApollo({ defaultClient: createDefaultClient(), @@ -35,7 +35,7 @@ export const initAdminProjects = () => { return new Vue({ el, - router: createRouter(), + router: createRouter(basePath), apolloProvider, name: 'AdminProjectsRoot', render(createElement) { diff --git a/app/assets/javascripts/admin/projects/index/routes.js b/app/assets/javascripts/admin/projects/index/routes.js index 02647cbed73def..06ba37c4e30592 100644 --- a/app/assets/javascripts/admin/projects/index/routes.js +++ b/app/assets/javascripts/admin/projects/index/routes.js @@ -1,16 +1,16 @@ -import { BASE_ROUTE, ADMIN_PROJECTS_ROUTE_NAME, ADMIN_PROJECTS_TABS } from './constants'; +import { ADMIN_PROJECTS_ROUTE_NAME, ADMIN_PROJECTS_TABS } from './constants'; import AdminProjectsApp from './components/app.vue'; export default [ { name: ADMIN_PROJECTS_ROUTE_NAME, - path: BASE_ROUTE, + path: '/', component: AdminProjectsApp, }, ...ADMIN_PROJECTS_TABS.map(({ value }) => ({ name: value, - path: `${BASE_ROUTE}/${value}`, + path: `/${value}`, component: AdminProjectsApp, })), ]; diff --git a/ee/app/assets/javascripts/admin/groups/index/graphql/fragments/admin_group.fragment.graphql b/ee/app/assets/javascripts/admin/groups/index/graphql/fragments/admin_group.fragment.graphql index e100dbae9a8ca3..03a84fc47fcc42 100644 --- a/ee/app/assets/javascripts/admin/groups/index/graphql/fragments/admin_group.fragment.graphql +++ b/ee/app/assets/javascripts/admin/groups/index/graphql/fragments/admin_group.fragment.graphql @@ -1,6 +1,8 @@ # id field is requested by ~/graphql_shared/fragments/group.fragment.graphql # eslint-disable-next-line @graphql-eslint/require-selections fragment AdminGroup on Group { + adminShowPath + adminEditPath projectStatistics { storageSize } diff --git a/spec/frontend/admin/groups/index/components/app_spec.js b/spec/frontend/admin/groups/index/components/app_spec.js index d63f71761a762e..52313cca02dac7 100644 --- a/spec/frontend/admin/groups/index/components/app_spec.js +++ b/spec/frontend/admin/groups/index/components/app_spec.js @@ -94,9 +94,7 @@ describe('AdminGroupsApp', () => { }); }); - it('renders relative URL that supports relative_url_root', async () => { - window.gon = { relative_url_root: '/gitlab' }; - + it('uses adminShowPath for avatar link', async () => { await createComponent({ mountFn: mountExtended, handlers: [[adminGroupsQuery, jest.fn().mockResolvedValue(adminGroupsGraphQlResponse)]], @@ -112,7 +110,29 @@ describe('AdminGroupsApp', () => { } = adminGroupsGraphQlResponse; expect(wrapper.findByRole('link', { name: expectedGroup.fullName }).attributes('href')).toBe( - `/gitlab/admin/groups/${expectedGroup.fullPath}`, + expectedGroup.adminShowPath, + ); + }); + + it('uses adminEditPath for edit link', async () => { + await createComponent({ + mountFn: mountExtended, + handlers: [[adminGroupsQuery, jest.fn().mockResolvedValue(adminGroupsGraphQlResponse)]], + }); + await waitForPromises(); + + const { + data: { + groups: { + nodes: [expectedGroup], + }, + }, + } = adminGroupsGraphQlResponse; + + await wrapper.findByRole('button', { name: 'Actions' }).trigger('click'); + + expect(wrapper.findByRole('link', { name: 'Edit' }).attributes('href')).toBe( + expectedGroup.adminEditPath, ); }); diff --git a/spec/frontend/admin/projects/index/components/app_spec.js b/spec/frontend/admin/projects/index/components/app_spec.js index c0d96ce30c4e38..a9adf5fdf360e9 100644 --- a/spec/frontend/admin/projects/index/components/app_spec.js +++ b/spec/frontend/admin/projects/index/components/app_spec.js @@ -123,9 +123,7 @@ describe('AdminProjectsApp', () => { expect(wrapper.findByRole('button', { name: 'Delete immediately' }).exists()).toBe(true); }); - it('renders relative URL that supports relative_url_root', async () => { - window.gon = { relative_url_root: '/gitlab' }; - + it('uses adminShowPath for avatar link', async () => { await createComponent({ mountFn: mountExtended, handlers: [[adminProjectsQuery, jest.fn().mockResolvedValue(adminProjectsGraphQlResponse)]], @@ -142,7 +140,29 @@ describe('AdminProjectsApp', () => { expect( wrapper.findByRole('link', { name: expectedProject.nameWithNamespace }).attributes('href'), - ).toBe(`/gitlab/admin/projects/${expectedProject.fullPath}`); + ).toBe(expectedProject.adminShowPath); + }); + + it('uses adminEditPath for edit link', async () => { + await createComponent({ + mountFn: mountExtended, + handlers: [[adminProjectsQuery, jest.fn().mockResolvedValue(adminProjectsGraphQlResponse)]], + }); + await waitForPromises(); + + const { + data: { + projects: { + nodes: [expectedProject], + }, + }, + } = adminProjectsGraphQlResponse; + + await wrapper.findByRole('button', { name: 'Actions' }).trigger('click'); + + expect(wrapper.findByRole('link', { name: 'Edit' }).attributes('href')).toBe( + expectedProject.adminEditPath, + ); }); it('uses keyset pagination', async () => { -- GitLab From 98f419260db8c4851fa67f3b8e3e68885c0ae877 Mon Sep 17 00:00:00 2001 From: Peter Hegman Date: Sun, 7 Dec 2025 19:25:10 -0800 Subject: [PATCH 2/2] Move base route back to a constant Per reviewer suggestion --- app/assets/javascripts/admin/groups/index/constants.js | 2 ++ app/assets/javascripts/admin/groups/index/routes.js | 6 +++--- app/assets/javascripts/admin/projects/index/constants.js | 2 ++ app/assets/javascripts/admin/projects/index/routes.js | 6 +++--- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/admin/groups/index/constants.js b/app/assets/javascripts/admin/groups/index/constants.js index b04633f3743008..2db4d0a805bdea 100644 --- a/app/assets/javascripts/admin/groups/index/constants.js +++ b/app/assets/javascripts/admin/groups/index/constants.js @@ -95,6 +95,8 @@ export const INACTIVE_TAB = { export const ADMIN_GROUPS_TABS = [ACTIVE_TAB, INACTIVE_TAB]; +export const BASE_ROUTE = '/'; + export const ADMIN_GROUPS_ROUTE_NAME = 'admin-groups'; export const FIRST_TAB_ROUTE_NAMES = [ADMIN_GROUPS_ROUTE_NAME]; diff --git a/app/assets/javascripts/admin/groups/index/routes.js b/app/assets/javascripts/admin/groups/index/routes.js index 8cc4565cc0fb0e..37b17637dd0238 100644 --- a/app/assets/javascripts/admin/groups/index/routes.js +++ b/app/assets/javascripts/admin/groups/index/routes.js @@ -1,16 +1,16 @@ -import { ADMIN_GROUPS_ROUTE_NAME, ADMIN_GROUPS_TABS } from './constants'; +import { BASE_ROUTE, ADMIN_GROUPS_ROUTE_NAME, ADMIN_GROUPS_TABS } from './constants'; import AdminGroupsApp from './components/app.vue'; export default [ { name: ADMIN_GROUPS_ROUTE_NAME, - path: '/', + path: BASE_ROUTE, component: AdminGroupsApp, }, ...ADMIN_GROUPS_TABS.map(({ value }) => ({ name: value, - path: `/${value}`, + path: `${BASE_ROUTE}${value}`, component: AdminGroupsApp, })), ]; diff --git a/app/assets/javascripts/admin/projects/index/constants.js b/app/assets/javascripts/admin/projects/index/constants.js index 0f575c49a754fc..ab237eef9e240d 100644 --- a/app/assets/javascripts/admin/projects/index/constants.js +++ b/app/assets/javascripts/admin/projects/index/constants.js @@ -105,6 +105,8 @@ export const INACTIVE_TAB = { export const ADMIN_PROJECTS_TABS = [ACTIVE_TAB, INACTIVE_TAB]; +export const BASE_ROUTE = '/'; + export const ADMIN_PROJECTS_ROUTE_NAME = 'admin-projects'; export const FIRST_TAB_ROUTE_NAMES = [ADMIN_PROJECTS_ROUTE_NAME]; diff --git a/app/assets/javascripts/admin/projects/index/routes.js b/app/assets/javascripts/admin/projects/index/routes.js index 06ba37c4e30592..d17116b81047c3 100644 --- a/app/assets/javascripts/admin/projects/index/routes.js +++ b/app/assets/javascripts/admin/projects/index/routes.js @@ -1,16 +1,16 @@ -import { ADMIN_PROJECTS_ROUTE_NAME, ADMIN_PROJECTS_TABS } from './constants'; +import { BASE_ROUTE, ADMIN_PROJECTS_ROUTE_NAME, ADMIN_PROJECTS_TABS } from './constants'; import AdminProjectsApp from './components/app.vue'; export default [ { name: ADMIN_PROJECTS_ROUTE_NAME, - path: '/', + path: BASE_ROUTE, component: AdminProjectsApp, }, ...ADMIN_PROJECTS_TABS.map(({ value }) => ({ name: value, - path: `/${value}`, + path: `${BASE_ROUTE}${value}`, component: AdminProjectsApp, })), ]; -- GitLab