diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-11-09 03:42:22 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-11-09 03:42:22 +0000 |
commit | 7dd9256e5eac896fb422566376086a0befc79151 (patch) | |
tree | be65227875ddb0cff4961ae2d97380bbf64dd851 /app/assets/javascripts | |
parent | 2295d352f6073101497f9bf4e4981c7ae72706a3 (diff) | |
download | gitlab-ce-7dd9256e5eac896fb422566376086a0befc79151.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts')
25 files changed, 413 insertions, 140 deletions
diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue index 64f598b4064..45381ab1021 100644 --- a/app/assets/javascripts/boards/components/board_form.vue +++ b/app/assets/javascripts/boards/components/board_form.vue @@ -2,10 +2,10 @@ import { GlModal, GlAlert } from '@gitlab/ui'; import { mapGetters, mapActions, mapState } from 'vuex'; import { TYPE_USER, TYPE_ITERATION, TYPE_MILESTONE } from '~/graphql_shared/constants'; -import { convertToGraphQLId } from '~/graphql_shared/utils'; +import { convertToGraphQLId, getZeroBasedIdFromGraphQLId } from '~/graphql_shared/utils'; import { getParameterByName, visitUrl } from '~/lib/utils/url_utility'; import { __, s__ } from '~/locale'; -import { fullLabelId, fullBoardId } from '../boards_util'; +import { fullLabelId } from '../boards_util'; import { formType } from '../constants'; import createBoardMutation from '../graphql/board_create.mutation.graphql'; @@ -18,11 +18,11 @@ const boardDefaults = { name: '', labels: [], milestone: {}, - iteration_id: undefined, + iteration: {}, assignee: {}, weight: null, - hide_backlog_list: false, - hide_closed_list: false, + hideBacklogList: false, + hideClosedList: false, }; export default { @@ -144,17 +144,16 @@ export default { return destroyBoardMutation; }, baseMutationVariables() { - const { board } = this; - const variables = { - name: board.name, - hideBacklogList: board.hide_backlog_list, - hideClosedList: board.hide_closed_list, - }; + const { + board: { name, hideBacklogList, hideClosedList, id }, + } = this; + + const variables = { name, hideBacklogList, hideClosedList }; - return board.id + return id ? { ...variables, - id: fullBoardId(board.id), + id, } : { ...variables, @@ -168,11 +167,13 @@ export default { assigneeId: this.board.assignee?.id ? convertToGraphQLId(TYPE_USER, this.board.assignee.id) : null, + // Temporarily converting to milestone ID due to https://gitlab.com/gitlab-org/gitlab/-/issues/344779 milestoneId: this.board.milestone?.id - ? convertToGraphQLId(TYPE_MILESTONE, this.board.milestone.id) + ? convertToGraphQLId(TYPE_MILESTONE, getZeroBasedIdFromGraphQLId(this.board.milestone.id)) : null, - iterationId: this.board.iteration_id - ? convertToGraphQLId(TYPE_ITERATION, this.board.iteration_id) + // Temporarily converting to iteration ID due to https://gitlab.com/gitlab-org/gitlab/-/issues/344779 + iterationId: this.board.iteration?.id + ? convertToGraphQLId(TYPE_ITERATION, getZeroBasedIdFromGraphQLId(this.board.iteration.id)) : null, }; }, @@ -226,7 +227,7 @@ export default { await this.$apollo.mutate({ mutation: this.deleteMutation, variables: { - id: fullBoardId(this.board.id), + id: this.board.id, }, }); }, @@ -262,7 +263,9 @@ export default { } }, setIteration(iterationId) { - this.board.iteration_id = iterationId; + this.$set(this.board, 'iteration', { + id: iterationId, + }); }, setBoardLabels(labels) { this.board.labels = labels; @@ -329,8 +332,8 @@ export default { </div> <board-configuration-options - :hide-backlog-list.sync="board.hide_backlog_list" - :hide-closed-list.sync="board.hide_closed_list" + :hide-backlog-list.sync="board.hideBacklogList" + :hide-closed-list.sync="board.hideClosedList" :readonly="readonly" /> diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue index 84de37c2ef1..2f2ac5409e0 100644 --- a/app/assets/javascripts/boards/components/boards_selector.vue +++ b/app/assets/javascripts/boards/components/boards_selector.vue @@ -9,17 +9,20 @@ import { GlModalDirective, } from '@gitlab/ui'; import { throttle } from 'lodash'; -import { mapGetters, mapState } from 'vuex'; +import { mapActions, mapGetters, mapState } from 'vuex'; import BoardForm from 'ee_else_ce/boards/components/board_form.vue'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import axios from '~/lib/utils/axios_utils'; import httpStatusCodes from '~/lib/utils/http_status'; +import { s__ } from '~/locale'; import eventHub from '../eventhub'; -import groupQuery from '../graphql/group_boards.query.graphql'; -import projectQuery from '../graphql/project_boards.query.graphql'; +import groupBoardsQuery from '../graphql/group_boards.query.graphql'; +import projectBoardsQuery from '../graphql/project_boards.query.graphql'; +import groupBoardQuery from '../graphql/group_board.query.graphql'; +import projectBoardQuery from '../graphql/project_board.query.graphql'; const MIN_BOARDS_TO_VIEW_RECENT = 10; @@ -39,10 +42,6 @@ export default { }, inject: ['fullPath', 'recentBoardsEndpoint'], props: { - currentBoard: { - type: Object, - required: true, - }, throttleDuration: { type: Number, default: 200, @@ -86,14 +85,47 @@ export default { maxPosition: 0, filterTerm: '', currentPage: '', + board: {}, }; }, + apollo: { + board: { + query() { + return this.currentBoardQuery; + }, + variables() { + return { + fullPath: this.fullPath, + boardId: this.fullBoardId, + }; + }, + update(data) { + const board = data.workspace?.board; + return { + ...board, + labels: board?.labels?.nodes, + }; + }, + error() { + this.setError({ message: this.$options.i18n.errorFetchingBoard }); + }, + }, + }, computed: { - ...mapState(['boardType']), - ...mapGetters(['isGroupBoard']), + ...mapState(['boardType', 'fullBoardId']), + ...mapGetters(['isGroupBoard', 'isProjectBoard']), parentType() { return this.boardType; }, + currentBoardQueryCE() { + return this.isGroupBoard ? groupBoardQuery : projectBoardQuery; + }, + currentBoardQuery() { + return this.currentBoardQueryCE; + }, + isBoardLoading() { + return this.$apollo.queries.board.loading; + }, loading() { return this.loadingRecentBoards || Boolean(this.loadingBoards); }, @@ -102,9 +134,6 @@ export default { board.name.toLowerCase().includes(this.filterTerm.toLowerCase()), ); }, - board() { - return this.currentBoard; - }, showCreate() { return this.multipleIssueBoardsAvailable; }, @@ -137,6 +166,7 @@ export default { eventHub.$off('showBoardModal', this.showPage); }, methods: { + ...mapActions(['setError']), showPage(page) { this.currentPage = page; }, @@ -153,7 +183,7 @@ export default { })); }, boardQuery() { - return this.isGroupBoard ? groupQuery : projectQuery; + return this.isGroupBoard ? groupBoardsQuery : projectBoardsQuery; }, loadBoards(toggleDropdown = true) { if (toggleDropdown && this.boards.length > 0) { @@ -229,13 +259,18 @@ export default { this.hasScrollFade = this.isScrolledUp(); }, }, + i18n: { + errorFetchingBoard: s__('Board|An error occurred while fetching the board, please try again.'), + }, }; </script> <template> <div class="boards-switcher js-boards-selector gl-mr-3"> <span class="boards-selector-wrapper js-boards-selector-wrapper"> + <gl-loading-icon v-if="isBoardLoading" size="md" class="gl-mt-2" /> <gl-dropdown + v-else data-qa-selector="boards_dropdown" toggle-class="dropdown-menu-toggle js-dropdown-toggle" menu-class="flex-column dropdown-extended-height" @@ -336,7 +371,7 @@ export default { :can-admin-board="canAdminBoard" :scoped-issue-board-feature-enabled="scopedIssueBoardFeatureEnabled" :weights="weights" - :current-board="currentBoard" + :current-board="board" :current-page="currentPage" @cancel="cancel" /> diff --git a/app/assets/javascripts/boards/graphql/board_scope.fragment.graphql b/app/assets/javascripts/boards/graphql/board_scope.fragment.graphql new file mode 100644 index 00000000000..57f51822d91 --- /dev/null +++ b/app/assets/javascripts/boards/graphql/board_scope.fragment.graphql @@ -0,0 +1,6 @@ +fragment BoardScopeFragment on Board { + id + name + hideBacklogList + hideClosedList +} diff --git a/app/assets/javascripts/boards/graphql/group_board.query.graphql b/app/assets/javascripts/boards/graphql/group_board.query.graphql new file mode 100644 index 00000000000..77c8e0378f0 --- /dev/null +++ b/app/assets/javascripts/boards/graphql/group_board.query.graphql @@ -0,0 +1,9 @@ +#import "ee_else_ce/boards/graphql/board_scope.fragment.graphql" + +query GroupBoard($fullPath: ID!, $boardId: ID!) { + workspace: group(fullPath: $fullPath) { + board(id: $boardId) { + ...BoardScopeFragment + } + } +} diff --git a/app/assets/javascripts/boards/graphql/project_board.query.graphql b/app/assets/javascripts/boards/graphql/project_board.query.graphql new file mode 100644 index 00000000000..6e4cd6bed57 --- /dev/null +++ b/app/assets/javascripts/boards/graphql/project_board.query.graphql @@ -0,0 +1,9 @@ +#import "ee_else_ce/boards/graphql/board_scope.fragment.graphql" + +query ProjectBoard($fullPath: ID!, $boardId: ID!) { + workspace: project(fullPath: $fullPath) { + board(id: $boardId) { + ...BoardScopeFragment + } + } +} diff --git a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js index 43e1faa0785..26dd8b99f98 100644 --- a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js +++ b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js @@ -32,7 +32,6 @@ export default (params = {}) => { data() { const boardsSelectorProps = { ...dataset, - currentBoard: JSON.parse(dataset.currentBoard), hasMissingBoards: parseBoolean(dataset.hasMissingBoards), canAdminBoard: parseBoolean(dataset.canAdminBoard), multipleIssueBoardsAvailable: parseBoolean(dataset.multipleIssueBoardsAvailable), diff --git a/app/assets/javascripts/clusters_list/components/agent_empty_state.vue b/app/assets/javascripts/clusters_list/components/agent_empty_state.vue index 405339b3d36..06092039eb7 100644 --- a/app/assets/javascripts/clusters_list/components/agent_empty_state.vue +++ b/app/assets/javascripts/clusters_list/components/agent_empty_state.vue @@ -1,8 +1,9 @@ <script> import { GlButton, GlEmptyState, GlLink, GlSprintf, GlAlert, GlModalDirective } from '@gitlab/ui'; -import { INSTALL_AGENT_MODAL_ID } from '../constants'; +import { INSTALL_AGENT_MODAL_ID, I18N_AGENTS_EMPTY_STATE } from '../constants'; export default { + i18n: I18N_AGENTS_EMPTY_STATE, modalId: INSTALL_AGENT_MODAL_ID, components: { GlButton, @@ -17,7 +18,7 @@ export default { inject: [ 'emptyStateImage', 'projectPath', - 'agentDocsUrl', + 'multipleClustersDocsUrl', 'installDocsUrl', 'getStartedDocsUrl', 'integrationDocsUrl', @@ -37,22 +38,19 @@ export default { </script> <template> - <gl-empty-state - :svg-path="emptyStateImage" - :title="s__('ClusterAgents|Integrate Kubernetes with a GitLab Agent')" - class="empty-state--agent" - > + <gl-empty-state :svg-path="emptyStateImage" title="" class="agents-empty-state"> <template #description> - <p class="mw-460 gl-mx-auto"> - <gl-sprintf - :message=" - s__( - 'ClusterAgents|The GitLab Kubernetes Agent allows an Infrastructure as Code, GitOps approach to integrating Kubernetes clusters with GitLab. %{linkStart}Learn more.%{linkEnd}', - ) - " - > + <p class="mw-460 gl-mx-auto gl-text-left"> + {{ $options.i18n.introText }} + </p> + <p class="mw-460 gl-mx-auto gl-text-left"> + <gl-sprintf :message="$options.i18n.multipleClustersText"> <template #link="{ content }"> - <gl-link :href="agentDocsUrl" target="_blank" data-testid="agent-docs-link"> + <gl-link + :href="multipleClustersDocsUrl" + target="_blank" + data-testid="multiple-clusters-docs-link" + > {{ content }} </gl-link> </template> @@ -60,19 +58,9 @@ export default { </p> <p class="mw-460 gl-mx-auto"> - <gl-sprintf - :message=" - s__( - 'ClusterAgents|The GitLab Agent also requires %{linkStart}enabling the Agent Server%{linkEnd}', - ) - " - > - <template #link="{ content }"> - <gl-link :href="installDocsUrl" target="_blank" data-testid="install-docs-link"> - {{ content }} - </gl-link> - </template> - </gl-sprintf> + <gl-link :href="installDocsUrl" target="_blank" data-testid="install-docs-link"> + {{ $options.i18n.learnMoreText }} + </gl-link> </p> <gl-alert @@ -81,11 +69,7 @@ export default { class="gl-mb-5 text-left" :dismissible="false" > - {{ - s__( - 'ClusterAgents|To install an Agent you should create an agent directory in the Repository first. We recommend that you add the Agent configuration to the directory before you start the installation process.', - ) - }} + {{ $options.i18n.warningText }} <template #actions> <gl-button @@ -95,10 +79,10 @@ export default { target="_blank" class="gl-ml-0!" > - {{ s__('ClusterAgents|Read more about getting started') }} + {{ $options.i18n.readMoreText }} </gl-button> <gl-button category="secondary" variant="info" :href="repositoryPath"> - {{ s__('ClusterAgents|Go to the repository') }} + {{ $options.i18n.repositoryButtonText }} </gl-button> </template> </gl-alert> @@ -110,9 +94,9 @@ export default { :disabled="!hasConfigurations" data-testid="integration-primary-button" category="primary" - variant="success" + variant="confirm" > - {{ s__('ClusterAgents|Integrate with the GitLab Agent') }} + {{ $options.i18n.primaryButtonText }} </gl-button> </template> </gl-empty-state> diff --git a/app/assets/javascripts/clusters_list/components/clusters_empty_state.vue b/app/assets/javascripts/clusters_list/components/clusters_empty_state.vue index fa13bdadc1b..1f153a4f38e 100644 --- a/app/assets/javascripts/clusters_list/components/clusters_empty_state.vue +++ b/app/assets/javascripts/clusters_list/components/clusters_empty_state.vue @@ -1,22 +1,16 @@ <script> -import { GlEmptyState, GlButton, GlLink } from '@gitlab/ui'; +import { GlEmptyState, GlButton, GlLink, GlSprintf } from '@gitlab/ui'; import { mapState } from 'vuex'; -import { s__ } from '~/locale'; import { helpPagePath } from '~/helpers/help_page_helper'; +import { I18N_CLUSTERS_EMPTY_STATE } from '../constants'; export default { - i18n: { - title: s__('ClusterIntegration|Integrate Kubernetes with a cluster certificate'), - description: s__( - 'ClusterIntegration|Kubernetes clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way.', - ), - learnMoreLinkText: s__('ClusterIntegration|Learn more about Kubernetes'), - buttonText: s__('ClusterIntegration|Integrate with a cluster certificate'), - }, + i18n: I18N_CLUSTERS_EMPTY_STATE, components: { GlEmptyState, GlButton, GlLink, + GlSprintf, }, inject: ['emptyStateHelpText', 'clustersEmptyStateImage', 'newClusterPath'], learnMoreHelpUrl: helpPagePath('user/project/clusters/index'), @@ -27,11 +21,24 @@ export default { </script> <template> - <gl-empty-state :svg-path="clustersEmptyStateImage" :title="$options.i18n.title"> + <gl-empty-state :svg-path="clustersEmptyStateImage" title=""> <template #description> - <p> + <p class="gl-text-left"> {{ $options.i18n.description }} </p> + <p class="gl-text-left"> + <gl-sprintf :message="$options.i18n.multipleClustersText"> + <template #link="{ content }"> + <gl-link + :href="$options.multipleClustersHelpUrl" + target="_blank" + data-testid="multiple-clusters-docs-link" + > + {{ content }} + </gl-link> + </template> + </gl-sprintf> + </p> <p v-if="emptyStateHelpText" data-testid="clusters-empty-state-text"> {{ emptyStateHelpText }} diff --git a/app/assets/javascripts/clusters_list/constants.js b/app/assets/javascripts/clusters_list/constants.js index ffa28f516e6..d77e183f95b 100644 --- a/app/assets/javascripts/clusters_list/constants.js +++ b/app/assets/javascripts/clusters_list/constants.js @@ -141,3 +141,30 @@ export const AGENT_STATUSES = { }, }, }; + +export const I18N_AGENTS_EMPTY_STATE = { + introText: s__( + 'ClusterAgents|Use GitLab Agents to more securely integrate with your clusters to deploy your applications, run your pipelines, use review apps and much more.', + ), + multipleClustersText: s__( + 'ClusterAgents|If you are setting up multiple clusters and are using Auto DevOps, %{linkStart}read about using multiple Kubernetes clusters first.%{linkEnd}', + ), + learnMoreText: s__('ClusterAgents|Learn more about the GitLab Kubernetes Agent.'), + warningText: s__( + 'ClusterAgents|To install an Agent you should create an agent directory in the Repository first. We recommend that you add the Agent configuration to the directory before you start the installation process.', + ), + readMoreText: s__('ClusterAgents|Read more about getting started'), + repositoryButtonText: s__('ClusterAgents|Go to the repository'), + primaryButtonText: s__('ClusterAgents|Connect with a GitLab Agent'), +}; + +export const I18N_CLUSTERS_EMPTY_STATE = { + description: s__( + 'ClusterIntegration|Use certificates to integrate with your clusters to deploy your applications, run your pipelines, use review apps and much more in an easy way.', + ), + multipleClustersText: s__( + 'ClusterIntegration|If you are setting up multiple clusters and are using Auto DevOps, %{linkStart}read about using multiple Kubernetes clusters first.%{linkEnd}', + ), + learnMoreLinkText: s__('ClusterIntegration|Learn more about the GitLab managed clusters'), + buttonText: s__('ClusterIntegration|Connect with a certificate'), +}; diff --git a/app/assets/javascripts/clusters_list/load_agents.js b/app/assets/javascripts/clusters_list/load_agents.js index 3f00cabccdb..e65de67515a 100644 --- a/app/assets/javascripts/clusters_list/load_agents.js +++ b/app/assets/javascripts/clusters_list/load_agents.js @@ -14,7 +14,7 @@ export default (Vue, VueApollo) => { emptyStateImage, defaultBranchName, projectPath, - agentDocsUrl, + multipleClustersDocsUrl, installDocsUrl, getStartedDocsUrl, integrationDocsUrl, @@ -27,7 +27,7 @@ export default (Vue, VueApollo) => { provide: { emptyStateImage, projectPath, - agentDocsUrl, + multipleClustersDocsUrl, installDocsUrl, getStartedDocsUrl, integrationDocsUrl, diff --git a/app/assets/javascripts/clusters_list/store/actions.js b/app/assets/javascripts/clusters_list/store/actions.js index 5f35a0b26f3..f0f96fd7872 100644 --- a/app/assets/javascripts/clusters_list/store/actions.js +++ b/app/assets/javascripts/clusters_list/store/actions.js @@ -3,7 +3,7 @@ import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils'; import Poll from '~/lib/utils/poll'; -import { __ } from '~/locale'; +import { s__ } from '~/locale'; import { MAX_REQUESTS } from '../constants'; import * as types from './mutation_types'; @@ -65,7 +65,7 @@ export const fetchClusters = ({ state, commit, dispatch }) => { commit(types.SET_LOADING_CLUSTERS, false); commit(types.SET_LOADING_NODES, false); createFlash({ - message: __('Clusters|An error occurred while loading clusters'), + message: s__('Clusters|An error occurred while loading clusters'), }); dispatch('reportSentryError', { error: response, tag: 'fetchClustersErrorCallback' }); diff --git a/app/assets/javascripts/experimentation/utils.js b/app/assets/javascripts/experimentation/utils.js index 624a04fd7c2..1567bf14d0f 100644 --- a/app/assets/javascripts/experimentation/utils.js +++ b/app/assets/javascripts/experimentation/utils.js @@ -26,14 +26,14 @@ export function getExperimentVariant(experimentName) { return getExperimentData(experimentName)?.variant || DEFAULT_VARIANT; } -export function experiment(experimentName, variants) { +export function experiment(experimentName, { use, control, candidate, ...variants }) { const variant = getExperimentVariant(experimentName); switch (variant) { case DEFAULT_VARIANT: - return variants.use.call(); + return (use || control).call(); case CANDIDATE_VARIANT: - return variants.try.call(); + return (variants.try || candidate).call(); default: return variants[variant].call(); } diff --git a/app/assets/javascripts/graphql_shared/fragments/iteration.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/iteration.fragment.graphql new file mode 100644 index 00000000000..78a368089a8 --- /dev/null +++ b/app/assets/javascripts/graphql_shared/fragments/iteration.fragment.graphql @@ -0,0 +1,4 @@ +fragment Iteration on Iteration { + id + title +} diff --git a/app/assets/javascripts/graphql_shared/utils.js b/app/assets/javascripts/graphql_shared/utils.js index 828ddd95ffc..e459ebf629f 100644 --- a/app/assets/javascripts/graphql_shared/utils.js +++ b/app/assets/javascripts/graphql_shared/utils.js @@ -15,6 +15,8 @@ export const isGid = (id) => { return false; }; +const parseGid = (gid) => parseInt(`${gid}`.replace(/gid:\/\/gitlab\/.*\//g, ''), 10); + /** * Ids generated by GraphQL endpoints are usually in the format * gid://gitlab/Environments/123. This method extracts Id number @@ -23,8 +25,12 @@ export const isGid = (id) => { * @param {String} gid GraphQL global ID * @returns {Number} */ -export const getIdFromGraphQLId = (gid = '') => - parseInt(`${gid}`.replace(/gid:\/\/gitlab\/.*\//g, ''), 10) || null; +export const getIdFromGraphQLId = (gid = '') => parseGid(gid) || null; + +export const getZeroBasedIdFromGraphQLId = (gid = '') => { + const parsedGid = parseGid(gid); + return Number.isInteger(parsedGid) ? parsedGid : null; +}; export const MutationOperationMode = { Append: 'APPEND', diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js index 0fab4678bc3..7f4e79976bc 100644 --- a/app/assets/javascripts/pages/users/activity_calendar.js +++ b/app/assets/javascripts/pages/users/activity_calendar.js @@ -5,6 +5,7 @@ import { last } from 'lodash'; import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { getDayName, getDayDifference } from '~/lib/utils/datetime_utility'; +import { formatDate } from '~/lib/utils/datetime/date_format_utility'; import { n__, s__, __ } from '~/locale'; const d3 = { select }; @@ -294,7 +295,15 @@ export default class ActivityCalendar { }, responseType: 'text', }) - .then(({ data }) => $(this.activitiesContainer).html(data)) + .then(({ data }) => { + $(this.activitiesContainer).html(data); + document + .querySelector(this.activitiesContainer) + .querySelectorAll('.js-localtime') + .forEach((el) => { + el.setAttribute('title', formatDate(el.getAttribute('data-datetime'))); + }); + }) .catch(() => createFlash({ message: __('An error occurred while retrieving calendar activity'), diff --git a/app/assets/javascripts/projects/pipelines/charts/components/app.vue b/app/assets/javascripts/projects/pipelines/charts/components/app.vue index 25bacc1cc4a..7379d5caed7 100644 --- a/app/assets/javascripts/projects/pipelines/charts/components/app.vue +++ b/app/assets/javascripts/projects/pipelines/charts/components/app.vue @@ -11,12 +11,17 @@ export default { DeploymentFrequencyCharts: () => import('ee_component/dora/components/deployment_frequency_charts.vue'), LeadTimeCharts: () => import('ee_component/dora/components/lead_time_charts.vue'), + ProjectQualitySummary: () => import('ee_component/project_quality_summary/app.vue'), }, inject: { shouldRenderDoraCharts: { type: Boolean, default: false, }, + shouldRenderQualitySummary: { + type: Boolean, + default: false, + }, }, data() { return { @@ -31,6 +36,10 @@ export default { chartsToShow.push('deployment-frequency', 'lead-time'); } + if (this.shouldRenderQualitySummary) { + chartsToShow.push('project-quality'); + } + return chartsToShow; }, }, @@ -68,6 +77,9 @@ export default { <lead-time-charts /> </gl-tab> </template> + <gl-tab v-if="shouldRenderQualitySummary" :title="s__('QualitySummary|Project quality')"> + <project-quality-summary /> + </gl-tab> </gl-tabs> <pipeline-charts v-else /> </div> diff --git a/app/assets/javascripts/projects/pipelines/charts/index.js b/app/assets/javascripts/projects/pipelines/charts/index.js index 5f5ee44c204..003b61d94b1 100644 --- a/app/assets/javascripts/projects/pipelines/charts/index.js +++ b/app/assets/javascripts/projects/pipelines/charts/index.js @@ -14,6 +14,7 @@ const mountPipelineChartsApp = (el) => { const { projectPath } = el.dataset; const shouldRenderDoraCharts = parseBoolean(el.dataset.shouldRenderDoraCharts); + const shouldRenderQualitySummary = parseBoolean(el.dataset.shouldRenderQualitySummary); return new Vue({ el, @@ -25,6 +26,7 @@ const mountPipelineChartsApp = (el) => { provide: { projectPath, shouldRenderDoraCharts, + shouldRenderQualitySummary, }, render: (createElement) => createElement(ProjectPipelinesCharts, {}), }); diff --git a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue index e278cfec804..759f6b37696 100644 --- a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue +++ b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue @@ -10,10 +10,10 @@ import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vu import RunnerList from '../components/runner_list.vue'; import RunnerName from '../components/runner_name.vue'; import RunnerPagination from '../components/runner_pagination.vue'; +import RunnerTypeTabs from '../components/runner_type_tabs.vue'; import { statusTokenConfig } from '../components/search_tokens/status_token_config'; import { tagTokenConfig } from '../components/search_tokens/tag_token_config'; -import { typeTokenConfig } from '../components/search_tokens/type_token_config'; import { ADMIN_FILTERED_SEARCH_NAMESPACE, INSTANCE_TYPE, I18N_FETCH_ERROR } from '../constants'; import getRunnersQuery from '../graphql/get_runners.query.graphql'; import { @@ -32,6 +32,7 @@ export default { RunnerList, RunnerName, RunnerPagination, + RunnerTypeTabs, }, props: { activeRunnersCount: { @@ -94,7 +95,6 @@ export default { searchTokens() { return [ statusTokenConfig, - typeTokenConfig, { ...tagTokenConfig, recentTokenValuesStorageKey: `${this.$options.filteredSearchNamespace}-recent-tags`, @@ -128,7 +128,13 @@ export default { </script> <template> <div> - <div class="gl-py-3 gl-display-flex"> + <div class="gl-display-flex gl-align-items-center"> + <runner-type-tabs + v-model="search" + content-class="gl-display-none" + nav-class="gl-border-none!" + /> + <registration-dropdown class="gl-ml-auto" :registration-token="registrationToken" diff --git a/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue b/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue index e04ca8ddca0..a9dfec35479 100644 --- a/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue +++ b/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue @@ -2,6 +2,7 @@ import { cloneDeep } from 'lodash'; import { __ } from '~/locale'; import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; +import { searchValidator } from '~/runner/runner_search_utils'; import { CREATED_DESC, CREATED_ASC, CONTACTED_DESC, CONTACTED_ASC } from '../constants'; const sortOptions = [ @@ -31,9 +32,12 @@ export default { value: { type: Object, required: true, - validator(val) { - return Array.isArray(val?.filters) && typeof val?.sort === 'string'; - }, + validator: searchValidator, + }, + tokens: { + type: Array, + required: false, + default: () => [], }, namespace: { type: String, @@ -43,7 +47,7 @@ export default { data() { // filtered_search_bar_root.vue may mutate the inital // filters. Use `cloneDeep` to prevent those mutations - // from affecting this component + // from affecting this component const { filters, sort } = cloneDeep(this.value); return { initialFilterValue: filters, @@ -52,19 +56,17 @@ export default { }, methods: { onFilter(filters) { - const { sort } = this.value; - + // Apply new filters, from page 1 this.$emit('input', { + ...this.value, filters, - sort, pagination: { page: 1 }, }); }, onSort(sort) { - const { filters } = this.value; - + // Apply new sort, from page 1 this.$emit('input', { - filters, + ...this.value, sort, pagination: { page: 1 }, }); @@ -74,13 +76,16 @@ export default { }; </script> <template> - <div> + <div + class="gl-bg-gray-10 gl-p-5 gl-border-solid gl-border-gray-100 gl-border-0 gl-border-t-1 gl-border-b-1" + > <filtered-search v-bind="$attrs" :namespace="namespace" recent-searches-storage-key="runners-search" :sort-options="$options.sortOptions" :initial-filter-value="initialFilterValue" + :tokens="tokens" :initial-sort-by="initialSortBy" :search-input-placeholder="__('Search or filter results...')" data-testid="runners-filtered-search" diff --git a/app/assets/javascripts/runner/components/runner_type_tabs.vue b/app/assets/javascripts/runner/components/runner_type_tabs.vue new file mode 100644 index 00000000000..9de444ea42f --- /dev/null +++ b/app/assets/javascripts/runner/components/runner_type_tabs.vue @@ -0,0 +1,63 @@ +<script> +import { GlTabs, GlTab } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import { searchValidator } from '~/runner/runner_search_utils'; +import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../constants'; + +const tabs = [ + { + title: s__('Runners|All'), + runnerType: null, + }, + { + title: s__('Runners|Instance'), + runnerType: INSTANCE_TYPE, + }, + { + title: s__('Runners|Group'), + runnerType: GROUP_TYPE, + }, + { + title: s__('Runners|Project'), + runnerType: PROJECT_TYPE, + }, +]; + +export default { + components: { + GlTabs, + GlTab, + }, + props: { + value: { + type: Object, + required: true, + validator: searchValidator, + }, + }, + methods: { + onTabSelected({ runnerType }) { + this.$emit('input', { + ...this.value, + runnerType, + pagination: { page: 1 }, + }); + }, + isTabActive({ runnerType }) { + return runnerType === this.value.runnerType; + }, + }, + tabs, +}; +</script> +<template> + <gl-tabs v-bind="$attrs"> + <gl-tab + v-for="tab in $options.tabs" + :key="`${tab.runnerType}`" + :active="isTabActive(tab)" + :title="tab.title" + @click="onTabSelected(tab)" + /> + </gl-tabs> +</template> diff --git a/app/assets/javascripts/runner/components/search_tokens/type_token_config.js b/app/assets/javascripts/runner/components/search_tokens/type_token_config.js deleted file mode 100644 index 1da61c53386..00000000000 --- a/app/assets/javascripts/runner/components/search_tokens/type_token_config.js +++ /dev/null @@ -1,20 +0,0 @@ -import { __, s__ } from '~/locale'; -import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants'; -import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue'; -import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE, PARAM_KEY_RUNNER_TYPE } from '../../constants'; - -export const typeTokenConfig = { - icon: 'file-tree', - title: __('Type'), - type: PARAM_KEY_RUNNER_TYPE, - token: BaseToken, - unique: true, - options: [ - { value: INSTANCE_TYPE, title: s__('Runners|instance') }, - { value: GROUP_TYPE, title: s__('Runners|group') }, - { value: PROJECT_TYPE, title: s__('Runners|project') }, - ], - // TODO We should support more complex search rules, - // search for multiple states (OR) or have NOT operators - operators: OPERATOR_IS_ONLY, -}; diff --git a/app/assets/javascripts/runner/group_runners/group_runners_app.vue b/app/assets/javascripts/runner/group_runners/group_runners_app.vue index a029200f4ae..c3dfa885f27 100644 --- a/app/assets/javascripts/runner/group_runners/group_runners_app.vue +++ b/app/assets/javascripts/runner/group_runners/group_runners_app.vue @@ -10,9 +10,9 @@ import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vu import RunnerList from '../components/runner_list.vue'; import RunnerName from '../components/runner_name.vue'; import RunnerPagination from '../components/runner_pagination.vue'; +import RunnerTypeTabs from '../components/runner_type_tabs.vue'; import { statusTokenConfig } from '../components/search_tokens/status_token_config'; -import { typeTokenConfig } from '../components/search_tokens/type_token_config'; import { I18N_FETCH_ERROR, GROUP_FILTERED_SEARCH_NAMESPACE, @@ -36,6 +36,7 @@ export default { RunnerList, RunnerName, RunnerPagination, + RunnerTypeTabs, }, props: { registrationToken: { @@ -112,7 +113,7 @@ export default { }); }, searchTokens() { - return [statusTokenConfig, typeTokenConfig]; + return [statusTokenConfig]; }, filteredSearchNamespace() { return `${GROUP_FILTERED_SEARCH_NAMESPACE}/${this.groupFullPath}`; @@ -144,7 +145,13 @@ export default { <template> <div> - <div class="gl-py-3 gl-display-flex"> + <div class="gl-display-flex gl-align-items-center"> + <runner-type-tabs + v-model="search" + content-class="gl-display-none" + nav-class="gl-border-none!" + /> + <registration-dropdown class="gl-ml-auto" :registration-token="registrationToken" diff --git a/app/assets/javascripts/runner/runner_search_utils.js b/app/assets/javascripts/runner/runner_search_utils.js index 0a817ea0acf..b88023720e8 100644 --- a/app/assets/javascripts/runner/runner_search_utils.js +++ b/app/assets/javascripts/runner/runner_search_utils.js @@ -18,6 +18,50 @@ import { RUNNER_PAGE_SIZE, } from './constants'; +/** + * The filters and sorting of the runners are built around + * an object called "search" that contains the current state + * of search in the UI. For example: + * + * ``` + * const search = { + * // The current tab + * runnerType: 'INSTANCE_TYPE', + * + * // Filters in the search bar + * filters: [ + * { type: 'status', value: { data: 'ACTIVE', operator: '=' } }, + * { type: 'filtered-search-term', value: { data: '' } }, + * ], + * + * // Current sorting value + * sort: 'CREATED_DESC', + * + * // Pagination information + * pagination: { page: 1 }, + * }; + * ``` + * + * An object in this format can be used to generate URLs + * with the search parameters or by runner components + * a input using a v-model. + * + * @module runner_search_utils + */ + +/** + * Validates a search value + * @param {Object} search + * @returns {boolean} True if the value follows the search format. + */ +export const searchValidator = ({ runnerType, filters, sort }) => { + return ( + (runnerType === null || typeof runnerType === 'string') && + Array.isArray(filters) && + typeof sort === 'string' + ); +}; + const getPaginationFromParams = (params) => { const page = parseInt(params[PARAM_KEY_PAGE], 10); const after = params[PARAM_KEY_AFTER]; @@ -35,13 +79,20 @@ const getPaginationFromParams = (params) => { }; }; +/** + * Takes a URL query and transforms it into a "search" object + * @param {String?} query + * @returns {Object} A search object + */ export const fromUrlQueryToSearch = (query = window.location.search) => { const params = queryToObject(query, { gatherArrays: true }); + const runnerType = params[PARAM_KEY_RUNNER_TYPE]?.[0] || null; return { + runnerType, filters: prepareTokens( urlQueryToFilter(query, { - filterNamesAllowList: [PARAM_KEY_STATUS, PARAM_KEY_RUNNER_TYPE, PARAM_KEY_TAG], + filterNamesAllowList: [PARAM_KEY_STATUS, PARAM_KEY_TAG], filteredSearchTermKey: PARAM_KEY_SEARCH, }), ), @@ -50,8 +101,15 @@ export const fromUrlQueryToSearch = (query = window.location.search) => { }; }; +/** + * Takes a "search" object and transforms it into a URL. + * + * @param {Object} search + * @param {String} url + * @returns {String} New URL for the page + */ export const fromSearchToUrl = ( - { filters = [], sort = null, pagination = {} }, + { runnerType = null, filters = [], sort = null, pagination = {} }, url = window.location.href, ) => { const filterParams = { @@ -65,6 +123,10 @@ export const fromSearchToUrl = ( }), }; + if (runnerType) { + filterParams[PARAM_KEY_RUNNER_TYPE] = [runnerType]; + } + if (!filterParams[PARAM_KEY_SEARCH]) { filterParams[PARAM_KEY_SEARCH] = null; } @@ -82,21 +144,31 @@ export const fromSearchToUrl = ( return setUrlParams({ ...filterParams, ...otherParams }, url, false, true, true); }; -export const fromSearchToVariables = ({ filters = [], sort = null, pagination = {} } = {}) => { +/** + * Takes a "search" object and transforms it into variables for runner a GraphQL query. + * + * @param {Object} search + * @returns {Object} Hash of filter values + */ +export const fromSearchToVariables = ({ + runnerType = null, + filters = [], + sort = null, + pagination = {}, +} = {}) => { const variables = {}; const queryObj = filterToQueryObject(processFilters(filters), { filteredSearchTermKey: PARAM_KEY_SEARCH, }); - variables.search = queryObj[PARAM_KEY_SEARCH]; - - // TODO Get more than one value when GraphQL API supports OR for "status" or "runner_type" [variables.status] = queryObj[PARAM_KEY_STATUS] || []; - [variables.type] = queryObj[PARAM_KEY_RUNNER_TYPE] || []; - + variables.search = queryObj[PARAM_KEY_SEARCH]; variables.tagList = queryObj[PARAM_KEY_TAG]; + if (runnerType) { + variables.type = runnerType; + } if (sort) { variables.sort = sort; } diff --git a/app/assets/javascripts/security_configuration/components/constants.js b/app/assets/javascripts/security_configuration/components/constants.js index 6a282df99bf..b6f0bd18d0f 100644 --- a/app/assets/javascripts/security_configuration/components/constants.js +++ b/app/assets/javascripts/security_configuration/components/constants.js @@ -3,6 +3,7 @@ import { __, s__ } from '~/locale'; import { REPORT_TYPE_SAST, + REPORT_TYPE_SAST_IAC, REPORT_TYPE_DAST, REPORT_TYPE_DAST_PROFILES, REPORT_TYPE_SECRET_DETECTION, @@ -30,6 +31,16 @@ export const SAST_CONFIG_HELP_PATH = helpPagePath('user/application_security/sas anchor: 'configuration', }); +export const SAST_IAC_NAME = __('Infrastructure as Code (IaC) Scanning'); +export const SAST_IAC_SHORT_NAME = s__('ciReport|IaC Scanning'); +export const SAST_IAC_DESCRIPTION = __( + 'Analyze your infrastructure as code configuration files for known vulnerabilities.', +); +export const SAST_IAC_HELP_PATH = helpPagePath('user/application_security/sast/index'); +export const SAST_IAC_CONFIG_HELP_PATH = helpPagePath('user/application_security/sast/index', { + anchor: 'configuration', +}); + export const DAST_NAME = __('Dynamic Application Security Testing (DAST)'); export const DAST_SHORT_NAME = s__('ciReport|DAST'); export const DAST_DESCRIPTION = __('Analyze a review version of your web application.'); @@ -141,6 +152,22 @@ export const securityFeatures = [ // https://gitlab.com/gitlab-org/gitlab/-/issues/331621 canEnableByMergeRequest: true, }, + ...(gon?.features?.configureIacScanningViaMr + ? [ + { + name: SAST_IAC_NAME, + shortName: SAST_IAC_SHORT_NAME, + description: SAST_IAC_DESCRIPTION, + helpPath: SAST_IAC_HELP_PATH, + configurationHelpPath: SAST_IAC_CONFIG_HELP_PATH, + type: REPORT_TYPE_SAST_IAC, + + // This field will eventually come from the backend, the progress is + // tracked in https://gitlab.com/gitlab-org/gitlab/-/issues/331621 + canEnableByMergeRequest: true, + }, + ] + : []), { name: DAST_NAME, shortName: DAST_SHORT_NAME, diff --git a/app/assets/javascripts/vue_shared/security_reports/constants.js b/app/assets/javascripts/vue_shared/security_reports/constants.js index b024e92bd0e..fafbd02634f 100644 --- a/app/assets/javascripts/vue_shared/security_reports/constants.js +++ b/app/assets/javascripts/vue_shared/security_reports/constants.js @@ -17,6 +17,7 @@ export const REPORT_FILE_TYPES = { * Security scan report types, as provided by the backend. */ export const REPORT_TYPE_SAST = 'sast'; +export const REPORT_TYPE_SAST_IAC = 'sast_iac'; export const REPORT_TYPE_DAST = 'dast'; export const REPORT_TYPE_DAST_PROFILES = 'dast_profiles'; export const REPORT_TYPE_SECRET_DETECTION = 'secret_detection'; |