diff --git a/app/assets/javascripts/environments/components/kubernetes_agent_info.vue b/app/assets/javascripts/environments/components/kubernetes_agent_info.vue index 7660912f93a12cc505bf778179e60750dfbe2d64..03bde8d64ac206c1eb3ef247a80b3d86c59dc886 100644 --- a/app/assets/javascripts/environments/components/kubernetes_agent_info.vue +++ b/app/assets/javascripts/environments/components/kubernetes_agent_info.vue @@ -1,68 +1,37 @@ <script> -import { GlIcon, GlLink, GlSprintf, GlLoadingIcon, GlAlert } from '@gitlab/ui'; +import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getAgentLastContact, getAgentStatus } from '~/clusters_list/clusters_util'; import { AGENT_STATUSES } from '~/clusters_list/constants'; import { s__ } from '~/locale'; -import getK8sClusterAgentQuery from '../graphql/queries/k8s_cluster_agent.query.graphql'; export default { components: { GlIcon, GlLink, GlSprintf, - GlLoadingIcon, TimeAgoTooltip, - GlAlert, }, props: { - agentName: { - required: true, - type: String, - }, - agentId: { - required: true, - type: String, - }, - agentProjectPath: { - required: true, - type: String, - }, - }, - apollo: { clusterAgent: { - query: getK8sClusterAgentQuery, - variables() { - return { - agentName: this.agentName, - projectPath: this.agentProjectPath, - }; - }, - update: (data) => data?.project?.clusterAgent, - error() { - this.clusterAgent = null; - }, + required: true, + type: Object, }, }, - data() { - return { - clusterAgent: null, - }; - }, computed: { - isLoading() { - return this.$apollo.queries.clusterAgent.loading; - }, agentLastContact() { return getAgentLastContact(this.clusterAgent.tokens.nodes); }, agentStatus() { return getAgentStatus(this.agentLastContact); }, + agentId() { + return getIdFromGraphQLId(this.clusterAgent.id); + }, }, methods: {}, i18n: { - loadingError: s__('ClusterAgents|An error occurred while loading your agent'), agentId: s__('ClusterAgents|Agent ID #%{agentId}'), neverConnectedText: s__('ClusterAgents|Never'), }, @@ -70,8 +39,7 @@ export default { }; </script> <template> - <gl-loading-icon v-if="isLoading" inline /> - <div v-else-if="clusterAgent" class="gl-text-gray-900"> + <div class="gl-text-gray-900"> <gl-icon name="kubernetes-agent" class="gl-text-gray-500" /> <gl-link :href="clusterAgent.webPath" class="gl-mr-3"> <gl-sprintf :message="$options.i18n.agentId" @@ -92,8 +60,4 @@ export default { <span v-else>{{ $options.i18n.neverConnectedText }}</span> </span> </div> - - <gl-alert v-else variant="danger" :dismissible="false"> - {{ $options.i18n.loadingError }} - </gl-alert> </template> diff --git a/app/assets/javascripts/environments/components/kubernetes_overview.vue b/app/assets/javascripts/environments/components/kubernetes_overview.vue index 1f15c4daa2f360afde3affd714a7c0c887e8b40f..a849adfc7559bd98b6188f51e460224ed59de993 100644 --- a/app/assets/javascripts/environments/components/kubernetes_overview.vue +++ b/app/assets/javascripts/environments/components/kubernetes_overview.vue @@ -2,7 +2,7 @@ import { GlCollapse, GlButton, GlAlert } from '@gitlab/ui'; import { __, s__ } from '~/locale'; import csrf from '~/lib/utils/csrf'; -import { getIdFromGraphQLId, isGid } from '~/graphql_shared/utils'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import KubernetesAgentInfo from './kubernetes_agent_info.vue'; import KubernetesPods from './kubernetes_pods.vue'; import KubernetesTabs from './kubernetes_tabs.vue'; @@ -18,17 +18,9 @@ export default { }, inject: ['kasTunnelUrl'], props: { - agentName: { + clusterAgent: { required: true, - type: String, - }, - agentId: { - required: true, - type: String, - }, - agentProjectPath: { - required: true, - type: String, + type: Object, }, namespace: { required: false, @@ -50,8 +42,7 @@ export default { return this.isVisible ? this.$options.i18n.collapse : this.$options.i18n.expand; }, gitlabAgentId() { - const id = isGid(this.agentId) ? getIdFromGraphQLId(this.agentId) : this.agentId; - return id.toString(); + return getIdFromGraphQLId(this.clusterAgent.id).toString(); }, k8sAccessConfiguration() { return { @@ -91,11 +82,7 @@ export default { </p> <gl-collapse :visible="isVisible" class="gl-md-pl-7 gl-md-pr-5 gl-mt-4"> <template v-if="isVisible"> - <kubernetes-agent-info - :agent-name="agentName" - :agent-id="agentId" - :agent-project-path="agentProjectPath" - class="gl-mb-5" /> + <kubernetes-agent-info :cluster-agent="clusterAgent" class="gl-mb-5" /> <gl-alert v-if="error" variant="danger" :dismissible="false" class="gl-mb-5"> {{ error }} diff --git a/app/assets/javascripts/environments/components/new_environment_item.vue b/app/assets/javascripts/environments/components/new_environment_item.vue index 9ad31688329cad72dac65e212afc9c12b0dc8c80..72323c0e43e355337b313a18f58f2b9e21f91965 100644 --- a/app/assets/javascripts/environments/components/new_environment_item.vue +++ b/app/assets/javascripts/environments/components/new_environment_item.vue @@ -13,6 +13,7 @@ import { truncate } from '~/lib/utils/text_utility'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import isLastDeployment from '../graphql/queries/is_last_deployment.query.graphql'; +import getEnvironmentClusterAgent from '../graphql/queries/environment_cluster_agent.query.graphql'; import ExternalUrl from './environment_external_url.vue'; import Actions from './environment_actions.vue'; import StopComponent from './environment_stop.vue'; @@ -51,7 +52,7 @@ export default { GlTooltip, }, mixins: [glFeatureFlagsMixin()], - inject: ['helpPagePath'], + inject: ['helpPagePath', 'projectPath'], props: { environment: { required: true, @@ -81,7 +82,7 @@ export default { tierTooltip: s__('Environment|Deployment tier'), }, data() { - return { visible: false }; + return { visible: false, clusterAgent: null }; }, computed: { icon() { @@ -163,23 +164,33 @@ export default { rolloutStatus() { return this.environment?.rolloutStatus; }, - agent() { - return this.environment?.agent || {}; - }, isKubernetesOverviewAvailable() { return this.glFeatures?.kasUserAccessProject; }, - hasRequiredAgentData() { - const { project, id, name } = this.agent || {}; - return project && id && name; - }, showKubernetesOverview() { - return this.isKubernetesOverviewAvailable && this.hasRequiredAgentData; + return Boolean(this.isKubernetesOverviewAvailable && this.clusterAgent); }, }, methods: { - toggleCollapse() { + toggleEnvironmentCollapse() { this.visible = !this.visible; + + if (this.visible) { + this.getClusterAgent(); + } + }, + getClusterAgent() { + if (!this.isKubernetesOverviewAvailable || this.clusterAgent) return; + + this.$apollo.addSmartQuery('environmentClusterAgent', { + variables() { + return { environmentName: this.environment.name, projectFullPath: this.projectPath }; + }, + query: getEnvironmentClusterAgent, + update(data) { + this.clusterAgent = data?.project?.environment?.clusterAgent; + }, + }); }, }, deploymentClasses: [ @@ -222,7 +233,7 @@ export default { :aria-label="label" size="small" category="secondary" - @click="toggleCollapse" + @click="toggleEnvironmentCollapse" /> <gl-link v-gl-tooltip @@ -359,10 +370,8 @@ export default { </div> <div v-if="showKubernetesOverview" :class="$options.kubernetesOverviewClasses"> <kubernetes-overview - :agent-project-path="agent.project" - :agent-name="agent.name" - :agent-id="agent.id" - :namespace="agent.kubernetesNamespace" + :cluster-agent="clusterAgent" + :namespace="environment.kubernetesNamespace" /> </div> <div v-if="rolloutStatus" :class="$options.deployBoardClasses"> diff --git a/app/assets/javascripts/environments/graphql/queries/environment_cluster_agent.query.graphql b/app/assets/javascripts/environments/graphql/queries/environment_cluster_agent.query.graphql new file mode 100644 index 0000000000000000000000000000000000000000..760f1fba897afb94688ff8afc2dfa3b73bbe9f84 --- /dev/null +++ b/app/assets/javascripts/environments/graphql/queries/environment_cluster_agent.query.graphql @@ -0,0 +1,19 @@ +query getEnvironmentClusterAgent($projectFullPath: ID!, $environmentName: String) { + project(fullPath: $projectFullPath) { + id + environment(name: $environmentName) { + id + clusterAgent { + id + name + webPath + tokens { + nodes { + id + lastUsedAt + } + } + } + } + } +} diff --git a/app/assets/javascripts/environments/graphql/queries/k8s_cluster_agent.query.graphql b/app/assets/javascripts/environments/graphql/queries/k8s_cluster_agent.query.graphql deleted file mode 100644 index bd45d2dba2fbcf8c56513b0150c90a1b85de96ad..0000000000000000000000000000000000000000 --- a/app/assets/javascripts/environments/graphql/queries/k8s_cluster_agent.query.graphql +++ /dev/null @@ -1,15 +0,0 @@ -query getK8sClusterAgentQuery($projectPath: ID!, $agentName: String!) { - project(fullPath: $projectPath) { - id - clusterAgent(name: $agentName) { - id - webPath - tokens { - nodes { - id - lastUsedAt - } - } - } - } -} diff --git a/spec/frontend/environments/environment_folder_spec.js b/spec/frontend/environments/environment_folder_spec.js index 4716f8076573657bbea12e77edd4424243b698ce..65c16697d4473f2b1dbbec8859005e4fcf1f158a 100644 --- a/spec/frontend/environments/environment_folder_spec.js +++ b/spec/frontend/environments/environment_folder_spec.js @@ -35,7 +35,7 @@ describe('~/environments/components/environments_folder.vue', () => { ...propsData, }, stubs: { transition: stubTransition() }, - provide: { helpPagePath: '/help', projectId: '1' }, + provide: { helpPagePath: '/help', projectId: '1', projectPath: 'path/to/project' }, }); beforeEach(() => { diff --git a/spec/frontend/environments/graphql/mock_data.js b/spec/frontend/environments/graphql/mock_data.js index addbf2c21dc98deea72073e88b9c2a72468af181..91268ade1e9350881bfb59d0a4e6bac73eb8f4d6 100644 --- a/spec/frontend/environments/graphql/mock_data.js +++ b/spec/frontend/environments/graphql/mock_data.js @@ -800,12 +800,14 @@ export const resolvedDeploymentDetails = { }; export const agent = { - project: 'agent-project', id: 'gid://gitlab/ClusterAgent/1', name: 'agent-name', - kubernetesNamespace: 'agent-namespace', + webPath: 'path/to/agent-page', + tokens: { nodes: [] }, }; +export const kubernetesNamespace = 'agent-namespace'; + const runningPod = { status: { phase: 'Running' } }; const pendingPod = { status: { phase: 'Pending' } }; const succeededPod = { status: { phase: 'Succeeded' } }; diff --git a/spec/frontend/environments/kubernetes_agent_info_spec.js b/spec/frontend/environments/kubernetes_agent_info_spec.js index b1795065281ab8691fff3b5da4965fa9d09992b9..9169b9284f428f995e72f29384d6c3a5500290cb 100644 --- a/spec/frontend/environments/kubernetes_agent_info_spec.js +++ b/spec/frontend/environments/kubernetes_agent_info_spec.js @@ -1,26 +1,14 @@ import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; -import VueApollo from 'vue-apollo'; -import { GlIcon, GlLink, GlSprintf, GlLoadingIcon, GlAlert } from '@gitlab/ui'; +import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import KubernetesAgentInfo from '~/environments/components/kubernetes_agent_info.vue'; import { AGENT_STATUSES, ACTIVE_CONNECTION_TIME } from '~/clusters_list/constants'; -import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import getK8sClusterAgentQuery from '~/environments/graphql/queries/k8s_cluster_agent.query.graphql'; -Vue.use(VueApollo); - -const propsData = { - agentName: 'my-agent', - agentId: '1', - agentProjectPath: 'path/to/agent-config-project', -}; - -const mockClusterAgent = { - id: '1', - name: 'token-1', +const defaultClusterAgent = { + name: 'my-agent', + id: 'gid://gitlab/ClusterAgent/1', webPath: 'path/to/agent-page', }; @@ -29,27 +17,16 @@ const connectedTimeInactive = new Date(connectedTimeNow.getTime() - ACTIVE_CONNE describe('~/environments/components/kubernetes_agent_info.vue', () => { let wrapper; - let agentQueryResponse; - const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findAgentLink = () => wrapper.findComponent(GlLink); const findAgentStatus = () => wrapper.findByTestId('agent-status'); const findAgentStatusIcon = () => findAgentStatus().findComponent(GlIcon); const findAgentLastUsedDate = () => wrapper.findByTestId('agent-last-used-date'); - const findAlert = () => wrapper.findComponent(GlAlert); - - const createWrapper = ({ tokens = [], queryResponse = null } = {}) => { - const clusterAgent = { ...mockClusterAgent, tokens: { nodes: tokens } }; - - agentQueryResponse = - queryResponse || - jest.fn().mockResolvedValue({ data: { project: { id: 'project-1', clusterAgent } } }); - const apolloProvider = createMockApollo([[getK8sClusterAgentQuery, agentQueryResponse]]); + const createWrapper = ({ tokens = [] } = {}) => { wrapper = extendedWrapper( shallowMount(KubernetesAgentInfo, { - apolloProvider, - propsData, + propsData: { clusterAgent: { ...defaultClusterAgent, tokens: { nodes: tokens } } }, stubs: { TimeAgoTooltip, GlSprintf }, }), ); @@ -60,28 +37,9 @@ describe('~/environments/components/kubernetes_agent_info.vue', () => { createWrapper(); }); - it('shows loading icon while fetching the agent details', async () => { - expect(findLoadingIcon().exists()).toBe(true); - await waitForPromises(); - expect(findLoadingIcon().exists()).toBe(false); - }); - - it('sends expected params', async () => { - await waitForPromises(); - - const variables = { - agentName: propsData.agentName, - projectPath: propsData.agentProjectPath, - }; - - expect(agentQueryResponse).toHaveBeenCalledWith(variables); - }); - - it('renders the agent name with the link', async () => { - await waitForPromises(); - - expect(findAgentLink().attributes('href')).toBe(mockClusterAgent.webPath); - expect(findAgentLink().text()).toContain(mockClusterAgent.id); + it('renders the agent name with the link', () => { + expect(findAgentLink().attributes('href')).toBe(defaultClusterAgent.webPath); + expect(findAgentLink().text()).toContain('1'); }); }); @@ -110,15 +68,4 @@ describe('~/environments/components/kubernetes_agent_info.vue', () => { expect(findAgentLastUsedDate().text()).toBe(lastUsedText); }); }); - - describe('when the agent query has errored', () => { - beforeEach(() => { - createWrapper({ clusterAgent: null, queryResponse: jest.fn().mockRejectedValue() }); - return waitForPromises(); - }); - - it('displays an alert message', () => { - expect(findAlert().text()).toBe(KubernetesAgentInfo.i18n.loadingError); - }); - }); }); diff --git a/spec/frontend/environments/kubernetes_overview_spec.js b/spec/frontend/environments/kubernetes_overview_spec.js index 394fd200edfdf80b10d3d5d118e389d9be8bb66b..d4ba7323aafc74438f2446e1a97845881a7ba24d 100644 --- a/spec/frontend/environments/kubernetes_overview_spec.js +++ b/spec/frontend/environments/kubernetes_overview_spec.js @@ -5,14 +5,12 @@ import KubernetesOverview from '~/environments/components/kubernetes_overview.vu import KubernetesAgentInfo from '~/environments/components/kubernetes_agent_info.vue'; import KubernetesPods from '~/environments/components/kubernetes_pods.vue'; import KubernetesTabs from '~/environments/components/kubernetes_tabs.vue'; -import { agent } from './graphql/mock_data'; +import { agent, kubernetesNamespace } from './graphql/mock_data'; import { mockKasTunnelUrl } from './mock_data'; const propsData = { - agentId: agent.id, - agentName: agent.name, - agentProjectPath: agent.project, - namespace: agent.kubernetesNamespace, + clusterAgent: agent, + namespace: kubernetesNamespace, }; const provide = { @@ -91,23 +89,19 @@ describe('~/environments/components/kubernetes_overview.vue', () => { }); it('renders kubernetes agent info', () => { - expect(findAgentInfo().props()).toEqual({ - agentName: agent.name, - agentId: agent.id, - agentProjectPath: agent.project, - }); + expect(findAgentInfo().props('clusterAgent')).toEqual(agent); }); it('renders kubernetes pods', () => { expect(findKubernetesPods().props()).toEqual({ - namespace: agent.kubernetesNamespace, + namespace: kubernetesNamespace, configuration, }); }); it('renders kubernetes tabs', () => { expect(findKubernetesTabs().props()).toEqual({ - namespace: agent.kubernetesNamespace, + namespace: kubernetesNamespace, configuration, }); }); diff --git a/spec/frontend/environments/new_environment_item_spec.js b/spec/frontend/environments/new_environment_item_spec.js index 51d66043d0a8dc988824e8630daff4e9bbcc2c8b..02100046167ac0bb85f0128eff18e4c965337f9f 100644 --- a/spec/frontend/environments/new_environment_item_spec.js +++ b/spec/frontend/environments/new_environment_item_spec.js @@ -3,6 +3,7 @@ import Vue from 'vue'; import { GlCollapse, GlIcon } from '@gitlab/ui'; import createMockApollo from 'helpers/mock_apollo_helper'; import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; import { stubTransition } from 'helpers/stub_transition'; import { formatDate, getTimeago } from '~/lib/utils/datetime_utility'; import { __, s__, sprintf } from '~/locale'; @@ -11,6 +12,7 @@ import EnvironmentActions from '~/environments/components/environment_actions.vu import Deployment from '~/environments/components/deployment.vue'; import DeployBoardWrapper from '~/environments/components/deploy_board_wrapper.vue'; import KubernetesOverview from '~/environments/components/kubernetes_overview.vue'; +import getEnvironmentClusterAgent from '~/environments/graphql/queries/environment_cluster_agent.query.graphql'; import { resolvedEnvironment, rolloutStatus, agent } from './graphql/mock_data'; import { mockKasTunnelUrl } from './mock_data'; @@ -18,9 +20,24 @@ Vue.use(VueApollo); describe('~/environments/components/new_environment_item.vue', () => { let wrapper; + let queryResponseHandler; - const createApolloProvider = () => { - return createMockApollo(); + const projectPath = '/1'; + + const createApolloProvider = (clusterAgent = null) => { + const response = { + data: { + project: { + id: '1', + environment: { + id: '1', + clusterAgent, + }, + }, + }, + }; + queryResponseHandler = jest.fn().mockResolvedValue(response); + return createMockApollo([[getEnvironmentClusterAgent, queryResponseHandler]]); }; const createWrapper = ({ propsData = {}, provideData = {}, apolloProvider } = {}) => @@ -30,7 +47,7 @@ describe('~/environments/components/new_environment_item.vue', () => { provide: { helpPagePath: '/help', projectId: '1', - projectPath: '/1', + projectPath, kasTunnelUrl: mockKasTunnelUrl, ...provideData, }, @@ -501,68 +518,69 @@ describe('~/environments/components/new_environment_item.vue', () => { }); describe('kubernetes overview', () => { - const environmentWithAgent = { - ...resolvedEnvironment, - agent, - }; - - it('should render if the feature flag is enabled and the environment has an agent object with the required data specified', () => { + it('should request agent data when the environment is visible if the feature flag is enabled', async () => { wrapper = createWrapper({ - propsData: { environment: environmentWithAgent }, + propsData: { environment: resolvedEnvironment }, provideData: { glFeatures: { kasUserAccessProject: true, }, }, - apolloProvider: createApolloProvider(), + apolloProvider: createApolloProvider(agent), }); - expandCollapsedSection(); + await expandCollapsedSection(); - expect(findKubernetesOverview().props()).toMatchObject({ - agentProjectPath: agent.project, - agentName: agent.name, - agentId: agent.id, - namespace: agent.kubernetesNamespace, + expect(queryResponseHandler).toHaveBeenCalledWith({ + environmentName: resolvedEnvironment.name, + projectFullPath: projectPath, }); }); - it('should not render if the feature flag is not enabled', () => { + it('should render if the feature flag is enabled and the environment has an agent associated', async () => { wrapper = createWrapper({ - propsData: { environment: environmentWithAgent }, - apolloProvider: createApolloProvider(), + propsData: { environment: resolvedEnvironment }, + provideData: { + glFeatures: { + kasUserAccessProject: true, + }, + }, + apolloProvider: createApolloProvider(agent), }); - expandCollapsedSection(); + await expandCollapsedSection(); + await waitForPromises(); - expect(findKubernetesOverview().exists()).toBe(false); + expect(findKubernetesOverview().props()).toMatchObject({ + clusterAgent: agent, + }); }); - it('should not render if the environment has no agent object', () => { + it('should not render if the feature flag is not enabled', async () => { wrapper = createWrapper({ - apolloProvider: createApolloProvider(), + propsData: { environment: resolvedEnvironment }, + apolloProvider: createApolloProvider(agent), }); - expandCollapsedSection(); + await expandCollapsedSection(); + expect(queryResponseHandler).not.toHaveBeenCalled(); expect(findKubernetesOverview().exists()).toBe(false); }); - it('should not render if the environment has an agent object without agent id specified', () => { - const environment = { - ...resolvedEnvironment, - agent: { - project: agent.project, - name: agent.name, - }, - }; - + it('should not render if the environment has no agent object', async () => { wrapper = createWrapper({ - propsData: { environment }, + propsData: { environment: resolvedEnvironment }, + provideData: { + glFeatures: { + kasUserAccessProject: true, + }, + }, apolloProvider: createApolloProvider(), }); - expandCollapsedSection(); + await expandCollapsedSection(); + await waitForPromises(); expect(findKubernetesOverview().exists()).toBe(false); });