diff --git a/app/assets/javascripts/clusters/agents/components/show.vue b/app/assets/javascripts/clusters/agents/components/show.vue
index 9d7d68ee31c42857eccabe052926d74d0289e8dc..c1a6f7e0800503c5294aa76e012d2ee2163d9eec 100644
--- a/app/assets/javascripts/clusters/agents/components/show.vue
+++ b/app/assets/javascripts/clusters/agents/components/show.vue
@@ -161,6 +161,8 @@ export default {
+
+
diff --git a/ee/app/assets/javascripts/clusters/agents/components/show.vue b/ee/app/assets/javascripts/clusters/agents/components/show.vue
index fe31d0f50251684851ba57cdf55e17e062749cf1..87724c037e63f47d5a46990c5a60429c769f7ffa 100644
--- a/ee/app/assets/javascripts/clusters/agents/components/show.vue
+++ b/ee/app/assets/javascripts/clusters/agents/components/show.vue
@@ -5,21 +5,27 @@ import { s__ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import AgentShowPage from '~/clusters/agents/components/show.vue';
import AgentVulnerabilityReport from 'ee/security_dashboard/components/agent/agent_vulnerability_report.vue';
+import AgentWorkspacesList from 'ee/remote_development/components/agent_admin_ui/agent_workspaces_list.vue';
export default {
i18n: {
securityTabTitle: s__('ClusterAgents|Security'),
+ workspacesTabTitle: s__('RemoteDevelopment|Workspaces'),
},
components: {
AgentShowPage,
GlTab,
AgentVulnerabilityReport,
+ AgentWorkspacesList,
},
mixins: [glFeatureFlagMixin()],
computed: {
showSecurityTab() {
return this.glFeatures.kubernetesClusterVulnerabilities;
},
+ showAgentWorkspacesTab() {
+ return this.glFeatures.remoteDevelopment;
+ },
},
};
@@ -31,5 +37,10 @@ export default {
+
+
+
+
+
diff --git a/ee/app/assets/javascripts/remote_development/components/agent_admin_ui/agent_workspaces_list.vue b/ee/app/assets/javascripts/remote_development/components/agent_admin_ui/agent_workspaces_list.vue
new file mode 100644
index 0000000000000000000000000000000000000000..4d272408b7bc365624cafd2fd70576ae951bd260
--- /dev/null
+++ b/ee/app/assets/javascripts/remote_development/components/agent_admin_ui/agent_workspaces_list.vue
@@ -0,0 +1,110 @@
+
+
+
+
+
diff --git a/ee/app/assets/javascripts/remote_development/components/common/workspaces_list.vue b/ee/app/assets/javascripts/remote_development/components/common/workspaces_list.vue
new file mode 100644
index 0000000000000000000000000000000000000000..b4625f0feea00fa2e27de7702ca741b1ae32d45d
--- /dev/null
+++ b/ee/app/assets/javascripts/remote_development/components/common/workspaces_list.vue
@@ -0,0 +1,103 @@
+
+
+
+
+
+ {{ error }}
+
+
+
+
{{ $options.i18n.heading }}
+
+
+ {{ $options.i18n.learnMoreHelpLink }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ee/app/assets/javascripts/remote_development/constants.js b/ee/app/assets/javascripts/remote_development/constants.js
index 4d15df1a89fb482240757d2c9ccdb3f0eab9c00f..4608a479f842368ad91dabecdf2b205af2a55f9d 100644
--- a/ee/app/assets/javascripts/remote_development/constants.js
+++ b/ee/app/assets/javascripts/remote_development/constants.js
@@ -1,4 +1,5 @@
import { pick } from 'lodash';
+import { s__ } from '~/locale';
export const DEFAULT_DEVFILE_PATH = '.devfile.yaml';
export const DEFAULT_EDITOR = 'webide';
@@ -44,3 +45,7 @@ export const PROJECT_VISIBILITY = {
export const EXCLUDED_WORKSPACE_AGE_IN_DAYS = 5;
export const WORKSPACES_LIST_PAGE_SIZE = 10;
export const WORKSPACES_DROPDOWN_GROUP_PAGE_SIZE = 20;
+
+export const I18N_LOADING_WORKSPACES_FAILED = s__(
+ 'Workspaces|Unable to load current Workspaces. Please try again or contact an administrator.',
+);
diff --git a/ee/app/assets/javascripts/remote_development/graphql/queries/agent_workspaces_list.query.graphql b/ee/app/assets/javascripts/remote_development/graphql/queries/agent_workspaces_list.query.graphql
new file mode 100644
index 0000000000000000000000000000000000000000..423fefc75b8726afe3ba9c384a9c97466db9b8f6
--- /dev/null
+++ b/ee/app/assets/javascripts/remote_development/graphql/queries/agent_workspaces_list.query.graphql
@@ -0,0 +1,33 @@
+#import "~/graphql_shared/fragments/page_info.fragment.graphql"
+
+query agentWorkspaces(
+ $first: Int
+ $before: String
+ $after: String
+ $agentName: String!
+ $projectPath: ID!
+) {
+ project(fullPath: $projectPath) {
+ id
+ clusterAgent(name: $agentName) {
+ id
+ workspaces(first: $first, before: $before, after: $after) {
+ nodes {
+ id
+ name
+ namespace
+ projectId
+ desiredState
+ actualState
+ url
+ devfileRef
+ devfilePath
+ createdAt
+ }
+ pageInfo {
+ ...PageInfo
+ }
+ }
+ }
+ }
+}
diff --git a/ee/app/assets/javascripts/remote_development/pages/list.vue b/ee/app/assets/javascripts/remote_development/pages/list.vue
index c562aed91954626c6170cd8edc466f1d3ae7395e..9f30f0c5a5f7bf106d192ab92cef21d779b9c16f 100644
--- a/ee/app/assets/javascripts/remote_development/pages/list.vue
+++ b/ee/app/assets/javascripts/remote_development/pages/list.vue
@@ -1,38 +1,27 @@
-
-
- {{ error }}
-
-
-
-
{{ $options.i18n.heading }}
-
-
- {{ $options.i18n.learnMoreHelpLink }}
- {{ $options.i18n.newWorkspaceButton }}
-
-
-
-
-
-
-
-
-
-
-
+
+
+ {{ $options.i18n.newWorkspaceButton }}
+
-
+
diff --git a/ee/app/assets/javascripts/remote_development/services/utils.js b/ee/app/assets/javascripts/remote_development/services/utils.js
index 0ba5e02af33b9d5ccee3e2f386b424afc3176237..18a1789c2223aa6c3649eab05281c2360f916876 100644
--- a/ee/app/assets/javascripts/remote_development/services/utils.js
+++ b/ee/app/assets/javascripts/remote_development/services/utils.js
@@ -1,3 +1,5 @@
+import userWorkspacesProjectsNamesQuery from '../graphql/queries/user_workspaces_projects_names.query.graphql';
+
export const populateWorkspacesWithProjectNames = (workspaces, projects) => {
return workspaces.map((workspace) => {
const project = projects.find((p) => p.id === workspace.projectId);
@@ -8,3 +10,21 @@ export const populateWorkspacesWithProjectNames = (workspaces, projects) => {
};
});
};
+export const fetchProjectNames = async (apollo, workspaces) => {
+ const projectIds = workspaces.map(({ projectId }) => projectId);
+
+ try {
+ const {
+ data: { projects },
+ } = await apollo.query({
+ query: userWorkspacesProjectsNamesQuery,
+ variables: { ids: projectIds },
+ });
+
+ return {
+ projects: projects.nodes,
+ };
+ } catch (error) {
+ return { error };
+ }
+};
diff --git a/ee/app/controllers/ee/projects/cluster_agents_controller.rb b/ee/app/controllers/ee/projects/cluster_agents_controller.rb
index 0f4234b5e84c9ff9c354482932f1bd5effea5444..4fe724cfcd76ce9a4acb24c4e2d10325afd2be79 100644
--- a/ee/app/controllers/ee/projects/cluster_agents_controller.rb
+++ b/ee/app/controllers/ee/projects/cluster_agents_controller.rb
@@ -9,6 +9,7 @@ module ClusterAgentsController
before_action do
push_licensed_feature(:kubernetes_cluster_vulnerabilities, project)
push_licensed_feature(:cluster_agents_ci_impersonation, project)
+ push_licensed_feature(:remote_development, project)
end
end
end
diff --git a/ee/spec/frontend/clusters/agents/components/show_spec.js b/ee/spec/frontend/clusters/agents/components/show_spec.js
index cdb81ddf836e00fa214128d45169f6509a3421f6..f33677f91934ef9d4532e6f7ddb3fdf7d64c0725 100644
--- a/ee/spec/frontend/clusters/agents/components/show_spec.js
+++ b/ee/spec/frontend/clusters/agents/components/show_spec.js
@@ -2,6 +2,7 @@ import { GlTab } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { stubComponent } from 'helpers/stub_component';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import AgentWorkspacesList from 'ee/remote_development/components/agent_admin_ui/agent_workspaces_list.vue';
import ClusterAgentShow from 'ee/clusters/agents/components/show.vue';
import AgentShowPage from '~/clusters/agents/components/show.vue';
import AgentVulnerabilityReport from 'ee/security_dashboard/components/agent/agent_vulnerability_report.vue';
@@ -10,15 +11,30 @@ describe('ClusterAgentShow', () => {
let wrapper;
const clusterAgentId = 'gid://gitlab/Clusters::Agent/1';
+
+ // FIXME: We should try to use the real AgentShowPage since we're quite coupled to it
const AgentShowPageStub = stubComponent(AgentShowPage, {
- provide: { agentName: 'test', projectPath: 'test' },
- template: `
`,
+ inject: ['agentName', 'projectPath', 'clusterAgentId'],
+ template: `
+
+
+
`,
});
- const createWrapper = ({ glFeatures = { kubernetesClusterVulnerabilities: true } } = {}) => {
+ const createWrapper = ({
+ glFeatures = {
+ kubernetesClusterVulnerabilities: true,
+ remoteDevelopment: true,
+ },
+ } = {}) => {
wrapper = extendedWrapper(
shallowMount(ClusterAgentShow, {
- provide: { glFeatures },
+ provide: {
+ glFeatures,
+ agentName: 'test-agent',
+ projectPath: 'test-project',
+ clusterAgentId,
+ },
stubs: {
AgentShowPage: AgentShowPageStub,
},
@@ -26,30 +42,68 @@ describe('ClusterAgentShow', () => {
);
};
- const findAgentVulnerabilityReport = () => wrapper.findComponent(AgentVulnerabilityReport);
- const findTab = () => wrapper.findComponent(GlTab);
+ const createEmptyWrapper = () => wrapper.find('does-not-exist');
+ const findTab = (title) =>
+ wrapper.findAllComponents(GlTab).wrappers.find((x) => x.attributes('title') === title) ||
+ createEmptyWrapper();
- describe('when a user does have permission', () => {
- beforeEach(() => {
- createWrapper();
- });
+ describe('security tab', () => {
+ const findSecurityTab = () => findTab('Security');
+ const findAgentVulnerabilityReport = () =>
+ findSecurityTab().findComponent(AgentVulnerabilityReport);
+
+ describe('when a user does have permission', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('does not display the tab', () => {
+ expect(findSecurityTab().exists()).toBe(true);
+ });
- it('does not display the tab', () => {
- expect(findTab().exists()).toBe(true);
+ it('does display the cluster agent id', () => {
+ expect(findAgentVulnerabilityReport().props('clusterAgentId')).toBe(clusterAgentId);
+ });
});
- it('does display the cluster agent id', () => {
- expect(findAgentVulnerabilityReport().props('clusterAgentId')).toBe(clusterAgentId);
+ describe('without access', () => {
+ beforeEach(() => {
+ createWrapper({ glFeatures: { kubernetesClusterVulnerabilities: false } });
+ });
+
+ it('when a user does not have permission', () => {
+ expect(findSecurityTab().exists()).toBe(false);
+ });
});
});
- describe('without access', () => {
- beforeEach(() => {
- createWrapper({ glFeatures: { kubernetesClusterVulnerabilities: false } });
+ describe('workspaces tab', () => {
+ const findAgentWorkspacesTab = () => findTab('Workspaces');
+ const findAgentWorkspacesList = () =>
+ findAgentWorkspacesTab().findComponent(AgentWorkspacesList);
+
+ describe('when remote development feature is enabled', () => {
+ beforeEach(() => {
+ createWrapper({ glFeatures: { remoteDevelopment: true } });
+ });
+
+ it('shows the tab', () => {
+ expect(findAgentWorkspacesList().exists()).toBe(true);
+ expect(findAgentWorkspacesList().props()).toEqual({
+ agentName: 'test-agent',
+ projectPath: 'test-project',
+ });
+ });
});
- it('when a user does not have permission', () => {
- expect(findTab().exists()).toBe(false);
+ describe('when remote development feature is disabled', () => {
+ beforeEach(() => {
+ createWrapper({ glFeatures: { remoteDevelopment: false } });
+ });
+
+ it('does not show the tab', () => {
+ expect(findAgentWorkspacesTab().exists()).toBe(false);
+ });
});
});
});
diff --git a/ee/spec/frontend/remote_development/components/agent_admin_ui/agent_workspaces_list_spec.js b/ee/spec/frontend/remote_development/components/agent_admin_ui/agent_workspaces_list_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..58c4c52e83e38004db98c3e99806ae268bf5a5de
--- /dev/null
+++ b/ee/spec/frontend/remote_development/components/agent_admin_ui/agent_workspaces_list_spec.js
@@ -0,0 +1,235 @@
+import { mount } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import Vue, { nextTick } from 'vue';
+import { GlAlert, GlLink, GlSkeletonLoader } from '@gitlab/ui';
+import { logError } from '~/lib/logger';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import AgentWorkspacesList from 'ee/remote_development/components/agent_admin_ui/agent_workspaces_list.vue';
+import WorkspaceEmptyState from 'ee/remote_development/components/list/empty_state.vue';
+import WorkspacesTable from 'ee/remote_development/components/list/workspaces_table.vue';
+import WorkspacesListPagination from 'ee/remote_development/components/list/workspaces_list_pagination.vue';
+import agentWorkspacesListQuery from 'ee/remote_development/graphql/queries/agent_workspaces_list.query.graphql';
+import userWorkspacesProjectsNamesQuery from 'ee/remote_development/graphql/queries/user_workspaces_projects_names.query.graphql';
+import { populateWorkspacesWithProjectNames } from 'ee/remote_development/services/utils';
+import {
+ AGENT_WORKSPACES_LIST_QUERY_RESULT,
+ AGENT_WORKSPACES_LIST_QUERY_EMPTY_RESULT,
+ WORKSPACES_PROJECT_NAMES_QUERY_RESULT,
+} from '../../mock_data';
+
+jest.mock('~/lib/logger');
+
+Vue.use(VueApollo);
+
+const SVG_PATH = '/assets/illustrations/empty_states/empty_workspaces.svg';
+const AGENT_NAME = 'agent-name';
+const PROJECT_PATH = 'project/path';
+
+describe('remote_development/components/agent_admin_ui/agent_workspaces_list.vue', () => {
+ let wrapper;
+ let mockApollo;
+ let agentWorkspacesListQueryHandler;
+ let userWorkspacesProjectNamesQueryHandler;
+
+ const buildMockApollo = () => {
+ agentWorkspacesListQueryHandler = jest
+ .fn()
+ .mockResolvedValueOnce(AGENT_WORKSPACES_LIST_QUERY_RESULT);
+ userWorkspacesProjectNamesQueryHandler = jest
+ .fn()
+ .mockResolvedValueOnce(WORKSPACES_PROJECT_NAMES_QUERY_RESULT);
+
+ mockApollo = createMockApollo([
+ [agentWorkspacesListQuery, agentWorkspacesListQueryHandler],
+ [userWorkspacesProjectsNamesQuery, userWorkspacesProjectNamesQueryHandler],
+ ]);
+ };
+ const createWrapper = () => {
+ // noinspection JSCheckFunctionSignatures
+ wrapper = mount(AgentWorkspacesList, {
+ apolloProvider: mockApollo,
+ provide: {
+ emptyStateSvgPath: SVG_PATH,
+ },
+ props: {
+ agentName: AGENT_NAME,
+ projectPath: PROJECT_PATH,
+ },
+ });
+ };
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findHelpLink = () => wrapper.findComponent(GlLink);
+ const findTable = () => wrapper.findComponent(WorkspacesTable);
+ const findPagination = () => wrapper.findComponent(WorkspacesListPagination);
+
+ beforeEach(() => {
+ buildMockApollo();
+ });
+
+ describe('when no workspaces are available', () => {
+ beforeEach(async () => {
+ agentWorkspacesListQueryHandler.mockReset();
+ agentWorkspacesListQueryHandler.mockResolvedValueOnce(
+ AGENT_WORKSPACES_LIST_QUERY_EMPTY_RESULT,
+ );
+
+ createWrapper();
+ await waitForPromises();
+ });
+
+ it('renders empty state when no workspaces are available', () => {
+ expect(wrapper.findComponent(WorkspaceEmptyState).exists()).toBe(true);
+ });
+
+ it('does not render the workspaces table', () => {
+ expect(findTable().exists()).toBe(false);
+ });
+
+ it('does not render the workspaces pagination', () => {
+ expect(findPagination().exists()).toBe(false);
+ });
+ });
+
+ it('shows loading state when workspaces are being fetched', () => {
+ createWrapper();
+ expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true);
+ });
+
+ describe('default (with nodes)', () => {
+ beforeEach(async () => {
+ createWrapper();
+ await waitForPromises();
+ });
+
+ it('renders table', () => {
+ expect(findTable().exists()).toBe(true);
+ });
+
+ it('renders pagination', () => {
+ expect(findPagination().exists()).toBe(true);
+ });
+
+ it('provides workspaces data to the workspaces table', () => {
+ expect(findTable(wrapper).props('workspaces')).toEqual(
+ populateWorkspacesWithProjectNames(
+ AGENT_WORKSPACES_LIST_QUERY_RESULT.data.project.clusterAgent.workspaces.nodes,
+ WORKSPACES_PROJECT_NAMES_QUERY_RESULT.data.projects.nodes,
+ ),
+ );
+ });
+
+ it('does not call log error', () => {
+ expect(logError).not.toHaveBeenCalled();
+ });
+
+ it('does not show alert', () => {
+ expect(findAlert(wrapper).exists()).toBe(false);
+ });
+
+ describe('when pagination component emits input event', () => {
+ it('refetches workspaces starting at the specified cursor', async () => {
+ const pageVariables = {
+ after: 'end',
+ first: 10,
+ agentName: AGENT_NAME,
+ projectPath: PROJECT_PATH,
+ };
+
+ createWrapper();
+
+ await waitForPromises();
+
+ expect(agentWorkspacesListQueryHandler).toHaveBeenCalledTimes(1);
+
+ findPagination().vm.$emit('input', pageVariables);
+
+ await waitForPromises();
+
+ expect(agentWorkspacesListQueryHandler).toHaveBeenCalledTimes(2);
+ expect(agentWorkspacesListQueryHandler).toHaveBeenLastCalledWith(pageVariables);
+ });
+ });
+ });
+
+ describe('when workspace table emits updateFailed event', () => {
+ const error = 'Failed to stop workspace';
+
+ beforeEach(async () => {
+ createWrapper();
+ await waitForPromises();
+
+ findTable().vm.$emit('updateFailed', { error });
+ });
+
+ it('displays the error attached to the event', async () => {
+ await nextTick();
+
+ expect(findAlert().text()).toBe(error);
+ });
+
+ describe('when workspace table emits updateSucceed event', () => {
+ it('dismisses the previous update error', async () => {
+ expect(findAlert().text()).toBe(error);
+
+ findTable().vm.$emit('updateSucceed');
+
+ await nextTick();
+
+ expect(findAlert().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe.each`
+ query | queryHandlerFactory
+ ${'userWorkspaces'} | ${() => agentWorkspacesListQueryHandler}
+ ${'userWorkspacesProjectsNames'} | ${() => userWorkspacesProjectNamesQueryHandler}
+ `('when $query query fails', ({ queryHandlerFactory }) => {
+ const ERROR = new Error('Something bad!');
+
+ beforeEach(async () => {
+ const queryHandler = queryHandlerFactory();
+
+ queryHandler.mockReset();
+ queryHandler.mockRejectedValueOnce(ERROR);
+
+ createWrapper();
+ await waitForPromises();
+ });
+
+ it('does not render table', () => {
+ expect(findTable().exists()).toBe(false);
+ });
+
+ it('logs error', () => {
+ expect(logError).toHaveBeenCalledWith(ERROR);
+ });
+
+ it('shows alert', () => {
+ expect(findAlert().text()).toBe(
+ 'Unable to load current Workspaces. Please try again or contact an administrator.',
+ );
+ });
+
+ it('hides error when alert is dismissed', async () => {
+ findAlert().vm.$emit('dismiss');
+
+ await nextTick();
+
+ expect(findAlert().exists()).toBe(false);
+ });
+ });
+
+ describe('fixed elements', () => {
+ beforeEach(async () => {
+ createWrapper();
+
+ await waitForPromises();
+ });
+
+ it('displays a link that navigates to the workspaces help page', () => {
+ expect(findHelpLink().attributes().href).toContain('user/workspace/index.md');
+ });
+ });
+});
diff --git a/ee/spec/frontend/remote_development/components/list/workspaces_table_spec.js b/ee/spec/frontend/remote_development/components/list/workspaces_table_spec.js
index b798b30488d3dd405d5c2bbca40fb835560c04d6..3c6ff6b91997b50c8ec8813b317509ef9438ac39 100644
--- a/ee/spec/frontend/remote_development/components/list/workspaces_table_spec.js
+++ b/ee/spec/frontend/remote_development/components/list/workspaces_table_spec.js
@@ -11,7 +11,7 @@ import { populateWorkspacesWithProjectNames } from 'ee/remote_development/servic
import { WORKSPACE_STATES, WORKSPACE_DESIRED_STATES } from 'ee/remote_development/constants';
import {
USER_WORKSPACES_LIST_QUERY_RESULT,
- USER_WORKSPACES_PROJECT_NAMES_QUERY_RESULT,
+ WORKSPACES_PROJECT_NAMES_QUERY_RESULT,
} from '../../mock_data';
jest.mock('~/lib/logger');
@@ -53,7 +53,7 @@ describe('remote_development/components/list/workspaces_table.vue', () => {
const createWrapper = ({
workspaces = populateWorkspacesWithProjectNames(
USER_WORKSPACES_LIST_QUERY_RESULT.data.currentUser.workspaces.nodes,
- USER_WORKSPACES_PROJECT_NAMES_QUERY_RESULT.data.projects.nodes,
+ WORKSPACES_PROJECT_NAMES_QUERY_RESULT.data.projects.nodes,
),
} = {}) => {
updateWorkspaceMutationMock = jest.fn();
@@ -98,7 +98,7 @@ describe('remote_development/components/list/workspaces_table.vue', () => {
expect(findTableRowsAsData(wrapper)).toEqual(
populateWorkspacesWithProjectNames(
USER_WORKSPACES_LIST_QUERY_RESULT.data.currentUser.workspaces.nodes,
- USER_WORKSPACES_PROJECT_NAMES_QUERY_RESULT.data.projects.nodes,
+ WORKSPACES_PROJECT_NAMES_QUERY_RESULT.data.projects.nodes,
).map((x) => {
return {
nameText: `${x.projectName} ${x.name}`,
diff --git a/ee/spec/frontend/remote_development/components/workspaces_dropdown_group/workspaces_dropdown_group_spec.js b/ee/spec/frontend/remote_development/components/workspaces_dropdown_group/workspaces_dropdown_group_spec.js
index 2da1ffcc1b4ff35f10857624ca1fccba21e243c3..ad6667b4baf9ae23236d21d717ba2da48f2f52f5 100644
--- a/ee/spec/frontend/remote_development/components/workspaces_dropdown_group/workspaces_dropdown_group_spec.js
+++ b/ee/spec/frontend/remote_development/components/workspaces_dropdown_group/workspaces_dropdown_group_spec.js
@@ -18,7 +18,7 @@ import {
WORKSPACE_DESIRED_STATES,
} from 'ee/remote_development/constants';
import {
- USER_WORKSPACES_QUERY_EMPTY_RESULT,
+ USER_WORKSPACES_LIST_QUERY_EMPTY_RESULT,
USER_WORKSPACES_LIST_QUERY_RESULT,
PROJECT_ID,
PROJECT_FULL_PATH,
@@ -231,7 +231,7 @@ describe('remote_development/components/workspaces_dropdown_group/workspaces_dro
describe('when user does not have workspaces', () => {
beforeEach(async () => {
userWorkspacesListQueryHandler.mockReset();
- userWorkspacesListQueryHandler.mockResolvedValueOnce(USER_WORKSPACES_QUERY_EMPTY_RESULT);
+ userWorkspacesListQueryHandler.mockResolvedValueOnce(USER_WORKSPACES_LIST_QUERY_EMPTY_RESULT);
createWrapper();
diff --git a/ee/spec/frontend/remote_development/mock_data/index.js b/ee/spec/frontend/remote_development/mock_data/index.js
index 1c81b95e994502a86940ec04253d557e620c04ef..cc34435dce8d131bf0dd3fc2a8715b538490161e 100644
--- a/ee/spec/frontend/remote_development/mock_data/index.js
+++ b/ee/spec/frontend/remote_development/mock_data/index.js
@@ -66,7 +66,7 @@ export const USER_WORKSPACES_LIST_QUERY_RESULT = {
},
};
-export const USER_WORKSPACES_QUERY_EMPTY_RESULT = {
+export const USER_WORKSPACES_LIST_QUERY_EMPTY_RESULT = {
data: {
currentUser: {
id: 1,
@@ -83,6 +83,71 @@ export const USER_WORKSPACES_QUERY_EMPTY_RESULT = {
},
};
+export const AGENT_WORKSPACES_LIST_QUERY_RESULT = {
+ data: {
+ project: {
+ id: 1,
+ clusterAgent: {
+ id: 1,
+ workspaces: {
+ nodes: [
+ {
+ id: 'gid://gitlab/RemoteDevelopment::Workspace/2',
+ name: 'workspace-1-1-idmi02',
+ namespace: 'gl-rd-ns-1-1-idmi02',
+ desiredState: 'Stopped',
+ actualState: 'CreationRequested',
+ url: 'https://8000-workspace-1-1-idmi02.workspaces.localdev.me?tkn=password',
+ devfileRef: 'main',
+ devfilePath: '.devfile.yaml',
+ projectId: 'gid://gitlab/Project/1',
+ createdAt: '2023-04-29T18:24:34Z',
+ },
+ {
+ id: 'gid://gitlab/RemoteDevelopment::Workspace/1',
+ name: 'workspace-1-1-rfu27q',
+ namespace: 'gl-rd-ns-1-1-rfu27q',
+ desiredState: 'Running',
+ actualState: 'Running',
+ url: 'https://8000-workspace-1-1-rfu27q.workspaces.localdev.me?tkn=password',
+ devfileRef: 'main',
+ devfilePath: '.devfile.yaml',
+ projectId: 'gid://gitlab/Project/1',
+ createdAt: '2023-05-01T18:24:34Z',
+ },
+ ],
+ pageInfo: {
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: null,
+ endCursor: null,
+ },
+ },
+ },
+ },
+ },
+};
+
+export const AGENT_WORKSPACES_LIST_QUERY_EMPTY_RESULT = {
+ data: {
+ project: {
+ id: 1,
+ clusterAgent: {
+ id: 1,
+ workspaces: {
+ nodes: [],
+ pageInfo: {
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: null,
+ endCursor: null,
+ },
+ },
+ },
+ },
+ },
+};
+
export const SEARCH_PROJECTS_QUERY_RESULT = {
data: {
projects: {
@@ -234,7 +299,7 @@ export const WORKSPACE_UPDATE_MUTATION_RESULT = {
},
};
-export const USER_WORKSPACES_PROJECT_NAMES_QUERY_RESULT = {
+export const WORKSPACES_PROJECT_NAMES_QUERY_RESULT = {
data: {
projects: {
nodes: [
diff --git a/ee/spec/frontend/remote_development/pages/list_spec.js b/ee/spec/frontend/remote_development/pages/list_spec.js
index 9a9831cfaaddf59a38ada6d5cbe8ea9b90c672fa..9d57d617f40a29a500920509b56cc25c81346c9c 100644
--- a/ee/spec/frontend/remote_development/pages/list_spec.js
+++ b/ee/spec/frontend/remote_development/pages/list_spec.js
@@ -5,7 +5,7 @@ import { GlAlert, GlButton, GlLink, GlSkeletonLoader } from '@gitlab/ui';
import { logError } from '~/lib/logger';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import WorkspaceList from 'ee/remote_development/pages/list.vue';
+import List from 'ee/remote_development/pages/list.vue';
import WorkspaceEmptyState from 'ee/remote_development/components/list/empty_state.vue';
import WorkspacesTable from 'ee/remote_development/components/list/workspaces_table.vue';
import WorkspacesListPagination from 'ee/remote_development/components/list/workspaces_list_pagination.vue';
@@ -15,8 +15,8 @@ import { ROUTES } from 'ee/remote_development/constants';
import { populateWorkspacesWithProjectNames } from 'ee/remote_development/services/utils';
import {
USER_WORKSPACES_LIST_QUERY_RESULT,
- USER_WORKSPACES_QUERY_EMPTY_RESULT,
- USER_WORKSPACES_PROJECT_NAMES_QUERY_RESULT,
+ USER_WORKSPACES_LIST_QUERY_EMPTY_RESULT,
+ WORKSPACES_PROJECT_NAMES_QUERY_RESULT,
} from '../mock_data';
jest.mock('~/lib/logger');
@@ -37,7 +37,7 @@ describe('remote_development/pages/list.vue', () => {
.mockResolvedValueOnce(USER_WORKSPACES_LIST_QUERY_RESULT);
userWorkspacesProjectNamesQueryHandler = jest
.fn()
- .mockResolvedValueOnce(USER_WORKSPACES_PROJECT_NAMES_QUERY_RESULT);
+ .mockResolvedValueOnce(WORKSPACES_PROJECT_NAMES_QUERY_RESULT);
mockApollo = createMockApollo([
[userWorkspacesListQuery, userWorkspacesListQueryHandler],
@@ -45,7 +45,7 @@ describe('remote_development/pages/list.vue', () => {
]);
};
const createWrapper = () => {
- wrapper = mount(WorkspaceList, {
+ wrapper = mount(List, {
apolloProvider: mockApollo,
provide: {
emptyStateSvgPath: SVG_PATH,
@@ -65,7 +65,7 @@ describe('remote_development/pages/list.vue', () => {
describe('when no workspaces are available', () => {
beforeEach(async () => {
userWorkspacesListQueryHandler.mockReset();
- userWorkspacesListQueryHandler.mockResolvedValueOnce(USER_WORKSPACES_QUERY_EMPTY_RESULT);
+ userWorkspacesListQueryHandler.mockResolvedValueOnce(USER_WORKSPACES_LIST_QUERY_EMPTY_RESULT);
createWrapper();
await waitForPromises();
@@ -107,7 +107,7 @@ describe('remote_development/pages/list.vue', () => {
expect(findTable(wrapper).props('workspaces')).toEqual(
populateWorkspacesWithProjectNames(
USER_WORKSPACES_LIST_QUERY_RESULT.data.currentUser.workspaces.nodes,
- USER_WORKSPACES_PROJECT_NAMES_QUERY_RESULT.data.projects.nodes,
+ WORKSPACES_PROJECT_NAMES_QUERY_RESULT.data.projects.nodes,
),
);
});
diff --git a/ee/spec/frontend/remote_development/router/index_spec.js b/ee/spec/frontend/remote_development/router/index_spec.js
index 30bbd2a70f99ea86d0a18f75e07dfe7d38d1d7c1..87ec6f90c8e9220d302afa3da16d2416c0692557 100644
--- a/ee/spec/frontend/remote_development/router/index_spec.js
+++ b/ee/spec/frontend/remote_development/router/index_spec.js
@@ -11,8 +11,8 @@ import { ROUTES } from 'ee/remote_development/constants';
import createMockApollo from 'helpers/mock_apollo_helper';
import userWorkspacesListQuery from 'ee/remote_development/graphql/queries/user_workspaces_list.query.graphql';
import {
- USER_WORKSPACES_PROJECT_NAMES_QUERY_RESULT,
- USER_WORKSPACES_QUERY_EMPTY_RESULT,
+ WORKSPACES_PROJECT_NAMES_QUERY_RESULT,
+ USER_WORKSPACES_LIST_QUERY_EMPTY_RESULT,
} from '../mock_data';
Vue.use(VueRouter);
@@ -38,10 +38,13 @@ describe('remote_development/router/index.js', () => {
wrapper = mountExtended(App, {
router,
apolloProvider: createMockApollo([
- [userWorkspacesListQuery, jest.fn().mockResolvedValue(USER_WORKSPACES_QUERY_EMPTY_RESULT)],
+ [
+ userWorkspacesListQuery,
+ jest.fn().mockResolvedValue(USER_WORKSPACES_LIST_QUERY_EMPTY_RESULT),
+ ],
[
userWorkspacesProjectsNamesQuery,
- jest.fn().mockResolvedValueOnce(USER_WORKSPACES_PROJECT_NAMES_QUERY_RESULT),
+ jest.fn().mockResolvedValueOnce(WORKSPACES_PROJECT_NAMES_QUERY_RESULT),
],
]),
provide: {
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index f66d21bc1ad4bcce768be93d03d8702bd26cc605..2f95310299c3c95ad51c4983052fa5f8c9c009e0 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -40272,6 +40272,9 @@ msgstr ""
msgid "Remote object has no absolute path."
msgstr ""
+msgid "RemoteDevelopment|Workspaces"
+msgstr ""
+
msgid "Remove"
msgstr ""
diff --git a/qa/qa/ee/page/workspace/list.rb b/qa/qa/ee/page/workspace/list.rb
index 58a528c513b2646a5ef7d40c4db52fbe926c6d61..8b9c266cd3326cae4bb2b0d165492952eb2c7632 100644
--- a/qa/qa/ee/page/workspace/list.rb
+++ b/qa/qa/ee/page/workspace/list.rb
@@ -7,6 +7,9 @@ module Workspace
class List < QA::Page::Base
view 'ee/app/assets/javascripts/remote_development/pages/list.vue' do
element 'list-new-workspace-button'
+ end
+
+ view 'ee/app/assets/javascripts/remote_development/components/common/workspaces_list.vue' do
element 'workspace-list-item'
end
diff --git a/spec/frontend/clusters/agents/components/show_spec.js b/spec/frontend/clusters/agents/components/show_spec.js
index 019f789d875112e2639107563fb061f904316f7e..8a40c528c1d6dd22796e8f18e35a2b1936011ea7 100644
--- a/spec/frontend/clusters/agents/components/show_spec.js
+++ b/spec/frontend/clusters/agents/components/show_spec.js
@@ -76,6 +76,7 @@ describe('ClusterAgentShow', () => {
const findPaginationButtons = () => wrapper.findComponent(GlKeysetPagination);
const findTokenCount = () => wrapper.findByTestId('cluster-agent-token-count').text();
const findEESecurityTabSlot = () => wrapper.findByTestId('ee-security-tab');
+ const findEEWorkspacesTabSlot = () => wrapper.findByTestId('ee-workspaces-tab');
const findActivity = () => wrapper.findComponent(ActivityEvents);
const findIntegrationStatus = () => wrapper.findComponent(IntegrationStatus);
@@ -253,4 +254,23 @@ describe('ClusterAgentShow', () => {
expect(findEESecurityTabSlot().exists()).toBe(true);
});
});
+
+ describe('ee-workspaces-tab slot', () => {
+ it('does not display when a slot is not passed in', async () => {
+ createWrapperWithoutApollo({ clusterAgent: defaultClusterAgent });
+ await nextTick();
+ expect(findEEWorkspacesTabSlot().exists()).toBe(false);
+ });
+
+ it('does display when a slot is passed in', async () => {
+ createWrapperWithoutApollo({
+ clusterAgent: defaultClusterAgent,
+ slots: {
+ 'ee-workspaces-tab': `Workspaces Tab!`,
+ },
+ });
+ await nextTick();
+ expect(findEEWorkspacesTabSlot().exists()).toBe(true);
+ });
+ });
});