diff --git a/ee/app/assets/javascripts/remote_development/graphql/queries/user_workspaces_projects_names.query.graphql b/ee/app/assets/javascripts/remote_development/graphql/queries/user_workspaces_projects_names.query.graphql new file mode 100644 index 0000000000000000000000000000000000000000..e585033b1b483b3ef92fc814ddb7b9e8256982e3 --- /dev/null +++ b/ee/app/assets/javascripts/remote_development/graphql/queries/user_workspaces_projects_names.query.graphql @@ -0,0 +1,8 @@ +query userWorkspacesProjectsNames($ids: [ID!]) { + projects(ids: $ids) { + nodes { + id + nameWithNamespace + } + } +} diff --git a/ee/app/assets/javascripts/remote_development/init_workspaces_app.js b/ee/app/assets/javascripts/remote_development/init_workspaces_app.js index 714829376f1c1a55a7d58f019119aaee26930cf3..d00d87276bef49259139623dc6ad45cea340d91b 100644 --- a/ee/app/assets/javascripts/remote_development/init_workspaces_app.js +++ b/ee/app/assets/javascripts/remote_development/init_workspaces_app.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import { random } from 'lodash'; import VueApollo from 'vue-apollo'; import createDefaultClient from '~/lib/graphql'; import App from './pages/app.vue'; @@ -9,7 +10,7 @@ import { WORKSPACE_STATES, WORKSPACE_DESIRED_STATES } from './constants'; Vue.use(VueApollo); const generateDummyWorkspace = (actualState, desiredState, createdAt = new Date()) => { - const id = Math.random(0, 100000).toString(16).substring(0, 9); + const id = random(0, 100000).toString(16).substring(0, 9); return { id: `gid://gitlab/RemoteDevelopment::Workspace/${id}`, @@ -21,7 +22,7 @@ const generateDummyWorkspace = (actualState, desiredState, createdAt = new Date( actualState, desiredState, createdAt: createdAt.toISOString(), - projectId: 'gid://gitlab/Project/2', + projectId: random(0, 1) === 1 ? 'gid://gitlab/Project/2' : 'gid://gitlab/Project/2qweqweqw', }; }; diff --git a/ee/app/assets/javascripts/remote_development/pages/list.vue b/ee/app/assets/javascripts/remote_development/pages/list.vue index 9b59c767e2bc40e5209f24cd83842b05f71dc0ec..ef8b9ca7e22c3c304590c42bfda491e48fc8ae1a 100644 --- a/ee/app/assets/javascripts/remote_development/pages/list.vue +++ b/ee/app/assets/javascripts/remote_development/pages/list.vue @@ -4,14 +4,9 @@ import { logError } from '~/lib/logger'; import { s__, __ } from '~/locale'; import { convertToGraphQLId } from '~/graphql_shared/utils'; import { TYPE_WORKSPACE } from '~/graphql_shared/constants'; -import { getDayDifference } from '~/lib/utils/datetime_utility'; -import { - WORKSPACE_STATES, - ROUTES, - WORKSPACES_LIST_POLL_INTERVAL, - EXCLUDED_WORKSPACE_AGE_IN_DAYS, -} from '../constants'; +import { WORKSPACE_STATES, ROUTES, WORKSPACES_LIST_POLL_INTERVAL } from '../constants'; import userWorkspacesListQuery from '../graphql/queries/user_workspaces_list.query.graphql'; +import userWorkspacesProjectsNamesQuery from '../graphql/queries/user_workspaces_projects_names.query.graphql'; import workspaceUpdateMutation from '../graphql/mutations/workspace_update.mutation.graphql'; import WorkspaceEmptyState from '../components/list/empty_state.vue'; import WorkspaceStateIndicator from '../components/list/workspace_state_indicator.vue'; @@ -33,12 +28,6 @@ const sortWorkspacesByTerminatedState = (workspaceA, workspaceB) => { return -1; // Place workspaceA before workspaceB since it is not terminated. }; -const excludeOldTerminatedWorkspaces = ({ createdAt, actualState }) => { - return actualState === WORKSPACE_STATES.terminated - ? getDayDifference(new Date(createdAt), new Date()) <= EXCLUDED_WORKSPACE_AGE_IN_DAYS - : true; -}; - export const i18n = { updateWorkspaceFailedMessage: s__('Workspaces|Failed to update workspace'), tableColumnHeaders: { @@ -47,6 +36,9 @@ export const i18n = { }, heading: s__('Workspaces|Workspaces'), newWorkspaceButton: s__('Workspaces|New workspace'), + loadingWorkspacesFailed: s__( + 'Workspaces|Unable to load current Workspaces. Please try again or contact an administrator.', + ), }; export default { @@ -69,9 +61,24 @@ export default { }, error(err) { logError(err); - this.error = __( - 'Unable to load current Workspaces. Please try again or contact an administrator.', - ); + }, + async result({ data, error }) { + if (error) { + this.error = i18n.loadingWorkspacesFailed; + return; + } + const workspaces = data.currentUser.workspaces.nodes; + const result = await this.fetchProjectNames(workspaces); + + if (result.error) { + this.error = i18n.loadingWorkspacesFailed; + return; + } + + this.workspaces = workspaces.map((workspace) => ({ + ...workspace, + projectName: result.projectIdToNameMap[workspace.projectId] || workspace.projectId, + })); }, }, }, @@ -106,15 +113,42 @@ export default { return this.$apollo.loading; }, sortedWorkspaces() { - return [...this.workspaces] - .filter(excludeOldTerminatedWorkspaces) - .sort(sortWorkspacesByTerminatedState); + return [...this.workspaces].sort(sortWorkspacesByTerminatedState); }, }, methods: { clearError() { this.error = ''; }, + async fetchProjectNames(workspaces) { + const projectIds = workspaces.map(({ projectId }) => projectId); + + try { + const { + data: { projects }, + error, + } = await this.$apollo.query({ + query: userWorkspacesProjectsNamesQuery, + variables: { ids: projectIds }, + }); + + if (error) { + return { error }; + } + + return { + projectIdToNameMap: projects.nodes.reduce( + (map, project) => ({ + ...map, + [project.id]: project.nameWithNamespace, + }), + {}, + ), + }; + } catch (error) { + return { error }; + } + }, updateWorkspace(id, desiredState) { return this.$apollo .mutate({ @@ -171,7 +205,7 @@ export default { <div class="gl-display-flex gl-text-gray-500 gl-align-items-center"> <workspace-state-indicator :workspace-state="item.actualState" class="gl-mr-5" /> <div class="gl-display-flex gl-flex-direction-column"> - <span> {{ item.projectId }} </span> + <span> {{ item.projectName }} </span> <span> {{ item.name }} </span> </div> </div> diff --git a/ee/spec/frontend/remote_development/mock_data/index.js b/ee/spec/frontend/remote_development/mock_data/index.js index 806ee156c512bd19a93248409f7d1ba3c05f17b0..87a1d44f8366de898ed12181fd3e5ea91f346ff2 100644 --- a/ee/spec/frontend/remote_development/mock_data/index.js +++ b/ee/spec/frontend/remote_development/mock_data/index.js @@ -133,3 +133,18 @@ export const WORKSPACE_CREATE_MUTATION_RESULT = { }, }, }; + +export const USER_WORKSPACES_PROJECT_NAMES_QUERY_RESULT = { + data: { + projects: { + nodes: [ + { + id: 'gid://gitlab/Project/2', + nameWithNamespace: 'Gitlab Org / Gitlab Shell', + __typename: 'Project', + }, + ], + __typename: 'ProjectConnection', + }, + }, +}; diff --git a/ee/spec/frontend/remote_development/pages/list_spec.js b/ee/spec/frontend/remote_development/pages/list_spec.js index cd2edffa2bcb13785b38e0ed87591bb975e10305..57cf1c5d86074448e3d3ed6cf703423c362763a5 100644 --- a/ee/spec/frontend/remote_development/pages/list_spec.js +++ b/ee/spec/frontend/remote_development/pages/list_spec.js @@ -13,6 +13,8 @@ import WorkspaceActions from 'ee/remote_development/components/list/workspace_ac import WorkspaceEmptyState from 'ee/remote_development/components/list/empty_state.vue'; import WorkspaceStateIndicator from 'ee/remote_development/components/list/workspace_state_indicator.vue'; import userWorkspacesListQuery from 'ee/remote_development/graphql/queries/user_workspaces_list.query.graphql'; +import userWorkspacesProjectsNamesQuery from 'ee/remote_development/graphql/queries/user_workspaces_projects_names.query.graphql'; + import { useFakeDate } from 'helpers/fake_date'; import { @@ -24,6 +26,7 @@ import { CURRENT_USERNAME, USER_WORKSPACES_QUERY_RESULT, USER_WORKSPACES_QUERY_EMPTY_RESULT, + USER_WORKSPACES_PROJECT_NAMES_QUERY_RESULT, } from '../mock_data'; jest.mock('~/lib/logger'); @@ -57,14 +60,21 @@ const findWorkspaceActions = (tableRow) => tableRow.findComponent(WorkspaceActio describe('remote_development/pages/list.vue', () => { let wrapper; let userWorkspacesListQueryHandler; + let userWorkspacesProjectNamesQueryHandler; let workspaceUpdateMutationHandler; const createWrapper = (mockData = USER_WORKSPACES_QUERY_RESULT) => { userWorkspacesListQueryHandler = jest.fn().mockResolvedValueOnce(mockData); + userWorkspacesProjectNamesQueryHandler = jest + .fn() + .mockResolvedValueOnce(USER_WORKSPACES_PROJECT_NAMES_QUERY_RESULT); workspaceUpdateMutationHandler = jest.fn(); const mockApollo = createMockApollo( - [[userWorkspacesListQuery, userWorkspacesListQueryHandler]], + [ + [userWorkspacesListQuery, userWorkspacesListQueryHandler], + [userWorkspacesProjectsNamesQuery, userWorkspacesProjectNamesQueryHandler], + ], { Mutation: { workspaceUpdate: workspaceUpdateMutationHandler, @@ -117,20 +127,26 @@ describe('remote_development/pages/list.vue', () => { it('displays user workspaces correctly', () => { expect(findTableRowsAsData(wrapper)).toEqual( - USER_WORKSPACES_QUERY_RESULT.data.currentUser.workspaces.nodes.map((x) => ({ - nameText: `${x.projectId} ${x.name}`, - workspaceState: x.actualState, - actionsProps: { - actualState: x.actualState, - desiredState: x.desiredState, - }, - ...(x.actualState === WORKSPACE_STATES.running - ? { - previewText: x.url, - previewHref: x.url, - } - : {}), - })), + USER_WORKSPACES_QUERY_RESULT.data.currentUser.workspaces.nodes.map((x) => { + const projectName = USER_WORKSPACES_PROJECT_NAMES_QUERY_RESULT.data.projects.nodes.find( + (project) => project.id === x.projectId, + ).nameWithNamespace; + + return { + nameText: `${projectName} ${x.name}`, + workspaceState: x.actualState, + actionsProps: { + actualState: x.actualState, + desiredState: x.desiredState, + }, + ...(x.actualState === WORKSPACE_STATES.running + ? { + previewText: x.url, + previewHref: x.url, + } + : {}), + }; + }), ); }); @@ -153,69 +169,6 @@ describe('remote_development/pages/list.vue', () => { expect(findTableRowsAsData(wrapper).pop().workspaceState).toBe(WORKSPACE_STATES.terminated); }); }); - - describe('when the query returns terminated workspaces older than five days', () => { - const oldTerminatedWorkspaceName = 'terminated-workspace-older-than-five-days'; - const oldRunningWorkspaceName = 'running-workspace-older-than-five-days'; - const createdAt = new Date(2023, 3, 1); - - beforeEach(async () => { - const customData = setupMockTerminatedWorkspace({ - name: oldTerminatedWorkspaceName, - createdAt, - }); - const oldRunningWorkspace = { - ...customData.data.currentUser.workspaces.nodes[0], - actualState: WORKSPACE_STATES.running, - name: oldRunningWorkspaceName, - createdAt, - }; - customData.data.currentUser.workspaces.nodes.unshift(oldRunningWorkspace); - - createWrapper(customData); - - await waitForPromises(); - }); - - it('excludes terminated workspaces older than five days from the workspaces list', () => { - expect(findTableRowsAsData(wrapper)).not.toContainEqual( - expect.objectContaining({ - nameText: expect.stringContaining(oldTerminatedWorkspaceName), - }), - ); - }); - - it('displays non-terminated older than five days from the workspaces list', () => { - expect(findTableRowsAsData(wrapper)).toContainEqual( - expect.objectContaining({ - nameText: expect.stringContaining(oldRunningWorkspaceName), - }), - ); - }); - }); - }); - - describe('when the query returns only terminated workspaces older than five days', () => { - beforeEach(async () => { - const customData = setupMockTerminatedWorkspace({ - name: 'terminated-workspace-older-than-five-days', - createdAt: new Date(2023, 3, 1), - }); - customData.data.currentUser.workspaces.nodes = [ - customData.data.currentUser.workspaces.nodes.shift(), - ]; - createWrapper(customData); - - await waitForPromises(); - }); - - it('displays empty state illustration', () => { - expect(wrapper.findComponent(WorkspaceEmptyState).exists()).toBe(true); - }); - - it('hides workspaces table', () => { - expect(findTable(wrapper).exists()).toBe(false); - }); }); describe('workspace actions is clicked', () => { diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 4dd83d4e4161e2692ab3b91b8298216464e3e215..791bf7fa45c26407dffa9748c2746c4c52e2356b 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -47477,9 +47477,6 @@ msgstr "" msgid "Unable to load commits. Try again later." msgstr "" -msgid "Unable to load current Workspaces. Please try again or contact an administrator." -msgstr "" - msgid "Unable to load file contents. Try again later." msgstr "" @@ -50925,6 +50922,9 @@ msgstr "" msgid "Workspaces|To create a workspace, add a devfile to this project. A devfile is a configuration file for your workspace." msgstr "" +msgid "Workspaces|Unable to load current Workspaces. Please try again or contact an administrator." +msgstr "" + msgid "Workspaces|Unknown state" msgstr ""