diff options
Diffstat (limited to 'app')
46 files changed, 452 insertions, 168 deletions
diff --git a/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_job_details_container.vue b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_job_details_container.vue index f04987a87b5..a8b29e7c581 100644 --- a/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_job_details_container.vue +++ b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_job_details_container.vue @@ -4,6 +4,7 @@ import { mapState } from 'vuex'; import { GlBadge } from '@gitlab/ui'; import { helpPagePath } from '~/helpers/help_page_helper'; import { timeIntervalInWords } from '~/lib/utils/datetime_utility'; +import { mergeUrlParams } from '~/lib/utils/url_utility'; import { __, sprintf } from '~/locale'; import timeagoMixin from '~/vue_shared/mixins/timeago'; import DetailRow from './sidebar_detail_row.vue'; @@ -15,8 +16,9 @@ export default { GlBadge, }, mixins: [timeagoMixin], + inject: ['pipelineTestReportUrl'], computed: { - ...mapState(['job']), + ...mapState(['job', 'testSummary']), coverage() { return `${this.job.coverage}%`; }, @@ -74,6 +76,32 @@ export default { runnerAdminPath() { return this.job?.runner?.admin_path || ''; }, + hasTestSummaryDetails() { + return Object.keys(this.testSummary).length > 0; + }, + testSummaryDescription() { + let message; + + if (this.testSummary?.total?.failed > 0) { + message = sprintf(__('%{failures} of %{total} failed'), { + failures: this.testSummary?.total?.failed, + total: this.testSummary?.total.count, + }); + } else { + message = sprintf(__('%{total}'), { + total: this.testSummary?.total.count, + }); + } + + return message; + }, + testReportUrlWithJobName() { + const urlParams = { + job_name: this.job.name, + }; + + return mergeUrlParams(urlParams, this.pipelineTestReportUrl); + }, }, i18n: { COVERAGE: __('Coverage'), @@ -82,6 +110,7 @@ export default { QUEUED: __('Queued'), RUNNER: __('Runner'), TAGS: __('Tags'), + TEST_SUMMARY: __('Test summary'), TIMEOUT: __('Timeout'), }, TIMEOUT_HELP_URL: helpPagePath('/ci/pipelines/settings.md', { @@ -115,6 +144,13 @@ export default { :path="runnerAdminPath" /> <detail-row v-if="job.coverage" :value="coverage" :title="$options.i18n.COVERAGE" /> + <detail-row + v-if="hasTestSummaryDetails" + :value="testSummaryDescription" + :title="$options.i18n.TEST_SUMMARY" + :path="testReportUrlWithJobName" + data-testid="test-summary" + /> <p v-if="hasTags" class="build-detail-row" data-testid="job-tags"> <span class="font-weight-bold">{{ $options.i18n.TAGS }}:</span> diff --git a/app/assets/javascripts/ci/job_details/index.js b/app/assets/javascripts/ci/job_details/index.js index 20235015ce6..dee77d404a8 100644 --- a/app/assets/javascripts/ci/job_details/index.js +++ b/app/assets/javascripts/ci/job_details/index.js @@ -30,11 +30,13 @@ export const initJobDetails = () => { projectPath, retryOutdatedJobDocsUrl, aiRootCauseAnalysisAvailable, + testReportSummaryUrl, + pipelineTestReportUrl, } = el.dataset; // init store to start fetching log const store = createStore(); - store.dispatch('init', { endpoint, pagePath }); + store.dispatch('init', { endpoint, pagePath, testReportSummaryUrl }); return new Vue({ el, @@ -44,6 +46,7 @@ export const initJobDetails = () => { projectPath, retryOutdatedJobDocsUrl, aiRootCauseAnalysisAvailable: parseBoolean(aiRootCauseAnalysisAvailable), + pipelineTestReportUrl, }, render(h) { return h(JobApp, { diff --git a/app/assets/javascripts/ci/job_details/store/actions.js b/app/assets/javascripts/ci/job_details/store/actions.js index 6f538e3b3d4..adcba0f2409 100644 --- a/app/assets/javascripts/ci/job_details/store/actions.js +++ b/app/assets/javascripts/ci/job_details/store/actions.js @@ -15,10 +15,11 @@ import { __ } from '~/locale'; import { reportToSentry } from '~/ci/utils'; import * as types from './mutation_types'; -export const init = ({ dispatch }, { endpoint, pagePath }) => { +export const init = ({ dispatch }, { endpoint, pagePath, testReportSummaryUrl }) => { dispatch('setJobLogOptions', { endpoint, pagePath, + testReportSummaryUrl, }); return dispatch('fetchJob'); @@ -170,6 +171,7 @@ export const fetchJobLog = ({ dispatch, state }) => if (data.complete) { dispatch('stopPollingJobLog'); + dispatch('requestTestSummary'); } else if (!state.jobLogTimeout) { dispatch('startPollingJobLog'); } @@ -273,3 +275,23 @@ export const triggerManualJob = ({ state }, variables) => { }), ); }; + +export const requestTestSummary = ({ state, commit, dispatch }) => { + if (!state.testSummaryComplete) { + axios + .get(state.testReportSummaryUrl) + .then(({ data }) => { + dispatch('receiveTestSummarySuccess', data); + }) + .catch((e) => { + reportToSentry('job_test_summary_report', e); + }) + .finally(() => { + commit(types.RECEIVE_TEST_SUMMARY_COMPLETE); + }); + } +}; + +export const receiveTestSummarySuccess = ({ commit }, data) => { + commit(types.RECEIVE_TEST_SUMMARY_SUCCESS, data); +}; diff --git a/app/assets/javascripts/ci/job_details/store/mutation_types.js b/app/assets/javascripts/ci/job_details/store/mutation_types.js index e125538317d..86ebbe57714 100644 --- a/app/assets/javascripts/ci/job_details/store/mutation_types.js +++ b/app/assets/javascripts/ci/job_details/store/mutation_types.js @@ -28,3 +28,6 @@ export const SET_SELECTED_STAGE = 'SET_SELECTED_STAGE'; export const REQUEST_JOBS_FOR_STAGE = 'REQUEST_JOBS_FOR_STAGE'; export const RECEIVE_JOBS_FOR_STAGE_SUCCESS = 'RECEIVE_JOBS_FOR_STAGE_SUCCESS'; export const RECEIVE_JOBS_FOR_STAGE_ERROR = 'RECEIVE_JOBS_FOR_STAGE_ERROR'; + +export const RECEIVE_TEST_SUMMARY_SUCCESS = 'RECEIVE_TEST_SUMMARY_SUCCESS'; +export const RECEIVE_TEST_SUMMARY_COMPLETE = 'RECEIVE_TEST_SUMMARY_COMPLETE'; diff --git a/app/assets/javascripts/ci/job_details/store/mutations.js b/app/assets/javascripts/ci/job_details/store/mutations.js index fe6506bf8a5..ad0603c050e 100644 --- a/app/assets/javascripts/ci/job_details/store/mutations.js +++ b/app/assets/javascripts/ci/job_details/store/mutations.js @@ -6,6 +6,7 @@ export default { [types.SET_JOB_LOG_OPTIONS](state, options = {}) { state.jobLogEndpoint = options.pagePath; state.jobEndpoint = options.endpoint; + state.testReportSummaryUrl = options.testReportSummaryUrl; }, [types.HIDE_SIDEBAR](state) { @@ -127,4 +128,10 @@ export default { state.isLoadingJobs = false; state.jobs = []; }, + [types.RECEIVE_TEST_SUMMARY_SUCCESS](state, testSummary) { + state.testSummary = testSummary; + }, + [types.RECEIVE_TEST_SUMMARY_COMPLETE](state) { + state.testSummaryComplete = true; + }, }; diff --git a/app/assets/javascripts/ci/job_details/store/state.js b/app/assets/javascripts/ci/job_details/store/state.js index dfff65c364d..aa4497d0e37 100644 --- a/app/assets/javascripts/ci/job_details/store/state.js +++ b/app/assets/javascripts/ci/job_details/store/state.js @@ -1,9 +1,12 @@ export default () => ({ jobEndpoint: null, jobLogEndpoint: null, + testReportSummaryUrl: null, // sidebar isSidebarOpen: true, + testSummary: {}, + testSummaryComplete: false, isLoading: false, hasError: false, diff --git a/app/assets/javascripts/ci/pipeline_details/test_reports/test_reports.vue b/app/assets/javascripts/ci/pipeline_details/test_reports/test_reports.vue index a7737d33285..6e9a705c046 100644 --- a/app/assets/javascripts/ci/pipeline_details/test_reports/test_reports.vue +++ b/app/assets/javascripts/ci/pipeline_details/test_reports/test_reports.vue @@ -2,6 +2,7 @@ import { GlLoadingIcon } from '@gitlab/ui'; // eslint-disable-next-line no-restricted-imports import { mapActions, mapGetters, mapState } from 'vuex'; +import { getParameterValues } from '~/lib/utils/url_utility'; import EmptyState from './empty_state.vue'; import TestSuiteTable from './test_suite_table.vue'; import TestSummary from './test_summary.vue'; @@ -19,7 +20,7 @@ export default { inject: ['blobPath', 'summaryEndpoint', 'suiteEndpoint'], computed: { ...mapState('testReports', ['isLoading', 'selectedSuiteIndex', 'testReports']), - ...mapGetters('testReports', ['getSelectedSuite']), + ...mapGetters('testReports', ['getSelectedSuite', 'getTestSuites']), showSuite() { return this.selectedSuiteIndex !== null; }, @@ -28,8 +29,16 @@ export default { return testSuites.length > 0; }, }, - created() { - this.fetchSummary(); + async created() { + await this.fetchSummary(); + const jobName = getParameterValues('job_name')[0] || ''; + if (jobName.length > 0) { + // get the index from the job name + const indexToSelect = this.getTestSuites.findIndex((test) => test.name === jobName); + + this.setSelectedSuiteIndex(indexToSelect); + this.fetchTestSuite(indexToSelect); + } }, methods: { ...mapActions('testReports', [ diff --git a/app/assets/javascripts/environments/components/kubernetes_pods.vue b/app/assets/javascripts/environments/components/kubernetes_pods.vue index 3f040f1f40a..743159d6256 100644 --- a/app/assets/javascripts/environments/components/kubernetes_pods.vue +++ b/app/assets/javascripts/environments/components/kubernetes_pods.vue @@ -1,14 +1,20 @@ <script> import { GlLoadingIcon } from '@gitlab/ui'; -import { GlSingleStat } from '@gitlab/ui/dist/charts'; import { s__ } from '~/locale'; +import { + PHASE_RUNNING, + PHASE_PENDING, + PHASE_SUCCEEDED, + PHASE_FAILED, + STATUS_LABELS, +} from '~/kubernetes_dashboard/constants'; +import WorkloadStats from '~/kubernetes_dashboard/components/workload_stats.vue'; import k8sPodsQuery from '../graphql/queries/k8s_pods.query.graphql'; -import { PHASE_RUNNING, PHASE_PENDING, PHASE_SUCCEEDED, PHASE_FAILED } from '../constants'; export default { components: { GlLoadingIcon, - GlSingleStat, + WorkloadStats, }, apollo: { k8sPods: { @@ -53,19 +59,19 @@ export default { return [ { value: this.countPodsByPhase(PHASE_RUNNING), - title: this.$options.i18n.runningPods, + title: STATUS_LABELS[PHASE_RUNNING], }, { value: this.countPodsByPhase(PHASE_PENDING), - title: this.$options.i18n.pendingPods, + title: STATUS_LABELS[PHASE_PENDING], }, { value: this.countPodsByPhase(PHASE_SUCCEEDED), - title: this.$options.i18n.succeededPods, + title: STATUS_LABELS[PHASE_SUCCEEDED], }, { value: this.countPodsByPhase(PHASE_FAILED), - title: this.$options.i18n.failedPods, + title: STATUS_LABELS[PHASE_FAILED], }, ]; }, @@ -84,10 +90,6 @@ export default { }, i18n: { podsTitle: s__('Environment|Pods'), - runningPods: s__('Environment|Running'), - pendingPods: s__('Environment|Pending'), - succeededPods: s__('Environment|Succeeded'), - failedPods: s__('Environment|Failed'), }, }; </script> @@ -96,18 +98,6 @@ export default { <p class="gl-text-gray-500">{{ $options.i18n.podsTitle }}</p> <gl-loading-icon v-if="loading" /> - - <div - v-else-if="podStats && !error" - class="gl-display-flex gl-flex-wrap gl-sm-flex-nowrap gl-mx-n3 gl-mt-n3" - > - <gl-single-stat - v-for="(stat, index) in podStats" - :key="index" - class="gl-w-full gl-flex-direction-column gl-align-items-center gl-justify-content-center gl-bg-white gl-border gl-border-gray-a-08 gl-mx-3 gl-p-3 gl-mt-3" - :value="stat.value" - :title="stat.title" - /> - </div> + <workload-stats v-else-if="podStats && !error" :stats="podStats" /> </div> </template> diff --git a/app/assets/javascripts/environments/constants.js b/app/assets/javascripts/environments/constants.js index e97720312b0..4f0eabb26a4 100644 --- a/app/assets/javascripts/environments/constants.js +++ b/app/assets/javascripts/environments/constants.js @@ -155,11 +155,6 @@ export const SYNC_STATUS_BADGES = { export const STATUS_TRUE = 'True'; export const STATUS_FALSE = 'False'; -export const PHASE_RUNNING = 'Running'; -export const PHASE_PENDING = 'Pending'; -export const PHASE_SUCCEEDED = 'Succeeded'; -export const PHASE_FAILED = 'Failed'; - const ERROR_UNAUTHORIZED = 'unauthorized'; const ERROR_FORBIDDEN = 'forbidden'; const ERROR_NOT_FOUND = 'not found'; diff --git a/app/assets/javascripts/environments/graphql/resolvers/kubernetes.js b/app/assets/javascripts/environments/graphql/resolvers/kubernetes.js index 1bc8706cfce..06a55158152 100644 --- a/app/assets/javascripts/environments/graphql/resolvers/kubernetes.js +++ b/app/assets/javascripts/environments/graphql/resolvers/kubernetes.js @@ -7,6 +7,10 @@ import { EVENT_DATA, } from '@gitlab/cluster-client'; import produce from 'immer'; +import { + getK8sPods, + handleClusterError, +} from '~/kubernetes_dashboard/graphql/helpers/resolver_helpers'; import { humanizeClusterErrors } from '../../helpers/k8s_integration_helper'; import k8sPodsQuery from '../queries/k8s_pods.query.graphql'; import k8sWorkloadsQuery from '../queries/k8s_workloads.query.graphql'; @@ -54,42 +58,6 @@ const mapWorkloadItems = (items, kind) => { }); }; -const handleClusterError = async (err) => { - if (!err.response) { - throw err; - } - - const errorData = await err.response.json(); - throw errorData; -}; - -const watchPods = ({ configuration, namespace, client }) => { - const path = namespace ? `/api/v1/namespaces/${namespace}/pods` : '/api/v1/pods'; - const config = new Configuration(configuration); - const watcherApi = new WatchApi(config); - - watcherApi - .subscribeToStream(path, { watch: true }) - .then((watcher) => { - let result = []; - - watcher.on(EVENT_DATA, (data) => { - result = data.map((item) => { - return { status: { phase: item.status.phase } }; - }); - - client.writeQuery({ - query: k8sPodsQuery, - variables: { configuration, namespace }, - data: { k8sPods: result }, - }); - }); - }) - .catch((err) => { - handleClusterError(err); - }); -}; - const watchWorkloadItems = ({ kind, apiVersion, configuration, namespace, client }) => { const itemKind = kind.toLowerCase().replace('list', 's'); @@ -130,28 +98,9 @@ const watchWorkloadItems = ({ kind, apiVersion, configuration, namespace, client export default { k8sPods(_, { configuration, namespace }, { client }) { - const config = new Configuration(configuration); - - const coreV1Api = new CoreV1Api(config); - const podsApi = namespace - ? coreV1Api.listCoreV1NamespacedPod({ namespace }) - : coreV1Api.listCoreV1PodForAllNamespaces(); - - return podsApi - .then((res) => { - if (gon.features?.k8sWatchApi) { - watchPods({ configuration, namespace, client }); - } - - return res?.items || []; - }) - .catch(async (err) => { - try { - await handleClusterError(err); - } catch (error) { - throw new Error(error.message); - } - }); + const query = k8sPodsQuery; + const enableWatch = gon.features?.k8sWatchApi; + return getK8sPods({ client, query, configuration, namespace, enableWatch }); }, k8sServices(_, { configuration, namespace }) { const coreV1Api = new CoreV1Api(new Configuration(configuration)); diff --git a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue index 4d4bae12570..95ae5e5a92c 100644 --- a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue +++ b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue @@ -22,7 +22,7 @@ import { helpPagePath } from '~/helpers/help_page_helper'; import AccessorUtils from '~/lib/utils/accessor'; import { __ } from '~/locale'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; -import { sanitizeUrl } from '~/lib/utils/url_utility'; +import { sanitizeUrl, joinPaths } from '~/lib/utils/url_utility'; import { trackErrorListViewsOptions, trackErrorStatusUpdateOptions, @@ -225,7 +225,7 @@ export default { if (!isValidErrorId(errorId)) { return 'about:blank'; } - return `error_tracking/${errorId}/details`; + return joinPaths(this.listPath, errorId, 'details'); }, goToNextPage() { this.pageValue = this.$options.NEXT_PAGE; diff --git a/app/assets/javascripts/kubernetes_dashboard/components/workload_stats.vue b/app/assets/javascripts/kubernetes_dashboard/components/workload_stats.vue new file mode 100644 index 00000000000..31b931e1855 --- /dev/null +++ b/app/assets/javascripts/kubernetes_dashboard/components/workload_stats.vue @@ -0,0 +1,27 @@ +<script> +import { GlSingleStat } from '@gitlab/ui/dist/charts'; + +export default { + components: { + GlSingleStat, + }, + props: { + stats: { + type: Array, + required: true, + }, + }, +}; +</script> + +<template> + <div class="gl-display-flex gl-flex-wrap gl-sm-flex-nowrap gl-mx-n3 gl-mt-n3"> + <gl-single-stat + v-for="(stat, index) in stats" + :key="index" + class="gl-w-full gl-flex-direction-column gl-align-items-center gl-justify-content-center gl-bg-white gl-border gl-border-gray-a-08 gl-mx-3 gl-p-3 gl-mt-3" + :value="stat.value" + :title="stat.title" + /> + </div> +</template> diff --git a/app/assets/javascripts/kubernetes_dashboard/constants.js b/app/assets/javascripts/kubernetes_dashboard/constants.js new file mode 100644 index 00000000000..2aeb2a4c113 --- /dev/null +++ b/app/assets/javascripts/kubernetes_dashboard/constants.js @@ -0,0 +1,13 @@ +import { s__ } from '~/locale'; + +export const PHASE_RUNNING = 'Running'; +export const PHASE_PENDING = 'Pending'; +export const PHASE_SUCCEEDED = 'Succeeded'; +export const PHASE_FAILED = 'Failed'; + +export const STATUS_LABELS = { + [PHASE_RUNNING]: s__('KubernetesDashboard|Running'), + [PHASE_PENDING]: s__('KubernetesDashboard|Pending'), + [PHASE_SUCCEEDED]: s__('KubernetesDashboard|Succeeded'), + [PHASE_FAILED]: s__('KubernetesDashboard|Failed'), +}; diff --git a/app/assets/javascripts/kubernetes_dashboard/graphql/client.js b/app/assets/javascripts/kubernetes_dashboard/graphql/client.js new file mode 100644 index 00000000000..0b9de9c2a61 --- /dev/null +++ b/app/assets/javascripts/kubernetes_dashboard/graphql/client.js @@ -0,0 +1,25 @@ +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; +import typeDefs from '~/environments/graphql/typedefs.graphql'; +import k8sPodsQuery from './queries/k8s_dashboard_pods.query.graphql'; +import { resolvers } from './resolvers'; + +export const apolloProvider = () => { + const defaultClient = createDefaultClient(resolvers, { + typeDefs, + }); + const { cache } = defaultClient; + + cache.writeQuery({ + query: k8sPodsQuery, + data: { + status: { + phase: 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 new file mode 100644 index 00000000000..22ddd7c6a61 --- /dev/null +++ b/app/assets/javascripts/kubernetes_dashboard/graphql/helpers/resolver_helpers.js @@ -0,0 +1,68 @@ +import { CoreV1Api, Configuration, WatchApi, EVENT_DATA } from '@gitlab/cluster-client'; + +export const handleClusterError = async (err) => { + if (!err.response) { + throw err; + } + + const errorData = await err.response.json(); + throw errorData; +}; + +export const watchPods = ({ client, query, configuration, namespace }) => { + const path = namespace ? `/api/v1/namespaces/${namespace}/pods` : '/api/v1/pods'; + const config = new Configuration(configuration); + const watcherApi = new WatchApi(config); + + watcherApi + .subscribeToStream(path, { watch: true }) + .then((watcher) => { + let result = []; + + watcher.on(EVENT_DATA, (data) => { + result = data.map((item) => { + return { status: { phase: item.status.phase } }; + }); + + client.writeQuery({ + query, + variables: { configuration, namespace }, + data: { k8sPods: result }, + }); + }); + }) + .catch((err) => { + handleClusterError(err); + }); +}; + +export const getK8sPods = ({ + client, + query, + configuration, + namespace = '', + enableWatch = false, +}) => { + const config = new Configuration(configuration); + + const coreV1Api = new CoreV1Api(config); + const podsApi = namespace + ? coreV1Api.listCoreV1NamespacedPod({ namespace }) + : coreV1Api.listCoreV1PodForAllNamespaces(); + + return podsApi + .then((res) => { + if (enableWatch) { + watchPods({ client, query, configuration, namespace }); + } + + return res?.items || []; + }) + .catch(async (err) => { + try { + await handleClusterError(err); + } catch (error) { + throw new Error(error.message); + } + }); +}; diff --git a/app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_pods.query.graphql b/app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_pods.query.graphql new file mode 100644 index 00000000000..17d264d32ec --- /dev/null +++ b/app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_pods.query.graphql @@ -0,0 +1,7 @@ +query getK8sDashboardPods($configuration: LocalConfiguration) { + k8sPods(configuration: $configuration) @client { + status { + phase + } + } +} diff --git a/app/assets/javascripts/kubernetes_dashboard/graphql/resolvers.js b/app/assets/javascripts/kubernetes_dashboard/graphql/resolvers.js new file mode 100644 index 00000000000..b99ffff5bd1 --- /dev/null +++ b/app/assets/javascripts/kubernetes_dashboard/graphql/resolvers.js @@ -0,0 +1,7 @@ +import kubernetesQueries from './resolvers/kubernetes'; + +export const resolvers = { + Query: { + ...kubernetesQueries, + }, +}; diff --git a/app/assets/javascripts/kubernetes_dashboard/graphql/resolvers/kubernetes.js b/app/assets/javascripts/kubernetes_dashboard/graphql/resolvers/kubernetes.js new file mode 100644 index 00000000000..db289541105 --- /dev/null +++ b/app/assets/javascripts/kubernetes_dashboard/graphql/resolvers/kubernetes.js @@ -0,0 +1,9 @@ +import { getK8sPods } from '../helpers/resolver_helpers'; +import k8sDashboardPodsQuery from '../queries/k8s_dashboard_pods.query.graphql'; + +export default { + k8sPods(_, { configuration }, { client }) { + const query = k8sDashboardPodsQuery; + return getK8sPods({ client, query, configuration }); + }, +}; diff --git a/app/assets/javascripts/kubernetes_dashboard/init_kubernetes_dashboard.js b/app/assets/javascripts/kubernetes_dashboard/init_kubernetes_dashboard.js index 2299207ce29..6c8e8a2eb31 100644 --- a/app/assets/javascripts/kubernetes_dashboard/init_kubernetes_dashboard.js +++ b/app/assets/javascripts/kubernetes_dashboard/init_kubernetes_dashboard.js @@ -1,7 +1,13 @@ import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import { removeLastSlashInUrlPath } from '~/lib/utils/url_utility'; +import csrf from '~/lib/utils/csrf'; +import { apolloProvider as createApolloProvider } from './graphql/client'; import App from './pages/app.vue'; import createRouter from './router/index'; +Vue.use(VueApollo); + const initKubernetesDashboard = () => { const el = document.querySelector('.js-kubernetes-app'); @@ -9,7 +15,18 @@ const initKubernetesDashboard = () => { return null; } - const { basePath, agent } = el.dataset; + const { basePath, agent, kasTunnelUrl } = el.dataset; + const agentObject = JSON.parse(agent); + + const configuration = { + basePath: removeLastSlashInUrlPath(kasTunnelUrl), + headers: { + 'GitLab-Agent-Id': agentObject.id, + 'Content-Type': 'application/json', + ...csrf.headers, + }, + credentials: 'include', + }; const router = createRouter({ base: basePath, @@ -19,8 +36,10 @@ const initKubernetesDashboard = () => { el, name: 'KubernetesDashboardRoot', router, + apolloProvider: createApolloProvider(), provide: { - agent: JSON.parse(agent), + agent: agentObject, + configuration, }, render: (createElement) => createElement(App), }); diff --git a/app/assets/javascripts/kubernetes_dashboard/pages/app.vue b/app/assets/javascripts/kubernetes_dashboard/pages/app.vue index d8cb9ccd5fa..e135afed9fe 100644 --- a/app/assets/javascripts/kubernetes_dashboard/pages/app.vue +++ b/app/assets/javascripts/kubernetes_dashboard/pages/app.vue @@ -8,5 +8,6 @@ export default { <template> <div class="gl-mt-5"> <page-title> {{ $route.meta.title }} </page-title> + <router-view /> </div> </template> diff --git a/app/assets/javascripts/kubernetes_dashboard/pages/pods_page.vue b/app/assets/javascripts/kubernetes_dashboard/pages/pods_page.vue new file mode 100644 index 00000000000..5d2c3252c15 --- /dev/null +++ b/app/assets/javascripts/kubernetes_dashboard/pages/pods_page.vue @@ -0,0 +1,81 @@ +<script> +import { GlLoadingIcon, GlAlert } from '@gitlab/ui'; +import WorkloadStats from '../components/workload_stats.vue'; +import k8sPodsQuery from '../graphql/queries/k8s_dashboard_pods.query.graphql'; +import { + PHASE_RUNNING, + PHASE_PENDING, + PHASE_SUCCEEDED, + PHASE_FAILED, + STATUS_LABELS, +} from '../constants'; + +export default { + components: { + GlLoadingIcon, + GlAlert, + WorkloadStats, + }, + inject: ['configuration'], + apollo: { + k8sPods: { + query: k8sPodsQuery, + variables() { + return { + configuration: this.configuration, + }; + }, + update(data) { + return data?.k8sPods || []; + }, + error(err) { + this.errorMessage = err?.message; + }, + }, + }, + data() { + return { + errorMessage: '', + }; + }, + computed: { + podStats() { + return [ + { + value: this.countPodsByPhase(PHASE_RUNNING), + title: STATUS_LABELS[PHASE_RUNNING], + }, + { + value: this.countPodsByPhase(PHASE_PENDING), + title: STATUS_LABELS[PHASE_PENDING], + }, + { + value: this.countPodsByPhase(PHASE_SUCCEEDED), + title: STATUS_LABELS[PHASE_SUCCEEDED], + }, + { + value: this.countPodsByPhase(PHASE_FAILED), + title: STATUS_LABELS[PHASE_FAILED], + }, + ]; + }, + loading() { + return this.$apollo?.queries?.k8sPods?.loading; + }, + }, + methods: { + countPodsByPhase(phase) { + const filteredPods = this.k8sPods?.filter((item) => item.status.phase === phase) || []; + + return filteredPods.length; + }, + }, +}; +</script> +<template> + <gl-loading-icon v-if="loading" /> + <gl-alert v-else-if="errorMessage" variant="danger" :dismissible="false" class="gl-mb-5"> + {{ errorMessage }} + </gl-alert> + <workload-stats v-else :stats="podStats" /> +</template> diff --git a/app/assets/javascripts/kubernetes_dashboard/router/routes.js b/app/assets/javascripts/kubernetes_dashboard/router/routes.js index 4c2ff299912..33ae36716ff 100644 --- a/app/assets/javascripts/kubernetes_dashboard/router/routes.js +++ b/app/assets/javascripts/kubernetes_dashboard/router/routes.js @@ -1,10 +1,12 @@ import { s__ } from '~/locale'; +import PodsPage from '../pages/pods_page.vue'; import { PODS_ROUTE_NAME, PODS_ROUTE_PATH } from './constants'; export default [ { name: PODS_ROUTE_NAME, path: PODS_ROUTE_PATH, + component: PodsPage, meta: { title: s__('KubernetesDashboard|Pods'), }, diff --git a/app/assets/javascripts/repository/mixins/highlight_mixin.js b/app/assets/javascripts/repository/mixins/highlight_mixin.js index fa4f0f48512..422a84dff40 100644 --- a/app/assets/javascripts/repository/mixins/highlight_mixin.js +++ b/app/assets/javascripts/repository/mixins/highlight_mixin.js @@ -3,6 +3,7 @@ import { EVENT_ACTION, EVENT_LABEL_FALLBACK, LINES_PER_CHUNK, + ROUGE_TO_HLJS_LANGUAGE_MAP, } from '~/vue_shared/components/source_viewer/constants'; import { splitIntoChunks } from '~/vue_shared/components/source_viewer/workers/highlight_utils'; import LineHighlighter from '~/blob/line_highlighter'; @@ -29,8 +30,9 @@ export default { this.track(EVENT_ACTION, { label, property: language }); }, isUnsupportedLanguage(language) { + const mappedLanguage = ROUGE_TO_HLJS_LANGUAGE_MAP[language]; const supportedLanguages = Object.keys(languageLoader); - const isUnsupportedLanguage = !supportedLanguages.includes(language); + const isUnsupportedLanguage = !supportedLanguages.includes(mappedLanguage); return LEGACY_FALLBACKS.includes(language) || isUnsupportedLanguage; }, diff --git a/app/assets/javascripts/super_sidebar/components/help_center.vue b/app/assets/javascripts/super_sidebar/components/help_center.vue index 752f077ca02..e354d82b76a 100644 --- a/app/assets/javascripts/super_sidebar/components/help_center.vue +++ b/app/assets/javascripts/super_sidebar/components/help_center.vue @@ -14,9 +14,6 @@ import { STORAGE_KEY } from '~/whats_new/utils/notification'; import Tracking from '~/tracking'; import { DROPDOWN_Y_OFFSET, HELP_MENU_TRACKING_DEFAULTS, helpCenterState } from '../constants'; -// Left offset required for the dropdown to be aligned with the super sidebar -const DROPDOWN_X_OFFSET = -4; - export default { components: { GlBadge, @@ -204,7 +201,7 @@ export default { }); }, }, - dropdownOffset: { mainAxis: DROPDOWN_Y_OFFSET, crossAxis: DROPDOWN_X_OFFSET }, + dropdownOffset: { mainAxis: DROPDOWN_Y_OFFSET }, }; </script> diff --git a/app/finders/autocomplete/group_users_finder.rb b/app/finders/autocomplete/group_users_finder.rb index b24f3f7f032..749d7821f44 100644 --- a/app/finders/autocomplete/group_users_finder.rb +++ b/app/finders/autocomplete/group_users_finder.rb @@ -12,8 +12,9 @@ module Autocomplete class GroupUsersFinder include Gitlab::Utils::StrongMemoize - def initialize(group:) + def initialize(group:, members_relation: false) @group = group + @members_relation = members_relation # If true, it will return Member relation instead end def execute @@ -22,6 +23,8 @@ module Autocomplete .with(descendant_projects_cte.to_arel) # rubocop:disable CodeReuse/ActiveRecord .from_union(member_relations, remove_duplicates: false) + return members if @members_relation + User .id_in(members.select(:user_id)) .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/420387") @@ -31,11 +34,11 @@ module Autocomplete def member_relations [ - members_from_group_hierarchy.select(:user_id), - members_from_hierarchy_group_shares.select(:user_id), - members_from_descendant_projects.select(:user_id), - members_from_descendant_project_shares.select(:user_id) - ] + members_from_group_hierarchy, + members_from_hierarchy_group_shares, + members_from_descendant_projects, + members_from_descendant_project_shares + ].map { |relation| relation.select(:id, :user_id) } end def members_from_group_hierarchy diff --git a/app/finders/autocomplete/users_finder.rb b/app/finders/autocomplete/users_finder.rb index e7a24cde2bd..ddd68524a04 100644 --- a/app/finders/autocomplete/users_finder.rb +++ b/app/finders/autocomplete/users_finder.rb @@ -9,6 +9,7 @@ module Autocomplete # consistent and removes the need for implementing keyset pagination to # ensure good performance. LIMIT = 20 + BATCH_SIZE = 1000 attr_reader :current_user, :project, :group, :search, :author_id, :todo_filter, :todo_state_filter, @@ -62,7 +63,19 @@ module Autocomplete # When changing the order of these method calls, make sure that # reorder_by_name() is called _before_ optionally_search(), otherwise # reorder_by_name will break the ORDER BY applied in optionally_search(). - find_users + if project + project.authorized_users.union_with_user(author_id).merge(users_relation) + elsif group + find_group_users(group) + elsif current_user + users_relation + else + User.none + end.to_a + end + + def users_relation + User .where(state: states) .non_internal .reorder_by_name @@ -73,7 +86,6 @@ module Autocomplete todo_state: todo_state_filter ) .limit(LIMIT) - .to_a end # rubocop: enable CodeReuse/ActiveRecord @@ -85,15 +97,33 @@ module Autocomplete author_id.present? && current_user end - def find_users - if project - project.authorized_users.union_with_user(author_id) - elsif group - ::Autocomplete::GroupUsersFinder.new(group: group).execute # rubocop: disable CodeReuse/Finder - elsif current_user - User.all + def find_group_users(group) + if Feature.enabled?(:group_users_autocomplete_using_batch_reduction, current_user) + members_relation = ::Autocomplete::GroupUsersFinder.new(group: group, members_relation: true).execute # rubocop: disable CodeReuse/Finder + + user_ids = Set.new + members_relation.each_batch(of: BATCH_SIZE) do |relation| + user_ids += relation.pluck_user_ids + end + + user_relations = [] + user_ids.to_a.in_groups_of(BATCH_SIZE, false) do |batch_user_ids| + user_relations << users_relation.id_in(batch_user_ids) + end + + # When there is more than 1 batch, we need to apply users_relation again on + # the top results per-batch so that we return results in the correct order. + if user_relations.empty? + User.none + elsif user_relations.one? + user_relations.first + else + # We pluck the ids per batch so that we don't send an unbounded number of ids in one query + user_ids = user_relations.flat_map(&:pluck_primary_key) + users_relation.id_in(user_ids) + end else - User.none + ::Autocomplete::GroupUsersFinder.new(group: group).execute.merge(users_relation) # rubocop: disable CodeReuse/Finder end end diff --git a/app/helpers/ci/jobs_helper.rb b/app/helpers/ci/jobs_helper.rb index 216a8bc8fa1..5e902ce7bc6 100644 --- a/app/helpers/ci/jobs_helper.rb +++ b/app/helpers/ci/jobs_helper.rb @@ -12,7 +12,9 @@ module Ci "runner_settings_url" => project_runners_path(build.project, anchor: 'js-runners-settings'), "build_status" => build.status, "build_stage" => build.stage_name, - "retry_outdated_job_docs_url" => help_page_path('ci/pipelines/settings', anchor: 'retry-outdated-jobs') + "retry_outdated_job_docs_url" => help_page_path('ci/pipelines/settings', anchor: 'retry-outdated-jobs'), + "test_report_summary_url" => test_report_summary_project_job_path(project, build, format: :json), + "pipeline_test_report_url" => test_report_project_pipeline_path(project, build.pipeline) } end diff --git a/app/models/ci/build_need.rb b/app/models/ci/build_need.rb index fb0a02ca56e..54a54c42fd1 100644 --- a/app/models/ci/build_need.rb +++ b/app/models/ci/build_need.rb @@ -4,13 +4,10 @@ module Ci class BuildNeed < Ci::ApplicationRecord include Ci::Partitionable include IgnorableColumns - include SafelyChangeColumnDefault include BulkInsertSafe MAX_JOB_NAME_LENGTH = 255 - columns_changing_default :partition_id - belongs_to :build, ->(need) { in_partition(need) }, class_name: 'Ci::Processable', diff --git a/app/models/ci/build_pending_state.rb b/app/models/ci/build_pending_state.rb index 0b88f745d78..966884ae158 100644 --- a/app/models/ci/build_pending_state.rb +++ b/app/models/ci/build_pending_state.rb @@ -2,9 +2,6 @@ class Ci::BuildPendingState < Ci::ApplicationRecord include Ci::Partitionable - include SafelyChangeColumnDefault - - columns_changing_default :partition_id belongs_to :build, class_name: 'Ci::Build', foreign_key: :build_id, inverse_of: :pending_state diff --git a/app/models/ci/build_report_result.rb b/app/models/ci/build_report_result.rb index 1dabf05e03c..bf58ebfd568 100644 --- a/app/models/ci/build_report_result.rb +++ b/app/models/ci/build_report_result.rb @@ -3,9 +3,6 @@ module Ci class BuildReportResult < Ci::ApplicationRecord include Ci::Partitionable - include SafelyChangeColumnDefault - - columns_changing_default :partition_id self.primary_key = :build_id diff --git a/app/models/ci/build_runner_session.rb b/app/models/ci/build_runner_session.rb index 223cb9c5d68..c102b95a6a4 100644 --- a/app/models/ci/build_runner_session.rb +++ b/app/models/ci/build_runner_session.rb @@ -5,9 +5,6 @@ module Ci # Data will be removed after transitioning from running to any state. class BuildRunnerSession < Ci::ApplicationRecord include Ci::Partitionable - include SafelyChangeColumnDefault - - columns_changing_default :partition_id TERMINAL_SUBPROTOCOL = 'terminal.gitlab.com' DEFAULT_SERVICE_NAME = 'build' diff --git a/app/models/ci/build_trace_chunk.rb b/app/models/ci/build_trace_chunk.rb index 0a0f401c9d5..7ab89533d76 100644 --- a/app/models/ci/build_trace_chunk.rb +++ b/app/models/ci/build_trace_chunk.rb @@ -8,9 +8,6 @@ module Ci include ::Checksummable include ::Gitlab::ExclusiveLeaseHelpers include ::Gitlab::OptimisticLocking - include SafelyChangeColumnDefault - - columns_changing_default :partition_id belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id, inverse_of: :trace_chunks diff --git a/app/models/ci/build_trace_metadata.rb b/app/models/ci/build_trace_metadata.rb index 859dad29a06..ac0d7ce4e76 100644 --- a/app/models/ci/build_trace_metadata.rb +++ b/app/models/ci/build_trace_metadata.rb @@ -3,9 +3,6 @@ module Ci class BuildTraceMetadata < Ci::ApplicationRecord include Ci::Partitionable - include SafelyChangeColumnDefault - - columns_changing_default :partition_id MAX_ATTEMPTS = 5 self.table_name = 'ci_build_trace_metadata' diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index fe4437a4ad6..d3705265d91 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -13,9 +13,6 @@ module Ci include FileStoreMounter include EachBatch include Gitlab::Utils::StrongMemoize - include SafelyChangeColumnDefault - - columns_changing_default :partition_id enum accessibility: { public: 0, private: 1 }, _suffix: true diff --git a/app/models/ci/job_variable.rb b/app/models/ci/job_variable.rb index 21c9842399e..573999995bc 100644 --- a/app/models/ci/job_variable.rb +++ b/app/models/ci/job_variable.rb @@ -5,11 +5,8 @@ module Ci include Ci::Partitionable include Ci::NewHasVariable include Ci::RawVariable - include SafelyChangeColumnDefault include BulkInsertSafe - columns_changing_default :partition_id - belongs_to :job, class_name: "Ci::Build", foreign_key: :job_id, inverse_of: :job_variables partitionable scope: :job diff --git a/app/models/ci/pending_build.rb b/app/models/ci/pending_build.rb index 80cc74a9946..1e628381233 100644 --- a/app/models/ci/pending_build.rb +++ b/app/models/ci/pending_build.rb @@ -4,9 +4,6 @@ module Ci class PendingBuild < Ci::ApplicationRecord include EachBatch include Ci::Partitionable - include SafelyChangeColumnDefault - - columns_changing_default :partition_id belongs_to :project diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 0a910d6b125..f771c264651 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -17,9 +17,6 @@ module Ci include UpdatedAtFilterable include EachBatch include FastDestroyAll::Helpers - include SafelyChangeColumnDefault - - columns_changing_default :partition_id include IgnorableColumns ignore_column :id_convert_to_bigint, remove_with: '16.3', remove_after: '2023-08-22' diff --git a/app/models/ci/pipeline_variable.rb b/app/models/ci/pipeline_variable.rb index a422aaa7daa..8c248c77130 100644 --- a/app/models/ci/pipeline_variable.rb +++ b/app/models/ci/pipeline_variable.rb @@ -6,9 +6,7 @@ module Ci include Ci::HasVariable include Ci::RawVariable include IgnorableColumns - include SafelyChangeColumnDefault - columns_changing_default :partition_id ignore_column :pipeline_id_convert_to_bigint, remove_with: '16.5', remove_after: '2023-10-22' belongs_to :pipeline diff --git a/app/models/ci/running_build.rb b/app/models/ci/running_build.rb index 22753930b3c..e70ba3c97c3 100644 --- a/app/models/ci/running_build.rb +++ b/app/models/ci/running_build.rb @@ -10,9 +10,6 @@ module Ci # of the running builds there is worth the additional pressure. class RunningBuild < Ci::ApplicationRecord include Ci::Partitionable - include SafelyChangeColumnDefault - - columns_changing_default :partition_id partitionable scope: :build diff --git a/app/models/ci/sources/pipeline.rb b/app/models/ci/sources/pipeline.rb index 6c9d3b9c19f..51c8fb787c7 100644 --- a/app/models/ci/sources/pipeline.rb +++ b/app/models/ci/sources/pipeline.rb @@ -5,9 +5,6 @@ module Ci class Pipeline < Ci::ApplicationRecord include Ci::Partitionable include Ci::NamespacedModelName - include SafelyChangeColumnDefault - - columns_changing_default :partition_id, :source_partition_id self.table_name = "ci_sources_pipelines" diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index e413ed8a668..becb8f204bf 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -7,11 +7,8 @@ module Ci include Ci::HasStatus include Gitlab::OptimisticLocking include Presentable - include SafelyChangeColumnDefault include IgnorableColumns - columns_changing_default :partition_id - ignore_column :pipeline_id_convert_to_bigint, remove_with: '16.6', remove_after: '2023-10-22' partitionable scope: :pipeline diff --git a/app/models/ci/unit_test_failure.rb b/app/models/ci/unit_test_failure.rb index bec74d8be20..97e07463921 100644 --- a/app/models/ci/unit_test_failure.rb +++ b/app/models/ci/unit_test_failure.rb @@ -3,9 +3,6 @@ module Ci class UnitTestFailure < Ci::ApplicationRecord include Ci::Partitionable - include SafelyChangeColumnDefault - - columns_changing_default :partition_id REPORT_WINDOW = 14.days diff --git a/app/models/repository.rb b/app/models/repository.rb index a6160a4c3b4..72a40ce5f01 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -1287,6 +1287,17 @@ class Repository paths - generated_files end + def object_format + return unless exists? + + case raw.object_format + when :OBJECT_FORMAT_SHA1 + FORMAT_SHA1 + when :OBJECT_FORMAT_SHA256 + FORMAT_SHA256 + end + end + private def ancestor_cache_key(ancestor_id, descendant_id) diff --git a/app/models/user.rb b/app/models/user.rb index bb3b0100591..a7c90765be1 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1814,6 +1814,8 @@ class User < MainClusterwide::ApplicationRecord end def owns_runner?(runner) + runner = runner.__getobj__ if runner.is_a?(Ci::RunnerPresenter) + ci_owned_runners.include?(runner) end diff --git a/app/views/clusters/agents/dashboard/show.html.haml b/app/views/clusters/agents/dashboard/show.html.haml index adf71863669..5b6cfdf8c03 100644 --- a/app/views/clusters/agents/dashboard/show.html.haml +++ b/app/views/clusters/agents/dashboard/show.html.haml @@ -2,5 +2,5 @@ - add_to_breadcrumbs s_('KubernetesDashboard|Agents'), kubernetes_dashboard_path - page_title s_('KubernetesDashboard|Dashboard') -.js-kubernetes-app{ data: { base_path: kubernetes_dashboard_path, agent: @agent.to_json } } +.js-kubernetes-app{ data: { base_path: kubernetes_dashboard_path, agent: @agent.to_json, kas_tunnel_url: ::Gitlab::Kas.tunnel_url } } = gl_loading_icon(css_class: 'gl-my-5', size: 'md') diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index 0753a021f1f..bb29ea92704 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -1,4 +1,4 @@ -- action = current_action?(:edit) || current_action?(:update) ? 'edit' : 'create' +- action = current_action?(:edit, :update) ? 'edit' : 'create' - file_name = params[:id].split("/").last ||= "" - is_markdown = Gitlab::MarkupHelper.gitlab_markdown?(file_name) @@ -6,12 +6,17 @@ .js-file-title.file-title.gl-display-flex.gl-align-items-center.gl-rounded-top-base{ data: { current_action: action } } .editor-ref.block-truncated.has-tooltip{ title: ref } = sprite_icon('branch', size: 12) - = ref - - if current_action?(:edit) || current_action?(:update) + - if current_action?(:edit, :update) + %span#editor_ref + = ref + - if current_action?(:new, :create) + %span#editor_path + = params[:id] + - if current_action?(:edit, :update) - input_options = { id: 'file_path', name: 'file_path', value: (params[:file_path] || @path), class: 'new-file-path js-file-path-name-input' } = render 'filepath_form', input_options: input_options - - if current_action?(:new) || current_action?(:create) + - if current_action?(:new, :create) - input_options = { id: 'file_name', name: 'file_name', value: params[:file_name] || (should_suggest_gitlab_ci_yml? ? '.gitlab-ci.yml' : ''), required: true, placeholder: "Filename", testid: 'file-name-field', class: 'new-file-name js-file-path-name-input' } = render 'filepath_form', input_options: input_options - if should_suggest_gitlab_ci_yml? |