summaryrefslogtreecommitdiff
path: root/app/assets
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-05-08 21:15:10 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2023-05-08 21:15:10 +0000
commit7db94a9807df03ce7a4f210b513816a47f34e15b (patch)
treea20574d4297ba13e3340bfae217e3035e77d6423 /app/assets
parent3a563d7c1e15023f205d2a357e5d8a38a3b53ecc (diff)
downloadgitlab-ce-7db94a9807df03ce7a4f210b513816a47f34e15b.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets')
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/components/base.vue6
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/constants.js2
-rw-r--r--app/assets/javascripts/analytics/shared/constants.js20
-rw-r--r--app/assets/javascripts/ci/artifacts/components/app.vue8
-rw-r--r--app/assets/javascripts/environments/components/kubernetes_overview.vue1
-rw-r--r--app/assets/javascripts/environments/components/kubernetes_summary.vue180
-rw-r--r--app/assets/javascripts/environments/components/kubernetes_tabs.vue8
-rw-r--r--app/assets/javascripts/environments/graphql/client.js52
-rw-r--r--app/assets/javascripts/environments/graphql/queries/k8s_workloads.query.graphql50
-rw-r--r--app/assets/javascripts/environments/graphql/resolvers.js99
-rw-r--r--app/assets/javascripts/environments/graphql/typedefs.graphql69
-rw-r--r--app/assets/javascripts/environments/helpers/k8s_integration_helper.js105
-rw-r--r--app/assets/stylesheets/framework/mixins.scss18
-rw-r--r--app/assets/stylesheets/page_bundles/login.scss4
-rw-r--r--app/assets/stylesheets/page_bundles/signup.scss16
-rw-r--r--app/assets/stylesheets/startup/startup-signin.scss34
16 files changed, 622 insertions, 50 deletions
diff --git a/app/assets/javascripts/analytics/cycle_analytics/components/base.vue b/app/assets/javascripts/analytics/cycle_analytics/components/base.vue
index 52e36749351..39da3484dfe 100644
--- a/app/assets/javascripts/analytics/cycle_analytics/components/base.vue
+++ b/app/assets/javascripts/analytics/cycle_analytics/components/base.vue
@@ -9,7 +9,7 @@ import PathNavigation from '~/analytics/cycle_analytics/components/path_navigati
import StageTable from '~/analytics/cycle_analytics/components/stage_table.vue';
import ValueStreamFilters from '~/analytics/cycle_analytics/components/value_stream_filters.vue';
import UrlSync from '~/vue_shared/components/url_sync.vue';
-import { __ } from '~/locale';
+import { __, s__ } from '~/locale';
import { SUMMARY_METRICS_REQUEST, METRICS_REQUESTS } from '../constants';
const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed';
@@ -79,7 +79,9 @@ export default {
}
return this.selectedStageError
? this.selectedStageError
- : __("We don't have enough data to show this stage.");
+ : s__(
+ 'ValueStreamAnalyticsStage|There are 0 items to show in this stage, for these filters, within this time range.',
+ );
},
emptyStageText() {
if (this.displayNoAccess) {
diff --git a/app/assets/javascripts/analytics/cycle_analytics/constants.js b/app/assets/javascripts/analytics/cycle_analytics/constants.js
index ebb2775b378..bea562fb18c 100644
--- a/app/assets/javascripts/analytics/cycle_analytics/constants.js
+++ b/app/assets/javascripts/analytics/cycle_analytics/constants.js
@@ -14,7 +14,7 @@ export const DEFAULT_VALUE_STREAM = {
};
export const NOT_ENOUGH_DATA_ERROR = s__(
- "ValueStreamAnalyticsStage|We don't have enough data to show this stage.",
+ 'ValueStreamAnalyticsStage|There are 0 items to show in this stage, for these filters, within this time range.',
);
export const PAGINATION_TYPE = 'keyset';
diff --git a/app/assets/javascripts/analytics/shared/constants.js b/app/assets/javascripts/analytics/shared/constants.js
index a07e2c3b799..3ac54900d37 100644
--- a/app/assets/javascripts/analytics/shared/constants.js
+++ b/app/assets/javascripts/analytics/shared/constants.js
@@ -38,6 +38,14 @@ const VSA_FLOW_METRICS_GROUP = {
export const VSA_METRICS_GROUPS = [VSA_FLOW_METRICS_GROUP];
+export const VULNERABILITY_CRITICAL_TYPE = 'vulnerability_critical';
+export const VULNERABILITY_HIGH_TYPE = 'vulnerability_high';
+
+export const VULNERABILITY_METRICS = {
+ CRITICAL: VULNERABILITY_CRITICAL_TYPE,
+ HIGH: VULNERABILITY_HIGH_TYPE,
+};
+
export const METRIC_TOOLTIPS = {
[DORA_METRICS.DEPLOYMENT_FREQUENCY]: {
description: s__(
@@ -101,6 +109,18 @@ export const METRIC_TOOLTIPS = {
projectLink: '-/analytics/merge_request_analytics',
docsLink: helpPagePath('user/analytics/merge_request_analytics'),
},
+ [VULNERABILITY_METRICS.CRITICAL]: {
+ description: s__('ValueStreamAnalytics|Total Critical vulnerabilities.'),
+ groupLink: '-/security/vulnerabilities',
+ projectLink: '-/security/vulnerability_report',
+ docsLink: helpPagePath('user/application_security/vulnerability_report/index'),
+ },
+ [VULNERABILITY_METRICS.HIGH]: {
+ description: s__('ValueStreamAnalytics|Total High vulnerabilities.'),
+ groupLink: '-/security/vulnerabilities',
+ projectLink: '-/security/vulnerability_report',
+ docsLink: helpPagePath('user/application_security/vulnerability_report/index'),
+ },
};
// TODO: Remove this once the migration to METRIC_TOOLTIPS is complete
diff --git a/app/assets/javascripts/ci/artifacts/components/app.vue b/app/assets/javascripts/ci/artifacts/components/app.vue
index 3a07be65341..c89df2610eb 100644
--- a/app/assets/javascripts/ci/artifacts/components/app.vue
+++ b/app/assets/javascripts/ci/artifacts/components/app.vue
@@ -18,12 +18,8 @@ export default {
variables() {
return { projectPath: this.projectPath };
},
- update({
- project: {
- statistics: { buildArtifactsSize },
- },
- }) {
- return buildArtifactsSize;
+ update({ project: { statistics } }) {
+ return statistics?.buildArtifactsSize ?? null;
},
},
},
diff --git a/app/assets/javascripts/environments/components/kubernetes_overview.vue b/app/assets/javascripts/environments/components/kubernetes_overview.vue
index 41abfcf6dc8..1f15c4daa2f 100644
--- a/app/assets/javascripts/environments/components/kubernetes_overview.vue
+++ b/app/assets/javascripts/environments/components/kubernetes_overview.vue
@@ -108,6 +108,7 @@ export default {
@cluster-error="onClusterError" />
<kubernetes-tabs
:configuration="k8sAccessConfiguration"
+ :namespace="namespace"
class="gl-mb-5"
@cluster-error="onClusterError"
/></template>
diff --git a/app/assets/javascripts/environments/components/kubernetes_summary.vue b/app/assets/javascripts/environments/components/kubernetes_summary.vue
new file mode 100644
index 00000000000..85fc1c1a07d
--- /dev/null
+++ b/app/assets/javascripts/environments/components/kubernetes_summary.vue
@@ -0,0 +1,180 @@
+<script>
+import { GlTab, GlLoadingIcon, GlBadge } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import k8sWorkloadsQuery from '../graphql/queries/k8s_workloads.query.graphql';
+import {
+ getDeploymentsStatuses,
+ getDaemonSetStatuses,
+ getStatefulSetStatuses,
+ getReplicaSetStatuses,
+ getJobsStatuses,
+ getCronJobsStatuses,
+} from '../helpers/k8s_integration_helper';
+
+export default {
+ components: {
+ GlTab,
+ GlBadge,
+ GlLoadingIcon,
+ },
+ apollo: {
+ k8sWorkloads: {
+ query: k8sWorkloadsQuery,
+ variables() {
+ return {
+ configuration: this.configuration,
+ namespace: this.namespace,
+ };
+ },
+ update(data) {
+ return data?.k8sWorkloads || {};
+ },
+ error(error) {
+ this.$emit('cluster-error', error);
+ },
+ },
+ },
+ props: {
+ configuration: {
+ required: true,
+ type: Object,
+ },
+ namespace: {
+ required: true,
+ type: String,
+ },
+ },
+ computed: {
+ summaryLoading() {
+ return this.$apollo.queries.k8sWorkloads.loading;
+ },
+ summaryCount() {
+ return this.k8sWorkloads ? Object.values(this.k8sWorkloads).flat().length : 0;
+ },
+ summaryObjects() {
+ return [
+ this.deploymentsItems,
+ this.daemonSetsItems,
+ this.statefulSetItems,
+ this.replicaSetItems,
+ this.jobItems,
+ this.cronJobItems,
+ ].filter(Boolean);
+ },
+ deploymentsItems() {
+ const items = this.k8sWorkloads?.DeploymentList;
+ if (!items?.length) {
+ return null;
+ }
+
+ return {
+ name: this.$options.i18n.deployments,
+ items: getDeploymentsStatuses(items),
+ };
+ },
+ daemonSetsItems() {
+ const items = this.k8sWorkloads?.DaemonSetList;
+ if (!items?.length) {
+ return null;
+ }
+
+ return {
+ name: this.$options.i18n.daemonSets,
+ items: getDaemonSetStatuses(items),
+ };
+ },
+ statefulSetItems() {
+ const items = this.k8sWorkloads?.StatefulSetList;
+ if (!items?.length) {
+ return null;
+ }
+
+ return {
+ name: this.$options.i18n.statefulSets,
+ items: getStatefulSetStatuses(items),
+ };
+ },
+ replicaSetItems() {
+ const items = this.k8sWorkloads?.ReplicaSetList;
+ if (!items?.length) {
+ return null;
+ }
+
+ return {
+ name: this.$options.i18n.replicaSets,
+ items: getReplicaSetStatuses(items),
+ };
+ },
+ jobItems() {
+ const items = this.k8sWorkloads?.JobList;
+ if (!items?.length) {
+ return null;
+ }
+
+ return {
+ name: this.$options.i18n.jobs,
+ items: getJobsStatuses(items),
+ };
+ },
+ cronJobItems() {
+ const items = this.k8sWorkloads?.CronJobList;
+ if (!items?.length) {
+ return null;
+ }
+
+ return {
+ name: this.$options.i18n.cronJobs,
+ items: getCronJobsStatuses(items),
+ };
+ },
+ },
+ i18n: {
+ summaryTitle: s__('Environment|Summary'),
+ deployments: s__('Environment|Deployments'),
+ daemonSets: s__('Environment|DaemonSets'),
+ statefulSets: s__('Environment|StatefulSets'),
+ replicaSets: s__('Environment|ReplicaSets'),
+ jobs: s__('Environment|Jobs'),
+ cronJobs: s__('Environment|CronJobs'),
+ },
+ badgeVariants: {
+ ready: 'success',
+ completed: 'success',
+ failed: 'danger',
+ suspended: 'neutral',
+ },
+ icons: {
+ Active: { icon: 'status_success', class: 'gl-text-green-500' },
+ },
+};
+</script>
+<template>
+ <gl-tab>
+ <template #title>
+ {{ $options.i18n.summaryTitle }}
+ <gl-badge size="sm" class="gl-tab-counter-badge">{{ summaryCount }}</gl-badge>
+ </template>
+
+ <gl-loading-icon v-if="summaryLoading" />
+
+ <ul v-else class="gl-mt-3 gl-list-style-none gl-bg-white gl-pl-0 gl-mb-0">
+ <li
+ v-for="object in summaryObjects"
+ :key="object.name"
+ class="gl-display-flex gl-align-items-center gl-p-3 gl-border-t gl-text-gray-700"
+ data-testid="summary-list-item"
+ >
+ <div class="gl-flex-grow-1">{{ object.name }}</div>
+
+ <gl-badge
+ v-for="(item, key) in object.items"
+ :key="key"
+ :variant="$options.badgeVariants[key]"
+ size="sm"
+ class="gl-ml-2"
+ >{{ item.length }} {{ key }}</gl-badge
+ >
+ </li>
+ </ul>
+ </gl-tab>
+</template>
diff --git a/app/assets/javascripts/environments/components/kubernetes_tabs.vue b/app/assets/javascripts/environments/components/kubernetes_tabs.vue
index b1eb92a4049..b900c23b2b7 100644
--- a/app/assets/javascripts/environments/components/kubernetes_tabs.vue
+++ b/app/assets/javascripts/environments/components/kubernetes_tabs.vue
@@ -4,6 +4,7 @@ import { __, s__ } from '~/locale';
import k8sServicesQuery from '../graphql/queries/k8s_services.query.graphql';
import { generateServicePortsString, getServiceAge } from '../helpers/k8s_integration_helper';
import { SERVICES_LIMIT_PER_PAGE } from '../constants';
+import KubernetesSummary from './kubernetes_summary.vue';
const tableHeadingClasses = 'gl-bg-gray-50! gl-font-weight-bold gl-white-space-nowrap';
@@ -15,6 +16,7 @@ export default {
GlTable,
GlPagination,
GlLoadingIcon,
+ KubernetesSummary,
},
apollo: {
k8sServices: {
@@ -37,6 +39,10 @@ export default {
required: true,
type: Object,
},
+ namespace: {
+ required: true,
+ type: String,
+ },
},
data() {
return {
@@ -128,6 +134,8 @@ export default {
</script>
<template>
<gl-tabs>
+ <kubernetes-summary :namespace="namespace" :configuration="configuration" />
+
<gl-tab>
<template #title>
{{ $options.i18n.servicesTitle }}
diff --git a/app/assets/javascripts/environments/graphql/client.js b/app/assets/javascripts/environments/graphql/client.js
index b7754558b10..6d06cff06b9 100644
--- a/app/assets/javascripts/environments/graphql/client.js
+++ b/app/assets/javascripts/environments/graphql/client.js
@@ -7,6 +7,7 @@ import environmentToRollbackQuery from './queries/environment_to_rollback.query.
import environmentToStopQuery from './queries/environment_to_stop.query.graphql';
import k8sPodsQuery from './queries/k8s_pods.query.graphql';
import k8sServicesQuery from './queries/k8s_services.query.graphql';
+import k8sWorkloadsQuery from './queries/k8s_workloads.query.graphql';
import { resolvers } from './resolvers';
import typeDefs from './typedefs.graphql';
@@ -109,6 +110,57 @@ export const apolloProvider = (endpoint) => {
},
},
});
+ cache.writeQuery({
+ query: k8sWorkloadsQuery,
+ data: {
+ DeploymentList: {
+ status: {
+ conditions: [],
+ },
+ },
+ DaemonSetList: {
+ status: {
+ numberMisscheduled: 0,
+ numberReady: 0,
+ desiredNumberScheduled: 0,
+ },
+ },
+ StatefulSetList: {
+ status: {
+ readyReplicas: 0,
+ },
+ spec: {
+ replicas: 0,
+ },
+ },
+ ReplicaSetList: {
+ status: {
+ readyReplicas: 0,
+ },
+ spec: {
+ replicas: 0,
+ },
+ },
+ JobList: {
+ status: {
+ failed: 0,
+ succeeded: 0,
+ },
+ spec: {
+ completions: 0,
+ },
+ },
+ CronJobList: {
+ status: {
+ active: 0,
+ lastScheduleTime: '',
+ },
+ spec: {
+ suspend: false,
+ },
+ },
+ },
+ });
return new VueApollo({
defaultClient,
});
diff --git a/app/assets/javascripts/environments/graphql/queries/k8s_workloads.query.graphql b/app/assets/javascripts/environments/graphql/queries/k8s_workloads.query.graphql
new file mode 100644
index 00000000000..27d39272734
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/queries/k8s_workloads.query.graphql
@@ -0,0 +1,50 @@
+query getK8sWorkloads($configuration: LocalConfiguration, $namespace: String) {
+ k8sWorkloads(configuration: $configuration, namespace: $namespace) @client {
+ DeploymentList {
+ status {
+ conditions
+ }
+ }
+ DaemonSetList {
+ status {
+ numberMisscheduled
+ numberReady
+ desiredNumberScheduled
+ }
+ }
+ StatefulSetList {
+ status {
+ readyReplicas
+ }
+ spec {
+ replicas
+ }
+ }
+ ReplicaSetList {
+ status {
+ readyReplicas
+ }
+ spec {
+ replicas
+ }
+ }
+ JobList {
+ status {
+ failed
+ succeeded
+ }
+ spec {
+ completions
+ }
+ }
+ CronJobList {
+ status {
+ active
+ lastScheduleTime
+ }
+ spec {
+ suspend
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/environments/graphql/resolvers.js b/app/assets/javascripts/environments/graphql/resolvers.js
index 8ebeeb92a53..044e7927606 100644
--- a/app/assets/javascripts/environments/graphql/resolvers.js
+++ b/app/assets/javascripts/environments/graphql/resolvers.js
@@ -1,4 +1,4 @@
-import { CoreV1Api, Configuration } from '@gitlab/cluster-client';
+import { CoreV1Api, Configuration, AppsV1Api, BatchV1Api } from '@gitlab/cluster-client';
import axios from '~/lib/utils/axios_utils';
import { s__ } from '~/locale';
import {
@@ -29,6 +29,49 @@ const mapEnvironment = (env) => ({
__typename: 'LocalEnvironment',
});
+const mapWorkloadItems = (items, kind) => {
+ return items.map((item) => {
+ const updatedItem = {
+ status: {},
+ spec: {},
+ };
+
+ switch (kind) {
+ case 'DeploymentList':
+ updatedItem.status.conditions = item.status.conditions || [];
+ break;
+ case 'DaemonSetList':
+ updatedItem.status = {
+ numberMisscheduled: item.status.numberMisscheduled || 0,
+ numberReady: item.status.numberReady || 0,
+ desiredNumberScheduled: item.status.desiredNumberScheduled || 0,
+ };
+ break;
+ case 'StatefulSetList':
+ case 'ReplicaSetList':
+ updatedItem.status.readyReplicas = item.status.readyReplicas || 0;
+ updatedItem.spec.replicas = item.spec.replicas || 0;
+ break;
+ case 'JobList':
+ updatedItem.status.failed = item.status.failed || 0;
+ updatedItem.status.succeeded = item.status.succeeded || 0;
+ updatedItem.spec.completions = item.spec.completions || 0;
+ break;
+ case 'CronJobList':
+ updatedItem.status.active = item.status.active || 0;
+ updatedItem.status.lastScheduleTime = item.status.lastScheduleTime || '';
+ updatedItem.spec.suspend = item.spec.suspend || 0;
+ break;
+ default:
+ updatedItem.status = item?.status;
+ updatedItem.spec = item?.spec;
+ break;
+ }
+
+ return updatedItem;
+ });
+};
+
export const resolvers = (endpoint) => ({
Query: {
environmentApp(_context, { page, scope, search }, { cache }) {
@@ -109,6 +152,60 @@ export const resolvers = (endpoint) => ({
throw error;
});
},
+ k8sWorkloads(_, { configuration, namespace }) {
+ const appsV1api = new AppsV1Api(configuration);
+ const batchV1api = new BatchV1Api(configuration);
+
+ let promises;
+
+ if (namespace) {
+ promises = [
+ appsV1api.listAppsV1NamespacedDeployment(namespace),
+ appsV1api.listAppsV1NamespacedDaemonSet(namespace),
+ appsV1api.listAppsV1NamespacedStatefulSet(namespace),
+ appsV1api.listAppsV1NamespacedReplicaSet(namespace),
+ batchV1api.listBatchV1NamespacedJob(namespace),
+ batchV1api.listBatchV1NamespacedCronJob(namespace),
+ ];
+ } else {
+ promises = [
+ appsV1api.listAppsV1DeploymentForAllNamespaces(),
+ appsV1api.listAppsV1DaemonSetForAllNamespaces(),
+ appsV1api.listAppsV1StatefulSetForAllNamespaces(),
+ appsV1api.listAppsV1ReplicaSetForAllNamespaces(),
+ batchV1api.listBatchV1JobForAllNamespaces(),
+ batchV1api.listBatchV1CronJobForAllNamespaces(),
+ ];
+ }
+
+ const summaryList = {
+ DeploymentList: [],
+ DaemonSetList: [],
+ StatefulSetList: [],
+ ReplicaSetList: [],
+ JobList: [],
+ CronJobList: [],
+ };
+
+ return Promise.allSettled(promises).then((results) => {
+ if (results.every((res) => res.status === 'rejected')) {
+ const error = results[0].reason;
+ const errorMessage = error?.response?.data?.message ?? error;
+ throw new Error(errorMessage);
+ }
+ for (const promiseResult of results) {
+ if (promiseResult.status === 'fulfilled' && promiseResult?.value?.data) {
+ const { kind, items } = promiseResult.value.data;
+
+ if (items?.length > 0) {
+ summaryList[kind] = mapWorkloadItems(items, kind);
+ }
+ }
+ }
+
+ return summaryList;
+ });
+ },
},
Mutation: {
stopEnvironmentREST(_, { environment }, { client }) {
diff --git a/app/assets/javascripts/environments/graphql/typedefs.graphql b/app/assets/javascripts/environments/graphql/typedefs.graphql
index b85bf1e10d3..7e46385946f 100644
--- a/app/assets/javascripts/environments/graphql/typedefs.graphql
+++ b/app/assets/javascripts/environments/graphql/typedefs.graphql
@@ -93,6 +93,74 @@ type LocalK8sServices {
spec: k8sServiceSpec
}
+type k8sDeploymentStatus {
+ conditions: JSON
+}
+
+type localK8sDeployment {
+ status: k8sDeploymentStatus
+}
+
+type k8sDaemonSetStatus {
+ IntMisscheduled: Int
+ IntReady: Int
+ desiredIntScheduled: Int
+}
+
+type localK8sDaemonSet {
+ status: k8sDaemonSetStatus
+}
+
+type k8sSetStatus {
+ readyReplicas: Int
+}
+
+type k8sSetSpec {
+ replicas: Int
+}
+
+type localK8sSet {
+ status: k8sSetStatus
+ spec: k8sSetSpec
+}
+
+type k8sJobStatus {
+ failed: Int
+ succeeded: Int
+}
+
+type k8sJobSpec {
+ completions: Int
+}
+
+type localK8sJob {
+ status: k8sJobStatus
+ spec: k8sJobSpec
+}
+
+type k8sCronJobStatus {
+ active: Int
+ lastScheduleTime: String
+}
+
+type k8sCronJobSpec {
+ suspend: Boolean
+}
+
+type localK8sCronJob {
+ status: k8sCronJobStatus
+ spec: k8sCronJobSpec
+}
+
+type LocalK8sWorkloads {
+ DeploymentList: [localK8sDeployment]
+ DaemonSetList: [localK8sDaemonSet]
+ StatefulSetList: [localK8sSet]
+ ReplicaSetList: [localK8sSet]
+ JobList: [localK8sJob]
+ CronJobList: [localK8sCronJob]
+}
+
extend type Query {
environmentApp(page: Int, scope: String): LocalEnvironmentApp
folder(environment: NestedLocalEnvironmentInput): LocalEnvironmentFolder
@@ -104,6 +172,7 @@ extend type Query {
isLastDeployment(environment: LocalEnvironmentInput): Boolean
k8sPods(configuration: LocalConfiguration, namespace: String): [LocalK8sPods]
k8sServices(configuration: LocalConfiguration): [LocalK8sServices]
+ k8sWorkloads(configuration: LocalConfiguration, namespace: String): LocalK8sWorkloads
}
extend type Mutation {
diff --git a/app/assets/javascripts/environments/helpers/k8s_integration_helper.js b/app/assets/javascripts/environments/helpers/k8s_integration_helper.js
index 6a94ac31fa1..45c65c93a91 100644
--- a/app/assets/javascripts/environments/helpers/k8s_integration_helper.js
+++ b/app/assets/javascripts/environments/helpers/k8s_integration_helper.js
@@ -34,3 +34,108 @@ export function getServiceAge(creationTimestamp) {
return ageString;
}
+
+export function getDeploymentsStatuses(items) {
+ const failed = [];
+ const ready = [];
+
+ items.forEach((item) => {
+ const [available, progressing] = item.status?.conditions ?? [];
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ if (available.status === 'True') {
+ ready.push(item);
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ } else if (available.status !== 'True' && progressing.status !== 'True') {
+ failed.push(item);
+ }
+ });
+
+ return {
+ ...(failed.length && { failed }),
+ ...(ready.length && { ready }),
+ };
+}
+
+export function getDaemonSetStatuses(items) {
+ const failed = items.filter((item) => {
+ return (
+ item.status?.numberMisscheduled > 0 ||
+ item.status?.numberReady !== item.status?.desiredNumberScheduled
+ );
+ });
+ const ready = items.filter((item) => {
+ return (
+ item.status?.numberReady === item.status?.desiredNumberScheduled &&
+ !item.status?.numberMisscheduled
+ );
+ });
+
+ return {
+ ...(failed.length && { failed }),
+ ...(ready.length && { ready }),
+ };
+}
+
+export function getStatefulSetStatuses(items) {
+ const failed = items.filter((item) => {
+ return item.status?.readyReplicas < item.spec?.replicas;
+ });
+ const ready = items.filter((item) => {
+ return item.status?.readyReplicas === item.spec?.replicas;
+ });
+
+ return {
+ ...(failed.length && { failed }),
+ ...(ready.length && { ready }),
+ };
+}
+
+export function getReplicaSetStatuses(items) {
+ const failed = items.filter((item) => {
+ return item.status?.readyReplicas < item.spec?.replicas;
+ });
+ const ready = items.filter((item) => {
+ return item.status?.readyReplicas === item.spec?.replicas;
+ });
+
+ return {
+ ...(failed.length && { failed }),
+ ...(ready.length && { ready }),
+ };
+}
+
+export function getJobsStatuses(items) {
+ const failed = items.filter((item) => {
+ return item.status.failed > 0 || item.status?.succeeded !== item.spec?.completions;
+ });
+ const completed = items.filter((item) => {
+ return item.status?.succeeded === item.spec?.completions;
+ });
+
+ return {
+ ...(failed.length && { failed }),
+ ...(completed.length && { completed }),
+ };
+}
+
+export function getCronJobsStatuses(items) {
+ const failed = [];
+ const ready = [];
+ const suspended = [];
+
+ items.forEach((item) => {
+ if (item.status?.active > 0 && !item.status?.lastScheduleTime) {
+ failed.push(item);
+ } else if (item.spec?.suspend) {
+ suspended.push(item);
+ } else if (item.status?.lastScheduleTime) {
+ ready.push(item);
+ }
+ });
+
+ return {
+ ...(failed.length && { failed }),
+ ...(suspended.length && { suspended }),
+ ...(ready.length && { ready }),
+ };
+}
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index aefac300839..15a31fbb3d9 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -451,3 +451,21 @@
color: $gl-text-color;
}
}
+
+@mixin omniauth-divider {
+ &::before,
+ &::after {
+ content: '';
+ flex: 1;
+ border-bottom: 1px solid var(--gray-100, $gray-100);
+ margin: $gl-padding-24 0;
+ }
+
+ &::before {
+ margin-right: $gl-padding;
+ }
+
+ &::after {
+ margin-left: $gl-padding;
+ }
+}
diff --git a/app/assets/stylesheets/page_bundles/login.scss b/app/assets/stylesheets/page_bundles/login.scss
index 495b7d58788..1ae7230772d 100644
--- a/app/assets/stylesheets/page_bundles/login.scss
+++ b/app/assets/stylesheets/page_bundles/login.scss
@@ -221,6 +221,10 @@
color: $red-700;
}
}
+
+ .omniauth-divider {
+ @include omniauth-divider;
+ }
}
@include media-breakpoint-down(xs) {
diff --git a/app/assets/stylesheets/page_bundles/signup.scss b/app/assets/stylesheets/page_bundles/signup.scss
index 4fc671dace8..2bb3ff77132 100644
--- a/app/assets/stylesheets/page_bundles/signup.scss
+++ b/app/assets/stylesheets/page_bundles/signup.scss
@@ -9,21 +9,7 @@
}
.omniauth-divider {
- &::before,
- &::after {
- content: '';
- flex: 1;
- border-bottom: 1px solid var(--gray-100, $gray-100);
- margin: $gl-padding-24 0;
- }
-
- &::before {
- margin-right: $gl-padding;
- }
-
- &::after {
- margin-left: $gl-padding;
- }
+ @include omniauth-divider;
}
.decline-page {
diff --git a/app/assets/stylesheets/startup/startup-signin.scss b/app/assets/stylesheets/startup/startup-signin.scss
index cd768c3bbc0..f676782de2a 100644
--- a/app/assets/stylesheets/startup/startup-signin.scss
+++ b/app/assets/stylesheets/startup/startup-signin.scss
@@ -360,11 +360,6 @@ input.btn-block[type="button"] {
align-items: center;
justify-content: space-between;
}
-.clearfix::after {
- display: block;
- clear: both;
- content: "";
-}
.fixed-top {
position: fixed;
top: 0;
@@ -783,11 +778,8 @@ svg {
.gl-display-inline-block {
display: inline-block;
}
-.gl-flex-wrap {
- flex-wrap: wrap;
-}
-.gl-justify-content-center {
- justify-content: center;
+.gl-align-items-center {
+ align-items: center;
}
.gl-justify-content-space-between {
justify-content: space-between;
@@ -801,9 +793,6 @@ svg {
.gl-w-half {
width: 50%;
}
-.gl-w-90p {
- width: 90%;
-}
.gl-w-full {
width: 100%;
}
@@ -812,9 +801,6 @@ svg {
width: 100%;
}
}
-.gl-p-5 {
- padding: 1rem;
-}
.gl-px-5 {
padding-left: 1rem;
padding-right: 1rem;
@@ -822,6 +808,13 @@ svg {
.gl-pt-5 {
padding-top: 1rem;
}
+.gl-pb-5 {
+ padding-bottom: 1rem;
+}
+.gl-py-5 {
+ padding-top: 1rem;
+ padding-bottom: 1rem;
+}
.gl-mt-3 {
margin-top: 0.5rem;
}
@@ -831,9 +824,6 @@ svg {
.gl-mr-auto {
margin-right: auto;
}
-.gl-mr-2 {
- margin-right: 0.25rem;
-}
.gl-mb-1 {
margin-bottom: 0.125rem;
}
@@ -846,9 +836,6 @@ svg {
.gl-ml-auto {
margin-left: auto;
}
-.gl-ml-2 {
- margin-left: 0.25rem;
-}
@media (min-width: 576px) {
.gl-sm-mt-0 {
margin-top: 0;
@@ -860,9 +847,6 @@ svg {
.gl-font-size-h2 {
font-size: 1.1875rem;
}
-.gl-font-weight-normal {
- font-weight: 400;
-}
.gl-font-weight-bold {
font-weight: 600;
}