diff options
Diffstat (limited to 'app/assets/javascripts/kubernetes_dashboard')
16 files changed, 658 insertions, 55 deletions
diff --git a/app/assets/javascripts/kubernetes_dashboard/components/workload_details.vue b/app/assets/javascripts/kubernetes_dashboard/components/workload_details.vue index 0d219f915c9..bcc0ddf824a 100644 --- a/app/assets/javascripts/kubernetes_dashboard/components/workload_details.vue +++ b/app/assets/javascripts/kubernetes_dashboard/components/workload_details.vue @@ -14,8 +14,7 @@ export default { item: { type: Object, required: true, - validator: (item) => - ['name', 'kind', 'labels', 'annotations', 'status'].every((key) => item[key]), + validator: (item) => ['name', 'kind', 'labels', 'annotations'].every((key) => item[key]), }, }, computed: { @@ -51,7 +50,7 @@ export default { <template> <ul class="gl-list-style-none"> <workload-details-item :label="$options.i18n.name"> - {{ item.name }} + <span class="gl-word-break-word"> {{ item.name }}</span> </workload-details-item> <workload-details-item :label="$options.i18n.kind"> {{ item.kind }} @@ -63,7 +62,7 @@ export default { </gl-badge> </div> </workload-details-item> - <workload-details-item :label="$options.i18n.status"> + <workload-details-item v-if="item.status" :label="$options.i18n.status"> <gl-badge :variant="$options.WORKLOAD_STATUS_BADGE_VARIANTS[item.status]">{{ item.status }}</gl-badge></workload-details-item diff --git a/app/assets/javascripts/kubernetes_dashboard/components/workload_layout.vue b/app/assets/javascripts/kubernetes_dashboard/components/workload_layout.vue index 8c6a08ad504..6579e0229e6 100644 --- a/app/assets/javascripts/kubernetes_dashboard/components/workload_layout.vue +++ b/app/assets/javascripts/kubernetes_dashboard/components/workload_layout.vue @@ -1,6 +1,7 @@ <script> import { GlLoadingIcon, GlAlert, GlDrawer } from '@gitlab/ui'; import { DRAWER_Z_INDEX } from '~/lib/utils/constants'; +import { getContentWrapperHeight } from '~/lib/utils/dom_utils'; import WorkloadStats from './workload_stats.vue'; import WorkloadTable from './workload_table.vue'; import WorkloadDetails from './workload_details.vue'; @@ -33,6 +34,11 @@ export default { type: Array, required: true, }, + fields: { + type: Array, + required: false, + default: undefined, + }, }, data() { return { @@ -40,6 +46,11 @@ export default { selectedItem: {}, }; }, + computed: { + getDrawerHeaderHeight() { + return getContentWrapperHeight(); + }, + }, methods: { closeDetailsDrawer() { this.showDetailsDrawer = false; @@ -59,16 +70,18 @@ export default { </gl-alert> <div v-else> <workload-stats :stats="stats" /> - <workload-table :items="items" @select-item="onItemSelect" /> + <workload-table :items="items" :fields="fields" @select-item="onItemSelect" /> <gl-drawer :open="showDetailsDrawer" - header-height="calc(var(--top-bar-height) + var(--performance-bar-height))" + :header-height="getDrawerHeaderHeight" :z-index="$options.DRAWER_Z_INDEX" @close="closeDetailsDrawer" > <template #title> - <h4 class="gl-font-weight-bold gl-font-size-h2 gl-m-0">{{ selectedItem.name }}</h4> + <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" /> diff --git a/app/assets/javascripts/kubernetes_dashboard/components/workload_table.vue b/app/assets/javascripts/kubernetes_dashboard/components/workload_table.vue index d3704863538..83940fb91c8 100644 --- a/app/assets/javascripts/kubernetes_dashboard/components/workload_table.vue +++ b/app/assets/javascripts/kubernetes_dashboard/components/workload_table.vue @@ -1,9 +1,9 @@ <script> import { GlTable, GlBadge, GlPagination } from '@gitlab/ui'; +import { __ } from '~/locale'; import { WORKLOAD_STATUS_BADGE_VARIANTS, PAGE_SIZE, - TABLE_HEADING_CLASSES, DEFAULT_WORKLOAD_TABLE_FIELDS, } from '../constants'; @@ -34,7 +34,6 @@ export default { return this.fields.map((field) => { return { ...field, - thClass: TABLE_HEADING_CLASSES, sortable: true, }; }); @@ -45,6 +44,9 @@ export default { this.$emit('select-item', item); }, }, + i18n: { + emptyText: __('No results found'), + }, PAGE_SIZE, WORKLOAD_STATUS_BADGE_VARIANTS, TABLE_CELL_CLASSES: 'gl-p-2', @@ -58,9 +60,10 @@ export default { :fields="tableFields" :per-page="$options.PAGE_SIZE" :current-page="currentPage" + :empty-text="$options.i18n.emptyText" tbody-tr-class="gl-hover-cursor-pointer" + show-empty stacked="md" - bordered hover @row-clicked="selectItem" > diff --git a/app/assets/javascripts/kubernetes_dashboard/constants.js b/app/assets/javascripts/kubernetes_dashboard/constants.js index b93740aec90..458a79cbcb6 100644 --- a/app/assets/javascripts/kubernetes_dashboard/constants.js +++ b/app/assets/javascripts/kubernetes_dashboard/constants.js @@ -1,10 +1,12 @@ -import { s__ } from '~/locale'; +import { __, s__ } from '~/locale'; export const STATUS_RUNNING = 'Running'; export const STATUS_PENDING = 'Pending'; export const STATUS_SUCCEEDED = 'Succeeded'; export const STATUS_FAILED = 'Failed'; export const STATUS_READY = 'Ready'; +export const STATUS_COMPLETED = 'Completed'; +export const STATUS_SUSPENDED = 'Suspended'; export const STATUS_LABELS = { [STATUS_RUNNING]: s__('KubernetesDashboard|Running'), @@ -12,6 +14,8 @@ export const STATUS_LABELS = { [STATUS_SUCCEEDED]: s__('KubernetesDashboard|Succeeded'), [STATUS_FAILED]: s__('KubernetesDashboard|Failed'), [STATUS_READY]: s__('KubernetesDashboard|Ready'), + [STATUS_COMPLETED]: s__('KubernetesDashboard|Completed'), + [STATUS_SUSPENDED]: s__('KubernetesDashboard|Suspended'), }; export const WORKLOAD_STATUS_BADGE_VARIANTS = { @@ -20,24 +24,27 @@ export const WORKLOAD_STATUS_BADGE_VARIANTS = { [STATUS_SUCCEEDED]: 'success', [STATUS_FAILED]: 'danger', [STATUS_READY]: 'success', + [STATUS_COMPLETED]: 'success', + [STATUS_SUSPENDED]: 'neutral', }; export const PAGE_SIZE = 20; -export const TABLE_HEADING_CLASSES = 'gl-bg-gray-50! gl-font-weight-bold gl-white-space-nowrap'; - export const DEFAULT_WORKLOAD_TABLE_FIELDS = [ { key: 'name', label: s__('KubernetesDashboard|Name'), + tdClass: 'gl-md-w-half gl-lg-w-40p gl-word-break-word', }, { key: 'status', label: s__('KubernetesDashboard|Status'), + tdClass: 'gl-md-w-15', }, { key: 'namespace', label: s__('KubernetesDashboard|Namespace'), + tdClass: 'gl-md-w-30p gl-lg-w-40p gl-word-break-word', }, { key: 'age', @@ -47,3 +54,34 @@ export const DEFAULT_WORKLOAD_TABLE_FIELDS = [ export const STATUS_TRUE = 'True'; export const STATUS_FALSE = 'False'; + +export const SERVICES_TABLE_FIELDS = [ + { + key: 'name', + label: __('Name'), + }, + { + key: 'namespace', + label: __('Namespace'), + }, + { + key: 'type', + label: __('Type'), + }, + { + key: 'clusterIP', + label: s__('Environment|Cluster IP'), + }, + { + key: 'externalIP', + label: s__('Environment|External IP'), + }, + { + key: 'ports', + label: s__('Environment|Ports'), + }, + { + key: 'age', + label: s__('Environment|Age'), + }, +]; diff --git a/app/assets/javascripts/kubernetes_dashboard/graphql/client.js b/app/assets/javascripts/kubernetes_dashboard/graphql/client.js index 5894472d83b..9454465df9d 100644 --- a/app/assets/javascripts/kubernetes_dashboard/graphql/client.js +++ b/app/assets/javascripts/kubernetes_dashboard/graphql/client.js @@ -6,6 +6,9 @@ import k8sDeploymentsQuery from './queries/k8s_dashboard_deployments.query.graph import k8sStatefulSetsQuery from './queries/k8s_dashboard_stateful_sets.query.graphql'; import k8sReplicaSetsQuery from './queries/k8s_dashboard_replica_sets.query.graphql'; import k8sDaemonSetsQuery from './queries/k8s_dashboard_daemon_sets.query.graphql'; +import k8sJobsQuery from './queries/k8s_dashboard_jobs.query.graphql'; +import k8sCronJobsQuery from './queries/k8s_dashboard_cron_jobs.query.graphql'; +import k8sServicesQuery from './queries/k8s_dashboard_services.query.graphql'; import { resolvers } from './resolvers'; export const apolloProvider = () => { @@ -14,16 +17,18 @@ export const apolloProvider = () => { }); const { cache } = defaultClient; + const metadata = { + name: null, + namespace: null, + creationTimestamp: null, + labels: null, + annotations: null, + }; + cache.writeQuery({ query: k8sPodsQuery, data: { - metadata: { - name: null, - namespace: null, - creationTimestamp: null, - labels: null, - annotations: null, - }, + metadata, status: { phase: null, }, @@ -33,13 +38,7 @@ export const apolloProvider = () => { cache.writeQuery({ query: k8sDeploymentsQuery, data: { - metadata: { - name: null, - namespace: null, - creationTimestamp: null, - labels: null, - annotations: null, - }, + metadata, status: { conditions: null, }, @@ -49,13 +48,7 @@ export const apolloProvider = () => { cache.writeQuery({ query: k8sStatefulSetsQuery, data: { - metadata: { - name: null, - namespace: null, - creationTimestamp: null, - labels: null, - annotations: null, - }, + metadata, status: { readyReplicas: null, }, @@ -68,13 +61,7 @@ export const apolloProvider = () => { cache.writeQuery({ query: k8sReplicaSetsQuery, data: { - metadata: { - name: null, - namespace: null, - creationTimestamp: null, - labels: null, - annotations: null, - }, + metadata, status: { readyReplicas: null, }, @@ -87,13 +74,7 @@ export const apolloProvider = () => { cache.writeQuery({ query: k8sDaemonSetsQuery, data: { - metadata: { - name: null, - namespace: null, - creationTimestamp: null, - labels: null, - annotations: null, - }, + metadata, status: { numberMisscheduled: null, numberReady: null, @@ -102,6 +83,47 @@ export const apolloProvider = () => { }, }); + cache.writeQuery({ + query: k8sJobsQuery, + data: { + metadata, + status: { + failed: null, + succeeded: null, + }, + spec: { + completions: null, + }, + }, + }); + + cache.writeQuery({ + query: k8sCronJobsQuery, + data: { + metadata, + status: { + active: null, + lastScheduleTime: null, + }, + spec: { + suspend: null, + }, + }, + }); + + cache.writeQuery({ + query: k8sServicesQuery, + data: { + metadata, + spec: { + type: null, + clusterIP: null, + externalIP: null, + ports: null, + }, + }, + }); + return new VueApollo({ defaultClient, }); diff --git a/app/assets/javascripts/kubernetes_dashboard/graphql/helpers/resolver_helpers.js b/app/assets/javascripts/kubernetes_dashboard/graphql/helpers/resolver_helpers.js index 47c2f543357..b9c195d83d0 100644 --- a/app/assets/javascripts/kubernetes_dashboard/graphql/helpers/resolver_helpers.js +++ b/app/assets/javascripts/kubernetes_dashboard/graphql/helpers/resolver_helpers.js @@ -43,6 +43,62 @@ export const mapSetItem = (item) => { return { status, metadata, spec }; }; +export const mapJobItem = (item) => { + const metadata = { + ...item.metadata, + annotations: item.metadata?.annotations || {}, + labels: item.metadata?.labels || {}, + }; + + const status = { + failed: item.status?.failed || 0, + succeeded: item.status?.succeeded || 0, + }; + + return { + status, + metadata, + spec: item.spec, + }; +}; + +export const mapServicesItems = (item) => { + const { type, clusterIP, externalIP, ports } = item.spec; + + return { + metadata: { + ...item.metadata, + annotations: item.metadata?.annotations || {}, + labels: item.metadata?.labels || {}, + }, + spec: { + type, + clusterIP: clusterIP || '-', + externalIP: externalIP || '-', + ports, + }, + }; +}; + +export const mapCronJobItem = (item) => { + const metadata = { + ...item.metadata, + annotations: item.metadata?.annotations || {}, + labels: item.metadata?.labels || {}, + }; + + const status = { + active: item.status?.active || 0, + lastScheduleTime: item.status?.lastScheduleTime || null, + }; + + return { + status, + metadata, + spec: item.spec, + }; +}; + export const watchWorkloadItems = ({ client, query, diff --git a/app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_cron_jobs.query.graphql b/app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_cron_jobs.query.graphql new file mode 100644 index 00000000000..fe20cd2e70e --- /dev/null +++ b/app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_cron_jobs.query.graphql @@ -0,0 +1,18 @@ +query getK8sDashboardCronJobs($configuration: LocalConfiguration) { + k8sCronJobs(configuration: $configuration) @client { + metadata { + name + namespace + creationTimestamp + labels + annotations + } + status { + active + lastScheduleTime + } + spec { + suspend + } + } +} diff --git a/app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_jobs.query.graphql b/app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_jobs.query.graphql new file mode 100644 index 00000000000..86afb47f2f9 --- /dev/null +++ b/app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_jobs.query.graphql @@ -0,0 +1,18 @@ +query getK8sDashboardJobs($configuration: LocalConfiguration) { + k8sJobs(configuration: $configuration) @client { + metadata { + name + namespace + creationTimestamp + labels + annotations + } + status { + failed + succeeded + } + spec { + completions + } + } +} diff --git a/app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_services.query.graphql b/app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_services.query.graphql new file mode 100644 index 00000000000..7d42d66183e --- /dev/null +++ b/app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_services.query.graphql @@ -0,0 +1,17 @@ +query getK8sDashboardServices($configuration: LocalConfiguration) { + k8sServices(configuration: $configuration) @client { + metadata { + name + namespace + creationTimestamp + labels + annotations + } + spec { + type + clusterIP + externalIP + ports + } + } +} diff --git a/app/assets/javascripts/kubernetes_dashboard/graphql/resolvers/kubernetes.js b/app/assets/javascripts/kubernetes_dashboard/graphql/resolvers/kubernetes.js index e59bed5581b..75285ad2cca 100644 --- a/app/assets/javascripts/kubernetes_dashboard/graphql/resolvers/kubernetes.js +++ b/app/assets/javascripts/kubernetes_dashboard/graphql/resolvers/kubernetes.js @@ -1,4 +1,4 @@ -import { Configuration, AppsV1Api } from '@gitlab/cluster-client'; +import { Configuration, CoreV1Api, AppsV1Api, BatchV1Api } from '@gitlab/cluster-client'; import { getK8sPods, @@ -7,12 +7,18 @@ import { mapSetItem, buildWatchPath, watchWorkloadItems, + mapJobItem, + mapCronJobItem, + mapServicesItems, } from '../helpers/resolver_helpers'; import k8sDashboardPodsQuery from '../queries/k8s_dashboard_pods.query.graphql'; import k8sDashboardDeploymentsQuery from '../queries/k8s_dashboard_deployments.query.graphql'; import k8sDashboardStatefulSetsQuery from '../queries/k8s_dashboard_stateful_sets.query.graphql'; import k8sDashboardReplicaSetsQuery from '../queries/k8s_dashboard_replica_sets.query.graphql'; import k8sDaemonSetsQuery from '../queries/k8s_dashboard_daemon_sets.query.graphql'; +import k8sJobsQuery from '../queries/k8s_dashboard_jobs.query.graphql'; +import k8sCronJobsQuery from '../queries/k8s_dashboard_cron_jobs.query.graphql'; +import k8sServicesQuery from '../queries/k8s_dashboard_services.query.graphql'; export default { k8sPods(_, { configuration }, { client }) { @@ -61,10 +67,10 @@ export default { const config = new Configuration(configuration); const appsV1api = new AppsV1Api(config); - const deploymentsApi = namespace + const statefulSetsApi = namespace ? appsV1api.listAppsV1NamespacedStatefulSet({ namespace }) : appsV1api.listAppsV1StatefulSetForAllNamespaces(); - return deploymentsApi + return statefulSetsApi .then((res) => { const watchPath = buildWatchPath({ resource: 'statefulsets', @@ -98,10 +104,10 @@ export default { const config = new Configuration(configuration); const appsV1api = new AppsV1Api(config); - const deploymentsApi = namespace + const replicaSetsApi = namespace ? appsV1api.listAppsV1NamespacedReplicaSet({ namespace }) : appsV1api.listAppsV1ReplicaSetForAllNamespaces(); - return deploymentsApi + return replicaSetsApi .then((res) => { const watchPath = buildWatchPath({ resource: 'replicasets', @@ -135,10 +141,10 @@ export default { const config = new Configuration(configuration); const appsV1api = new AppsV1Api(config); - const deploymentsApi = namespace + const daemonSetsApi = namespace ? appsV1api.listAppsV1NamespacedDaemonSet({ namespace }) : appsV1api.listAppsV1DaemonSetForAllNamespaces(); - return deploymentsApi + return daemonSetsApi .then((res) => { const watchPath = buildWatchPath({ resource: 'daemonsets', @@ -166,4 +172,114 @@ export default { } }); }, + + k8sJobs(_, { configuration, namespace = '' }, { client }) { + const config = new Configuration(configuration); + + const batchV1api = new BatchV1Api(config); + const jobsApi = namespace + ? batchV1api.listBatchV1NamespacedJob({ namespace }) + : batchV1api.listBatchV1JobForAllNamespaces(); + return jobsApi + .then((res) => { + const watchPath = buildWatchPath({ + resource: 'jobs', + api: 'apis/batch/v1', + namespace, + }); + watchWorkloadItems({ + client, + query: k8sJobsQuery, + configuration, + namespace, + watchPath, + queryField: 'k8sJobs', + mapFn: mapJobItem, + }); + + const data = res?.items || []; + + return data.map(mapJobItem); + }) + .catch(async (err) => { + try { + await handleClusterError(err); + } catch (error) { + throw new Error(error.message); + } + }); + }, + + k8sCronJobs(_, { configuration, namespace = '' }, { client }) { + const config = new Configuration(configuration); + + const batchV1api = new BatchV1Api(config); + const cronJobsApi = namespace + ? batchV1api.listBatchV1NamespacedCronJob({ namespace }) + : batchV1api.listBatchV1CronJobForAllNamespaces(); + return cronJobsApi + .then((res) => { + const watchPath = buildWatchPath({ + resource: 'cronjobs', + api: 'apis/batch/v1', + namespace, + }); + watchWorkloadItems({ + client, + query: k8sCronJobsQuery, + configuration, + namespace, + watchPath, + queryField: 'k8sCronJobs', + mapFn: mapCronJobItem, + }); + + const data = res?.items || []; + + return data.map(mapCronJobItem); + }) + .catch(async (err) => { + try { + await handleClusterError(err); + } catch (error) { + throw new Error(error.message); + } + }); + }, + + k8sServices(_, { configuration, namespace = '' }, { client }) { + const config = new Configuration(configuration); + + const coreV1Api = new CoreV1Api(config); + const servicesApi = namespace + ? coreV1Api.listCoreV1NamespacedService({ namespace }) + : coreV1Api.listCoreV1ServiceForAllNamespaces(); + return servicesApi + .then((res) => { + const watchPath = buildWatchPath({ + resource: 'services', + namespace, + }); + watchWorkloadItems({ + client, + query: k8sServicesQuery, + configuration, + namespace, + watchPath, + queryField: 'k8sServices', + mapFn: mapServicesItems, + }); + + const data = res?.items || []; + + return data.map(mapServicesItems); + }) + .catch(async (err) => { + try { + await handleClusterError(err); + } catch (error) { + throw new Error(error.message); + } + }); + }, }; diff --git a/app/assets/javascripts/kubernetes_dashboard/helpers/k8s_integration_helper.js b/app/assets/javascripts/kubernetes_dashboard/helpers/k8s_integration_helper.js index 24f43e21506..d3116fd611a 100644 --- a/app/assets/javascripts/kubernetes_dashboard/helpers/k8s_integration_helper.js +++ b/app/assets/javascripts/kubernetes_dashboard/helpers/k8s_integration_helper.js @@ -5,6 +5,8 @@ import { STATUS_PENDING, STATUS_READY, STATUS_FAILED, + STATUS_COMPLETED, + STATUS_SUSPENDED, } from '../constants'; export function getAge(creationTimestamp) { @@ -58,3 +60,31 @@ export function calculateDaemonSetStatus(item) { } return STATUS_FAILED; } + +export function calculateJobStatus(item) { + if (item.status.failed > 0 || item.status?.succeeded !== item.spec?.completions) { + return STATUS_FAILED; + } + return STATUS_COMPLETED; +} + +export function calculateCronJobStatus(item) { + if (item.status?.active > 0 && !item.status?.lastScheduleTime) { + return STATUS_FAILED; + } + if (item.spec?.suspend) { + return STATUS_SUSPENDED; + } + return STATUS_READY; +} + +export function generateServicePortsString(ports) { + if (!ports?.length) return ''; + + return ports + .map((port) => { + const nodePort = port.nodePort ? `:${port.nodePort}` : ''; + return `${port.port}${nodePort}/${port.protocol}`; + }) + .join(', '); +} diff --git a/app/assets/javascripts/kubernetes_dashboard/pages/cron_jobs_page.vue b/app/assets/javascripts/kubernetes_dashboard/pages/cron_jobs_page.vue new file mode 100644 index 00000000000..2d57bfdc9fc --- /dev/null +++ b/app/assets/javascripts/kubernetes_dashboard/pages/cron_jobs_page.vue @@ -0,0 +1,84 @@ +<script> +import { s__ } from '~/locale'; +import { getAge, calculateCronJobStatus } from '../helpers/k8s_integration_helper'; +import WorkloadLayout from '../components/workload_layout.vue'; +import k8sCronJobsQuery from '../graphql/queries/k8s_dashboard_cron_jobs.query.graphql'; +import { STATUS_FAILED, STATUS_READY, STATUS_SUSPENDED, STATUS_LABELS } from '../constants'; + +export default { + components: { + WorkloadLayout, + }, + inject: ['configuration'], + apollo: { + k8sCronJobs: { + query: k8sCronJobsQuery, + variables() { + return { + configuration: this.configuration, + }; + }, + update(data) { + return ( + data?.k8sCronJobs?.map((job) => { + return { + name: job.metadata?.name, + namespace: job.metadata?.namespace, + status: calculateCronJobStatus(job), + age: getAge(job.metadata?.creationTimestamp), + labels: job.metadata?.labels, + annotations: job.metadata?.annotations, + kind: s__('KubernetesDashboard|CronJob'), + }; + }) || [] + ); + }, + error(err) { + this.errorMessage = err?.message; + }, + }, + }, + data() { + return { + k8sCronJobs: [], + errorMessage: '', + }; + }, + computed: { + cronJobsStats() { + return [ + { + value: this.countJobsByStatus(STATUS_READY), + title: STATUS_LABELS[STATUS_READY], + }, + { + value: this.countJobsByStatus(STATUS_FAILED), + title: STATUS_LABELS[STATUS_FAILED], + }, + { + value: this.countJobsByStatus(STATUS_SUSPENDED), + title: STATUS_LABELS[STATUS_SUSPENDED], + }, + ]; + }, + loading() { + return this.$apollo.queries.k8sCronJobs.loading; + }, + }, + methods: { + countJobsByStatus(phase) { + const filteredJobs = this.k8sCronJobs.filter((item) => item.status === phase) || []; + + return filteredJobs.length; + }, + }, +}; +</script> +<template> + <workload-layout + :loading="loading" + :error-message="errorMessage" + :stats="cronJobsStats" + :items="k8sCronJobs" + /> +</template> diff --git a/app/assets/javascripts/kubernetes_dashboard/pages/jobs_page.vue b/app/assets/javascripts/kubernetes_dashboard/pages/jobs_page.vue new file mode 100644 index 00000000000..f9dbb53e8b4 --- /dev/null +++ b/app/assets/javascripts/kubernetes_dashboard/pages/jobs_page.vue @@ -0,0 +1,80 @@ +<script> +import { s__ } from '~/locale'; +import { getAge, calculateJobStatus } from '../helpers/k8s_integration_helper'; +import WorkloadLayout from '../components/workload_layout.vue'; +import k8sJobsQuery from '../graphql/queries/k8s_dashboard_jobs.query.graphql'; +import { STATUS_FAILED, STATUS_COMPLETED, STATUS_LABELS } from '../constants'; + +export default { + components: { + WorkloadLayout, + }, + inject: ['configuration'], + apollo: { + k8sJobs: { + query: k8sJobsQuery, + variables() { + return { + configuration: this.configuration, + }; + }, + update(data) { + return ( + data?.k8sJobs?.map((job) => { + return { + name: job.metadata?.name, + namespace: job.metadata?.namespace, + status: calculateJobStatus(job), + age: getAge(job.metadata?.creationTimestamp), + labels: job.metadata?.labels, + annotations: job.metadata?.annotations, + kind: s__('KubernetesDashboard|Job'), + }; + }) || [] + ); + }, + error(err) { + this.errorMessage = err?.message; + }, + }, + }, + data() { + return { + k8sJobs: [], + errorMessage: '', + }; + }, + computed: { + jobsStats() { + return [ + { + value: this.countJobsByStatus(STATUS_COMPLETED), + title: STATUS_LABELS[STATUS_COMPLETED], + }, + { + value: this.countJobsByStatus(STATUS_FAILED), + title: STATUS_LABELS[STATUS_FAILED], + }, + ]; + }, + loading() { + return this.$apollo.queries.k8sJobs.loading; + }, + }, + methods: { + countJobsByStatus(phase) { + const filteredJobs = this.k8sJobs.filter((item) => item.status === phase) || []; + + return filteredJobs.length; + }, + }, +}; +</script> +<template> + <workload-layout + :loading="loading" + :error-message="errorMessage" + :stats="jobsStats" + :items="k8sJobs" + /> +</template> diff --git a/app/assets/javascripts/kubernetes_dashboard/pages/services_page.vue b/app/assets/javascripts/kubernetes_dashboard/pages/services_page.vue new file mode 100644 index 00000000000..4dc8fb6b6c0 --- /dev/null +++ b/app/assets/javascripts/kubernetes_dashboard/pages/services_page.vue @@ -0,0 +1,69 @@ +<script> +import { s__ } from '~/locale'; +import { getAge, generateServicePortsString } from '../helpers/k8s_integration_helper'; +import { SERVICES_TABLE_FIELDS } from '../constants'; +import WorkloadLayout from '../components/workload_layout.vue'; +import k8sServicesQuery from '../graphql/queries/k8s_dashboard_services.query.graphql'; + +export default { + components: { + WorkloadLayout, + }, + inject: ['configuration'], + apollo: { + k8sServices: { + query: k8sServicesQuery, + variables() { + return { + configuration: this.configuration, + }; + }, + update(data) { + return ( + data?.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), + labels: service.metadata?.labels, + annotations: service.metadata?.annotations, + kind: s__('KubernetesDashboard|Service'), + }; + }) || [] + ); + }, + error(err) { + this.errorMessage = err?.message; + }, + }, + }, + data() { + return { + k8sServices: [], + errorMessage: '', + }; + }, + computed: { + loading() { + return this.$apollo.queries.k8sServices.loading; + }, + servicesStats() { + return []; + }, + }, + SERVICES_TABLE_FIELDS, +}; +</script> +<template> + <workload-layout + :loading="loading" + :error-message="errorMessage" + :stats="servicesStats" + :items="k8sServices" + :fields="$options.SERVICES_TABLE_FIELDS" + /> +</template> diff --git a/app/assets/javascripts/kubernetes_dashboard/router/constants.js b/app/assets/javascripts/kubernetes_dashboard/router/constants.js index 700f501ade4..f02c01d7973 100644 --- a/app/assets/javascripts/kubernetes_dashboard/router/constants.js +++ b/app/assets/javascripts/kubernetes_dashboard/router/constants.js @@ -3,9 +3,15 @@ export const DEPLOYMENTS_ROUTE_NAME = 'deployments'; export const STATEFUL_SETS_ROUTE_NAME = 'statefulSets'; export const REPLICA_SETS_ROUTE_NAME = 'replicaSets'; export const DAEMON_SETS_ROUTE_NAME = 'daemonSets'; +export const JOBS_ROUTE_NAME = 'jobs'; +export const CRON_JOBS_ROUTE_NAME = 'cronJobs'; +export const SERVICES_ROUTE_NAME = 'services'; export const PODS_ROUTE_PATH = '/pods'; export const DEPLOYMENTS_ROUTE_PATH = '/deployments'; export const STATEFUL_SETS_ROUTE_PATH = '/statefulsets'; export const REPLICA_SETS_ROUTE_PATH = '/replicasets'; export const DAEMON_SETS_ROUTE_PATH = '/daemonsets'; +export const JOBS_ROUTE_PATH = '/jobs'; +export const CRON_JOBS_ROUTE_PATH = '/cronjobs'; +export const SERVICES_ROUTE_PATH = '/services'; diff --git a/app/assets/javascripts/kubernetes_dashboard/router/routes.js b/app/assets/javascripts/kubernetes_dashboard/router/routes.js index a1684a62ca4..7448508de8a 100644 --- a/app/assets/javascripts/kubernetes_dashboard/router/routes.js +++ b/app/assets/javascripts/kubernetes_dashboard/router/routes.js @@ -4,6 +4,10 @@ import DeploymentsPage from '../pages/deployments_page.vue'; import StatefulSetsPage from '../pages/stateful_sets_page.vue'; import ReplicaSetsPage from '../pages/replica_sets_page.vue'; import DaemonSetsPage from '../pages/daemon_sets_page.vue'; +import JobsPage from '../pages/jobs_page.vue'; +import CronJobsPage from '../pages/cron_jobs_page.vue'; +import ServicesPage from '../pages/services_page.vue'; + import { PODS_ROUTE_NAME, PODS_ROUTE_PATH, @@ -15,6 +19,12 @@ import { REPLICA_SETS_ROUTE_PATH, DAEMON_SETS_ROUTE_NAME, DAEMON_SETS_ROUTE_PATH, + JOBS_ROUTE_NAME, + JOBS_ROUTE_PATH, + CRON_JOBS_ROUTE_NAME, + CRON_JOBS_ROUTE_PATH, + SERVICES_ROUTE_NAME, + SERVICES_ROUTE_PATH, } from './constants'; export default [ @@ -58,4 +68,28 @@ export default [ title: s__('KubernetesDashboard|DaemonSets'), }, }, + { + name: JOBS_ROUTE_NAME, + path: JOBS_ROUTE_PATH, + component: JobsPage, + meta: { + title: s__('KubernetesDashboard|Jobs'), + }, + }, + { + name: CRON_JOBS_ROUTE_NAME, + path: CRON_JOBS_ROUTE_PATH, + component: CronJobsPage, + meta: { + title: s__('KubernetesDashboard|CronJobs'), + }, + }, + { + name: SERVICES_ROUTE_NAME, + path: SERVICES_ROUTE_PATH, + component: ServicesPage, + meta: { + title: s__('KubernetesDashboard|Services'), + }, + }, ]; |