summaryrefslogtreecommitdiff
path: root/app/assets/javascripts
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-11-09 03:42:22 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-11-09 03:42:22 +0000
commit7dd9256e5eac896fb422566376086a0befc79151 (patch)
treebe65227875ddb0cff4961ae2d97380bbf64dd851 /app/assets/javascripts
parent2295d352f6073101497f9bf4e4981c7ae72706a3 (diff)
downloadgitlab-ce-7dd9256e5eac896fb422566376086a0befc79151.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts')
-rw-r--r--app/assets/javascripts/boards/components/board_form.vue43
-rw-r--r--app/assets/javascripts/boards/components/boards_selector.vue63
-rw-r--r--app/assets/javascripts/boards/graphql/board_scope.fragment.graphql6
-rw-r--r--app/assets/javascripts/boards/graphql/group_board.query.graphql9
-rw-r--r--app/assets/javascripts/boards/graphql/project_board.query.graphql9
-rw-r--r--app/assets/javascripts/boards/mount_multiple_boards_switcher.js1
-rw-r--r--app/assets/javascripts/clusters_list/components/agent_empty_state.vue60
-rw-r--r--app/assets/javascripts/clusters_list/components/clusters_empty_state.vue31
-rw-r--r--app/assets/javascripts/clusters_list/constants.js27
-rw-r--r--app/assets/javascripts/clusters_list/load_agents.js4
-rw-r--r--app/assets/javascripts/clusters_list/store/actions.js4
-rw-r--r--app/assets/javascripts/experimentation/utils.js6
-rw-r--r--app/assets/javascripts/graphql_shared/fragments/iteration.fragment.graphql4
-rw-r--r--app/assets/javascripts/graphql_shared/utils.js10
-rw-r--r--app/assets/javascripts/pages/users/activity_calendar.js11
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/components/app.vue12
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/index.js2
-rw-r--r--app/assets/javascripts/runner/admin_runners/admin_runners_app.vue12
-rw-r--r--app/assets/javascripts/runner/components/runner_filtered_search_bar.vue27
-rw-r--r--app/assets/javascripts/runner/components/runner_type_tabs.vue63
-rw-r--r--app/assets/javascripts/runner/components/search_tokens/type_token_config.js20
-rw-r--r--app/assets/javascripts/runner/group_runners/group_runners_app.vue13
-rw-r--r--app/assets/javascripts/runner/runner_search_utils.js88
-rw-r--r--app/assets/javascripts/security_configuration/components/constants.js27
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/constants.js1
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';