diff --git a/ee/app/assets/javascripts/workspaces/agent_mapping/components/agent_data_label.vue b/ee/app/assets/javascripts/workspaces/agent_mapping/components/agent_data_label.vue
new file mode 100644
index 0000000000000000000000000000000000000000..9cc42e34a5b349a3591b94ab8ba7d7be8538dd40
--- /dev/null
+++ b/ee/app/assets/javascripts/workspaces/agent_mapping/components/agent_data_label.vue
@@ -0,0 +1,18 @@
+
+
+
+
diff --git a/ee/app/assets/javascripts/workspaces/agent_mapping/components/agent_details_popover.vue b/ee/app/assets/javascripts/workspaces/agent_mapping/components/agent_details_popover.vue
new file mode 100644
index 0000000000000000000000000000000000000000..d104e034e0286222e7017a4e0dd068c486012f17
--- /dev/null
+++ b/ee/app/assets/javascripts/workspaces/agent_mapping/components/agent_details_popover.vue
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
{{ agent.project.name }}
+
+
+
+ {{ badgeLabel }}
+
+
+
+
+
+
diff --git a/ee/app/assets/javascripts/workspaces/agent_mapping/components/agents_table.vue b/ee/app/assets/javascripts/workspaces/agent_mapping/components/agents_table.vue
index 5fc41fb787176cd814154b0532612beda292de4f..e1211e7014f7a4b3681db6342444dffa4b3d9a63 100644
--- a/ee/app/assets/javascripts/workspaces/agent_mapping/components/agents_table.vue
+++ b/ee/app/assets/javascripts/workspaces/agent_mapping/components/agents_table.vue
@@ -4,6 +4,7 @@ import { __ } from '~/locale';
import SafeHtml from '~/vue_shared/directives/safe_html';
import CrudComponent from '~/vue_shared/components/crud_component.vue';
import { AGENT_MAPPING_STATUS_MAPPED, AGENT_MAPPING_STATUS_UNMAPPED } from '../constants';
+import AgentDetailsPopover from './agent_details_popover.vue';
import AgentMappingStatusToggle from './agent_mapping_status_toggle.vue';
import ToggleAgentMappingStatusMutation from './toggle_agent_mapping_status_mutation.vue';
@@ -45,6 +46,7 @@ export default {
GlLoadingIcon,
GlTable,
CrudComponent,
+ AgentDetailsPopover,
AgentMappingStatusToggle,
ToggleAgentMappingStatusMutation,
},
@@ -113,7 +115,8 @@ export default {
- {{ item.name }}
+ {{ item.name }}
+
{{
diff --git a/ee/app/assets/javascripts/workspaces/agent_mapping/components/toggle_agent_mapping_status_mutation.vue b/ee/app/assets/javascripts/workspaces/agent_mapping/components/toggle_agent_mapping_status_mutation.vue
index bc41aef2392eb7420f6a4b9e5034dbc7ba7b9107..d55f23f4a2ca75282260ec08881af6b6c18a3cd4 100644
--- a/ee/app/assets/javascripts/workspaces/agent_mapping/components/toggle_agent_mapping_status_mutation.vue
+++ b/ee/app/assets/javascripts/workspaces/agent_mapping/components/toggle_agent_mapping_status_mutation.vue
@@ -93,6 +93,7 @@ export default {
(sourceData) =>
produce(sourceData, (draftData) => {
const { mappedAgents, unmappedAgents } = draftData.namespace;
+
let addTo;
let removeFrom;
@@ -104,8 +105,12 @@ export default {
removeFrom = unmappedAgents;
}
- addTo.nodes.push(agent);
- removeFrom.nodes = removeFrom.nodes.filter((node) => node.id !== agent.id);
+ const targetAgentIndex = removeFrom.nodes.findIndex(
+ (node) => node.id === agent.id,
+ );
+
+ addTo.nodes.push(removeFrom.nodes[targetAgentIndex]);
+ removeFrom.nodes.splice(targetAgentIndex, 1);
}),
);
},
diff --git a/ee/app/assets/javascripts/workspaces/agent_mapping/graphql/queries/get_agents_with_mapping_status.query.graphql b/ee/app/assets/javascripts/workspaces/agent_mapping/graphql/queries/get_agents_with_mapping_status.query.graphql
index c5d2ebfb24adec92eca3af9bbca9d40b2035a4dc..df074421b2f81239b9ef034c56f656b8446b19dc 100644
--- a/ee/app/assets/javascripts/workspaces/agent_mapping/graphql/queries/get_agents_with_mapping_status.query.graphql
+++ b/ee/app/assets/javascripts/workspaces/agent_mapping/graphql/queries/get_agents_with_mapping_status.query.graphql
@@ -5,12 +5,30 @@ query getAgentsWithMappingStatus($namespace: ID!) {
nodes {
id
name
+ project {
+ id
+ name
+ }
+ connections {
+ nodes {
+ connectedAt
+ }
+ }
}
}
unmappedAgents: remoteDevelopmentClusterAgents(filter: UNMAPPED) {
nodes {
id
name
+ project {
+ id
+ name
+ }
+ connections {
+ nodes {
+ connectedAt
+ }
+ }
}
}
}
diff --git a/ee/spec/features/groups/settings/remote_development/workspaces_spec.rb b/ee/spec/features/groups/settings/remote_development/workspaces_spec.rb
index 6864fd88300eb60b3247ccecf5a877cce4992904..6976ccc4d13a311728b0dd598d1d0efb4df7b7cf 100644
--- a/ee/spec/features/groups/settings/remote_development/workspaces_spec.rb
+++ b/ee/spec/features/groups/settings/remote_development/workspaces_spec.rb
@@ -19,9 +19,10 @@
group.add_owner(user)
end
+ include_context 'with kubernetes agent service'
+
before do
stub_licensed_features(remote_development: true)
-
sign_in(user)
visit group_settings_workspaces_path(group)
wait_for_requests
@@ -45,6 +46,10 @@
it 'displays agent in the agents table' do
expect(page).to have_content agent.name
+
+ click_button 'Agent Information'
+ expect(page).to have_content('Connected')
+ expect(page).to have_content(project.name)
end
end
diff --git a/ee/spec/features/remote_development/workspaces_spec.rb b/ee/spec/features/remote_development/workspaces_spec.rb
index f42a54eceb7b090390b04158ce15216dc170b730..204bccea41633dcbb6516dccc706c90764a51749 100644
--- a/ee/spec/features/remote_development/workspaces_spec.rb
+++ b/ee/spec/features/remote_development/workspaces_spec.rb
@@ -8,6 +8,7 @@
include_context 'with remote development shared fixtures'
include_context 'file upload requests helpers'
+ include_context 'with kubernetes agent service'
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group, name: 'test-group', developers: user, owners: user) }
diff --git a/ee/spec/frontend/workspaces/agent_mapping/components/agent_data_label_spec.js b/ee/spec/frontend/workspaces/agent_mapping/components/agent_data_label_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..bf64554b51c68ecc2d8d3509e6894fcb81827a9d
--- /dev/null
+++ b/ee/spec/frontend/workspaces/agent_mapping/components/agent_data_label_spec.js
@@ -0,0 +1,45 @@
+import { GlBadge } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import AgentDataLabel from 'ee/workspaces/agent_mapping/components/agent_data_label.vue';
+
+describe('ee/workspaces/agent_mapping/components/agent_data_label.vue', () => {
+ /** @type {import('@vue/test-utils').Wrapper} */
+
+ let wrapper;
+
+ const createWrapper = (propsData = {}) => {
+ wrapper = shallowMount(AgentDataLabel, {
+ propsData: {
+ label: 'Created in',
+ ...propsData,
+ },
+ slots: {
+ default: ['Test Agent Project', GlBadge],
+ },
+ });
+ };
+
+ describe('default', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('renders the label', () => {
+ expect(wrapper.text()).toContain('Created in');
+ });
+
+ it('renders the value', () => {
+ expect(wrapper.text()).toContain('Test Agent Project');
+ });
+ });
+
+ describe('When adding a component to default slot', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+ it('renders value as a badge if badge prop is provided', () => {
+ const badge = wrapper.findComponent(GlBadge);
+ expect(badge.exists()).toBe(true);
+ });
+ });
+});
diff --git a/ee/spec/frontend/workspaces/agent_mapping/components/agent_details_popover_spec.js b/ee/spec/frontend/workspaces/agent_mapping/components/agent_details_popover_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..484897a6e9ba679be99d2137fcc5ba64e3c0397c
--- /dev/null
+++ b/ee/spec/frontend/workspaces/agent_mapping/components/agent_details_popover_spec.js
@@ -0,0 +1,97 @@
+import { GlButton, GlPopover, GlBadge } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import AgentDetailsPopover from 'ee/workspaces/agent_mapping/components/agent_details_popover.vue';
+import AgentDataLabel from 'ee/workspaces/agent_mapping/components/agent_data_label.vue';
+import { MAPPED_CLUSTER_AGENT } from 'ee_jest/workspaces/mock_data';
+
+jest.mock('lodash/uniqueId', () => (val) => `${val}unique-id`);
+
+const TEST_AGENT_NO_CONNECTED = {
+ ...MAPPED_CLUSTER_AGENT,
+ connections: {
+ nodes: [],
+ },
+};
+const EXPECTED_BUTTON_ID = 'Agent-Details-Popover-unique-id';
+
+describe('ee/workspaces/agent_mapping/components/agent_details_popover.vue', () => {
+ /** @type {import('@vue/test-utils').Wrapper} */
+ let wrapper;
+
+ const createWrapper = (propsData = {}) => {
+ wrapper = shallowMount(AgentDetailsPopover, {
+ propsData: {
+ agent: MAPPED_CLUSTER_AGENT,
+ ...propsData,
+ },
+ });
+ };
+
+ const findAgentDataByLabel = (label) => {
+ const agentDataComponent = wrapper
+ .findAllComponents(AgentDataLabel)
+ .wrappers.find((x) => x.props('label') === label);
+
+ return agentDataComponent;
+ };
+
+ describe('default', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('renders button', () => {
+ const button = wrapper.findComponent(GlButton);
+
+ expect(button.attributes()).toMatchObject({
+ id: EXPECTED_BUTTON_ID,
+ 'aria-label': 'Agent Information',
+ });
+ });
+
+ it('render popover', () => {
+ const popover = wrapper.findComponent(GlPopover);
+
+ expect(popover.attributes()).toMatchObject({
+ target: EXPECTED_BUTTON_ID,
+ title: 'rootgroup-agent',
+ });
+ });
+
+ it('renders agent data for project name', () => {
+ expect(findAgentDataByLabel('Created in').text()).toEqual('GitLab Agent One');
+ });
+
+ it('renders agent data for status', () => {
+ const agentData = findAgentDataByLabel('Status');
+ const badge = agentData.findComponent(GlBadge);
+
+ expect(badge.props('variant')).toEqual('success');
+ expect(badge.text()).toEqual('Connected');
+ });
+ });
+
+ describe('with agent not connected', () => {
+ beforeEach(() => {
+ createWrapper({
+ agent: TEST_AGENT_NO_CONNECTED,
+ });
+ });
+
+ it('render popover', () => {
+ const popover = wrapper.findComponent(GlPopover);
+
+ expect(popover.attributes()).toMatchObject({
+ target: EXPECTED_BUTTON_ID,
+ title: 'rootgroup-agent',
+ });
+ });
+
+ it('renders agent data for status', () => {
+ const agentData = findAgentDataByLabel('Status');
+ const badge = agentData.findComponent(GlBadge);
+ expect(badge.props('variant')).toEqual('neutral');
+ expect(badge.text()).toEqual('Not Connected');
+ });
+ });
+});
diff --git a/ee/spec/frontend/workspaces/agent_mapping/components/agents_table_spec.js b/ee/spec/frontend/workspaces/agent_mapping/components/agents_table_spec.js
index 2ad787dee76526fd1181754aed506c97674c5fd0..cef8d7bc78aad68846739029dd4bd179d5541719 100644
--- a/ee/spec/frontend/workspaces/agent_mapping/components/agents_table_spec.js
+++ b/ee/spec/frontend/workspaces/agent_mapping/components/agents_table_spec.js
@@ -1,6 +1,7 @@
import { GlTable, GlLoadingIcon } from '@gitlab/ui';
import { stubComponent } from 'helpers/stub_component';
import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
+import AgentDetailsPopover from 'ee_component/workspaces/agent_mapping/components/agent_details_popover.vue';
import AgentsTable from 'ee_component/workspaces/agent_mapping/components/agents_table.vue';
import AgentMappingStatusToggle from 'ee_component/workspaces/agent_mapping/components/agent_mapping_status_toggle.vue';
import ToggleAgentMappingStatusMutation from 'ee_component/workspaces/agent_mapping/components/toggle_agent_mapping_status_mutation.vue';
@@ -132,6 +133,13 @@ describe('workspaces/agent_mapping/components/agents_table', () => {
expect(findAgentsTable().text()).toContain(MAPPED_CLUSTER_AGENT.name);
});
+ it('displays agent data popover', () => {
+ const popover = findAgentsTable().findAllComponents(AgentDetailsPopover);
+ agents.forEach((agent, index) => {
+ expect(popover.at(index).props().agent).toMatchObject(agent);
+ });
+ });
+
describe('when displayMappingStatus is true', () => {
it('displays agent status using label', () => {
buildWrapper(
diff --git a/ee/spec/frontend/workspaces/agent_mapping/components/get_agents_with_mapping_status_query_spec.js b/ee/spec/frontend/workspaces/agent_mapping/components/get_agents_with_mapping_status_query_spec.js
index 249d14cec7512dd47d9a02d1a747d6880ecbaf0a..4711b1f7c22680c7866f7775cdafbb43dab5df54 100644
--- a/ee/spec/frontend/workspaces/agent_mapping/components/get_agents_with_mapping_status_query_spec.js
+++ b/ee/spec/frontend/workspaces/agent_mapping/components/get_agents_with_mapping_status_query_spec.js
@@ -90,12 +90,34 @@ describe('workspaces/agent_mapping/components/get_agents_with_mapping_status_que
{
id: 'gid://gitlab/Clusters::Agent/1',
name: 'rootgroup-agent',
+ project: {
+ id: 'gid://gitlab/Project/101',
+ name: 'GitLab Agent One',
+ },
mappingStatus: AGENT_MAPPING_STATUS_MAPPED,
+ connections: {
+ nodes: [
+ {
+ connectedAt: '2023-04-29T18:24:34Z',
+ },
+ ],
+ },
},
{
id: 'gid://gitlab/Clusters::Agent/2',
name: 'rootgroup-agent-2',
+ project: {
+ name: 'GitLab Agent Two',
+ id: 'gid://gitlab/Project/102',
+ },
mappingStatus: AGENT_MAPPING_STATUS_UNMAPPED,
+ connections: {
+ nodes: [
+ {
+ connectedAt: '2023-04-29T18:24:34Z',
+ },
+ ],
+ },
},
],
},
diff --git a/ee/spec/frontend/workspaces/mock_data/index.js b/ee/spec/frontend/workspaces/mock_data/index.js
index c00193f4a25c2cabc56d68978e0f5f2b0d4f3e53..ac6f594d367f54a59656b77655157e0528dbc50e 100644
--- a/ee/spec/frontend/workspaces/mock_data/index.js
+++ b/ee/spec/frontend/workspaces/mock_data/index.js
@@ -396,43 +396,59 @@ export const GET_REMOTE_DEVELOPMENT_CLUSTER_AGENTS_QUERY_RESULT_TWO_AGENTS = {
},
};
+export const MAPPED_CLUSTER_AGENT = {
+ id: 'gid://gitlab/Clusters::Agent/1',
+ name: 'rootgroup-agent',
+ project: {
+ id: 'gid://gitlab/Project/101',
+ name: 'GitLab Agent One',
+ },
+ connections: {
+ nodes: [
+ {
+ connectedAt: '2023-04-29T18:24:34Z',
+ },
+ ],
+ },
+ mappingStatus: AGENT_MAPPING_STATUS_MAPPED,
+ workspacesAgentConfig: {
+ id: 'gid://gitlab/RemoteDevelopment::WorkspacesAgentConfig/999',
+ defaultMaxHoursBeforeTermination: 99,
+ maxHoursBeforeTerminationLimit: 999,
+ },
+};
+
+export const UNMAPPED_CLUSTER_AGENT = {
+ id: 'gid://gitlab/Clusters::Agent/2',
+ name: 'rootgroup-agent-2',
+ project: {
+ id: 'gid://gitlab/Project/102',
+ name: 'GitLab Agent Two',
+ },
+ connections: {
+ nodes: [
+ {
+ connectedAt: '2023-04-29T18:24:34Z',
+ },
+ ],
+ },
+ mappingStatus: AGENT_MAPPING_STATUS_UNMAPPED,
+ workspacesAgentConfig: {
+ id: 'gid://gitlab/RemoteDevelopment::WorkspacesAgentConfig/999',
+ defaultMaxHoursBeforeTermination: 99,
+ maxHoursBeforeTerminationLimit: 999,
+ },
+};
+
export const GET_AGENTS_WITH_MAPPING_STATUS_QUERY_RESULT = {
data: {
namespace: {
id: 'gid://gitlab/Group/81',
mappedAgents: {
- nodes: [
- {
- id: 'gid://gitlab/Clusters::Agent/1',
- name: 'rootgroup-agent',
- project: {
- id: 'gid://gitlab/Project/101',
- nameWithNamespace: 'GitLab Org / GitLab Agent One',
- },
- workspacesAgentConfig: {
- id: 'gid://gitlab/RemoteDevelopment::WorkspacesAgentConfig/999',
- defaultMaxHoursBeforeTermination: 99,
- maxHoursBeforeTerminationLimit: 999,
- },
- },
- ],
+ nodes: [cloneDeep(MAPPED_CLUSTER_AGENT)],
},
unmappedAgents: {
- nodes: [
- {
- id: 'gid://gitlab/Clusters::Agent/2',
- name: 'rootgroup-agent-2',
- project: {
- id: 'gid://gitlab/Project/102',
- nameWithNamespace: 'GitLab Org / GitLab Agent Two',
- },
- workspacesAgentConfig: {
- id: 'gid://gitlab/RemoteDevelopment::WorkspacesAgentConfig/999',
- defaultMaxHoursBeforeTermination: 99,
- maxHoursBeforeTerminationLimit: 999,
- },
- },
- ],
+ nodes: [cloneDeep(UNMAPPED_CLUSTER_AGENT)],
},
},
},
@@ -500,16 +516,4 @@ export const DELETE_CLUSTER_AGENT_MAPPING_MUTATION_WITH_ERROR_RESULT = {
},
};
-export const MAPPED_CLUSTER_AGENT = {
- id: 'gid://gitlab/Clusters::Agent/1',
- name: 'rootgroup-agent',
- mappingStatus: AGENT_MAPPING_STATUS_MAPPED,
-};
-
-export const UNMAPPED_CLUSTER_AGENT = {
- id: 'gid://gitlab/Clusters::Agent/2',
- name: 'rootgroup-agent-2',
- mappingStatus: AGENT_MAPPING_STATUS_UNMAPPED,
-};
-
export const NAMESPACE_ID = 'gid://gitlab/Group/81';
diff --git a/ee/spec/support/shared_contexts/features/workspaces/workspaces_shared_contexts.rb b/ee/spec/support/shared_contexts/features/workspaces/workspaces_shared_contexts.rb
new file mode 100644
index 0000000000000000000000000000000000000000..83322021679f21f3823182aac1a2f93220548c32
--- /dev/null
+++ b/ee/spec/support/shared_contexts/features/workspaces/workspaces_shared_contexts.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'with kubernetes agent service' do
+ before do
+ allow(Gitlab::Kas).to receive(:enabled?).and_return(true)
+ allow_next_instance_of(Gitlab::Kas::Client) do |client|
+ # rubocop:disable RSpec/VerifiedDoubles -- Prevent NoMethodError that occurs when using instance double
+ allow(client).to receive(:get_connected_agents_by_agent_ids).and_return([double(agent_id: agent.id,
+ connected_at: 30.minutes)])
+ # rubocop:enable RSpec/VerifiedDoubles
+ end
+ end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index e92bf1382844f2a5dbd96de66b08009b8e03acf5..4ebce4b791ab0508e70a52d9065864f73d52adbe 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -5049,6 +5049,9 @@ msgstr ""
msgid "After you've reviewed these contribution guidelines, you'll be all set to"
msgstr ""
+msgid "Agent Information"
+msgstr ""
+
msgid "Agent not found for provided id."
msgstr ""
@@ -16687,6 +16690,9 @@ msgstr ""
msgid "Created date"
msgstr ""
+msgid "Created in"
+msgstr ""
+
msgid "Created issue %{issueLink}"
msgstr ""
@@ -36651,6 +36657,9 @@ msgstr ""
msgid "Normal view"
msgstr ""
+msgid "Not Connected"
+msgstr ""
+
msgid "Not adopted"
msgstr ""