diff --git a/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_pods.vue b/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_pods.vue index 4a75d73823f84beb128975a6e4da8830cd7f0c65..8afac231f11375ce1d68c6ed7a8b35211016f8e4 100644 --- a/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_pods.vue +++ b/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_pods.vue @@ -30,15 +30,19 @@ export default { namespace: this.namespace, }; }, - update(data) { return ( data?.k8sPods?.map((pod) => { return { - name: pod.metadata?.name, - namespace: pod.metadata?.namespace, + name: pod.metadata.name, + namespace: pod.metadata.namespace, status: pod.status.phase, - age: getAge(pod.metadata?.creationTimestamp), + age: getAge(pod.metadata.creationTimestamp), + labels: pod.metadata.labels, + annotations: pod.metadata.annotations, + kind: s__('KubernetesDashboard|Pod'), + spec: pod.spec, + fullStatus: pod.status, }; }) || [] ); @@ -106,6 +110,12 @@ export default { return filteredPods.length; }, + onItemSelect(item) { + this.$emit('show-resource-details', item); + }, + onRemoveSelection() { + this.$emit('remove-selection'); + }, }, i18n: { podsTitle: s__('Environment|Pods'), @@ -128,8 +138,9 @@ export default { v-if="k8sPods" :items="k8sPods" :page-size="$options.PAGE_SIZE" - :row-clickable="false" class="gl-mt-8" + @select-item="onItemSelect" + @remove-selection="onRemoveSelection" /> </template> </gl-tab> diff --git a/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_services.vue b/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_services.vue index 7cc14ae50ec024f5992a28debf07c552d5581424..09022b59ce53ae19f78c723f3f0bebac836f5865 100644 --- a/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_services.vue +++ b/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_services.vue @@ -50,13 +50,18 @@ export default { return this.k8sServices.map((service) => { return { - name: service?.metadata?.name, - namespace: service?.metadata?.namespace, - type: service?.spec?.type, - clusterIP: service?.spec?.clusterIP, - externalIP: service?.spec?.externalIP, - ports: generateServicePortsString(service?.spec?.ports), - age: getAge(service?.metadata?.creationTimestamp), + name: service.metadata.name, + namespace: service.metadata.namespace, + type: service.spec.type, + clusterIP: service.spec.clusterIP || '-', + externalIP: service.spec.externalIP || '-', + ports: generateServicePortsString(service.spec.ports), + age: getAge(service.metadata.creationTimestamp), + labels: service.metadata.labels, + annotations: service.metadata.annotations, + kind: s__('KubernetesDashboard|Service'), + spec: service.spec, + fullStatus: service.status, }; }); }, @@ -64,6 +69,14 @@ export default { return this.$apollo.queries.k8sServices.loading; }, }, + methods: { + onItemSelect(item) { + this.$emit('show-resource-details', item); + }, + onRemoveSelection() { + this.$emit('remove-selection'); + }, + }, i18n: { servicesTitle: s__('Environment|Services'), }, @@ -85,8 +98,9 @@ export default { :items="servicesItems" :fields="$options.SERVICES_TABLE_FIELDS" :page-size="$options.SERVICES_LIMIT_PER_PAGE" - :row-clickable="false" class="gl-mt-5" + @select-item="onItemSelect" + @remove-selection="onRemoveSelection" /> </gl-tab> </template> diff --git a/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_tabs.vue b/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_tabs.vue index 45037647eb947fab68d542bd57626fdfdc4df1c5..ae534c6edd3432409d04fe7624938a37470ca5d8 100644 --- a/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_tabs.vue +++ b/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_tabs.vue @@ -1,6 +1,9 @@ <script> -import { GlTabs } from '@gitlab/ui'; +import { GlTabs, GlDrawer } from '@gitlab/ui'; +import { DRAWER_Z_INDEX } from '~/lib/utils/constants'; +import { getContentWrapperHeight } from '~/lib/utils/dom_utils'; import { k8sResourceType } from '~/environments/graphql/resolvers/kubernetes/constants'; +import WorkloadDetails from '~/kubernetes_dashboard/components/workload_details.vue'; import KubernetesPods from './kubernetes_pods.vue'; import KubernetesServices from './kubernetes_services.vue'; @@ -11,6 +14,8 @@ export default { GlTabs, KubernetesPods, KubernetesServices, + GlDrawer, + WorkloadDetails, }, props: { @@ -30,29 +35,68 @@ export default { data() { return { activeTabIndex: tabs.indexOf(this.value), + selectedItem: {}, + showDetailsDrawer: false, }; }, + computed: { + drawerHeaderHeight() { + return getContentWrapperHeight(); + }, + }, watch: { activeTabIndex(newValue) { this.$emit('input', tabs[newValue]); }, }, + methods: { + showResourceDetails(item) { + this.selectedItem = item; + this.showDetailsDrawer = true; + }, + closeDetailsDrawer() { + this.showDetailsDrawer = false; + }, + }, + DRAWER_Z_INDEX, }; </script> <template> - <gl-tabs v-model="activeTabIndex"> - <kubernetes-pods - :namespace="namespace" - :configuration="configuration" - @loading="$emit('loading', $event)" - @update-failed-state="$emit('update-failed-state', $event)" - @cluster-error="$emit('cluster-error', $event)" - /> + <div> + <gl-tabs v-model="activeTabIndex"> + <kubernetes-pods + :namespace="namespace" + :configuration="configuration" + @loading="$emit('loading', $event)" + @update-failed-state="$emit('update-failed-state', $event)" + @cluster-error="$emit('cluster-error', $event)" + @show-resource-details="showResourceDetails" + @remove-selection="closeDetailsDrawer" + /> + + <kubernetes-services + :namespace="namespace" + :configuration="configuration" + @cluster-error="$emit('cluster-error', $event)" + @show-resource-details="showResourceDetails" + @remove-selection="closeDetailsDrawer" + /> + </gl-tabs> - <kubernetes-services - :namespace="namespace" - :configuration="configuration" - @cluster-error="$emit('cluster-error', $event)" - /> - </gl-tabs> + <gl-drawer + :open="showDetailsDrawer" + :header-height="drawerHeaderHeight" + :z-index="$options.DRAWER_Z_INDEX" + @close="closeDetailsDrawer" + > + <template #title> + <h4 class="gl-font-weight-bold gl-font-size-h2 gl-m-0 gl-word-break-word"> + {{ selectedItem.name }} + </h4> + </template> + <template #default> + <workload-details :item="selectedItem" /> + </template> + </gl-drawer> + </div> </template> diff --git a/app/assets/javascripts/environments/graphql/client.js b/app/assets/javascripts/environments/graphql/client.js index d0b2af3229add3a9e795b8cca20492135ba488e8..0f47cbff15988413147123a1c7108250f72b3a02 100644 --- a/app/assets/javascripts/environments/graphql/client.js +++ b/app/assets/javascripts/environments/graphql/client.js @@ -21,6 +21,15 @@ export const apolloProvider = (endpoint) => { }); const { cache } = defaultClient; + const k8sMetadata = { + name: null, + namespace: null, + creationTimestamp: null, + labels: null, + annotations: null, + }; + const k8sData = { nodes: { metadata: k8sMetadata, status: {}, spec: {} } }; + cache.writeQuery({ query: environmentApp, data: { @@ -91,16 +100,7 @@ export const apolloProvider = (endpoint) => { }); cache.writeQuery({ query: k8sPodsQuery, - data: { - metadata: { - name: null, - namespace: null, - creationTimestamp: null, - }, - status: { - phase: null, - }, - }, + data: k8sData, }); cache.writeQuery({ query: k8sConnectionStatusQuery, @@ -117,19 +117,7 @@ export const apolloProvider = (endpoint) => { }); cache.writeQuery({ query: k8sServicesQuery, - data: { - metadata: { - name: null, - namespace: null, - creationTimestamp: null, - }, - spec: { - type: null, - clusterIP: null, - externalIP: null, - ports: [], - }, - }, + data: k8sData, }); cache.writeQuery({ query: k8sNamespacesQuery, diff --git a/app/assets/javascripts/environments/graphql/queries/k8s_pods.query.graphql b/app/assets/javascripts/environments/graphql/queries/k8s_pods.query.graphql index 1a2a23925b177cfdc6c579f03726f5dc3237d61c..711fa424f52a65ab3bdd7f24e2de555496ca17e8 100644 --- a/app/assets/javascripts/environments/graphql/queries/k8s_pods.query.graphql +++ b/app/assets/javascripts/environments/graphql/queries/k8s_pods.query.graphql @@ -1,12 +1,7 @@ +#import "~/kubernetes_dashboard/graphql/queries/workload_item.fragment.graphql" + query getK8sPods($configuration: LocalConfiguration, $namespace: String) { k8sPods(configuration: $configuration, namespace: $namespace) @client { - metadata { - name - namespace - creationTimestamp - } - status { - phase - } + ...WorkloadItem } } diff --git a/app/assets/javascripts/environments/graphql/queries/k8s_services.query.graphql b/app/assets/javascripts/environments/graphql/queries/k8s_services.query.graphql index 8fc4a54b08b19d2af11c4e3436f6138d93466143..4c084899028355d14577cf7ee9c7abc521ec7e15 100644 --- a/app/assets/javascripts/environments/graphql/queries/k8s_services.query.graphql +++ b/app/assets/javascripts/environments/graphql/queries/k8s_services.query.graphql @@ -1,15 +1,7 @@ +#import "~/kubernetes_dashboard/graphql/queries/workload_item.fragment.graphql" + query getK8sServices($configuration: LocalConfiguration, $namespace: String) { k8sServices(configuration: $configuration, namespace: $namespace) @client { - metadata { - name - namespace - creationTimestamp - } - spec { - type - clusterIP - externalIP - ports - } + ...WorkloadItem } } diff --git a/app/assets/javascripts/environments/graphql/resolvers/kubernetes/index.js b/app/assets/javascripts/environments/graphql/resolvers/kubernetes/index.js index 9c8781c921c3160c74411c109b99e1cb3c85c4de..a31ca497d9c41e7c53d66e4aa80722c0e1614a11 100644 --- a/app/assets/javascripts/environments/graphql/resolvers/kubernetes/index.js +++ b/app/assets/javascripts/environments/graphql/resolvers/kubernetes/index.js @@ -1,93 +1,21 @@ -import { - CoreV1Api, - Configuration, - WatchApi, - EVENT_DATA, - EVENT_TIMEOUT, - EVENT_ERROR, -} from '@gitlab/cluster-client'; +import { CoreV1Api, Configuration } from '@gitlab/cluster-client'; import { getK8sPods, watchWorkloadItems, handleClusterError, buildWatchPath, + mapWorkloadItem, } from '~/kubernetes_dashboard/graphql/helpers/resolver_helpers'; import { humanizeClusterErrors } from '../../../helpers/k8s_integration_helper'; import k8sPodsQuery from '../../queries/k8s_pods.query.graphql'; import k8sServicesQuery from '../../queries/k8s_services.query.graphql'; -import { updateConnectionStatus } from './k8s_connection_status'; -import { connectionStatus, k8sResourceType } from './constants'; - -const mapServicesItems = (items) => { - return items.map((item) => { - const { type, clusterIP, externalIP, ports } = item.spec; - return { - metadata: item.metadata, - spec: { - type, - clusterIP: clusterIP || '-', - externalIP: externalIP || '-', - ports, - }, - }; - }); -}; +import { k8sResourceType } from './constants'; const watchServices = ({ configuration, namespace, client }) => { - const path = buildWatchPath({ resource: 'services', namespace }); - const config = new Configuration(configuration); - const watcherApi = new WatchApi(config); - - updateConnectionStatus(client, { - configuration, - namespace, - resourceType: k8sResourceType.k8sServices, - status: connectionStatus.connecting, - }); - - watcherApi - .subscribeToStream(path, { watch: true }) - .then((watcher) => { - let result = []; - - watcher.on(EVENT_DATA, (data) => { - result = mapServicesItems(data); - - client.writeQuery({ - query: k8sServicesQuery, - variables: { configuration, namespace }, - data: { k8sServices: result }, - }); - - updateConnectionStatus(client, { - configuration, - namespace, - resourceType: k8sResourceType.k8sServices, - status: connectionStatus.connected, - }); - }); - - watcher.on(EVENT_TIMEOUT, () => { - updateConnectionStatus(client, { - configuration, - namespace, - resourceType: k8sResourceType.k8sServices, - status: connectionStatus.disconnected, - }); - }); - - watcher.on(EVENT_ERROR, () => { - updateConnectionStatus(client, { - configuration, - namespace, - resourceType: k8sResourceType.k8sServices, - status: connectionStatus.disconnected, - }); - }); - }) - .catch((err) => { - handleClusterError(err); - }); + const query = k8sServicesQuery; + const watchPath = buildWatchPath({ resource: 'services', namespace }); + const queryField = k8sResourceType.k8sServices; + watchWorkloadItems({ client, query, configuration, namespace, watchPath, queryField }); }; const watchPods = ({ configuration, namespace, client }) => { @@ -119,9 +47,7 @@ export const kubernetesQueries = { k8sPods(_, { configuration, namespace }, { client }) { const query = k8sPodsQuery; const enableWatch = gon.features?.k8sWatchApi; - // TODO: Remove mapping function once the drawer with the pods details is added under the Kubernetes overview section - const mapPodItem = (item) => item; - return getK8sPods({ client, query, configuration, namespace, enableWatch, mapFn: mapPodItem }); + return getK8sPods({ client, query, configuration, namespace, enableWatch }); }, k8sServices(_, { configuration, namespace }, { client }) { const coreV1Api = new CoreV1Api(new Configuration(configuration)); @@ -137,7 +63,7 @@ export const kubernetesQueries = { watchServices({ configuration, namespace, client }); } - return mapServicesItems(items); + return items.map(mapWorkloadItem); }) .catch(async (err) => { try { diff --git a/app/assets/javascripts/environments/graphql/typedefs.graphql b/app/assets/javascripts/environments/graphql/typedefs.graphql index d8b05102b53409674e084f96c1fc4c4e25e90d14..034b269e5cbc5e8919e770bcda8ff9da46b7c5f5 100644 --- a/app/assets/javascripts/environments/graphql/typedefs.graphql +++ b/app/assets/javascripts/environments/graphql/typedefs.graphql @@ -1,4 +1,5 @@ #import "~/graphql_shared/client/page_info.typedefs.graphql" +#import "~/kubernetes_dashboard/graphql/typedefs.graphql" type LocalEnvironment { id: Int! @@ -57,43 +58,11 @@ type LocalErrors { errors: [String!]! } -type k8sPodStatus { - phase: String -} - -type k8sPodMetadata { - name: String - namespace: String - creationTimestamp: String -} - -type LocalK8sPods { - status: k8sPodStatus - metadata: k8sPodMetadata -} - input LocalConfiguration { basePath: String baseOptions: JSON } -type k8sServiceMetadata { - name: String - namespace: String - creationTimestamp: String -} - -type k8sServiceSpec { - type: String - clusterIP: String - externalIP: String - ports: JSON -} - -type LocalK8sServices { - metadata: k8sServiceMetadata - spec: k8sServiceSpec -} type k8sNamespaceMetadata { name: String } @@ -123,8 +92,8 @@ extend type Query { environmentToStop: LocalEnvironment isEnvironmentStopping(environment: LocalEnvironmentInput): Boolean isLastDeployment(environment: LocalEnvironmentInput): Boolean - k8sPods(configuration: LocalConfiguration, namespace: String): [LocalK8sPods] - k8sServices(configuration: LocalConfiguration): [LocalK8sServices] + k8sPods(configuration: LocalConfiguration, namespace: String): [LocalWorkloadItem] + k8sServices(configuration: LocalConfiguration, namespace: String): [LocalWorkloadItem] k8sConnection(configuration: LocalConfiguration): K8sResources fluxKustomizationStatus( configuration: LocalConfiguration diff --git a/app/assets/javascripts/kubernetes_dashboard/components/workload_layout.vue b/app/assets/javascripts/kubernetes_dashboard/components/workload_layout.vue index bd081f32a01546d050be39e540babdc307c6da30..99ba982b2a5eeee0e884fb3ff60bae38f182a7d6 100644 --- a/app/assets/javascripts/kubernetes_dashboard/components/workload_layout.vue +++ b/app/assets/javascripts/kubernetes_dashboard/components/workload_layout.vue @@ -70,7 +70,13 @@ export default { </gl-alert> <div v-else> <workload-stats :stats="stats" /> - <workload-table :items="items" :fields="fields" class="gl-mt-8" @select-item="onItemSelect" /> + <workload-table + :items="items" + :fields="fields" + class="gl-mt-8" + @select-item="onItemSelect" + @remove-selection="closeDetailsDrawer" + /> <gl-drawer :open="showDetailsDrawer" diff --git a/app/assets/javascripts/kubernetes_dashboard/components/workload_table.vue b/app/assets/javascripts/kubernetes_dashboard/components/workload_table.vue index 297ceff31d1d65d8d1aa37a58f9771230ab7061f..6533feafa53a8544544260af21e4f9e34bf8b24d 100644 --- a/app/assets/javascripts/kubernetes_dashboard/components/workload_table.vue +++ b/app/assets/javascripts/kubernetes_dashboard/components/workload_table.vue @@ -28,11 +28,6 @@ export default { default: PAGE_SIZE, required: false, }, - rowClickable: { - type: Boolean, - default: true, - required: false, - }, }, data() { return { @@ -48,9 +43,6 @@ export default { }; }); }, - tableRowClass() { - return this.rowClickable ? 'gl-hover-cursor-pointer' : ''; - }, }, methods: { selectItem(item) { @@ -58,6 +50,8 @@ export default { if (selectedItem) { this.$emit('select-item', selectedItem); + } else { + this.$emit('remove-selection'); } }, }, @@ -76,10 +70,8 @@ export default { :per-page="pageSize" :current-page="currentPage" :empty-text="$options.i18n.emptyText" - :tbody-tr-class="tableRowClass" - :hover="rowClickable" - :selectable="rowClickable" - :no-select-on-click="!rowClickable" + hover + selectable select-mode="single" selected-variant="primary" show-empty diff --git a/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_pods_spec.js b/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_pods_spec.js index dbbf8e7f5d4c98fc4e9100a02c163409b0924baa..b7a77701d10d71fce13d7283b354a53ad070a4e9 100644 --- a/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_pods_spec.js +++ b/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_pods_spec.js @@ -8,8 +8,12 @@ import KubernetesPods from '~/environments/environment_details/components/kubern import WorkloadStats from '~/kubernetes_dashboard/components/workload_stats.vue'; import WorkloadTable from '~/kubernetes_dashboard/components/workload_table.vue'; import { useFakeDate } from 'helpers/fake_date'; -import { mockKasTunnelUrl } from '../../../mock_data'; -import { k8sPodsMock, k8sPodsStatsData, k8sPodsTableData } from '../../../graphql/mock_data'; +import { mockKasTunnelUrl } from 'jest/environments/mock_data'; +import { + k8sPodsMock, + mockPodStats, + mockPodsTableItems, +} from 'jest/kubernetes_dashboard/graphql/mock_data'; Vue.use(VueApollo); @@ -85,14 +89,14 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_po createWrapper(); await waitForPromises(); - expect(findWorkloadStats().props('stats')).toEqual(k8sPodsStatsData); + expect(findWorkloadStats().props('stats')).toEqual(mockPodStats); }); it('renders workload table with the correct data', async () => { createWrapper(); await waitForPromises(); - expect(findWorkloadTable().props('items')).toMatchObject(k8sPodsTableData); + expect(findWorkloadTable().props('items')).toMatchObject(mockPodsTableItems); }); it('emits a update-failed-state event for each pod', async () => { @@ -107,6 +111,26 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_po [{ pods: true }], ]); }); + + it('emits show-resource-details event on item select', async () => { + createWrapper(); + await waitForPromises(); + + expect(wrapper.emitted('show-resource-details')).toBeUndefined(); + + findWorkloadTable().vm.$emit('select-item', mockPodsTableItems[0]); + expect(wrapper.emitted('show-resource-details')).toEqual([[mockPodsTableItems[0]]]); + }); + + it('emits remove-selection event when receives it from the WorkloadTable component', async () => { + createWrapper(); + await waitForPromises(); + + expect(wrapper.emitted('remove-selection')).toBeUndefined(); + + findWorkloadTable().vm.$emit('remove-selection'); + expect(wrapper.emitted('remove-selection')).toHaveLength(1); + }); }); describe('when gets an error from the cluster_client API', () => { diff --git a/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_services_spec.js b/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_services_spec.js index 9ab97dc4bbec39cf7051c00e4d5318f1a3381357..29eefd92cd8ad068236df770c128353d4a3a089e 100644 --- a/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_services_spec.js +++ b/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_services_spec.js @@ -9,8 +9,11 @@ import KubernetesServices from '~/environments/environment_details/components/ku import WorkloadTable from '~/kubernetes_dashboard/components/workload_table.vue'; import { SERVICES_LIMIT_PER_PAGE } from '~/environments/constants'; import { SERVICES_TABLE_FIELDS } from '~/kubernetes_dashboard/constants'; -import { mockKasTunnelUrl } from '../../../mock_data'; -import { k8sServicesMock } from '../../../graphql/mock_data'; +import { mockKasTunnelUrl } from 'jest/environments/mock_data'; +import { + k8sServicesMock, + mockServicesTableItems, +} from 'jest/kubernetes_dashboard/graphql/mock_data'; Vue.use(VueApollo); @@ -64,7 +67,7 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_se }); describe('when gets services data', () => { - useFakeDate(2020, 6, 6); + useFakeDate(2023, 10, 23, 10, 10); it('hides the loading icon when the list of services loaded', async () => { createWrapper(); @@ -79,26 +82,27 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_se expect(findWorkloadTable().props('pageSize')).toBe(SERVICES_LIMIT_PER_PAGE); expect(findWorkloadTable().props('fields')).toBe(SERVICES_TABLE_FIELDS); - expect(findWorkloadTable().props('items')).toMatchObject([ - { - name: 'my-first-service', - namespace: 'default', - type: 'ClusterIP', - clusterIP: '10.96.0.1', - externalIP: '-', - ports: '443/TCP', - age: '0s', - }, - { - name: 'my-second-service', - namespace: 'default', - type: 'NodePort', - clusterIP: '10.105.219.238', - externalIP: '-', - ports: '80:31989/TCP, 443:32679/TCP', - age: '2d', - }, - ]); + expect(findWorkloadTable().props('items')).toMatchObject(mockServicesTableItems); + }); + + it('emits show-resource-details event on item select', async () => { + createWrapper(); + await waitForPromises(); + + expect(wrapper.emitted('show-resource-details')).toBeUndefined(); + + findWorkloadTable().vm.$emit('select-item', mockServicesTableItems[0]); + expect(wrapper.emitted('show-resource-details')).toEqual([[mockServicesTableItems[0]]]); + }); + + it('emits remove-selection event when receives it from the WorkloadTable component', async () => { + createWrapper(); + await waitForPromises(); + + expect(wrapper.emitted('remove-selection')).toBeUndefined(); + + findWorkloadTable().vm.$emit('remove-selection'); + expect(wrapper.emitted('remove-selection')).toHaveLength(1); }); it('emits an error message when gets an error from the cluster_client API', async () => { diff --git a/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_tabs_spec.js b/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_tabs_spec.js index 9ff6e66200b4ee492ea8664fbc0d708f2dc746f0..db74e48321f1868dd36ebcea80ede83cb4dea3d8 100644 --- a/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_tabs_spec.js +++ b/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_tabs_spec.js @@ -1,11 +1,13 @@ import { nextTick } from 'vue'; import { shallowMount } from '@vue/test-utils'; -import { GlTabs } from '@gitlab/ui'; +import { GlTabs, GlDrawer } from '@gitlab/ui'; import KubernetesTabs from '~/environments/environment_details/components/kubernetes/kubernetes_tabs.vue'; import KubernetesPods from '~/environments/environment_details/components/kubernetes/kubernetes_pods.vue'; import KubernetesServices from '~/environments/environment_details/components/kubernetes/kubernetes_services.vue'; +import WorkloadDetails from '~/kubernetes_dashboard/components/workload_details.vue'; import { k8sResourceType } from '~/environments/graphql/resolvers/kubernetes/constants'; -import { mockKasTunnelUrl } from '../../../mock_data'; +import { mockKasTunnelUrl } from 'jest/environments/mock_data'; +import { mockPodsTableItems } from 'jest/kubernetes_dashboard/graphql/mock_data'; describe('~/environments/environment_details/components/kubernetes/kubernetes_tabs.vue', () => { let wrapper; @@ -20,10 +22,13 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_ta const findTabs = () => wrapper.findComponent(GlTabs); const findKubernetesPods = () => wrapper.findComponent(KubernetesPods); const findKubernetesServices = () => wrapper.findComponent(KubernetesServices); + const findDrawer = () => wrapper.findComponent(GlDrawer); + const findWorkloadDetails = () => wrapper.findComponent(WorkloadDetails); const createWrapper = (activeTab = k8sResourceType.k8sPods) => { wrapper = shallowMount(KubernetesTabs, { propsData: { configuration, namespace, value: activeTab }, + stubs: { GlDrawer }, }); }; @@ -100,4 +105,46 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_ta expect(wrapper.emitted('cluster-error')).toEqual([[errorMessage]]); }); }); + + describe('resource details drawer', () => { + beforeEach(() => { + createWrapper(); + }); + + it('is closed by default', () => { + expect(findDrawer().props('open')).toBe(false); + }); + + describe('when receives show-resource-details event', () => { + beforeEach(() => { + findKubernetesPods().vm.$emit('show-resource-details', mockPodsTableItems[0]); + }); + + it('opens the drawer', () => { + expect(findDrawer().props('open')).toBe(true); + }); + + it('provides the resource details to the drawer', () => { + expect(findWorkloadDetails().props('item')).toEqual(mockPodsTableItems[0]); + }); + + it('renders a title with the selected item name', () => { + expect(findDrawer().text()).toContain(mockPodsTableItems[0].name); + }); + + it('is closed when clicked on a cross button', async () => { + expect(findDrawer().props('open')).toBe(true); + + await findDrawer().vm.$emit('close'); + expect(findDrawer().props('open')).toBe(false); + }); + + it('is closed on remove-selection event', async () => { + expect(findDrawer().props('open')).toBe(true); + + await findKubernetesPods().vm.$emit('remove-selection'); + expect(findDrawer().props('open')).toBe(false); + }); + }); + }); }); diff --git a/spec/frontend/environments/graphql/mock_data.js b/spec/frontend/environments/graphql/mock_data.js index a6aaea5899f6c2fb91e20738148743ba98af2bcf..4754d1c15a82e9f03a8ed99c8d5b188fcb108beb 100644 --- a/spec/frontend/environments/graphql/mock_data.js +++ b/spec/frontend/environments/graphql/mock_data.js @@ -810,140 +810,6 @@ export const agent = { export const kubernetesNamespace = 'agent-namespace'; -const runningPod = { - metadata: { name: 'pod-1', namespace: 'default', creationTimestamp: '2023-07-31T11:50:17Z' }, - status: { phase: 'Running' }, -}; -const pendingPod = { - metadata: { - name: 'pod-2', - namespace: 'new-namespace', - creationTimestamp: '2023-11-21T11:50:59Z', - }, - status: { phase: 'Pending' }, -}; -const succeededPod = { - metadata: { name: 'pod-3', namespace: 'default', creationTimestamp: '2023-07-31T11:50:17Z' }, - status: { phase: 'Succeeded' }, -}; -const failedPod = { - metadata: { name: 'pod-4', namespace: 'default', creationTimestamp: '2023-11-21T11:50:59Z' }, - status: { phase: 'Failed' }, -}; - -export const k8sPodsMock = [runningPod, runningPod, pendingPod, succeededPod, failedPod, failedPod]; - -export const k8sPodsStatsData = [ - { - value: 2, - title: 'Running', - }, - { - value: 1, - title: 'Pending', - }, - { - value: 1, - title: 'Succeeded', - }, - { - value: 2, - title: 'Failed', - }, -]; - -export const k8sPodsTableData = [ - { - name: 'pod-1', - namespace: 'default', - status: 'Running', - age: '114d', - }, - { - name: 'pod-1', - namespace: 'default', - status: 'Running', - age: '114d', - }, - { - name: 'pod-2', - namespace: 'new-namespace', - status: 'Pending', - age: '1d', - }, - { - name: 'pod-3', - namespace: 'default', - status: 'Succeeded', - age: '114d', - }, - { - name: 'pod-4', - namespace: 'default', - status: 'Failed', - age: '1d', - }, - { - name: 'pod-4', - namespace: 'default', - status: 'Failed', - age: '1d', - }, -]; - -export const k8sServicesMock = [ - { - metadata: { - name: 'my-first-service', - namespace: 'default', - creationTimestamp: new Date(), - }, - spec: { - ports: [ - { - name: 'https', - protocol: 'TCP', - port: 443, - targetPort: 8443, - }, - ], - clusterIP: '10.96.0.1', - externalIP: '-', - type: 'ClusterIP', - }, - }, - { - metadata: { - name: 'my-second-service', - namespace: 'default', - creationTimestamp: '2020-07-03T14:06:04Z', - }, - spec: { - ports: [ - { - name: 'http', - protocol: 'TCP', - appProtocol: 'http', - port: 80, - targetPort: 'http', - nodePort: 31989, - }, - { - name: 'https', - protocol: 'TCP', - appProtocol: 'https', - port: 443, - targetPort: 'https', - nodePort: 32679, - }, - ], - clusterIP: '10.105.219.238', - externalIP: '-', - type: 'NodePort', - }, - }, -]; - export const k8sNamespacesMock = [ { metadata: { name: 'default' } }, { metadata: { name: 'agent' } }, diff --git a/spec/frontend/environments/graphql/resolvers/kubernetes_spec.js b/spec/frontend/environments/graphql/resolvers/kubernetes_spec.js index 933825255d284a81ba8fb0e60a7cd1290edda1a3..18dcc0a1e66808a64a0219a44478c9860f24a72b 100644 --- a/spec/frontend/environments/graphql/resolvers/kubernetes_spec.js +++ b/spec/frontend/environments/graphql/resolvers/kubernetes_spec.js @@ -16,7 +16,8 @@ import { connectionStatus, k8sResourceType, } from '~/environments/graphql/resolvers/kubernetes/constants'; -import { k8sPodsMock, k8sServicesMock, k8sNamespacesMock } from '../mock_data'; +import { k8sPodsMock, k8sServicesMock } from 'jest/kubernetes_dashboard/graphql/mock_data'; +import { k8sNamespacesMock } from '../mock_data'; jest.mock('~/environments/graphql/resolvers/kubernetes/k8s_connection_status'); @@ -342,7 +343,7 @@ describe('~/frontend/environments/graphql/resolvers', () => { }); }); - it('should not watch pods from the cluster_client library when the services data is not present', async () => { + it('should not watch services from the cluster_client library when the services data is not present', async () => { jest.spyOn(CoreV1Api.prototype, 'listCoreV1NamespacedService').mockImplementation( jest.fn().mockImplementation(() => { return Promise.resolve({ diff --git a/spec/frontend/kubernetes_dashboard/components/workload_layout_spec.js b/spec/frontend/kubernetes_dashboard/components/workload_layout_spec.js index 1dc5bd4f1650191e4ef33764101f78e0728a0fb4..0f32bc3a2914931ea5174c7cac0fde9ad0f4c791 100644 --- a/spec/frontend/kubernetes_dashboard/components/workload_layout_spec.js +++ b/spec/frontend/kubernetes_dashboard/components/workload_layout_spec.js @@ -127,6 +127,14 @@ describe('Workload layout component', () => { expect(findDrawer().props('open')).toBe(false); }); + it('is closed on remove-selection event', async () => { + await findWorkloadTable().vm.$emit('select-item', mockPodsTableItems[0]); + expect(findDrawer().props('open')).toBe(true); + + await findWorkloadTable().vm.$emit('remove-selection'); + expect(findDrawer().props('open')).toBe(false); + }); + it('renders a title with the selected item name', async () => { await findWorkloadTable().vm.$emit('select-item', mockPodsTableItems[0]); expect(findDrawer().text()).toContain(mockPodsTableItems[0].name); diff --git a/spec/frontend/kubernetes_dashboard/components/workload_table_spec.js b/spec/frontend/kubernetes_dashboard/components/workload_table_spec.js index 1ddafeb0525de89231f2189dbfb2a54f373cf6e7..79fdfe666baa2144b4828807c76ad9c8027d68d6 100644 --- a/spec/frontend/kubernetes_dashboard/components/workload_table_spec.js +++ b/spec/frontend/kubernetes_dashboard/components/workload_table_spec.js @@ -159,10 +159,12 @@ describe('Workload table component', () => { }); }); - describe('on row click', () => { - it('emits an event on row click', () => { + describe('row selection', () => { + beforeEach(() => { createWrapper({ items: mockPodsTableItems }); + }); + it('emits row-selected event on row click', () => { mockPodsTableItems.forEach((data, index) => { findTable().vm.$emit('row-selected', [data]); @@ -170,68 +172,12 @@ describe('Workload table component', () => { }); }); - describe('be default', () => { - it('row has hover styles by default', () => { - createWrapper({ items: mockPodsTableItems }); - - expect(findRow(0).attributes('class')).toContain('gl-hover-cursor-pointer'); - }); - - it('table has hover state enabled by default', () => { - createWrapper({ items: mockPodsTableItems }, true); - - expect(findTable().props('hover')).toBe(true); - }); - - it('table is selectable by default', () => { - createWrapper({ items: mockPodsTableItems }, true); - - expect(findTable().props('selectable')).toBe(true); - }); - - it('table row is selectable on click', () => { - createWrapper({ items: mockPodsTableItems }, true); - - expect(findTable().props('noSelectOnClick')).toBe(false); - }); - }); - - describe('when rowClickable is false', () => { - it('row has no hover styles', () => { - createWrapper({ items: mockPodsTableItems, rowClickable: false }); - - expect(findRow(0).attributes('class')).not.toContain('gl-hover-cursor-pointer'); - }); - - it('table has no hover state enabled', () => { - createWrapper({ items: mockPodsTableItems, rowClickable: false }, true); - - expect(findTable().props('hover')).toBe(false); - }); - - it('table is not selectable', () => { - createWrapper({ items: mockPodsTableItems, rowClickable: false }, true); - - expect(findTable().props('selectable')).toBe(false); - }); - - it('table row is not selectable on click', () => { - createWrapper({ items: mockPodsTableItems, rowClickable: false }, true); - - expect(findTable().props('noSelectOnClick')).toBe(true); - }); - }); - - it('row has hover styles by default', () => { - createWrapper({ items: mockPodsTableItems }); - - expect(findRow(0).attributes('class')).toContain('gl-hover-cursor-pointer'); - }); - - it('row has no hover styles if rowClickable is false', () => { - createWrapper({ items: mockPodsTableItems, rowClickable: false }); + it('emits remove-selection event on the second click on the same item', () => { + findTable().vm.$emit('row-selected', [mockPodsTableItems[0]]); + expect(wrapper.emitted('select-item')).toEqual([[mockPodsTableItems[0]]]); - expect(findRow(0).attributes('class')).not.toContain('gl-hover-cursor-pointer'); + findTable().vm.$emit('row-selected', mockPodsTableItems[0]); + expect(wrapper.emitted('remove-selection')).toHaveLength(1); }); }); });