summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/vue_shared
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/vue_shared')
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue392
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/alert_metrics.vue56
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/alert_sidebar.vue86
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/alert_status.vue125
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/alert_summary_row.vue18
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignee.vue38
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue299
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_header.vue41
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue120
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_todo.vue149
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue48
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/constants.js31
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/graphql/fragments/alert_detail_item.fragment.graphql30
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_issue_create.mutation.graphql8
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql25
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_sidebar_status.mutation.graphql3
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_todo_create.mutation.graphql10
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/graphql/queries/alert_details.query.graphql11
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/graphql/queries/alert_sidebar_status.query.graphql3
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/index.js83
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/router.js13
-rw-r--r--app/assets/javascripts/vue_shared/components/actions_button.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/alert_details_table.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/awards_list.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/clone_dropdown.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/color_picker/color_picker.vue61
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_input.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/renamed.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/dismissible_feedback_alert.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/editor_lite.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/file_finder/index.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/file_finder/item.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/file_icon.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/file_row.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue9
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/actions.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/index.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/gfm_autocomplete/utils.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/gl_modal_vuex.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/header_ci_component.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/help_popover.vue40
-rw-r--r--app/assets/javascripts/vue_shared/components/issuable/issuable_header_warnings.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue17
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue9
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestions.vue16
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/modal_copy_button.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/navigation_tabs.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/system_note.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/code_instruction.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/list_item.vue16
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/registry_search.vue90
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js8
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/editor_service.js6
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html_block.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/sanitize_html.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_instructions/constants.js18
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_platforms.query.graphql20
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_setup.query.graphql16
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue261
-rw-r--r--app/assets/javascripts/vue_shared/components/settings/settings_block.vue45
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue17
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue19
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/multiselect_dropdown.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/getIssueParticipants.query.graphql13
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql19
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql19
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql (renamed from app/assets/javascripts/vue_shared/components/sidebar/queries/updateAssignees.mutation.graphql)18
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql21
-rw-r--r--app/assets/javascripts/vue_shared/components/split_button.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/timezone_dropdown.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/todo_button.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/toggle_button.vue88
-rw-r--r--app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue14
-rw-r--r--app/assets/javascripts/vue_shared/components/web_ide_link.vue2
-rw-r--r--app/assets/javascripts/vue_shared/directives/validation.js2
-rw-r--r--app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js67
-rw-r--r--app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js2
-rw-r--r--app/assets/javascripts/vue_shared/plugins/global_toast.js2
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/constants.js6
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue108
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/store/getters.js2
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/store/index.js4
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/store/modules/sast/actions.js2
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/store/modules/sast/index.js4
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/store/modules/sast/mutations.js2
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/index.js4
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/store/utils.js2
124 files changed, 2367 insertions, 516 deletions
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue b/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue
new file mode 100644
index 00000000000..bcea7ca654e
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue
@@ -0,0 +1,392 @@
+<script>
+import {
+ GlAlert,
+ GlBadge,
+ GlIcon,
+ GlLink,
+ GlLoadingIcon,
+ GlSprintf,
+ GlTabs,
+ GlTab,
+ GlButton,
+ GlSafeHtmlDirective,
+} from '@gitlab/ui';
+import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user';
+import { fetchPolicies } from '~/lib/graphql';
+import { toggleContainerClasses } from '~/lib/utils/dom_utils';
+import { visitUrl, joinPaths } from '~/lib/utils/url_utility';
+import { s__ } from '~/locale';
+import * as Sentry from '~/sentry/wrapper';
+import Tracking from '~/tracking';
+import initUserPopovers from '~/user_popovers';
+import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import { SEVERITY_LEVELS } from '../constants';
+import createIssueMutation from '../graphql/mutations/alert_issue_create.mutation.graphql';
+import toggleSidebarStatusMutation from '../graphql/mutations/alert_sidebar_status.mutation.graphql';
+import alertQuery from '../graphql/queries/alert_details.query.graphql';
+import sidebarStatusQuery from '../graphql/queries/alert_sidebar_status.query.graphql';
+import AlertMetrics from './alert_metrics.vue';
+import AlertSidebar from './alert_sidebar.vue';
+import AlertSummaryRow from './alert_summary_row.vue';
+import SystemNote from './system_notes/system_note.vue';
+
+const containerEl = document.querySelector('.page-with-contextual-sidebar');
+
+export default {
+ i18n: {
+ errorMsg: s__(
+ 'AlertManagement|There was an error displaying the alert. Please refresh the page to try again.',
+ ),
+ reportedAt: s__('AlertManagement|Reported %{when}'),
+ reportedAtWithTool: s__('AlertManagement|Reported %{when} by %{tool}'),
+ },
+ directives: {
+ SafeHtml: GlSafeHtmlDirective,
+ },
+ severityLabels: SEVERITY_LEVELS,
+ tabsConfig: [
+ {
+ id: 'overview',
+ title: s__('AlertManagement|Alert details'),
+ },
+ {
+ id: 'metrics',
+ title: s__('AlertManagement|Metrics'),
+ },
+ {
+ id: 'activity',
+ title: s__('AlertManagement|Activity feed'),
+ },
+ ],
+ components: {
+ AlertDetailsTable,
+ AlertSummaryRow,
+ GlBadge,
+ GlAlert,
+ GlIcon,
+ GlLink,
+ GlLoadingIcon,
+ GlSprintf,
+ GlTab,
+ GlTabs,
+ GlButton,
+ TimeAgoTooltip,
+ AlertSidebar,
+ SystemNote,
+ AlertMetrics,
+ },
+ inject: {
+ projectPath: {
+ default: '',
+ },
+ alertId: {
+ default: '',
+ },
+ isThreatMonitoringPage: {
+ default: false,
+ },
+ projectId: {
+ default: '',
+ },
+ projectIssuesPath: {
+ default: '',
+ },
+ trackAlertsDetailsViewsOptions: {
+ default: null,
+ },
+ },
+ apollo: {
+ alert: {
+ fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
+ query: alertQuery,
+ variables() {
+ return {
+ fullPath: this.projectPath,
+ alertId: this.alertId,
+ };
+ },
+ update(data) {
+ return data?.project?.alertManagementAlerts?.nodes?.[0] ?? null;
+ },
+ error(error) {
+ this.errored = true;
+ Sentry.captureException(error);
+ },
+ },
+ sidebarStatus: {
+ query: sidebarStatusQuery,
+ },
+ },
+ data() {
+ return {
+ alert: null,
+ errored: false,
+ sidebarStatus: false,
+ isErrorDismissed: false,
+ createIncidentError: '',
+ incidentCreationInProgress: false,
+ sidebarErrorMessage: '',
+ };
+ },
+ computed: {
+ loading() {
+ return this.$apollo.queries.alert.loading;
+ },
+ reportedAtMessage() {
+ return this.alert?.monitoringTool
+ ? this.$options.i18n.reportedAtWithTool
+ : this.$options.i18n.reportedAt;
+ },
+ showErrorMsg() {
+ return this.errored && !this.isErrorDismissed;
+ },
+ activeTab() {
+ return this.$route.params.tabId || this.$options.tabsConfig[0].id;
+ },
+ currentTabIndex: {
+ get() {
+ return this.$options.tabsConfig.findIndex((tab) => tab.id === this.activeTab);
+ },
+ set(tabIdx) {
+ const tabId = this.$options.tabsConfig[tabIdx].id;
+ this.$router.replace({ name: 'tab', params: { tabId } });
+ },
+ },
+ environmentName() {
+ return this.alert?.environment?.name;
+ },
+ environmentPath() {
+ return this.alert?.environment?.path;
+ },
+ },
+ mounted() {
+ if (this.trackAlertsDetailsViewsOptions) {
+ this.trackPageViews();
+ }
+ toggleContainerClasses(containerEl, {
+ 'issuable-bulk-update-sidebar': true,
+ 'right-sidebar-expanded': true,
+ });
+ },
+ updated() {
+ this.$nextTick(() => {
+ highlightCurrentUser(this.$el.querySelectorAll('.gfm-project_member'));
+ initUserPopovers(this.$el.querySelectorAll('.js-user-link'));
+ });
+ },
+ methods: {
+ dismissError() {
+ this.isErrorDismissed = true;
+ this.sidebarErrorMessage = '';
+ },
+ toggleSidebar() {
+ this.$apollo.mutate({ mutation: toggleSidebarStatusMutation });
+ toggleContainerClasses(containerEl, {
+ 'right-sidebar-collapsed': !this.sidebarStatus,
+ 'right-sidebar-expanded': this.sidebarStatus,
+ });
+ },
+ handleAlertSidebarError(errorMessage) {
+ this.errored = true;
+ this.sidebarErrorMessage = errorMessage;
+ },
+ createIncident() {
+ this.incidentCreationInProgress = true;
+
+ this.$apollo
+ .mutate({
+ mutation: createIssueMutation,
+ variables: {
+ iid: this.alert.iid,
+ projectPath: this.projectPath,
+ },
+ })
+ .then(
+ ({
+ data: {
+ createAlertIssue: { errors, issue },
+ },
+ }) => {
+ if (errors?.length) {
+ [this.createIncidentError] = errors;
+ this.incidentCreationInProgress = false;
+ } else if (issue) {
+ visitUrl(this.incidentPath(issue.iid));
+ }
+ },
+ )
+ .catch((error) => {
+ this.createIncidentError = error;
+ this.incidentCreationInProgress = false;
+ });
+ },
+ incidentPath(issueId) {
+ return joinPaths(this.projectIssuesPath, issueId);
+ },
+ trackPageViews() {
+ const { category, action } = this.trackAlertsDetailsViewsOptions;
+ Tracking.event(category, action);
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-alert v-if="showErrorMsg" variant="danger" @dismiss="dismissError">
+ <p v-safe-html="sidebarErrorMessage || $options.i18n.errorMsg"></p>
+ </gl-alert>
+ <gl-alert
+ v-if="createIncidentError"
+ variant="danger"
+ data-testid="incidentCreationError"
+ @dismiss="createIncidentError = null"
+ >
+ {{ createIncidentError }}
+ </gl-alert>
+ <div v-if="loading"><gl-loading-icon size="lg" class="gl-mt-5" /></div>
+ <div
+ v-if="alert"
+ class="alert-management-details gl-relative"
+ :class="{ 'pr-sm-8': sidebarStatus }"
+ >
+ <div
+ class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-px-1 py-3 py-md-4 gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid gl-flex-direction-column gl-sm-flex-direction-row"
+ >
+ <div data-testid="alert-header">
+ <gl-badge class="gl-mr-3">
+ <strong>{{ s__('AlertManagement|Alert') }}</strong>
+ </gl-badge>
+ <span>
+ <gl-sprintf :message="reportedAtMessage">
+ <template #when>
+ <time-ago-tooltip :time="alert.createdAt" />
+ </template>
+ <template #tool>{{ alert.monitoringTool }}</template>
+ </gl-sprintf>
+ </span>
+ </div>
+ <gl-button
+ v-if="alert.issueIid"
+ class="gl-mt-3 mt-sm-0 align-self-center align-self-sm-baseline alert-details-incident-button"
+ data-testid="viewIncidentBtn"
+ :href="incidentPath(alert.issueIid)"
+ category="primary"
+ variant="success"
+ >
+ {{ s__('AlertManagement|View incident') }}
+ </gl-button>
+ <gl-button
+ v-else
+ class="gl-mt-3 mt-sm-0 align-self-center align-self-sm-baseline alert-details-incident-button"
+ data-testid="createIncidentBtn"
+ :loading="incidentCreationInProgress"
+ category="primary"
+ variant="success"
+ @click="createIncident()"
+ >
+ {{ s__('AlertManagement|Create incident') }}
+ </gl-button>
+ <gl-button
+ :aria-label="__('Toggle sidebar')"
+ category="primary"
+ variant="default"
+ class="d-sm-none gl-absolute toggle-sidebar-mobile-button"
+ type="button"
+ icon="chevron-double-lg-left"
+ @click="toggleSidebar"
+ />
+ </div>
+ <div
+ v-if="alert"
+ class="gl-display-flex gl-justify-content-space-between gl-align-items-center"
+ >
+ <h2 data-testid="title">{{ alert.title }}</h2>
+ </div>
+ <gl-tabs v-if="alert" v-model="currentTabIndex" data-testid="alertDetailsTabs">
+ <gl-tab :data-testid="$options.tabsConfig[0].id" :title="$options.tabsConfig[0].title">
+ <alert-summary-row v-if="alert.severity" :label="`${s__('AlertManagement|Severity')}:`">
+ <span data-testid="severity">
+ <gl-icon
+ class="gl-vertical-align-middle"
+ :size="12"
+ :name="`severity-${alert.severity.toLowerCase()}`"
+ :class="`icon-${alert.severity.toLowerCase()}`"
+ />
+ {{ $options.severityLabels[alert.severity] }}
+ </span>
+ </alert-summary-row>
+ <alert-summary-row
+ v-if="environmentName"
+ :label="`${s__('AlertManagement|Environment')}:`"
+ >
+ <gl-link
+ v-if="environmentPath"
+ class="gl-display-inline-block"
+ data-testid="environmentPath"
+ :href="environmentPath"
+ >
+ {{ environmentName }}
+ </gl-link>
+ <span v-else data-testid="environmentName">{{ environmentName }}</span>
+ </alert-summary-row>
+ <alert-summary-row
+ v-if="alert.startedAt"
+ :label="`${s__('AlertManagement|Start time')}:`"
+ >
+ <time-ago-tooltip data-testid="startTimeItem" :time="alert.startedAt" />
+ </alert-summary-row>
+ <alert-summary-row
+ v-if="alert.eventCount"
+ :label="`${s__('AlertManagement|Events')}:`"
+ data-testid="eventCount"
+ >
+ {{ alert.eventCount }}
+ </alert-summary-row>
+ <alert-summary-row
+ v-if="alert.monitoringTool"
+ :label="`${s__('AlertManagement|Tool')}:`"
+ data-testid="monitoringTool"
+ >
+ {{ alert.monitoringTool }}
+ </alert-summary-row>
+ <alert-summary-row
+ v-if="alert.service"
+ :label="`${s__('AlertManagement|Service')}:`"
+ data-testid="service"
+ >
+ {{ alert.service }}
+ </alert-summary-row>
+ <alert-summary-row
+ v-if="alert.runbook"
+ :label="`${s__('AlertManagement|Runbook')}:`"
+ data-testid="runbook"
+ >
+ {{ alert.runbook }}
+ </alert-summary-row>
+ <alert-details-table :alert="alert" :loading="loading" />
+ </gl-tab>
+ <gl-tab
+ v-if="isThreatMonitoringPage"
+ :data-testid="$options.tabsConfig[1].id"
+ :title="$options.tabsConfig[1].title"
+ >
+ <alert-metrics :dashboard-url="alert.metricsDashboardUrl" />
+ </gl-tab>
+ <gl-tab :data-testid="$options.tabsConfig[2].id" :title="$options.tabsConfig[2].title">
+ <div v-if="alert.notes.nodes.length > 0" class="issuable-discussion">
+ <ul class="notes main-notes-list timeline">
+ <system-note v-for="note in alert.notes.nodes" :key="note.id" :note="note" />
+ </ul>
+ </div>
+ </gl-tab>
+ </gl-tabs>
+ <alert-sidebar
+ :alert="alert"
+ @toggle-sidebar="toggleSidebar"
+ @alert-error="handleAlertSidebarError"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/alert_metrics.vue b/app/assets/javascripts/vue_shared/alert_details/components/alert_metrics.vue
new file mode 100644
index 00000000000..dd4faa03c00
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/alert_details/components/alert_metrics.vue
@@ -0,0 +1,56 @@
+<script>
+import Vue from 'vue';
+import Vuex from 'vuex';
+import * as Sentry from '~/sentry/wrapper';
+
+Vue.use(Vuex);
+
+export default {
+ props: {
+ dashboardUrl: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ metricEmbedComponent: null,
+ namespace: 'alertMetrics',
+ };
+ },
+ mounted() {
+ if (this.dashboardUrl) {
+ Promise.all([
+ import('~/monitoring/components/embeds/metric_embed.vue'),
+ import('~/monitoring/stores'),
+ ])
+ .then(([{ default: MetricEmbed }, { monitoringDashboard }]) => {
+ this.$store = new Vuex.Store({
+ modules: {
+ [this.namespace]: monitoringDashboard,
+ },
+ });
+ this.metricEmbedComponent = MetricEmbed;
+ })
+ .catch((e) => Sentry.captureException(e));
+ }
+ },
+};
+</script>
+
+<template>
+ <div class="gl-py-3">
+ <div v-if="dashboardUrl" ref="metricsChart">
+ <component
+ :is="metricEmbedComponent"
+ v-if="metricEmbedComponent"
+ :dashboard-url="dashboardUrl"
+ :namespace="namespace"
+ />
+ </div>
+ <div v-else ref="emptyState">
+ {{ s__("AlertManagement|Metrics weren't available in the alerts payload.") }}
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/alert_sidebar.vue b/app/assets/javascripts/vue_shared/alert_details/components/alert_sidebar.vue
new file mode 100644
index 00000000000..a01bd462196
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/alert_details/components/alert_sidebar.vue
@@ -0,0 +1,86 @@
+<script>
+import sidebarStatusQuery from '../graphql/queries/alert_sidebar_status.query.graphql';
+import SidebarAssignees from './sidebar/sidebar_assignees.vue';
+import SidebarHeader from './sidebar/sidebar_header.vue';
+import SidebarStatus from './sidebar/sidebar_status.vue';
+import SidebarTodo from './sidebar/sidebar_todo.vue';
+
+export default {
+ components: {
+ SidebarAssignees,
+ SidebarHeader,
+ SidebarTodo,
+ SidebarStatus,
+ },
+ inject: {
+ projectPath: {
+ default: '',
+ },
+ projectId: {
+ default: '',
+ },
+ // TODO remove this limitation in https://gitlab.com/gitlab-org/gitlab/-/issues/296717
+ isThreatMonitoringPage: {
+ default: false,
+ },
+ },
+ props: {
+ alert: {
+ type: Object,
+ required: true,
+ },
+ },
+ apollo: {
+ sidebarStatus: {
+ query: sidebarStatusQuery,
+ },
+ },
+ data() {
+ return {
+ sidebarStatus: false,
+ };
+ },
+ computed: {
+ sidebarCollapsedClass() {
+ return this.sidebarStatus ? 'right-sidebar-collapsed' : 'right-sidebar-expanded';
+ },
+ },
+};
+</script>
+
+<template>
+ <aside :class="sidebarCollapsedClass" class="right-sidebar alert-sidebar">
+ <div class="issuable-sidebar js-issuable-update">
+ <sidebar-header
+ :sidebar-collapsed="sidebarStatus"
+ :project-path="projectPath"
+ :alert="alert"
+ @toggle-sidebar="$emit('toggle-sidebar')"
+ @alert-error="$emit('alert-error', $event)"
+ />
+ <sidebar-todo
+ v-if="sidebarStatus"
+ :project-path="projectPath"
+ :alert="alert"
+ :sidebar-collapsed="sidebarStatus"
+ @alert-error="$emit('alert-error', $event)"
+ />
+ <sidebar-status
+ v-if="!isThreatMonitoringPage"
+ :project-path="projectPath"
+ :alert="alert"
+ @toggle-sidebar="$emit('toggle-sidebar')"
+ @alert-error="$emit('alert-error', $event)"
+ />
+ <sidebar-assignees
+ :project-path="projectPath"
+ :project-id="projectId"
+ :alert="alert"
+ :sidebar-collapsed="sidebarStatus"
+ @toggle-sidebar="$emit('toggle-sidebar')"
+ @alert-error="$emit('alert-error', $event)"
+ />
+ <div class="block"></div>
+ </div>
+ </aside>
+</template>
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/alert_status.vue b/app/assets/javascripts/vue_shared/alert_details/components/alert_status.vue
new file mode 100644
index 00000000000..8d5eb24ed1d
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/alert_details/components/alert_status.vue
@@ -0,0 +1,125 @@
+<script>
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import updateAlertStatusMutation from '~/graphql_shared/mutations/alert_status_update.mutation.graphql';
+import { s__ } from '~/locale';
+import Tracking from '~/tracking';
+
+export default {
+ i18n: {
+ UPDATE_ALERT_STATUS_ERROR: s__(
+ 'AlertManagement|There was an error while updating the status of the alert.',
+ ),
+ UPDATE_ALERT_STATUS_INSTRUCTION: s__('AlertManagement|Please try again.'),
+ },
+ statuses: {
+ TRIGGERED: s__('AlertManagement|Triggered'),
+ ACKNOWLEDGED: s__('AlertManagement|Acknowledged'),
+ RESOLVED: s__('AlertManagement|Resolved'),
+ },
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ },
+ inject: {
+ trackAlertStatusUpdateOptions: {
+ default: null,
+ },
+ },
+ props: {
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ alert: {
+ type: Object,
+ required: true,
+ },
+ isDropdownShowing: {
+ type: Boolean,
+ required: false,
+ },
+ isSidebar: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ dropdownClass() {
+ // eslint-disable-next-line no-nested-ternary
+ return this.isSidebar ? (this.isDropdownShowing ? 'show' : 'gl-display-none') : '';
+ },
+ },
+ methods: {
+ updateAlertStatus(status) {
+ this.$emit('handle-updating', true);
+ this.$apollo
+ .mutate({
+ mutation: updateAlertStatusMutation,
+ variables: {
+ iid: this.alert.iid,
+ status: status.toUpperCase(),
+ projectPath: this.projectPath,
+ },
+ })
+ .then((resp) => {
+ if (this.trackAlertStatusUpdateOptions) {
+ this.trackStatusUpdate(status);
+ }
+ const errors = resp.data?.updateAlertStatus?.errors || [];
+
+ if (errors[0]) {
+ this.$emit(
+ 'alert-error',
+ `${this.$options.i18n.UPDATE_ALERT_STATUS_ERROR} ${errors[0]}`,
+ );
+ }
+
+ this.$emit('hide-dropdown');
+ })
+ .catch(() => {
+ this.$emit(
+ 'alert-error',
+ `${this.$options.i18n.UPDATE_ALERT_STATUS_ERROR} ${this.$options.i18n.UPDATE_ALERT_STATUS_INSTRUCTION}`,
+ );
+ })
+ .finally(() => {
+ this.$emit('handle-updating', false);
+ });
+ },
+ trackStatusUpdate(status) {
+ const { category, action, label } = this.trackAlertStatusUpdateOptions;
+ Tracking.event(category, action, { label, property: status });
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="dropdown dropdown-menu-selectable" :class="dropdownClass">
+ <gl-dropdown
+ ref="dropdown"
+ right
+ :text="$options.statuses[alert.status]"
+ class="w-100"
+ toggle-class="dropdown-menu-toggle"
+ @keydown.esc.native="$emit('hide-dropdown')"
+ @hide="$emit('hide-dropdown')"
+ >
+ <p v-if="isSidebar" class="gl-new-dropdown-header-top" data-testid="dropdown-header">
+ {{ s__('AlertManagement|Assign status') }}
+ </p>
+ <div class="dropdown-content dropdown-body">
+ <gl-dropdown-item
+ v-for="(label, field) in $options.statuses"
+ :key="field"
+ data-testid="statusDropdownItem"
+ :active="label.toUpperCase() === alert.status"
+ :active-class="'is-active'"
+ @click="updateAlertStatus(label)"
+ >
+ {{ label }}
+ </gl-dropdown-item>
+ </div>
+ </gl-dropdown>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/alert_summary_row.vue b/app/assets/javascripts/vue_shared/alert_details/components/alert_summary_row.vue
new file mode 100644
index 00000000000..13835b7e2fa
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/alert_details/components/alert_summary_row.vue
@@ -0,0 +1,18 @@
+<script>
+export default {
+ props: {
+ label: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <div class="gl-my-5 gl-display-flex">
+ <div class="gl-font-weight-bold gl-w-13 gl-text-right gl-pr-3">{{ label }}</div>
+ <div class="gl-pl-2">
+ <slot></slot>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignee.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignee.vue
new file mode 100644
index 00000000000..c39a72a45b9
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignee.vue
@@ -0,0 +1,38 @@
+<script>
+import { GlDropdownItem } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlDropdownItem,
+ },
+ props: {
+ user: {
+ type: Object,
+ required: true,
+ },
+ active: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ methods: {
+ isActive(name) {
+ return this.alert.assignees.nodes.some(({ username }) => username === name);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-dropdown-item
+ :key="user.username"
+ data-testid="assigneeDropdownItem"
+ :active="active"
+ active-class="is-active"
+ :avatar-url="user.avatar_url"
+ :secondary-text="`@${user.username}`"
+ @click="$emit('update-alert-assignees', user.username)"
+ >
+ {{ user.name }}
+ </gl-dropdown-item>
+</template>
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue
new file mode 100644
index 00000000000..2a999b908f9
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue
@@ -0,0 +1,299 @@
+<script>
+import {
+ GlIcon,
+ GlDropdown,
+ GlDropdownDivider,
+ GlDropdownSectionHeader,
+ GlDropdownItem,
+ GlSearchBoxByType,
+ GlLoadingIcon,
+ GlTooltip,
+ GlButton,
+ GlSprintf,
+} from '@gitlab/ui';
+import { debounce } from 'lodash';
+import axios from '~/lib/utils/axios_utils';
+import { s__, __ } from '~/locale';
+import alertSetAssignees from '../../graphql/mutations/alert_set_assignees.mutation.graphql';
+import SidebarAssignee from './sidebar_assignee.vue';
+
+const DATA_REFETCH_DELAY = 250;
+
+export default {
+ i18n: {
+ FETCH_USERS_ERROR: s__(
+ 'AlertManagement|There was an error while updating the assignee(s) list. Please try again.',
+ ),
+ UPDATE_ALERT_ASSIGNEES_ERROR: s__(
+ 'AlertManagement|There was an error while updating the assignee(s) of the alert. Please try again.',
+ ),
+ UPDATE_ALERT_ASSIGNEES_GRAPHQL_ERROR: s__(
+ 'AlertManagement|This assignee cannot be assigned to this alert.',
+ ),
+ ASSIGNEES_BLOCK: s__('AlertManagement|Alert assignee(s): %{assignees}'),
+ },
+ components: {
+ GlIcon,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownDivider,
+ GlDropdownSectionHeader,
+ GlSearchBoxByType,
+ GlLoadingIcon,
+ GlTooltip,
+ GlButton,
+ GlSprintf,
+ SidebarAssignee,
+ },
+ props: {
+ projectId: {
+ type: String,
+ required: true,
+ },
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ alert: {
+ type: Object,
+ required: true,
+ },
+ isEditable: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ sidebarCollapsed: {
+ type: Boolean,
+ required: false,
+ },
+ },
+ data() {
+ return {
+ isDropdownShowing: false,
+ isDropdownSearching: false,
+ isUpdating: false,
+ search: '',
+ users: [],
+ };
+ },
+ computed: {
+ currentUser() {
+ return gon?.current_username;
+ },
+ userName() {
+ return this.alert?.assignees?.nodes[0]?.username;
+ },
+ userFullName() {
+ return this.alert?.assignees?.nodes[0]?.name;
+ },
+ userImg() {
+ return this.alert?.assignees?.nodes[0]?.avatarUrl;
+ },
+ sortedUsers() {
+ return this.users
+ .map((user) => ({ ...user, active: this.isActive(user.username) }))
+ .sort((a, b) => (a.active === b.active ? 0 : a.active ? -1 : 1)); // eslint-disable-line no-nested-ternary
+ },
+ dropdownClass() {
+ return this.isDropdownShowing ? 'dropdown-menu-selectable show' : 'gl-display-none';
+ },
+ dropDownTitle() {
+ return this.userName ?? __('Select assignee');
+ },
+ userListValid() {
+ return !this.isDropdownSearching && this.users.length > 0;
+ },
+ userListEmpty() {
+ return !this.isDropdownSearching && this.users.length === 0;
+ },
+ },
+ watch: {
+ search: debounce(function debouncedUserSearch() {
+ this.updateAssigneesDropdown();
+ }, DATA_REFETCH_DELAY),
+ },
+ mounted() {
+ this.updateAssigneesDropdown();
+ },
+ methods: {
+ hideDropdown() {
+ this.isDropdownShowing = false;
+ },
+ toggleFormDropdown() {
+ this.isDropdownShowing = !this.isDropdownShowing;
+ const { dropdown } = this.$refs.dropdown.$refs;
+ if (dropdown && this.isDropdownShowing) {
+ dropdown.show();
+ }
+ },
+ isActive(name) {
+ return this.alert.assignees.nodes.some(({ username }) => username === name);
+ },
+ buildUrl(urlRoot, url) {
+ let newUrl;
+ if (urlRoot != null) {
+ newUrl = urlRoot.replace(/\/$/, '') + url;
+ }
+ return newUrl;
+ },
+ updateAssigneesDropdown() {
+ this.isDropdownSearching = true;
+ return axios
+ .get(this.buildUrl(gon.relative_url_root, '/-/autocomplete/users.json'), {
+ params: {
+ search: this.search,
+ per_page: 20,
+ active: true,
+ current_user: true,
+ project_id: this.projectId,
+ },
+ })
+ .then(({ data }) => {
+ this.users = data;
+ })
+ .catch(() => {
+ this.$emit('alert-error', this.$options.i18n.FETCH_USERS_ERROR);
+ })
+ .finally(() => {
+ this.isDropdownSearching = false;
+ });
+ },
+ updateAlertAssignees(assignees) {
+ this.isUpdating = true;
+ this.$apollo
+ .mutate({
+ mutation: alertSetAssignees,
+ variables: {
+ iid: this.alert.iid,
+ assigneeUsernames: [this.isActive(assignees) ? '' : assignees],
+ projectPath: this.projectPath,
+ },
+ })
+ .then(({ data: { alertSetAssignees: { errors } = [] } = {} } = {}) => {
+ this.hideDropdown();
+
+ if (errors[0]) {
+ this.$emit(
+ 'alert-error',
+ `${this.$options.i18n.UPDATE_ALERT_ASSIGNEES_GRAPHQL_ERROR} ${errors[0]}.`,
+ );
+ }
+ })
+ .catch(() => {
+ this.$emit('alert-error', this.$options.i18n.UPDATE_ALERT_ASSIGNEES_ERROR);
+ })
+ .finally(() => {
+ this.isUpdating = false;
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="block alert-assignees">
+ <div ref="assignees" class="sidebar-collapsed-icon" @click="$emit('toggle-sidebar')">
+ <gl-icon name="user" :size="14" />
+ <gl-loading-icon v-if="isUpdating" />
+ </div>
+ <gl-tooltip :target="() => $refs.assignees" boundary="viewport" placement="left">
+ <gl-sprintf :message="$options.i18n.ASSIGNEES_BLOCK">
+ <template #assignees>
+ {{ userName }}
+ </template>
+ </gl-sprintf>
+ </gl-tooltip>
+
+ <div class="hide-collapsed">
+ <p class="title gl-display-flex gl-justify-content-space-between">
+ {{ __('Assignee') }}
+ <a
+ v-if="isEditable"
+ ref="editButton"
+ class="btn-link"
+ href="#"
+ @click="toggleFormDropdown"
+ @keydown.esc="hideDropdown"
+ >
+ {{ __('Edit') }}
+ </a>
+ </p>
+
+ <gl-dropdown
+ ref="dropdown"
+ :text="dropDownTitle"
+ class="gl-w-full"
+ :class="dropdownClass"
+ toggle-class="dropdown-menu-toggle"
+ @keydown.esc.native="hideDropdown"
+ @hide="hideDropdown"
+ >
+ <p class="gl-new-dropdown-header-top">
+ {{ __('Assign To') }}
+ </p>
+ <gl-search-box-by-type v-model.trim="search" :placeholder="__('Search users')" />
+ <div class="dropdown-content dropdown-body">
+ <template v-if="userListValid">
+ <gl-dropdown-item
+ :active="!userName"
+ active-class="is-active"
+ @click="updateAlertAssignees('')"
+ >
+ {{ __('Unassigned') }}
+ </gl-dropdown-item>
+ <gl-dropdown-divider />
+
+ <gl-dropdown-section-header>
+ {{ __('Assignee') }}
+ </gl-dropdown-section-header>
+ <sidebar-assignee
+ v-for="user in sortedUsers"
+ :key="user.username"
+ :user="user"
+ :active="user.active"
+ @update-alert-assignees="updateAlertAssignees"
+ />
+ </template>
+ <p v-else-if="userListEmpty" class="gl-mx-5 gl-my-4">
+ {{ __('No Matching Results') }}
+ </p>
+ <gl-loading-icon v-else />
+ </div>
+ </gl-dropdown>
+ </div>
+
+ <gl-loading-icon v-if="isUpdating" :inline="true" />
+ <div v-else-if="!isDropdownShowing" class="value gl-m-0" :class="{ 'no-value': !userName }">
+ <div v-if="userName" class="gl-display-inline-flex gl-mt-2" data-testid="assigned-users">
+ <span class="gl-relative gl-mr-4">
+ <img
+ :alt="userName"
+ :src="userImg"
+ :width="32"
+ class="avatar avatar-inline gl-m-0 s32"
+ data-qa-selector="avatar_image"
+ />
+ </span>
+ <span class="gl-display-flex gl-flex-direction-column gl-overflow-hidden">
+ <strong class="dropdown-menu-user-full-name">
+ {{ userFullName }}
+ </strong>
+ <span class="dropdown-menu-user-username">@{{ userName }}</span>
+ </span>
+ </div>
+ <span v-else class="gl-display-flex gl-align-items-center gl-line-height-normal">
+ {{ __('None') }} -
+ <gl-button
+ class="gl-ml-2"
+ href="#"
+ variant="link"
+ data-testid="unassigned-users"
+ @click="updateAlertAssignees(currentUser)"
+ >
+ {{ __('assign yourself') }}
+ </gl-button>
+ </span>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_header.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_header.vue
new file mode 100644
index 00000000000..fd40b5d9f65
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_header.vue
@@ -0,0 +1,41 @@
+<script>
+import ToggleSidebar from '~/vue_shared/components/sidebar/toggle_sidebar.vue';
+import SidebarTodo from './sidebar_todo.vue';
+
+export default {
+ components: {
+ ToggleSidebar,
+ SidebarTodo,
+ },
+ props: {
+ alert: {
+ type: Object,
+ required: true,
+ },
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ sidebarCollapsed: {
+ type: Boolean,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="block gl-display-flex gl-justify-content-space-between">
+ <span class="issuable-header-text hide-collapsed">
+ {{ __('To Do') }}
+ </span>
+ <sidebar-todo
+ v-if="!sidebarCollapsed"
+ :project-path="projectPath"
+ :alert="alert"
+ :sidebar-collapsed="sidebarCollapsed"
+ @alert-error="$emit('alert-error', $event)"
+ />
+ <toggle-sidebar :collapsed="sidebarCollapsed" @toggle="$emit('toggle-sidebar')" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue
new file mode 100644
index 00000000000..0a2bad5510b
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue
@@ -0,0 +1,120 @@
+<script>
+import { GlIcon, GlLoadingIcon, GlTooltip, GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import AlertStatus from '../alert_status.vue';
+
+export default {
+ statuses: {
+ TRIGGERED: s__('AlertManagement|Triggered'),
+ ACKNOWLEDGED: s__('AlertManagement|Acknowledged'),
+ RESOLVED: s__('AlertManagement|Resolved'),
+ },
+ components: {
+ GlIcon,
+ GlLoadingIcon,
+ GlTooltip,
+ GlSprintf,
+ AlertStatus,
+ },
+ props: {
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ alert: {
+ type: Object,
+ required: true,
+ },
+ isEditable: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
+ data() {
+ return {
+ isDropdownShowing: false,
+ isUpdating: false,
+ };
+ },
+ computed: {
+ dropdownClass() {
+ return this.isDropdownShowing ? 'show' : 'gl-display-none';
+ },
+ },
+ methods: {
+ hideDropdown() {
+ this.isDropdownShowing = false;
+ },
+ toggleFormDropdown() {
+ this.isDropdownShowing = !this.isDropdownShowing;
+ const { dropdown } = this.$children[2].$refs.dropdown.$refs;
+ if (dropdown && this.isDropdownShowing) {
+ dropdown.show();
+ }
+ },
+ handleUpdating(updating) {
+ this.isUpdating = updating;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="block alert-status">
+ <div ref="status" class="sidebar-collapsed-icon" @click="$emit('toggle-sidebar')">
+ <gl-icon name="status" :size="14" />
+ <gl-loading-icon v-if="isUpdating" />
+ </div>
+ <gl-tooltip :target="() => $refs.status" boundary="viewport" placement="left">
+ <gl-sprintf :message="s__('AlertManagement|Alert status: %{status}')">
+ <template #status>
+ {{ alert.status.toLowerCase() }}
+ </template>
+ </gl-sprintf>
+ </gl-tooltip>
+
+ <div class="hide-collapsed">
+ <p class="title gl-display-flex justify-content-between">
+ {{ s__('AlertManagement|Status') }}
+ <a
+ v-if="isEditable"
+ ref="editButton"
+ class="btn-link"
+ href="#"
+ @click="toggleFormDropdown"
+ @keydown.esc="hideDropdown"
+ >
+ {{ s__('AlertManagement|Edit') }}
+ </a>
+ </p>
+
+ <alert-status
+ :alert="alert"
+ :project-path="projectPath"
+ :is-dropdown-showing="isDropdownShowing"
+ :is-sidebar="true"
+ @alert-error="$emit('alert-error', $event)"
+ @hide-dropdown="hideDropdown"
+ @handle-updating="handleUpdating"
+ />
+
+ <gl-loading-icon v-if="isUpdating" :inline="true" />
+ <p
+ v-else-if="!isDropdownShowing"
+ class="value gl-m-0"
+ :class="{ 'no-value': !$options.statuses[alert.status] }"
+ >
+ <span
+ v-if="$options.statuses[alert.status]"
+ class="gl-text-gray-500"
+ data-testid="status"
+ >{{ $options.statuses[alert.status] }}</span
+ >
+ <span v-else>
+ {{ s__('AlertManagement|None') }}
+ </span>
+ </p>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_todo.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_todo.vue
new file mode 100644
index 00000000000..39ac6c7feca
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_todo.vue
@@ -0,0 +1,149 @@
+<script>
+import produce from 'immer';
+import todoMarkDoneMutation from '~/graphql_shared/mutations/todo_mark_done.mutation.graphql';
+import { s__ } from '~/locale';
+import Todo from '~/sidebar/components/todo_toggle/todo.vue';
+import createAlertTodoMutation from '../../graphql/mutations/alert_todo_create.mutation.graphql';
+import alertQuery from '../../graphql/queries/alert_details.query.graphql';
+
+export default {
+ i18n: {
+ UPDATE_ALERT_TODO_ERROR: s__(
+ 'AlertManagement|There was an error while updating the to-do item of the alert.',
+ ),
+ },
+ components: {
+ Todo,
+ },
+ props: {
+ alert: {
+ type: Object,
+ required: true,
+ },
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ sidebarCollapsed: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isUpdating: false,
+ };
+ },
+ computed: {
+ alertID() {
+ return parseInt(this.alert.iid, 10);
+ },
+ firstToDoId() {
+ return this.alert?.todos?.nodes[0]?.id;
+ },
+ hasPendingTodos() {
+ return this.alert?.todos?.nodes.length > 0;
+ },
+ getAlertQueryVariables() {
+ return {
+ fullPath: this.projectPath,
+ alertId: this.alert.iid,
+ };
+ },
+ },
+ methods: {
+ updateToDoCount(add) {
+ const oldCount = parseInt(document.querySelector('.js-todos-count').innerText, 10);
+ const count = add ? oldCount + 1 : oldCount - 1;
+ const headerTodoEvent = new CustomEvent('todo:toggle', {
+ detail: {
+ count,
+ },
+ });
+
+ return document.dispatchEvent(headerTodoEvent);
+ },
+ addToDo() {
+ this.isUpdating = true;
+ return this.$apollo
+ .mutate({
+ mutation: createAlertTodoMutation,
+ variables: {
+ iid: this.alert.iid,
+ projectPath: this.projectPath,
+ },
+ })
+ .then(({ data: { errors = [] } }) => {
+ if (errors[0]) {
+ return this.throwError(errors[0]);
+ }
+ return this.updateToDoCount(true);
+ })
+ .catch(() => {
+ this.throwError();
+ })
+ .finally(() => {
+ this.isUpdating = false;
+ });
+ },
+ markAsDone() {
+ this.isUpdating = true;
+ return this.$apollo
+ .mutate({
+ mutation: todoMarkDoneMutation,
+ variables: {
+ id: this.firstToDoId,
+ },
+ update: this.updateCache,
+ })
+ .then(({ data: { errors = [] } }) => {
+ if (errors[0]) {
+ return this.throwError(errors[0]);
+ }
+ return this.updateToDoCount(false);
+ })
+ .catch(() => {
+ this.throwError();
+ })
+ .finally(() => {
+ this.isUpdating = false;
+ });
+ },
+ updateCache(store) {
+ const sourceData = store.readQuery({
+ query: alertQuery,
+ variables: this.getAlertQueryVariables,
+ });
+
+ const data = produce(sourceData, (draftData) => {
+ // eslint-disable-next-line no-param-reassign
+ draftData.project.alertManagementAlerts.nodes[0].todos.nodes = [];
+ });
+
+ store.writeQuery({
+ query: alertQuery,
+ variables: this.getAlertQueryVariables,
+ data,
+ });
+ },
+ throwError(err = '') {
+ const error = err || s__('AlertManagement|Please try again.');
+ this.$emit('alert-error', `${this.$options.i18n.UPDATE_ALERT_TODO_ERROR} ${error}`);
+ },
+ },
+};
+</script>
+
+<template>
+ <div :class="{ 'block todo': sidebarCollapsed, 'gl-ml-auto': !sidebarCollapsed }">
+ <todo
+ data-testid="alert-todo-button"
+ :collapsed="sidebarCollapsed"
+ :issuable-id="alertID"
+ :is-todo="hasPendingTodos"
+ :is-action-active="isUpdating"
+ issuable-type="alert"
+ @toggleTodo="hasPendingTodos ? markAsDone() : addToDo()"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue b/app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue
new file mode 100644
index 00000000000..3705e36a579
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue
@@ -0,0 +1,48 @@
+<script>
+/* eslint-disable vue/no-v-html */
+import { GlIcon } from '@gitlab/ui';
+import NoteHeader from '~/notes/components/note_header.vue';
+
+export default {
+ components: {
+ NoteHeader,
+ GlIcon,
+ },
+ props: {
+ note: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ noteAnchorId() {
+ return `note_${this.note?.id?.split('/').pop()}`;
+ },
+ noteAuthor() {
+ const {
+ author,
+ author: { id },
+ } = this.note;
+ return { ...author, id: id?.split('/').pop() };
+ },
+ },
+};
+</script>
+
+<template>
+ <li :id="noteAnchorId" class="timeline-entry note system-note note-wrapper gl-p-0!">
+ <div class="gl-display-inline-flex gl-align-items-center">
+ <div
+ class="gl-display-inline gl-bg-white gl-text-gray-200 gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-full gl-box-sizing-content-box gl-p-3 gl-mt-n2 gl-mr-6"
+ >
+ <gl-icon :name="note.systemNoteIconName" />
+ </div>
+
+ <div class="note-header">
+ <note-header :author="noteAuthor" :created-at="note.createdAt" :note-id="note.id">
+ <span v-html="note.bodyHtml"></span>
+ </note-header>
+ </div>
+ </div>
+ </li>
+</template>
diff --git a/app/assets/javascripts/vue_shared/alert_details/constants.js b/app/assets/javascripts/vue_shared/alert_details/constants.js
new file mode 100644
index 00000000000..2ab5160534c
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/alert_details/constants.js
@@ -0,0 +1,31 @@
+import { s__ } from '~/locale';
+
+export const SEVERITY_LEVELS = {
+ CRITICAL: s__('severity|Critical'),
+ HIGH: s__('severity|High'),
+ MEDIUM: s__('severity|Medium'),
+ LOW: s__('severity|Low'),
+ INFO: s__('severity|Info'),
+ UNKNOWN: s__('severity|Unknown'),
+};
+
+/* eslint-disable @gitlab/require-i18n-strings */
+export const PAGE_CONFIG = {
+ OPERATIONS: {
+ TITLE: 'OPERATIONS',
+ // Tracks snowplow event when user views alert details
+ TRACK_ALERTS_DETAILS_VIEWS_OPTIONS: {
+ category: 'Alert Management',
+ action: 'view_alert_details',
+ },
+ // Tracks snowplow event when alert status is updated
+ TRACK_ALERT_STATUS_UPDATE_OPTIONS: {
+ category: 'Alert Management',
+ action: 'update_alert_status',
+ label: 'Status',
+ },
+ },
+ THREAT_MONITORING: {
+ TITLE: 'THREAT_MONITORING',
+ },
+};
diff --git a/app/assets/javascripts/vue_shared/alert_details/graphql/fragments/alert_detail_item.fragment.graphql b/app/assets/javascripts/vue_shared/alert_details/graphql/fragments/alert_detail_item.fragment.graphql
new file mode 100644
index 00000000000..9a9ae369519
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/alert_details/graphql/fragments/alert_detail_item.fragment.graphql
@@ -0,0 +1,30 @@
+#import "~/graphql_shared/fragments/alert.fragment.graphql"
+#import "~/graphql_shared/fragments/alert_note.fragment.graphql"
+
+fragment AlertDetailItem on AlertManagementAlert {
+ ...AlertListItem
+ createdAt
+ monitoringTool
+ metricsDashboardUrl
+ service
+ description
+ updatedAt
+ endedAt
+ hosts
+ environment {
+ name
+ path
+ }
+ details
+ runbook
+ todos {
+ nodes {
+ id
+ }
+ }
+ notes {
+ nodes {
+ ...AlertNote
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_issue_create.mutation.graphql b/app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_issue_create.mutation.graphql
new file mode 100644
index 00000000000..bc4d91a51d1
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_issue_create.mutation.graphql
@@ -0,0 +1,8 @@
+mutation createAlertIssue($projectPath: ID!, $iid: String!) {
+ createAlertIssue(input: { iid: $iid, projectPath: $projectPath }) {
+ errors
+ issue {
+ iid
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql b/app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql
new file mode 100644
index 00000000000..63d952a4857
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql
@@ -0,0 +1,25 @@
+#import "~/graphql_shared/fragments/alert_note.fragment.graphql"
+
+mutation alertSetAssignees($projectPath: ID!, $assigneeUsernames: [String!]!, $iid: String!) {
+ alertSetAssignees(
+ input: { iid: $iid, assigneeUsernames: $assigneeUsernames, projectPath: $projectPath }
+ ) {
+ errors
+ alert {
+ iid
+ assignees {
+ nodes {
+ username
+ name
+ avatarUrl
+ webUrl
+ }
+ }
+ notes {
+ nodes {
+ ...AlertNote
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_sidebar_status.mutation.graphql b/app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_sidebar_status.mutation.graphql
new file mode 100644
index 00000000000..f666fcd6782
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_sidebar_status.mutation.graphql
@@ -0,0 +1,3 @@
+mutation toggleSidebarStatus {
+ toggleSidebarStatus @client
+}
diff --git a/app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_todo_create.mutation.graphql b/app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_todo_create.mutation.graphql
new file mode 100644
index 00000000000..dc961b5eb90
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_todo_create.mutation.graphql
@@ -0,0 +1,10 @@
+#import "../fragments/alert_detail_item.fragment.graphql"
+
+mutation alertTodoCreate($projectPath: ID!, $iid: String!) {
+ alertTodoCreate(input: { iid: $iid, projectPath: $projectPath }) {
+ errors
+ alert {
+ ...AlertDetailItem
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/alert_details/graphql/queries/alert_details.query.graphql b/app/assets/javascripts/vue_shared/alert_details/graphql/queries/alert_details.query.graphql
new file mode 100644
index 00000000000..5ee2cf7ca44
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/alert_details/graphql/queries/alert_details.query.graphql
@@ -0,0 +1,11 @@
+#import "../fragments/alert_detail_item.fragment.graphql"
+
+query alertDetails($fullPath: ID!, $alertId: String) {
+ project(fullPath: $fullPath) {
+ alertManagementAlerts(iid: $alertId) {
+ nodes {
+ ...AlertDetailItem
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/alert_details/graphql/queries/alert_sidebar_status.query.graphql b/app/assets/javascripts/vue_shared/alert_details/graphql/queries/alert_sidebar_status.query.graphql
new file mode 100644
index 00000000000..61c570c5cd0
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/alert_details/graphql/queries/alert_sidebar_status.query.graphql
@@ -0,0 +1,3 @@
+query sidebarStatus {
+ sidebarStatus @client
+}
diff --git a/app/assets/javascripts/vue_shared/alert_details/index.js b/app/assets/javascripts/vue_shared/alert_details/index.js
new file mode 100644
index 00000000000..3ea43d7a843
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/alert_details/index.js
@@ -0,0 +1,83 @@
+import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
+import produce from 'immer';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import AlertDetails from './components/alert_details.vue';
+import { PAGE_CONFIG } from './constants';
+import sidebarStatusQuery from './graphql/queries/alert_sidebar_status.query.graphql';
+import createRouter from './router';
+
+Vue.use(VueApollo);
+
+export default (selector) => {
+ const domEl = document.querySelector(selector);
+ const { alertId, projectPath, projectIssuesPath, projectId, page } = domEl.dataset;
+ const router = createRouter();
+
+ const resolvers = {
+ Mutation: {
+ toggleSidebarStatus: (_, __, { cache }) => {
+ const sourceData = cache.readQuery({ query: sidebarStatusQuery });
+ const data = produce(sourceData, (draftData) => {
+ // eslint-disable-next-line no-param-reassign
+ draftData.sidebarStatus = !draftData.sidebarStatus;
+ });
+ cache.writeQuery({ query: sidebarStatusQuery, data });
+ },
+ },
+ };
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(resolvers, {
+ cacheConfig: {
+ dataIdFromObject: (object) => {
+ // eslint-disable-next-line no-underscore-dangle
+ if (object.__typename === 'AlertManagementAlert') {
+ return object.iid;
+ }
+ return defaultDataIdFromObject(object);
+ },
+ },
+ assumeImmutableResults: true,
+ }),
+ });
+
+ apolloProvider.clients.defaultClient.cache.writeData({
+ data: {
+ sidebarStatus: false,
+ },
+ });
+
+ const provide = {
+ projectPath,
+ alertId,
+ page,
+ projectIssuesPath,
+ projectId,
+ };
+
+ if (page === PAGE_CONFIG.OPERATIONS.TITLE) {
+ const { TRACK_ALERTS_DETAILS_VIEWS_OPTIONS, TRACK_ALERT_STATUS_UPDATE_OPTIONS } = PAGE_CONFIG[
+ page
+ ];
+ provide.trackAlertsDetailsViewsOptions = TRACK_ALERTS_DETAILS_VIEWS_OPTIONS;
+ provide.trackAlertStatusUpdateOptions = TRACK_ALERT_STATUS_UPDATE_OPTIONS;
+ } else if (page === PAGE_CONFIG.THREAT_MONITORING.TITLE) {
+ provide.isThreatMonitoringPage = true;
+ }
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: selector,
+ components: {
+ AlertDetails,
+ },
+ provide,
+ apolloProvider,
+ router,
+ render(createElement) {
+ return createElement('alert-details', {});
+ },
+ });
+};
diff --git a/app/assets/javascripts/vue_shared/alert_details/router.js b/app/assets/javascripts/vue_shared/alert_details/router.js
new file mode 100644
index 00000000000..5687fe4e0f5
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/alert_details/router.js
@@ -0,0 +1,13 @@
+import Vue from 'vue';
+import VueRouter from 'vue-router';
+import { joinPaths } from '~/lib/utils/url_utility';
+
+Vue.use(VueRouter);
+
+export default function createRouter(base) {
+ return new VueRouter({
+ mode: 'hash',
+ base: joinPaths(gon.relative_url_root || '', base),
+ routes: [{ path: '/:tabId', name: 'tab' }],
+ });
+}
diff --git a/app/assets/javascripts/vue_shared/components/actions_button.vue b/app/assets/javascripts/vue_shared/components/actions_button.vue
index 2dc2c27f7ea..13472b48e84 100644
--- a/app/assets/javascripts/vue_shared/components/actions_button.vue
+++ b/app/assets/javascripts/vue_shared/components/actions_button.vue
@@ -76,14 +76,13 @@ export default {
<template v-for="(action, index) in actions">
<gl-dropdown-item
:key="action.key"
- class="gl-dropdown-item-deprecated-adapter"
:is-check-item="true"
:is-checked="action.key === selectedAction.key"
:secondary-text="action.secondaryText"
:data-testid="`action_${action.key}`"
@click="handleItemClick(action)"
>
- {{ action.text }}
+ <span class="gl-font-weight-bold">{{ action.text }}</span>
</gl-dropdown-item>
<gl-dropdown-divider v-if="index != actions.length - 1" :key="action.key + '_divider'" />
</template>
diff --git a/app/assets/javascripts/vue_shared/components/alert_details_table.vue b/app/assets/javascripts/vue_shared/components/alert_details_table.vue
index 655b867574d..3d49a1cb1c5 100644
--- a/app/assets/javascripts/vue_shared/components/alert_details_table.vue
+++ b/app/assets/javascripts/vue_shared/components/alert_details_table.vue
@@ -1,12 +1,12 @@
<script>
import { GlLoadingIcon, GlTable } from '@gitlab/ui';
import { reduce } from 'lodash';
-import { s__ } from '~/locale';
import {
capitalizeFirstCharacter,
convertToSentenceCase,
splitCamelCase,
} from '~/lib/utils/text_utility';
+import { s__ } from '~/locale';
const thClass = 'gl-bg-transparent! gl-border-1! gl-border-b-solid! gl-border-gray-200!';
const tdClass = 'gl-border-gray-100! gl-p-5!';
diff --git a/app/assets/javascripts/vue_shared/components/awards_list.vue b/app/assets/javascripts/vue_shared/components/awards_list.vue
index c1da2b8c305..ce67d33d4a1 100644
--- a/app/assets/javascripts/vue_shared/components/awards_list.vue
+++ b/app/assets/javascripts/vue_shared/components/awards_list.vue
@@ -1,9 +1,9 @@
<script>
/* eslint-disable vue/no-v-html */
-import { groupBy } from 'lodash';
import { GlIcon, GlButton, GlTooltipDirective } from '@gitlab/ui';
-import { glEmojiTag } from '../../emoji';
+import { groupBy } from 'lodash';
import { __, sprintf } from '~/locale';
+import { glEmojiTag } from '../../emoji';
// Internal constant, specific to this component, used when no `currentUserId` is given
const NO_USER_ID = -1;
diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js b/app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js
index 6f7723955bf..db61d0f6b05 100644
--- a/app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js
+++ b/app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js
@@ -1,5 +1,5 @@
-import { SNIPPET_MEASURE_BLOBS_CONTENT } from '~/performance/constants';
import eventHub from '~/blob/components/eventhub';
+import { SNIPPET_MEASURE_BLOBS_CONTENT } from '~/performance/constants';
export default {
props: {
diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue b/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue
index d0f5570db6b..a8a053c0d9e 100644
--- a/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue
@@ -1,8 +1,8 @@
<script>
import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import { handleBlobRichViewer } from '~/blob/viewer';
import MarkdownFieldView from '~/vue_shared/components/markdown/field_view.vue';
import ViewerMixin from './mixins';
-import { handleBlobRichViewer } from '~/blob/viewer';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue b/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue
index 646e1703f1e..5bb31f55e6c 100644
--- a/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue
@@ -1,8 +1,8 @@
<script>
/* eslint-disable vue/no-v-html */
import { GlIcon } from '@gitlab/ui';
-import ViewerMixin from './mixins';
import { HIGHLIGHT_CLASS_NAME } from './constants';
+import ViewerMixin from './mixins';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/clone_dropdown.vue b/app/assets/javascripts/vue_shared/components/clone_dropdown.vue
index 5c6bd5892ae..cd5f63afc79 100644
--- a/app/assets/javascripts/vue_shared/components/clone_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/clone_dropdown.vue
@@ -6,8 +6,8 @@ import {
GlButton,
GlTooltipDirective,
} from '@gitlab/ui';
-import { __, sprintf } from '~/locale';
import { getHTTPProtocol } from '~/lib/utils/url_utility';
+import { __, sprintf } from '~/locale';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/color_picker/color_picker.vue b/app/assets/javascripts/vue_shared/components/color_picker/color_picker.vue
index 6977692e30c..0ff33e462b4 100644
--- a/app/assets/javascripts/vue_shared/components/color_picker/color_picker.vue
+++ b/app/assets/javascripts/vue_shared/components/color_picker/color_picker.vue
@@ -3,12 +3,16 @@
* Renders a color picker input with preset colors to choose from
*
* @example
- * <color-picker :label="__('Background color')" set-color="#FF0000" />
+ * <color-picker
+ :invalid-feedback="__('Please enter a valid hex (#RRGGBB or #RGB) color value')"
+ :label="__('Background color')"
+ :value="#FF0000"
+ state="isValidColor"
+ />
*/
import { GlFormGroup, GlFormInput, GlFormInputGroup, GlLink, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
-const VALID_RGB_HEX_COLOR = /^#([0-9A-F]{3}){1,2}$/i;
const PREVIEW_COLOR_DEFAULT_CLASSES =
'gl-relative gl-w-7 gl-bg-gray-10 gl-rounded-top-left-base gl-rounded-bottom-left-base';
@@ -24,21 +28,26 @@ export default {
GlTooltip: GlTooltipDirective,
},
props: {
+ invalidFeedback: {
+ type: String,
+ required: false,
+ default: __('Please enter a valid hex (#RRGGBB or #RGB) color value'),
+ },
label: {
type: String,
required: false,
default: '',
},
- setColor: {
+ value: {
type: String,
required: false,
default: '',
},
- },
- data() {
- return {
- selectedColor: this.setColor.trim() || '',
- };
+ state: {
+ type: Boolean,
+ required: false,
+ default: null,
+ },
},
computed: {
description() {
@@ -50,46 +59,30 @@ export default {
return gon.suggested_label_colors;
},
previewColor() {
- if (this.isValidColor) {
- return { backgroundColor: this.selectedColor };
+ if (this.state) {
+ return { backgroundColor: this.value };
}
return {};
},
previewColorClasses() {
- const borderStyle = this.isInvalidColor
- ? 'gl-inset-border-1-red-500'
- : 'gl-inset-border-1-gray-400';
+ const borderStyle =
+ this.state === false ? 'gl-inset-border-1-red-500' : 'gl-inset-border-1-gray-400';
return `${PREVIEW_COLOR_DEFAULT_CLASSES} ${borderStyle}`;
},
hasSuggestedColors() {
return Object.keys(this.suggestedColors).length;
},
- isInvalidColor() {
- return this.isValidColor === false;
- },
- isValidColor() {
- if (this.selectedColor === '') {
- return null;
- }
-
- return VALID_RGB_HEX_COLOR.test(this.selectedColor);
- },
},
methods: {
handleColorChange(color) {
- this.selectedColor = color.trim();
-
- if (this.isValidColor) {
- this.$emit('input', this.selectedColor);
- }
+ this.$emit('input', color.trim());
},
},
i18n: {
fullDescription: __('Choose any color. Or you can choose one of the suggested colors below'),
shortDescription: __('Choose any color'),
- invalid: __('Please enter a valid hex (#RRGGBB or #RGB) color value'),
},
};
</script>
@@ -100,17 +93,17 @@ export default {
:label="label"
label-for="color-picker"
:description="description"
- :invalid-feedback="this.$options.i18n.invalid"
- :state="isValidColor"
+ :invalid-feedback="invalidFeedback"
+ :state="state"
:class="{ 'gl-mb-3!': hasSuggestedColors }"
>
<gl-form-input-group
id="color-picker"
- :state="isValidColor"
max-length="7"
type="text"
class="gl-align-center gl-rounded-0 gl-rounded-top-right-base gl-rounded-bottom-right-base"
- :value="selectedColor"
+ :value="value"
+ :state="state"
@input="handleColorChange"
>
<template #prepend>
@@ -119,7 +112,7 @@ export default {
type="color"
class="gl-absolute gl-top-0 gl-left-0 gl-h-full! gl-p-0! gl-m-0! gl-cursor-pointer gl-opacity-0"
tabindex="-1"
- :value="selectedColor"
+ :value="value"
@input="handleColorChange"
/>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue
index deca934e283..d1eee62683b 100644
--- a/app/assets/javascripts/vue_shared/components/commit.vue
+++ b/app/assets/javascripts/vue_shared/components/commit.vue
@@ -1,6 +1,6 @@
<script>
-import { isString, isEmpty } from 'lodash';
import { GlTooltipDirective, GlLink, GlIcon } from '@gitlab/ui';
+import { isString, isEmpty } from 'lodash';
import { __, sprintf } from '~/locale';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import UserAvatarLink from './user_avatar/user_avatar_link.vue';
@@ -133,6 +133,9 @@ export default {
? sprintf(__("%{username}'s avatar"), { username: this.author.username })
: null;
},
+ refUrl() {
+ return this.commitRef.ref_url || this.commitRef.path;
+ },
},
};
</script>
@@ -156,9 +159,10 @@ export default {
<gl-link
v-else
v-gl-tooltip
- :href="commitRef.ref_url"
+ :href="refUrl"
:title="commitRef.name"
class="ref-name"
+ data-testid="ref-name"
>{{ commitRef.name }}</gl-link
>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue
index 9ff35132ac9..1370f7b2a8c 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue
@@ -1,7 +1,7 @@
<script>
-import MarkdownViewer from './viewers/markdown_viewer.vue';
-import ImageViewer from './viewers/image_viewer.vue';
import DownloadViewer from './viewers/download_viewer.vue';
+import ImageViewer from './viewers/image_viewer.vue';
+import MarkdownViewer from './viewers/markdown_viewer.vue';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue
index 9ece6a52805..a49eb7fd611 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue
@@ -1,6 +1,7 @@
<script>
import { throttle } from 'lodash';
-import { numberToHumanSize } from '../../../../lib/utils/number_utils';
+import { numberToHumanSize } from '~/lib/utils/number_utils';
+import { encodeSaferUrl } from '~/lib/utils/url_utility';
export default {
props: {
@@ -43,6 +44,9 @@ export default {
hasDimensions() {
return this.width && this.height;
},
+ safePath() {
+ return encodeSaferUrl(this.path);
+ },
},
beforeDestroy() {
window.removeEventListener('resize', this.resizeThrottled, false);
@@ -84,7 +88,7 @@ export default {
<template>
<div data-testid="image-viewer" data-qa-selector="image_viewer_container">
<div :class="innerCssClasses" class="position-relative">
- <img ref="contentImg" :src="path" @load="onImgLoad" />
+ <img ref="contentImg" :src="safePath" @load="onImgLoad" />
<slot
name="image-overlay"
:rendered-width="renderedWidth"
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
index 24386c90954..3790a509f26 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
@@ -1,9 +1,8 @@
<script>
/* eslint-disable vue/no-v-html */
+import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
-
-import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import { forEach, escape } from 'lodash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
diff --git a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue
index d0c2672b162..1a96cabf755 100644
--- a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue
+++ b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue
@@ -1,8 +1,7 @@
<script>
import { GlIcon, GlButton, GlDropdown, GlDropdownItem, GlFormGroup } from '@gitlab/ui';
-import { __, sprintf } from '~/locale';
-
import { convertToFixedRange, isEqualTimeRanges, findTimeRange } from '~/lib/utils/datetime_range';
+import { __, sprintf } from '~/locale';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import DateTimePickerInput from './date_time_picker_input.vue';
diff --git a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_input.vue b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_input.vue
index 39c1caf928e..190d4e1f104 100644
--- a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_input.vue
+++ b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_input.vue
@@ -1,6 +1,6 @@
<script>
-import { uniqueId } from 'lodash';
import { GlFormGroup, GlFormInput } from '@gitlab/ui';
+import { uniqueId } from 'lodash';
import { __, sprintf } from '~/locale';
import { dateFormats } from './date_time_picker_lib';
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue
index e755494a668..7080e046b30 100644
--- a/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue
@@ -1,9 +1,9 @@
<script>
import { diffViewerModes, diffModes } from '~/ide/constants';
-import ImageDiffViewer from './viewers/image_diff_viewer.vue';
import DownloadDiffViewer from './viewers/download_diff_viewer.vue';
-import RenamedFile from './viewers/renamed.vue';
+import ImageDiffViewer from './viewers/image_diff_viewer.vue';
import ModeChanged from './viewers/mode_changed.vue';
+import RenamedFile from './viewers/renamed.vue';
export default {
props: {
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue
index 433aafdeb9e..a3d9b0ace34 100644
--- a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue
@@ -1,6 +1,6 @@
<script>
-import { pixeliseValue } from '../../../lib/utils/dom_utils';
import ImageViewer from '../../../content_viewer/viewers/image_viewer.vue';
+import { pixeliseValue } from '../../../lib/utils/dom_utils';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue
index acca6ba117f..41acc63cea9 100644
--- a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue
@@ -1,7 +1,7 @@
<script>
import { throttle } from 'lodash';
-import { pixeliseValue } from '../../../lib/utils/dom_utils';
import ImageViewer from '../../../content_viewer/viewers/image_viewer.vue';
+import { pixeliseValue } from '../../../lib/utils/dom_utils';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue
index 00033145603..f5ac65bcd1f 100644
--- a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue
@@ -1,9 +1,9 @@
<script>
import ImageViewer from '../../content_viewer/viewers/image_viewer.vue';
-import TwoUpViewer from './image_diff/two_up_viewer.vue';
-import SwipeViewer from './image_diff/swipe_viewer.vue';
-import OnionSkinViewer from './image_diff/onion_skin_viewer.vue';
import { diffModes, imageViewMode } from '../constants';
+import OnionSkinViewer from './image_diff/onion_skin_viewer.vue';
+import SwipeViewer from './image_diff/swipe_viewer.vue';
+import TwoUpViewer from './image_diff/two_up_viewer.vue';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/renamed.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/renamed.vue
index eba6dd4d14c..d6f99e9a049 100644
--- a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/renamed.vue
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/renamed.vue
@@ -1,8 +1,7 @@
<script>
-import { mapActions } from 'vuex';
import { GlAlert, GlLink, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
+import { mapActions } from 'vuex';
-import { __ } from '~/locale';
import {
TRANSITION_LOAD_START,
TRANSITION_LOAD_ERROR,
@@ -14,6 +13,7 @@ import {
RENAMED_DIFF_TRANSITIONS,
} from '~/diffs/constants';
import { truncateSha } from '~/lib/utils/text_utility';
+import { __ } from '~/locale';
export default {
STATE_LOADING,
diff --git a/app/assets/javascripts/vue_shared/components/dismissible_feedback_alert.vue b/app/assets/javascripts/vue_shared/components/dismissible_feedback_alert.vue
index 2a28b13e7bf..c4dfcf93a18 100644
--- a/app/assets/javascripts/vue_shared/components/dismissible_feedback_alert.vue
+++ b/app/assets/javascripts/vue_shared/components/dismissible_feedback_alert.vue
@@ -1,7 +1,7 @@
<script>
import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
-import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { slugifyWithUnderscore } from '~/lib/utils/text_utility';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
index 7e82d8f3f9c..eb8400e81c7 100644
--- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
@@ -36,10 +36,8 @@ export default {
aria-expanded="false"
>
<gl-loading-icon v-show="isLoading" :inline="true" />
- <template>
- <slot v-if="$slots.default"></slot>
- <span v-else class="dropdown-toggle-text"> {{ toggleText }} </span>
- </template>
+ <slot v-if="$slots.default"></slot>
+ <span v-else class="dropdown-toggle-text"> {{ toggleText }} </span>
<gl-icon
v-show="!isLoading"
class="gl-absolute gl-top-3 gl-right-3 gl-text-gray-500"
diff --git a/app/assets/javascripts/vue_shared/components/editor_lite.vue b/app/assets/javascripts/vue_shared/components/editor_lite.vue
index 7218b84cf8a..c3bddabea21 100644
--- a/app/assets/javascripts/vue_shared/components/editor_lite.vue
+++ b/app/assets/javascripts/vue_shared/components/editor_lite.vue
@@ -1,7 +1,7 @@
<script>
import { debounce } from 'lodash';
+import { CONTENT_UPDATE_DEBOUNCE, EDITOR_READY_EVENT } from '~/editor/constants';
import Editor from '~/editor/editor_lite';
-import { CONTENT_UPDATE_DEBOUNCE } from '~/editor/constants';
function initEditorLite({ el, ...args }) {
const editor = new Editor({
@@ -88,6 +88,7 @@ export default {
return this.editor;
},
},
+ readyEvent: EDITOR_READY_EVENT,
};
</script>
<template>
@@ -95,7 +96,7 @@ export default {
:id="`editor-lite-${fileGlobalId}`"
ref="editor"
data-editor-loading
- @editor-ready="$emit('editor-ready')"
+ @[$options.readyEvent]="$emit($options.readyEvent)"
>
<pre class="editor-loading-content">{{ value }}</pre>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/file_finder/index.vue b/app/assets/javascripts/vue_shared/components/file_finder/index.vue
index 27933f87929..4ec54b33bce 100644
--- a/app/assets/javascripts/vue_shared/components/file_finder/index.vue
+++ b/app/assets/javascripts/vue_shared/components/file_finder/index.vue
@@ -1,10 +1,10 @@
<script>
+import { GlIcon } from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import Mousetrap from 'mousetrap';
import VirtualList from 'vue-virtual-scroll-list';
-import { GlIcon } from '@gitlab/ui';
-import Item from './item.vue';
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
+import Item from './item.vue';
export const MAX_FILE_FINDER_RESULTS = 40;
export const FILE_FINDER_ROW_HEIGHT = 55;
diff --git a/app/assets/javascripts/vue_shared/components/file_finder/item.vue b/app/assets/javascripts/vue_shared/components/file_finder/item.vue
index 4c496ba3f9b..7816c1d74ec 100644
--- a/app/assets/javascripts/vue_shared/components/file_finder/item.vue
+++ b/app/assets/javascripts/vue_shared/components/file_finder/item.vue
@@ -1,8 +1,8 @@
<script>
-import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { GlIcon } from '@gitlab/ui';
-import FileIcon from '../file_icon.vue';
+import fuzzaldrinPlus from 'fuzzaldrin-plus';
import ChangedFileIcon from '../changed_file_icon.vue';
+import FileIcon from '../file_icon.vue';
const MAX_PATH_LENGTH = 60;
diff --git a/app/assets/javascripts/vue_shared/components/file_icon.vue b/app/assets/javascripts/vue_shared/components/file_icon.vue
index 6190b07962d..8ac8a3beb7d 100644
--- a/app/assets/javascripts/vue_shared/components/file_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/file_icon.vue
@@ -1,7 +1,7 @@
<script>
import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
-import getIconForFile from './file_icon/file_icon_map';
import { FILE_SYMLINK_MODE } from '../constants';
+import getIconForFile from './file_icon/file_icon_map';
/* This is a re-usable vue component for rendering a svg sprite
icon
@@ -90,6 +90,12 @@ export default {
<svg v-else-if="!folder" :key="spriteHref" :class="[iconSizeClass, cssClasses]">
<use v-bind="{ 'xlink:href': spriteHref }" />
</svg>
- <gl-icon v-else :name="folderIconName" :size="size" class="folder-icon" />
+ <gl-icon
+ v-else
+ :name="folderIconName"
+ :size="size"
+ class="folder-icon"
+ data-qa-selector="folder_icon_content"
+ />
</span>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue
index 96567111bbc..0b0a416b7ef 100644
--- a/app/assets/javascripts/vue_shared/components/file_row.vue
+++ b/app/assets/javascripts/vue_shared/components/file_row.vue
@@ -1,8 +1,8 @@
<script>
import { GlTruncate } from '@gitlab/ui';
-import FileHeader from '~/vue_shared/components/file_row_header.vue';
-import FileIcon from '~/vue_shared/components/file_icon.vue';
import { escapeFileUrl } from '~/lib/utils/url_utility';
+import FileIcon from '~/vue_shared/components/file_icon.vue';
+import FileHeader from '~/vue_shared/components/file_row_header.vue';
export default {
name: 'FileRow',
@@ -147,6 +147,7 @@ export default {
:style="levelIndentation"
class="file-row-name"
data-qa-selector="file_name_content"
+ :data-qa-file-name="file.name"
data-testid="file-row-name-container"
:class="[fileClasses, { 'str-truncated': !truncateMiddle, 'gl-min-w-0': truncateMiddle }]"
>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
index a4c5ca28494..97a8f681faf 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
@@ -10,14 +10,13 @@ import {
} from '@gitlab/ui';
import RecentSearchesStorageKeys from 'ee_else_ce/filtered_search/recent_searches_storage_keys';
-import { __ } from '~/locale';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
-
-import RecentSearchesStore from '~/filtered_search/stores/recent_searches_store';
import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
+import RecentSearchesStore from '~/filtered_search/stores/recent_searches_store';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
+import { __ } from '~/locale';
-import { stripQuotes, uniqueTokens } from './filtered_search_utils';
import { SortDirection } from './constants';
+import { stripQuotes, uniqueTokens } from './filtered_search_utils';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/actions.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/actions.js
index 411654d15f4..4dfc61f1fff 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/actions.js
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/actions.js
@@ -1,7 +1,7 @@
+import Api from '~/api';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
-import Api from '~/api';
import * as types from './mutation_types';
export const setEndpoints = ({ commit }, params) => {
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/index.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/index.js
index 665bb29a17e..cdcbecdd313 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/index.js
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/index.js
@@ -1,6 +1,6 @@
-import state from './state';
import * as actions from './actions';
import mutations from './mutations';
+import state from './state';
export default {
namespaced: true,
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
index d59e9200e6c..9c2a644b7a9 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
@@ -9,12 +9,11 @@ import {
import { debounce } from 'lodash';
import { deprecatedCreateFlash as createFlash } from '~/flash';
-import { __ } from '~/locale';
-
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { __ } from '~/locale';
-import { stripQuotes } from '../filtered_search_utils';
import { DEFAULT_LABELS, DEBOUNCE_DELAY } from '../constants';
+import { stripQuotes } from '../filtered_search_utils';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
index 0dd7820073a..cda6e4d6726 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
@@ -10,8 +10,8 @@ import { debounce } from 'lodash';
import createFlash from '~/flash';
import { __ } from '~/locale';
-import { stripQuotes } from '../filtered_search_utils';
import { DEFAULT_MILESTONES, DEBOUNCE_DELAY } from '../constants';
+import { stripQuotes } from '../filtered_search_utils';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/gfm_autocomplete/utils.js b/app/assets/javascripts/vue_shared/components/gfm_autocomplete/utils.js
index 809932b0f29..44c3fc34ba6 100644
--- a/app/assets/javascripts/vue_shared/components/gfm_autocomplete/utils.js
+++ b/app/assets/javascripts/vue_shared/components/gfm_autocomplete/utils.js
@@ -84,7 +84,7 @@ export const tributeConfig = {
value.type === groupType ? last(value.name.split(' / ')) : `${value.name}${value.username}`,
menuItemLimit: memberLimit,
menuItemTemplate: ({ original }) => {
- const commonClasses = 'gl-avatar gl-avatar-s24 gl-flex-shrink-0';
+ const commonClasses = 'gl-avatar gl-avatar-s32 gl-flex-shrink-0';
const noAvatarClasses = `${commonClasses} gl-rounded-small
gl-display-flex gl-align-items-center gl-justify-content-center`;
@@ -111,7 +111,7 @@ export const tributeConfig = {
return `
<div class="gl-display-flex gl-align-items-center">
${avatar}
- <div class="gl-font-sm gl-line-height-normal gl-ml-3">
+ <div class="gl-line-height-normal gl-ml-4">
<div>${escape(displayName)}${count}</div>
<div class="gl-text-gray-700">${escape(parentGroupOrUsername)}</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/gl_modal_vuex.vue b/app/assets/javascripts/vue_shared/components/gl_modal_vuex.vue
index e14f6a04d3c..96d99faa952 100644
--- a/app/assets/javascripts/vue_shared/components/gl_modal_vuex.vue
+++ b/app/assets/javascripts/vue_shared/components/gl_modal_vuex.vue
@@ -1,6 +1,7 @@
<script>
-import { mapState, mapActions } from 'vuex';
import { GlModal } from '@gitlab/ui';
+import { mapState, mapActions } from 'vuex';
+import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
/**
* This component keeps the GlModal's visibility in sync with the given vuex module.
@@ -46,11 +47,11 @@ export default {
},
}),
bsShow() {
- this.$root.$emit('bv::show::modal', this.modalId);
+ this.$root.$emit(BV_SHOW_MODAL, this.modalId);
},
bsHide() {
// $root.$emit is a workaround because other b-modal approaches don't work yet with gl-modal
- this.$root.$emit('bv::hide::modal', this.modalId);
+ this.$root.$emit(BV_HIDE_MODAL, this.modalId);
},
cancel() {
this.$emit('cancel');
diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
index 79d9ba6df57..80ca62a0e9b 100644
--- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue
+++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
@@ -1,11 +1,11 @@
<script>
/* eslint-disable vue/no-v-html */
import { GlTooltipDirective, GlLink, GlButton, GlTooltip } from '@gitlab/ui';
+import { glEmojiTag } from '../../emoji';
+import { __, sprintf } from '../../locale';
import CiIconBadge from './ci_badge_link.vue';
import TimeagoTooltip from './time_ago_tooltip.vue';
import UserAvatarImage from './user_avatar/user_avatar_image.vue';
-import { glEmojiTag } from '../../emoji';
-import { __, sprintf } from '../../locale';
/**
* Renders header component for job and pipeline page based on UI mockups
@@ -105,7 +105,7 @@ export default {
<section class="header-main-content">
<ci-icon-badge :status="status" />
- <strong> {{ itemName }} #{{ itemId }} </strong>
+ <strong data-testid="ci-header-item-text"> {{ itemName }} #{{ itemId }} </strong>
<template v-if="shouldRenderTriggeredLabel">{{ __('triggered') }}</template>
<template v-else>{{ __('created') }}</template>
@@ -142,13 +142,13 @@ export default {
</template>
</section>
- <section v-if="$slots.default" data-testid="headerButtons" class="gl-display-flex">
+ <section v-if="$slots.default" data-testid="ci-header-action-buttons" class="gl-display-flex">
<slot></slot>
</section>
<gl-button
v-if="hasSidebarButton"
class="d-sm-none js-sidebar-build-toggle gl-ml-auto"
- icon="angle-double-left"
+ icon="chevron-double-lg-left"
:aria-label="__('Toggle sidebar')"
@click="onClickSidebarButton"
/>
diff --git a/app/assets/javascripts/vue_shared/components/help_popover.vue b/app/assets/javascripts/vue_shared/components/help_popover.vue
index 821ae6cec52..051c65bae70 100644
--- a/app/assets/javascripts/vue_shared/components/help_popover.vue
+++ b/app/assets/javascripts/vue_shared/components/help_popover.vue
@@ -1,8 +1,5 @@
<script>
-import $ from 'jquery';
-import { GlButton } from '@gitlab/ui';
-import { inserted } from '~/feature_highlight/feature_highlight_helper';
-import { mouseenter, debouncedMouseleave, togglePopover } from '~/shared/popover';
+import { GlButton, GlPopover } from '@gitlab/ui';
/**
* Render a button with a question mark icon
@@ -12,6 +9,7 @@ export default {
name: 'HelpPopover',
components: {
GlButton,
+ GlPopover,
},
props: {
options: {
@@ -20,28 +18,20 @@ export default {
default: () => ({}),
},
},
- mounted() {
- const $el = $(this.$el);
-
- $el
- .popover({
- html: true,
- trigger: 'focus',
- container: 'body',
- placement: 'top',
- template:
- '<div class="popover" role="tooltip"><div class="arrow"></div><p class="popover-header"></p><div class="popover-body"></div></div>',
- ...this.options,
- })
- .on('mouseenter', mouseenter)
- .on('mouseleave', debouncedMouseleave(300))
- .on('inserted.bs.popover', inserted)
- .on('show.bs.popover', () => {
- window.addEventListener('scroll', togglePopover.bind($el, false), { once: true });
- });
- },
};
</script>
<template>
- <gl-button variant="link" icon="question" tabindex="0" />
+ <span>
+ <gl-button ref="popoverTrigger" variant="link" icon="question" tabindex="0" />
+ <gl-popover triggers="hover focus" :target="() => $refs.popoverTrigger.$el" v-bind="options">
+ <template #title>
+ <!-- eslint-disable-next-line vue/no-v-html -->
+ <span v-html="options.title"></span>
+ </template>
+ <template #default>
+ <!-- eslint-disable-next-line vue/no-v-html -->
+ <div v-html="options.content"></div>
+ </template>
+ </gl-popover>
+ </span>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/issuable/issuable_header_warnings.vue b/app/assets/javascripts/vue_shared/components/issuable/issuable_header_warnings.vue
index 37995b434c4..56adbe8c606 100644
--- a/app/assets/javascripts/vue_shared/components/issuable/issuable_header_warnings.vue
+++ b/app/assets/javascripts/vue_shared/components/issuable/issuable_header_warnings.vue
@@ -1,6 +1,6 @@
<script>
-import { mapGetters } from 'vuex';
import { GlIcon } from '@gitlab/ui';
+import { mapGetters } from 'vuex';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue b/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue
index c745ea61f8b..6a0c21602bd 100644
--- a/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue
+++ b/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue
@@ -1,8 +1,8 @@
<script>
import { GlTooltip, GlIcon } from '@gitlab/ui';
+import { timeFor, parsePikadayDate, dateInWords } from '~/lib/utils/datetime_utility';
import { __, sprintf } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago';
-import { timeFor, parsePikadayDate, dateInWords } from '~/lib/utils/datetime_utility';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue b/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
index 2ff4033a07e..be0c843ef00 100644
--- a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
+++ b/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
@@ -2,12 +2,12 @@
/* eslint-disable vue/no-v-html */
import '~/commons/bootstrap';
import { GlIcon, GlTooltip, GlTooltipDirective, GlButton } from '@gitlab/ui';
-import { sprintf } from '~/locale';
-import IssueMilestone from './issue_milestone.vue';
-import IssueAssignees from './issue_assignees.vue';
import IssueDueDate from '~/boards/components/issue_due_date.vue';
+import { sprintf } from '~/locale';
import relatedIssuableMixin from '../../mixins/related_issuable_mixin';
import CiIcon from '../ci_icon.vue';
+import IssueAssignees from './issue_assignees.vue';
+import IssueMilestone from './issue_milestone.vue';
export default {
name: 'IssueItem',
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index b6e167524aa..25d01dc550f 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -1,19 +1,19 @@
<script>
/* eslint-disable vue/no-v-html */
+import { GlIcon } from '@gitlab/ui';
import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
import { unescape } from 'lodash';
-import { GlIcon } from '@gitlab/ui';
-import { __, sprintf } from '~/locale';
-import { stripHtml } from '~/lib/utils/text_utility';
import { deprecatedCreateFlash as Flash } from '~/flash';
import GLForm from '~/gl_form';
-import MarkdownHeader from './header.vue';
-import MarkdownToolbar from './toolbar.vue';
+import axios from '~/lib/utils/axios_utils';
+import { stripHtml } from '~/lib/utils/text_utility';
+import { __, sprintf } from '~/locale';
import GfmAutocomplete from '~/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue';
import Suggestions from '~/vue_shared/components/markdown/suggestions.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import axios from '~/lib/utils/axios_utils';
+import MarkdownHeader from './header.vue';
+import MarkdownToolbar from './toolbar.vue';
export default {
components: {
@@ -110,11 +110,6 @@ export default {
return this.referencedUsers.length >= referencedUsersThreshold;
},
lineContent() {
- const [firstSuggestion] = this.suggestions;
- if (firstSuggestion) {
- return firstSuggestion.from_content;
- }
-
if (this.line) {
const { rich_text: richText, text } = this.line;
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index 173d192dab0..5bc1786d692 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -1,8 +1,8 @@
<script>
-import $ from 'jquery';
import { GlPopover, GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
-import { s__ } from '~/locale';
+import $ from 'jquery';
import { getSelectedFragment } from '~/lib/utils/common_utils';
+import { s__ } from '~/locale';
import { CopyAsGFM } from '../../../behaviors/markdown/copy_as_gfm';
import ToolbarButton from './toolbar_button.vue';
@@ -172,6 +172,7 @@ export default {
:cursor-offset="4"
:tag-content="lineContent"
icon="doc-code"
+ data-qa-selector="suggestion_button"
class="js-suggestion-btn"
@click="handleSuggestDismissed"
/>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue
index 93a270b8a97..bcd8c02e968 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue
@@ -1,7 +1,7 @@
<script>
+import { selectDiffLines } from '../lib/utils/diff_utils';
import SuggestionDiffHeader from './suggestion_diff_header.vue';
import SuggestionDiffRow from './suggestion_diff_row.vue';
-import { selectDiffLines } from '../lib/utils/diff_utils';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
index 63341b433e0..4c6fa71398d 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
@@ -88,7 +88,12 @@ export default {
applySuggestion(message) {
if (!this.canApply) return;
this.isApplyingSingle = true;
- this.$emit('apply', this.applySuggestionCallback, message);
+
+ this.$emit(
+ 'apply',
+ this.applySuggestionCallback,
+ gon.features?.suggestionsCustomCommit ? message : undefined,
+ );
},
applySuggestionCallback() {
this.isApplyingSingle = false;
@@ -131,6 +136,7 @@ export default {
<gl-button
v-gl-tooltip.viewport="__('This also resolves all related threads')"
class="btn-inverted js-apply-batch-btn btn-grouped"
+ data-qa-selector="apply_suggestions_batch_button"
:disabled="isApplying"
variant="success"
@click="applySuggestionBatch"
@@ -145,6 +151,7 @@ export default {
<gl-button
v-if="suggestionsCount > 1 && canBeBatched && !isDisableButton"
class="btn-inverted js-add-to-batch-btn btn-grouped"
+ data-qa-selector="add_suggestion_batch_button"
:disabled="isDisableButton"
@click="addSuggestionToBatch"
>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
index 5ee51764555..53d1cca7af3 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
@@ -1,9 +1,9 @@
<script>
-import Vue from 'vue';
import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import Vue from 'vue';
+import { deprecatedCreateFlash as Flash } from '~/flash';
import { __ } from '~/locale';
import SuggestionDiff from './suggestion_diff.vue';
-import { deprecatedCreateFlash as Flash } from '~/flash';
export default {
directives: {
@@ -64,6 +64,11 @@ export default {
mounted() {
this.renderSuggestions();
},
+ beforeDestroy() {
+ if (this.suggestionsWatch) {
+ this.suggestionsWatch();
+ }
+ },
methods: {
renderSuggestions() {
// swaps out suggestion(s) markdown with rich diff components
@@ -108,6 +113,13 @@ export default {
},
});
+ // We're using `$watch` as `suggestionsCount` updates do not
+ // propagate to this component for some unknown reason while
+ // using a traditional prop watcher.
+ this.suggestionsWatch = this.$watch('suggestionsCount', () => {
+ suggestionDiff.suggestionsCount = this.suggestionsCount;
+ });
+
suggestionDiff.$on('apply', ({ suggestionId, callback, message }) => {
this.$emit('apply', { suggestionId, callback, flashContainer: this.$el, message });
});
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
index 15c5b9d6733..387b100a04f 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
@@ -60,9 +60,7 @@ export default {
</div>
<span v-if="canAttachFile" class="uploading-container">
<span class="uploading-progress-container hide">
- <template>
- <gl-icon name="media" />
- </template>
+ <gl-icon name="media" />
<span class="attaching-file-message"></span>
<!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings -->
<span class="uploading-progress">0%</span>
diff --git a/app/assets/javascripts/vue_shared/components/modal_copy_button.vue b/app/assets/javascripts/vue_shared/components/modal_copy_button.vue
index e3a7f144321..7b36d57dfbf 100644
--- a/app/assets/javascripts/vue_shared/components/modal_copy_button.vue
+++ b/app/assets/javascripts/vue_shared/components/modal_copy_button.vue
@@ -2,6 +2,7 @@
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import Clipboard from 'clipboard';
import { uniqueId } from 'lodash';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
export default {
components: {
@@ -76,7 +77,7 @@ export default {
});
this.clipboard
.on('success', (e) => {
- this.$root.$emit('bv::hide::tooltip', this.id);
+ this.$root.$emit(BV_HIDE_TOOLTIP, this.id);
this.$emit('success', e);
// Clear the selection and blur the trigger so it loses its border
e.clearSelection();
diff --git a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue
index 653ee7f20e9..a069d1cd756 100644
--- a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue
+++ b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue
@@ -1,6 +1,6 @@
<script>
-import $ from 'jquery';
import { GlBadge, GlTabs, GlTab } from '@gitlab/ui';
+import $ from 'jquery';
/**
* Given an array of tabs, renders non linked bootstrap tabs.
diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
index cc1203f83f0..50972a8c32c 100644
--- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
@@ -18,8 +18,6 @@
* }"
* />
*/
-import $ from 'jquery';
-import { mapGetters, mapActions, mapState } from 'vuex';
import {
GlButton,
GlDeprecatedSkeletonLoading as GlSkeletonLoading,
@@ -27,12 +25,14 @@ import {
GlIcon,
GlSafeHtmlDirective as SafeHtml,
} from '@gitlab/ui';
+import $ from 'jquery';
+import { mapGetters, mapActions, mapState } from 'vuex';
import descriptionVersionHistoryMixin from 'ee_else_ce/notes/mixins/description_version_history';
+import initMRPopovers from '~/mr_popover/';
import noteHeader from '~/notes/components/note_header.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import TimelineEntryItem from './timeline_entry_item.vue';
import { spriteIcon } from '../../../lib/utils/common_utils';
-import initMRPopovers from '~/mr_popover/';
+import TimelineEntryItem from './timeline_entry_item.vue';
const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
diff --git a/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue
index d03987bbbe0..e2591362611 100644
--- a/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue
+++ b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue
@@ -1,13 +1,13 @@
<script>
import { GlAlert, GlBadge, GlPagination, GlTab, GlTabs } from '@gitlab/ui';
import Api from '~/api';
-import Tracking from '~/tracking';
-import { __ } from '~/locale';
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
-import { initialPaginationState, defaultI18n, defaultPageSize } from './constants';
-import { isAny } from './utils';
+import { __ } from '~/locale';
+import Tracking from '~/tracking';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
+import { initialPaginationState, defaultI18n, defaultPageSize } from './constants';
+import { isAny } from './utils';
export default {
defaultI18n,
diff --git a/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue b/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue
index 65bd4e4382d..ddc8bbf9b27 100644
--- a/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue
+++ b/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue
@@ -2,9 +2,9 @@
/* eslint-disable vue/no-v-html */
import { GlButton, GlIcon } from '@gitlab/ui';
import { isString } from 'lodash';
-import ProjectAvatar from '~/vue_shared/components/project_avatar/default.vue';
import highlight from '~/lib/utils/highlight';
import { truncateNamespace } from '~/lib/utils/text_utility';
+import ProjectAvatar from '~/vue_shared/components/project_avatar/default.vue';
export default {
name: 'ProjectListItem',
diff --git a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
index e659e2155fb..a0c5a0559de 100644
--- a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
+++ b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
@@ -1,6 +1,6 @@
<script>
-import { debounce } from 'lodash';
import { GlLoadingIcon, GlSearchBoxByType, GlInfiniteScroll } from '@gitlab/ui';
+import { debounce } from 'lodash';
import { __, n__, sprintf } from '~/locale';
import ProjectListItem from './project_list_item.vue';
diff --git a/app/assets/javascripts/vue_shared/components/registry/code_instruction.vue b/app/assets/javascripts/vue_shared/components/registry/code_instruction.vue
index 08ee23d25bf..bc7f8a2b17a 100644
--- a/app/assets/javascripts/vue_shared/components/registry/code_instruction.vue
+++ b/app/assets/javascripts/vue_shared/components/registry/code_instruction.vue
@@ -1,7 +1,7 @@
<script>
import { uniqueId } from 'lodash';
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import Tracking from '~/tracking';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
export default {
name: 'CodeInstruction',
diff --git a/app/assets/javascripts/vue_shared/components/registry/list_item.vue b/app/assets/javascripts/vue_shared/components/registry/list_item.vue
index 8965dba3e83..9db5d6953d7 100644
--- a/app/assets/javascripts/vue_shared/components/registry/list_item.vue
+++ b/app/assets/javascripts/vue_shared/components/registry/list_item.vue
@@ -54,19 +54,17 @@ export default {
class="gl-display-flex gl-flex-direction-column gl-border-b-solid gl-border-t-solid gl-border-t-1 gl-border-b-1"
:class="optionalClasses"
>
- <div class="gl-display-flex gl-align-items-center gl-py-5">
+ <div class="gl-display-flex gl-align-items-center gl-py-3">
<div
v-if="$slots['left-action']"
- class="gl-w-7 gl-display-none gl-display-sm-flex gl-justify-content-start gl-pl-2"
+ class="gl-w-7 gl-display-none gl-sm-display-flex gl-justify-content-start gl-pl-2"
>
<slot name="left-action"></slot>
</div>
<div
class="gl-display-flex gl-xs-flex-direction-column gl-justify-content-space-between gl-align-items-stretch gl-flex-fill-1"
>
- <div
- class="gl-display-flex gl-flex-direction-column gl-justify-content-space-between gl-xs-mb-3 gl-min-w-0 gl-flex-grow-1"
- >
+ <div class="gl-display-flex gl-flex-direction-column gl-xs-mb-3 gl-min-w-0 gl-flex-grow-1">
<div
v-if="$slots['left-primary']"
class="gl-display-flex gl-align-items-center gl-text-body gl-font-weight-bold gl-min-h-6 gl-min-w-0"
@@ -77,13 +75,13 @@ export default {
:selected="isDetailsShown"
icon="ellipsis_h"
size="small"
- class="gl-ml-2 gl-display-none gl-display-sm-block"
+ class="gl-ml-2 gl-display-none gl-sm-display-block"
@click="toggleDetails"
/>
</div>
<div
v-if="$slots['left-secondary']"
- class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-1 gl-min-h-6 gl-min-w-0 gl-flex-fill-1"
+ class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-min-h-6 gl-min-w-0 gl-flex-fill-1"
>
<slot name="left-secondary"></slot>
</div>
@@ -99,7 +97,7 @@ export default {
</div>
<div
v-if="$slots['right-secondary']"
- class="gl-display-flex gl-align-items-center gl-mt-1 gl-min-h-6"
+ class="gl-display-flex gl-align-items-center gl-min-h-6"
>
<slot name="right-secondary"></slot>
</div>
@@ -107,7 +105,7 @@ export default {
</div>
<div
v-if="$slots['right-action']"
- class="gl-w-9 gl-display-none gl-display-sm-flex gl-justify-content-end gl-pr-1"
+ class="gl-w-9 gl-display-none gl-sm-display-flex gl-justify-content-end gl-pr-1"
>
<slot name="right-action"></slot>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/registry/registry_search.vue b/app/assets/javascripts/vue_shared/components/registry/registry_search.vue
new file mode 100644
index 00000000000..62453a25f62
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/registry/registry_search.vue
@@ -0,0 +1,90 @@
+<script>
+import { GlSorting, GlSortingItem, GlFilteredSearch } from '@gitlab/ui';
+
+const ASCENDING_ORDER = 'asc';
+const DESCENDING_ORDER = 'desc';
+
+export default {
+ components: {
+ GlSorting,
+ GlSortingItem,
+ GlFilteredSearch,
+ },
+ props: {
+ filter: {
+ type: Array,
+ required: true,
+ },
+ sorting: {
+ type: Object,
+ required: true,
+ },
+ tokens: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ sortableFields: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ internalFilter: {
+ get() {
+ return this.filter;
+ },
+ set(value) {
+ this.$emit('filter:changed', value);
+ },
+ },
+ sortText() {
+ const field = this.sortableFields.find((s) => s.orderBy === this.sorting.orderBy);
+ return field ? field.label : '';
+ },
+ isSortAscending() {
+ return this.sorting.sort === ASCENDING_ORDER;
+ },
+ },
+ methods: {
+ onDirectionChange() {
+ const sort = this.isSortAscending ? DESCENDING_ORDER : ASCENDING_ORDER;
+ this.$emit('sorting:changed', { sort });
+ },
+ onSortItemClick(item) {
+ this.$emit('sorting:changed', { orderBy: item });
+ },
+ clearSearch() {
+ this.$emit('filter:changed', []);
+ this.$emit('filter:submit');
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-p-5 gl-bg-gray-10 gl-border-solid gl-border-1 gl-border-gray-100">
+ <gl-filtered-search
+ v-model="internalFilter"
+ class="gl-mr-4 gl-flex-fill-1"
+ :placeholder="__('Filter results')"
+ :available-tokens="tokens"
+ @submit="$emit('filter:submit')"
+ @clear="clearSearch"
+ />
+ <gl-sorting
+ :text="sortText"
+ :is-ascending="isSortAscending"
+ @sortDirectionChange="onDirectionChange"
+ >
+ <gl-sorting-item
+ v-for="item in sortableFields"
+ ref="packageListSortItem"
+ :key="item.orderBy"
+ @click="onSortItemClick(item.orderBy)"
+ >
+ {{ item.label }}
+ </gl-sorting-item>
+ </gl-sorting>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue b/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue
index 5d4c192c78f..8988dab85d2 100644
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue
@@ -2,9 +2,9 @@
import 'codemirror/lib/codemirror.css';
import '@toast-ui/editor/dist/toastui-editor.css';
+import { EDITOR_TYPES, EDITOR_HEIGHT, EDITOR_PREVIEW_STYLE, CUSTOM_EVENTS } from './constants';
import AddImageModal from './modals/add_image/add_image_modal.vue';
import InsertVideoModal from './modals/insert_video_modal.vue';
-import { EDITOR_TYPES, EDITOR_HEIGHT, EDITOR_PREVIEW_STYLE, CUSTOM_EVENTS } from './constants';
import {
registerHTMLToMarkdownRenderer,
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js
index 624b5b09b38..6ffd280e005 100644
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js
@@ -1,12 +1,12 @@
import { union, mapValues } from 'lodash';
-import renderBlockHtml from './renderers/render_html_block';
+import renderAttributeDefinition from './renderers/render_attribute_definition';
+import renderFontAwesomeHtmlInline from './renderers/render_font_awesome_html_inline';
import renderHeading from './renderers/render_heading';
+import renderBlockHtml from './renderers/render_html_block';
import renderIdentifierInstanceText from './renderers/render_identifier_instance_text';
import renderIdentifierParagraph from './renderers/render_identifier_paragraph';
-import renderFontAwesomeHtmlInline from './renderers/render_font_awesome_html_inline';
-import renderSoftbreak from './renderers/render_softbreak';
-import renderAttributeDefinition from './renderers/render_attribute_definition';
import renderListItem from './renderers/render_list_item';
+import renderSoftbreak from './renderers/render_softbreak';
const htmlInlineRenderers = [renderFontAwesomeHtmlInline];
const htmlBlockRenderers = [renderBlockHtml];
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/editor_service.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/editor_service.js
index be78651d38d..026a4069d9b 100644
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/editor_service.js
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/editor_service.js
@@ -1,9 +1,9 @@
-import Vue from 'vue';
import { defaults } from 'lodash';
+import Vue from 'vue';
+import { TOOLBAR_ITEM_CONFIGS, VIDEO_ATTRIBUTES } from '../constants';
import ToolbarItem from '../toolbar_item.vue';
-import buildHtmlToMarkdownRenderer from './build_html_to_markdown_renderer';
import buildCustomHTMLRenderer from './build_custom_renderer';
-import { TOOLBAR_ITEM_CONFIGS, VIDEO_ATTRIBUTES } from '../constants';
+import buildHtmlToMarkdownRenderer from './build_html_to_markdown_renderer';
import sanitizeHTML from './sanitize_html';
const buildWrapper = (propsData) => {
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html_block.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html_block.js
index 30012c1123f..710b807275b 100644
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html_block.js
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html_block.js
@@ -1,6 +1,6 @@
-import { buildUneditableHtmlAsTextTokens } from './build_uneditable_token';
-import { ALLOWED_VIDEO_ORIGINS } from '../../constants';
import { getURLOrigin } from '~/lib/utils/url_utility';
+import { ALLOWED_VIDEO_ORIGINS } from '../../constants';
+import { buildUneditableHtmlAsTextTokens } from './build_uneditable_token';
const isVideoFrame = (html) => {
const parser = new DOMParser();
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/sanitize_html.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/sanitize_html.js
index cb0f1d51cb1..486d88466b7 100644
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/sanitize_html.js
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/sanitize_html.js
@@ -1,6 +1,6 @@
import createSanitizer from 'dompurify';
-import { ALLOWED_VIDEO_ORIGINS } from '../constants';
import { getURLOrigin } from '~/lib/utils/url_utility';
+import { ALLOWED_VIDEO_ORIGINS } from '../constants';
const sanitizer = createSanitizer(window);
const ADD_TAGS = ['iframe'];
diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/constants.js b/app/assets/javascripts/vue_shared/components/runner_instructions/constants.js
new file mode 100644
index 00000000000..facace0d809
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/runner_instructions/constants.js
@@ -0,0 +1,18 @@
+import { s__ } from '~/locale';
+
+export const PLATFORMS_WITHOUT_ARCHITECTURES = ['docker', 'kubernetes'];
+
+export const INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES = {
+ docker: {
+ instructions: s__(
+ 'Runners|To install Runner in a container follow the instructions described in the GitLab documentation',
+ ),
+ link: 'https://docs.gitlab.com/runner/install/docker.html',
+ },
+ kubernetes: {
+ instructions: s__(
+ 'Runners|To install Runner in Kubernetes follow the instructions described in the GitLab documentation.',
+ ),
+ link: 'https://docs.gitlab.com/runner/install/kubernetes.html',
+ },
+};
diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_platforms.query.graphql b/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_platforms.query.graphql
new file mode 100644
index 00000000000..ff0626167a9
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_platforms.query.graphql
@@ -0,0 +1,20 @@
+query getRunnerPlatforms($projectPath: ID!, $groupPath: ID!) {
+ runnerPlatforms {
+ nodes {
+ name
+ humanReadableName
+ architectures {
+ nodes {
+ name
+ downloadLocation
+ }
+ }
+ }
+ }
+ project(fullPath: $projectPath) {
+ id
+ }
+ group(fullPath: $groupPath) {
+ id
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_setup.query.graphql b/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_setup.query.graphql
new file mode 100644
index 00000000000..643c1991807
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_setup.query.graphql
@@ -0,0 +1,16 @@
+query runnerSetupInstructions(
+ $platform: String!
+ $architecture: String!
+ $projectId: ID!
+ $groupId: ID!
+) {
+ runnerSetup(
+ platform: $platform
+ architecture: $architecture
+ projectId: $projectId
+ groupId: $groupId
+ ) {
+ installInstructions
+ registerInstructions
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue
new file mode 100644
index 00000000000..1d6db576942
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue
@@ -0,0 +1,261 @@
+<script>
+import {
+ GlAlert,
+ GlButton,
+ GlModal,
+ GlModalDirective,
+ GlButtonGroup,
+ GlDropdown,
+ GlDropdownItem,
+ GlIcon,
+} from '@gitlab/ui';
+import { __, s__ } from '~/locale';
+import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
+import {
+ PLATFORMS_WITHOUT_ARCHITECTURES,
+ INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES,
+} from './constants';
+import getRunnerPlatforms from './graphql/queries/get_runner_platforms.query.graphql';
+import getRunnerSetupInstructions from './graphql/queries/get_runner_setup.query.graphql';
+
+export default {
+ components: {
+ GlAlert,
+ GlButton,
+ GlButtonGroup,
+ GlDropdown,
+ GlDropdownItem,
+ GlModal,
+ GlIcon,
+ ModalCopyButton,
+ },
+ directives: {
+ GlModalDirective,
+ },
+ inject: {
+ projectPath: {
+ default: '',
+ },
+ groupPath: {
+ default: '',
+ },
+ },
+ apollo: {
+ runnerPlatforms: {
+ query: getRunnerPlatforms,
+ variables() {
+ return {
+ projectPath: this.projectPath,
+ groupPath: this.groupPath,
+ };
+ },
+ error() {
+ this.showAlert = true;
+ },
+ result({ data }) {
+ this.project = data?.project;
+ this.group = data?.group;
+
+ this.selectPlatform(this.platforms[0].name);
+ },
+ },
+ },
+ data() {
+ return {
+ showAlert: false,
+ selectedPlatformArchitectures: [],
+ selectedPlatform: {
+ name: '',
+ },
+ selectedArchitecture: {},
+ runnerPlatforms: {},
+ instructions: {},
+ project: {},
+ group: {},
+ };
+ },
+ computed: {
+ isPlatformSelected() {
+ return Object.keys(this.selectedPlatform).length > 0;
+ },
+ instructionsEmpty() {
+ return Object.keys(this.instructions).length === 0;
+ },
+ groupId() {
+ return this.group?.id ?? '';
+ },
+ projectId() {
+ return this.project?.id ?? '';
+ },
+ platforms() {
+ return this.runnerPlatforms?.nodes;
+ },
+ hasArchitecureList() {
+ return !PLATFORMS_WITHOUT_ARCHITECTURES.includes(this.selectedPlatform?.name);
+ },
+ instructionsWithoutArchitecture() {
+ return INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES[this.selectedPlatform.name]?.instructions;
+ },
+ runnerInstallationLink() {
+ return INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES[this.selectedPlatform.name]?.link;
+ },
+ },
+ methods: {
+ selectPlatform(name) {
+ this.selectedPlatform = this.platforms.find((platform) => platform.name === name);
+ if (this.hasArchitecureList) {
+ this.selectedPlatformArchitectures = this.selectedPlatform?.architectures?.nodes;
+ [this.selectedArchitecture] = this.selectedPlatformArchitectures;
+ this.selectArchitecture(this.selectedArchitecture);
+ }
+ },
+ selectArchitecture(architecture) {
+ this.selectedArchitecture = architecture;
+
+ this.$apollo.addSmartQuery('instructions', {
+ variables() {
+ return {
+ platform: this.selectedPlatform.name,
+ architecture: this.selectedArchitecture.name,
+ projectId: this.projectId,
+ groupId: this.groupId,
+ };
+ },
+ query: getRunnerSetupInstructions,
+ update(data) {
+ return data?.runnerSetup;
+ },
+ error() {
+ this.showAlert = true;
+ },
+ });
+ },
+ toggleAlert(state) {
+ this.showAlert = state;
+ },
+ },
+ modalId: 'installation-instructions-modal',
+ i18n: {
+ installARunner: s__('Runners|Install a Runner'),
+ architecture: s__('Runners|Architecture'),
+ downloadInstallBinary: s__('Runners|Download and Install Binary'),
+ downloadLatestBinary: s__('Runners|Download Latest Binary'),
+ registerRunner: s__('Runners|Register Runner'),
+ method: __('Method'),
+ fetchError: s__('Runners|An error has occurred fetching instructions'),
+ instructions: s__('Runners|Show Runner installation instructions'),
+ copyInstructions: s__('Runners|Copy instructions'),
+ },
+ closeButton: {
+ text: __('Close'),
+ attributes: [{ variant: 'default' }],
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-button
+ v-gl-modal-directive="$options.modalId"
+ class="gl-mt-4"
+ data-testid="show-modal-button"
+ >
+ {{ $options.i18n.instructions }}
+ </gl-button>
+ <gl-modal
+ :modal-id="$options.modalId"
+ :title="$options.i18n.installARunner"
+ :action-secondary="$options.closeButton"
+ >
+ <gl-alert v-if="showAlert" variant="danger" @dismiss="toggleAlert(false)">
+ {{ $options.i18n.fetchError }}
+ </gl-alert>
+ <h5>{{ __('Environment') }}</h5>
+ <gl-button-group class="gl-mb-5">
+ <gl-button
+ v-for="platform in platforms"
+ :key="platform.name"
+ data-testid="platform-button"
+ @click="selectPlatform(platform.name)"
+ >
+ {{ platform.humanReadableName }}
+ </gl-button>
+ </gl-button-group>
+ <template v-if="hasArchitecureList">
+ <template v-if="isPlatformSelected">
+ <h5>
+ {{ $options.i18n.architecture }}
+ </h5>
+ <gl-dropdown class="gl-mb-5" :text="selectedArchitecture.name">
+ <gl-dropdown-item
+ v-for="architecture in selectedPlatformArchitectures"
+ :key="architecture.name"
+ data-testid="architecture-dropdown-item"
+ @click="selectArchitecture(architecture)"
+ >
+ {{ architecture.name }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ <div class="gl-display-flex gl-align-items-center gl-mb-5">
+ <h5>{{ $options.i18n.downloadInstallBinary }}</h5>
+ <gl-button
+ class="gl-ml-auto"
+ :href="selectedArchitecture.downloadLocation"
+ download
+ data-testid="binary-download-button"
+ >
+ {{ $options.i18n.downloadLatestBinary }}
+ </gl-button>
+ </div>
+ </template>
+ <template v-if="!instructionsEmpty">
+ <div class="gl-display-flex">
+ <pre
+ class="gl-bg-gray gl-flex-fill-1 gl-white-space-pre-line"
+ data-testid="binary-instructions"
+ >
+
+ {{ instructions.installInstructions }}
+ </pre
+ >
+ <modal-copy-button
+ :title="$options.i18n.copyInstructions"
+ :text="instructions.installInstructions"
+ :modal-id="$options.modalId"
+ css-classes="gl-align-self-start gl-ml-2 gl-mt-2"
+ category="tertiary"
+ />
+ </div>
+
+ <hr />
+ <h5 class="gl-mb-5">{{ $options.i18n.registerRunner }}</h5>
+ <h5 class="gl-mb-5">{{ $options.i18n.method }}</h5>
+ <div class="gl-display-flex">
+ <pre
+ class="gl-bg-gray gl-flex-fill-1 gl-white-space-pre-line"
+ data-testid="runner-instructions"
+ >
+ {{ instructions.registerInstructions }}
+ </pre
+ >
+ <modal-copy-button
+ :title="$options.i18n.copyInstructions"
+ :text="instructions.registerInstructions"
+ :modal-id="$options.modalId"
+ css-classes="gl-align-self-start gl-ml-2 gl-mt-2"
+ category="tertiary"
+ />
+ </div>
+ </template>
+ </template>
+ <template v-else>
+ <div>
+ <p>{{ instructionsWithoutArchitecture }}</p>
+ <gl-button :href="runnerInstallationLink">
+ <gl-icon name="external-link" />
+ {{ s__('Runners|View installation instructions') }}
+ </gl-button>
+ </div>
+ </template>
+ </gl-modal>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/settings/settings_block.vue b/app/assets/javascripts/vue_shared/components/settings/settings_block.vue
new file mode 100644
index 00000000000..31094b985a2
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/settings/settings_block.vue
@@ -0,0 +1,45 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export default {
+ components: { GlButton },
+ props: {
+ defaultExpanded: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ },
+ data() {
+ return {
+ sectionExpanded: false,
+ };
+ },
+ computed: {
+ expanded() {
+ return this.defaultExpanded || this.sectionExpanded;
+ },
+ toggleText() {
+ return this.expanded ? __('Collapse') : __('Expand');
+ },
+ },
+};
+</script>
+
+<template>
+ <section class="settings no-animate" :class="{ expanded }">
+ <div class="settings-header">
+ <h4><slot name="title"></slot></h4>
+ <gl-button @click="sectionExpanded = !sectionExpanded">
+ {{ toggleText }}
+ </gl-button>
+ <p>
+ <slot name="description"></slot>
+ </p>
+ </div>
+ <div class="settings-content">
+ <slot></slot>
+ </div>
+ </section>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue
index 80c61627b8f..a1dca65a423 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue
@@ -1,7 +1,7 @@
<script>
+import { dateInWords, timeFor } from '~/lib/utils/datetime_utility';
import { __ } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago';
-import { dateInWords, timeFor } from '~/lib/utils/datetime_utility';
import collapsedCalendarIcon from './collapsed_calendar_icon.vue';
export default {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
index 6caf8bc92c2..075681de320 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
@@ -1,10 +1,10 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
+import { __ } from '~/locale';
+import { dateInWords } from '../../../lib/utils/datetime_utility';
import datePicker from '../pikaday.vue';
-import toggleSidebar from './toggle_sidebar.vue';
import collapsedCalendarIcon from './collapsed_calendar_icon.vue';
-import { dateInWords } from '../../../lib/utils/datetime_utility';
-import { __ } from '~/locale';
+import toggleSidebar from './toggle_sidebar.vue';
export default {
name: 'SidebarDatePicker',
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
index 22d86ee25d1..88c4d132d61 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
@@ -1,20 +1,19 @@
<script>
-import $ from 'jquery';
import { GlLoadingIcon } from '@gitlab/ui';
-import { __ } from '~/locale';
+import $ from 'jquery';
import LabelsSelect from '~/labels_select';
+import { __ } from '~/locale';
import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
-import DropdownTitle from './dropdown_title.vue';
-import DropdownValue from './dropdown_value.vue';
-import DropdownValueCollapsed from './dropdown_value_collapsed.vue';
+import { DropdownVariant } from '../labels_select_vue/constants';
import DropdownButton from './dropdown_button.vue';
+import DropdownCreateLabel from './dropdown_create_label.vue';
+import DropdownFooter from './dropdown_footer.vue';
import DropdownHeader from './dropdown_header.vue';
import DropdownSearchInput from './dropdown_search_input.vue';
-import DropdownFooter from './dropdown_footer.vue';
-import DropdownCreateLabel from './dropdown_create_label.vue';
-
-import { DropdownVariant } from '../labels_select_vue/constants';
+import DropdownTitle from './dropdown_title.vue';
+import DropdownValue from './dropdown_value.vue';
+import DropdownValueCollapsed from './dropdown_value_collapsed.vue';
export default {
DropdownVariant,
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue
index c65266fce5a..60111210f5d 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue
@@ -1,6 +1,6 @@
<script>
-import { mapActions, mapGetters } from 'vuex';
import { GlButton, GlIcon } from '@gitlab/ui';
+import { mapActions, mapGetters } from 'vuex';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue
index 267c3be5f50..e3704198ad0 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue
@@ -1,8 +1,8 @@
<script>
import { mapState } from 'vuex';
-import DropdownContentsLabelsView from './dropdown_contents_labels_view.vue';
import DropdownContentsCreateView from './dropdown_contents_create_view.vue';
+import DropdownContentsLabelsView from './dropdown_contents_labels_view.vue';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue
index 41308e352e3..f8cc981ba3d 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue
@@ -1,6 +1,6 @@
<script>
-import { mapState, mapActions } from 'vuex';
import { GlTooltipDirective, GlButton, GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui';
+import { mapState, mapActions } from 'vuex';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue
index a365673f7a1..6065b6c160c 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue
@@ -1,5 +1,4 @@
<script>
-import { mapState, mapGetters, mapActions } from 'vuex';
import {
GlIntersectionObserver,
GlLoadingIcon,
@@ -8,6 +7,7 @@ import {
GlLink,
} from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import { mapState, mapGetters, mapActions } from 'vuex';
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue
index 2d6a4a9758c..5d1663bc1fd 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue
@@ -1,6 +1,6 @@
<script>
-import { mapState, mapActions } from 'vuex';
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
+import { mapState, mapActions } from 'vuex';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue
index a6f99289df4..f173c8db540 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue
@@ -1,6 +1,6 @@
<script>
-import { mapState } from 'vuex';
import { GlLabel } from '@gitlab/ui';
+import { mapState } from 'vuex';
import { isScopedLabel } from '~/lib/utils/common_utils';
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
index 683889b8611..93fdae19a8d 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
@@ -7,14 +7,12 @@ import { __ } from '~/locale';
import DropdownValueCollapsed from '~/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue';
-import labelsSelectModule from './store';
-
-import DropdownTitle from './dropdown_title.vue';
-import DropdownValue from './dropdown_value.vue';
+import { DropdownVariant } from './constants';
import DropdownButton from './dropdown_button.vue';
import DropdownContents from './dropdown_contents.vue';
-
-import { DropdownVariant } from './constants';
+import DropdownTitle from './dropdown_title.vue';
+import DropdownValue from './dropdown_value.vue';
+import labelsSelectModule from './store';
Vue.use(Vuex);
@@ -35,11 +33,13 @@ export default {
},
allowLabelEdit: {
type: Boolean,
- required: true,
+ required: false,
+ default: false,
},
allowLabelCreate: {
type: Boolean,
- required: true,
+ required: false,
+ default: false,
},
allowMultiselect: {
type: Boolean,
@@ -48,7 +48,8 @@ export default {
},
allowScopedLabels: {
type: Boolean,
- required: true,
+ required: false,
+ default: false,
},
variant: {
type: String,
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js
index 14b46c1c431..89f96ab916b 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js
@@ -1,6 +1,6 @@
import { deprecatedCreateFlash as flash } from '~/flash';
-import { __ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
+import { __ } from '~/locale';
import * as types from './mutation_types';
export const setInitialState = ({ commit }, props) => commit(types.SET_INITIAL_STATE, props);
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
index 6de436ffd13..55716e1105e 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
@@ -1,5 +1,5 @@
-import * as types from './mutation_types';
import { DropdownVariant } from '../constants';
+import * as types from './mutation_types';
export default {
[types.SET_INITIAL_STATE](state, props) {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/multiselect_dropdown.vue b/app/assets/javascripts/vue_shared/components/sidebar/multiselect_dropdown.vue
index c5bbe1b33fb..132abcab82b 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/multiselect_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/multiselect_dropdown.vue
@@ -20,7 +20,7 @@ export default {
</script>
<template>
- <gl-dropdown class="show" :text="text" :header-text="headerText">
+ <gl-dropdown class="show" :text="text" :header-text="headerText" @toggle="$emit('toggle')">
<slot name="search"></slot>
<gl-dropdown-form>
<slot name="items"></slot>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/getIssueParticipants.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/getIssueParticipants.query.graphql
deleted file mode 100644
index 612a0c02e82..00000000000
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/getIssueParticipants.query.graphql
+++ /dev/null
@@ -1,13 +0,0 @@
-query issueParticipants($id: IssueID!) {
- issue(id: $id) {
- participants {
- nodes {
- username
- name
- webUrl
- avatarUrl
- id
- }
- }
- }
-}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql
new file mode 100644
index 00000000000..62c0b05426b
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql
@@ -0,0 +1,19 @@
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+
+query issueParticipants($fullPath: ID!, $iid: String!) {
+ project(fullPath: $fullPath) {
+ issuable: issue(iid: $iid) {
+ id
+ participants {
+ nodes {
+ ...User
+ }
+ }
+ assignees {
+ nodes {
+ ...User
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql
new file mode 100644
index 00000000000..a75ce85a1dc
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql
@@ -0,0 +1,19 @@
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+
+query getMrParticipants($fullPath: ID!, $iid: String!) {
+ project(fullPath: $fullPath) {
+ issuable: mergeRequest(iid: $iid) {
+ id
+ participants {
+ nodes {
+ ...User
+ }
+ }
+ assignees {
+ nodes {
+ ...User
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/updateAssignees.mutation.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql
index 9ead95a3801..2eb9bb4b07b 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/updateAssignees.mutation.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql
@@ -1,15 +1,19 @@
-mutation issueSetAssignees($iid: String!, $assigneeUsernames: [String!]!, $projectPath: ID!) {
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+
+mutation issueSetAssignees($iid: String!, $assigneeUsernames: [String!]!, $fullPath: ID!) {
issueSetAssignees(
- input: { iid: $iid, assigneeUsernames: $assigneeUsernames, projectPath: $projectPath }
+ input: { iid: $iid, assigneeUsernames: $assigneeUsernames, projectPath: $fullPath }
) {
issue {
+ id
assignees {
nodes {
- username
- id
- name
- webUrl
- avatarUrl
+ ...User
+ }
+ }
+ participants {
+ nodes {
+ ...User
}
}
}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql
new file mode 100644
index 00000000000..a0f15a07692
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql
@@ -0,0 +1,21 @@
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+
+mutation mergeRequestSetAssignees($iid: String!, $assigneeUsernames: [String!]!, $fullPath: ID!) {
+ mergeRequestSetAssignees(
+ input: { iid: $iid, assigneeUsernames: $assigneeUsernames, projectPath: $fullPath }
+ ) {
+ mergeRequest {
+ id
+ assignees {
+ nodes {
+ ...User
+ }
+ }
+ participants {
+ nodes {
+ ...User
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/split_button.vue b/app/assets/javascripts/vue_shared/components/split_button.vue
index 61b317d0d1d..994fa68fb1a 100644
--- a/app/assets/javascripts/vue_shared/components/split_button.vue
+++ b/app/assets/javascripts/vue_shared/components/split_button.vue
@@ -1,6 +1,6 @@
<script>
-import { isString } from 'lodash';
import { GlDropdown, GlDropdownDivider, GlDropdownItem } from '@gitlab/ui';
+import { isString } from 'lodash';
const isValidItem = (item) =>
isString(item.eventName) && isString(item.title) && isString(item.description);
diff --git a/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue b/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue
index 9b6d0a87374..e639a2d966b 100644
--- a/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue
+++ b/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue
@@ -1,7 +1,7 @@
<script>
import { GlTooltipDirective } from '@gitlab/ui';
-import { __ } from '~/locale';
import { roundDownFloat } from '~/lib/utils/common_utils';
+import { __ } from '~/locale';
export default {
directives: {
diff --git a/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue b/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue
index f1db26ff4fc..b9ee74d6a03 100644
--- a/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue
@@ -1,8 +1,8 @@
<script>
import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
+import { secondsToHours } from '~/lib/utils/datetime_utility';
import { __ } from '~/locale';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
-import { secondsToHours } from '~/lib/utils/datetime_utility';
export default {
name: 'TimezoneDropdown',
diff --git a/app/assets/javascripts/vue_shared/components/todo_button.vue b/app/assets/javascripts/vue_shared/components/todo_button.vue
index a9d4f8403fa..935d222a1a9 100644
--- a/app/assets/javascripts/vue_shared/components/todo_button.vue
+++ b/app/assets/javascripts/vue_shared/components/todo_button.vue
@@ -15,7 +15,7 @@ export default {
},
computed: {
buttonLabel() {
- return this.isTodo ? __('Mark as done') : __('Add a To Do');
+ return this.isTodo ? __('Mark as done') : __('Add a to do');
},
},
};
diff --git a/app/assets/javascripts/vue_shared/components/toggle_button.vue b/app/assets/javascripts/vue_shared/components/toggle_button.vue
deleted file mode 100644
index 861661d3519..00000000000
--- a/app/assets/javascripts/vue_shared/components/toggle_button.vue
+++ /dev/null
@@ -1,88 +0,0 @@
-<script>
-import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
-import { s__ } from '../../locale';
-
-const ICON_ON = 'status_success_borderless';
-const ICON_OFF = 'status_failed_borderless';
-const LABEL_ON = s__('ToggleButton|Toggle Status: ON');
-const LABEL_OFF = s__('ToggleButton|Toggle Status: OFF');
-
-export default {
- components: {
- GlIcon,
- GlLoadingIcon,
- },
-
- model: {
- prop: 'value',
- event: 'change',
- },
-
- props: {
- name: {
- type: String,
- required: false,
- default: null,
- },
- value: {
- type: Boolean,
- required: false,
- default: null,
- },
- disabledInput: {
- type: Boolean,
- required: false,
- default: false,
- },
- isLoading: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
-
- computed: {
- toggleIcon() {
- return this.value ? ICON_ON : ICON_OFF;
- },
- ariaLabel() {
- return this.value ? LABEL_ON : LABEL_OFF;
- },
- },
-
- methods: {
- toggleFeature() {
- if (!this.disabledInput) this.$emit('change', !this.value);
- },
- },
-};
-</script>
-
-<template>
- <label class="gl-mt-2">
- <input v-if="name" :name="name" :value="value" type="hidden" />
- <button
- type="button"
- role="switch"
- class="project-feature-toggle"
- :aria-label="ariaLabel"
- :aria-checked="value"
- :class="{
- 'is-checked': value,
- 'gl-blue-500': value,
- 'is-disabled': disabledInput,
- 'is-loading': isLoading,
- }"
- @click.prevent="toggleFeature"
- >
- <gl-loading-icon class="loading-icon" />
- <span class="toggle-icon">
- <gl-icon
- :size="18"
- :name="toggleIcon"
- :class="value ? 'gl-text-blue-500' : 'gl-text-gray-400'"
- />
- </span>
- </button>
- </label>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue
index b48dfa8b452..8aa6e29adf1 100644
--- a/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue
+++ b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue
@@ -1,7 +1,7 @@
<script>
import { isFunction } from 'lodash';
-import tooltip from '../directives/tooltip';
import { hasHorizontalOverflow } from '~/lib/utils/dom_utils';
+import tooltip from '../directives/tooltip';
export default {
directives: {
diff --git a/app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue b/app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue
index 01ba2cf5c39..5a08e992084 100644
--- a/app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue
+++ b/app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue
@@ -1,8 +1,8 @@
<script>
import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
import { __ } from '~/locale';
-import { isValidImage } from './utils';
import { VALID_DATA_TRANSFER_TYPE, VALID_IMAGE_FILE_MIMETYPE } from './constants';
+import { isValidImage } from './utils';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
index 2ab4c55d9b0..37bde089de8 100644
--- a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
+++ b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
@@ -6,9 +6,9 @@ import {
GlDeprecatedSkeletonLoading as GlSkeletonLoading,
GlIcon,
} from '@gitlab/ui';
-import UserAvailabilityStatus from '~/set_status_modal/components/user_availability_status.vue';
-import UserAvatarImage from '../user_avatar/user_avatar_image.vue';
+import UserNameWithStatus from '~/sidebar/components/assignees/user_name_with_status.vue';
import { glEmojiTag } from '../../../emoji';
+import UserAvatarImage from '../user_avatar/user_avatar_image.vue';
const MAX_SKELETON_LINES = 4;
@@ -26,7 +26,7 @@ export default {
GlPopover,
GlSkeletonLoading,
UserAvatarImage,
- UserAvailabilityStatus,
+ UserNameWithStatus,
},
props: {
target: {
@@ -66,7 +66,7 @@ export default {
);
},
availabilityStatus() {
- return this.user?.status?.availability || null;
+ return this.user?.status?.availability || '';
},
},
};
@@ -93,11 +93,7 @@ export default {
<template v-else>
<div class="gl-mb-3">
<h5 class="gl-m-0">
- {{ user.name }}
- <user-availability-status
- v-if="availabilityStatus"
- :availability="availabilityStatus"
- />
+ <user-name-with-status :name="user.name" :availability="availabilityStatus" />
</h5>
<span class="gl-text-gray-500">@{{ user.username }}</span>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/web_ide_link.vue b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
index c957876f8ab..4bd3e352fd2 100644
--- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue
+++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
@@ -1,8 +1,8 @@
<script>
import $ from 'jquery';
import { __ } from '~/locale';
-import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import ActionsButton from '~/vue_shared/components/actions_button.vue';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
const KEY_EDIT = 'edit';
const KEY_WEB_IDE = 'webide';
diff --git a/app/assets/javascripts/vue_shared/directives/validation.js b/app/assets/javascripts/vue_shared/directives/validation.js
index ece09df272c..176954891e9 100644
--- a/app/assets/javascripts/vue_shared/directives/validation.js
+++ b/app/assets/javascripts/vue_shared/directives/validation.js
@@ -86,7 +86,7 @@ const createValidator = (context, feedbackMap) => ({ el, reportInvalidInput = fa
* @param {Object<string, { message: string, isValid: ?function}>} customFeedbackMap
* @returns {{ inserted: function, update: function }} validateDirective
*/
-export default function (customFeedbackMap = {}) {
+export default function initValidation(customFeedbackMap = {}) {
const feedbackMap = merge(defaultFeedbackMap, customFeedbackMap);
const elDataMap = new WeakMap();
diff --git a/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js b/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js
deleted file mode 100644
index be04ff158e7..00000000000
--- a/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js
+++ /dev/null
@@ -1,67 +0,0 @@
-/**
- * API callbacks for pagination and tabs
- * shared between Pipelines and Environments table.
- *
- * Components need to have `scope`, `page` and `requestData`
- */
-import { historyPushState, buildUrlWithCurrentLocation } from '../../lib/utils/common_utils';
-import { validateParams } from '~/pipelines/utils';
-
-export default {
- methods: {
- onChangeTab(scope) {
- if (this.scope === scope) {
- return;
- }
-
- let params = {
- scope,
- page: '1',
- };
-
- params = this.onChangeWithFilter(params);
-
- this.updateContent(params);
- },
-
- onChangePage(page) {
- /* URLS parameters are strings, we need to parse to match types */
- let params = {
- page: Number(page).toString(),
- };
-
- if (this.scope) {
- params.scope = this.scope;
- }
-
- params = this.onChangeWithFilter(params);
-
- this.updateContent(params);
- },
-
- onChangeWithFilter(params) {
- return { ...params, ...validateParams(this.requestData) };
- },
-
- updateInternalState(parameters) {
- // stop polling
- this.poll.stop();
-
- const queryString = Object.keys(parameters)
- .map((parameter) => {
- const value = parameters[parameter];
- // update internal state for UI
- this[parameter] = value;
- return `${parameter}=${encodeURIComponent(value)}`;
- })
- .join('&');
-
- // update polling parameters
- this.requestData = parameters;
-
- historyPushState(buildUrlWithCurrentLocation(`?${queryString}`));
-
- this.isLoading = true;
- },
- },
-};
diff --git a/app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js b/app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js
index 56da2637825..52ded0e0cc1 100644
--- a/app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js
+++ b/app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js
@@ -1,6 +1,6 @@
import { isEmpty } from 'lodash';
-import { sprintf, __ } from '~/locale';
import { formatDate } from '~/lib/utils/datetime_utility';
+import { sprintf, __ } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago';
const mixins = {
diff --git a/app/assets/javascripts/vue_shared/plugins/global_toast.js b/app/assets/javascripts/vue_shared/plugins/global_toast.js
index 7a2e5d80a5d..bfea2bedd40 100644
--- a/app/assets/javascripts/vue_shared/plugins/global_toast.js
+++ b/app/assets/javascripts/vue_shared/plugins/global_toast.js
@@ -1,5 +1,5 @@
-import Vue from 'vue';
import { GlToast } from '@gitlab/ui';
+import Vue from 'vue';
Vue.use(GlToast);
const instance = new Vue();
diff --git a/app/assets/javascripts/vue_shared/security_reports/constants.js b/app/assets/javascripts/vue_shared/security_reports/constants.js
index dd591f7bba3..aac5a5c1def 100644
--- a/app/assets/javascripts/vue_shared/security_reports/constants.js
+++ b/app/assets/javascripts/vue_shared/security_reports/constants.js
@@ -17,7 +17,13 @@ export const REPORT_FILE_TYPES = {
* Security scan report types, as provided by the backend.
*/
export const REPORT_TYPE_SAST = 'sast';
+export const REPORT_TYPE_DAST = 'dast';
export const REPORT_TYPE_SECRET_DETECTION = 'secret_detection';
+export const REPORT_TYPE_DEPENDENCY_SCANNING = 'dependency_scanning';
+export const REPORT_TYPE_CONTAINER_SCANNING = 'container_scanning';
+export const REPORT_TYPE_COVERAGE_FUZZING = 'coverage_fuzzing';
+export const REPORT_TYPE_LICENSE_COMPLIANCE = 'license_compliance';
+export const REPORT_TYPE_API_FUZZING = 'api_fuzzing';
/**
* SecurityReportTypeEnum values for use with GraphQL.
diff --git a/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue b/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue
index a6c7b59aa71..b27dd33835f 100644
--- a/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue
+++ b/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue
@@ -1,31 +1,26 @@
<script>
import { mapActions, mapGetters } from 'vuex';
-import { GlLink, GlSprintf } from '@gitlab/ui';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import ReportSection from '~/reports/components/report_section.vue';
-import { LOADING, ERROR, SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR } from '~/reports/constants';
-import { s__ } from '~/locale';
-import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_utils';
import createFlash from '~/flash';
-import Api from '~/api';
+import { s__ } from '~/locale';
+import ReportSection from '~/reports/components/report_section.vue';
+import { ERROR, SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR } from '~/reports/constants';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import HelpIcon from './components/help_icon.vue';
import SecurityReportDownloadDropdown from './components/security_report_download_dropdown.vue';
import SecuritySummary from './components/security_summary.vue';
-import store from './store';
-import { MODULE_SAST, MODULE_SECRET_DETECTION } from './store/constants';
import {
REPORT_TYPE_SAST,
REPORT_TYPE_SECRET_DETECTION,
reportTypeToSecurityReportTypeEnum,
} from './constants';
import securityReportDownloadPathsQuery from './queries/security_report_download_paths.query.graphql';
+import store from './store';
+import { MODULE_SAST, MODULE_SECRET_DETECTION } from './store/constants';
import { extractSecurityReportArtifacts } from './utils';
export default {
store,
components: {
- GlLink,
- GlSprintf,
ReportSection,
HelpIcon,
SecurityReportDownloadDropdown,
@@ -101,9 +96,6 @@ export default {
),
};
},
- skip() {
- return !this.canShowDownloads;
- },
update(data) {
return extractSecurityReportArtifacts(this.$options.reportTypes, data);
},
@@ -124,9 +116,6 @@ export default {
},
computed: {
...mapGetters(['groupedSummaryText', 'summaryStatus']),
- canShowDownloads() {
- return this.glFeatures.coreSecurityMrWidgetDownloads;
- },
hasSecurityReports() {
return this.availableSecurityReports.length > 0;
},
@@ -139,23 +128,6 @@ export default {
isLoadingReportArtifacts() {
return this.$apollo.queries.reportArtifacts.loading;
},
- shouldShowDownloadGuidance() {
- return !this.canShowDownloads && this.summaryStatus !== LOADING;
- },
- scansHaveRunMessage() {
- return this.canShowDownloads
- ? this.$options.i18n.scansHaveRun
- : this.$options.i18n.scansHaveRunWithDownloadGuidance;
- },
- },
- created() {
- if (!this.canShowDownloads) {
- this.checkAvailableSecurityReports(this.$options.reportTypes)
- .then((availableSecurityReports) => {
- this.onCheckingAvailableSecurityReports(Array.from(availableSecurityReports));
- })
- .catch(this.showError);
- }
},
methods: {
...mapActions(MODULE_SAST, {
@@ -166,36 +138,6 @@ export default {
setSecretDetectionDiffEndpoint: 'setDiffEndpoint',
fetchSecretDetectionDiff: 'fetchDiff',
}),
- async checkAvailableSecurityReports(reportTypes) {
- const reportTypesSet = new Set(reportTypes);
- const availableReportTypes = new Set();
-
- let page = 1;
- while (page) {
- // eslint-disable-next-line no-await-in-loop
- const { data: jobs, headers } = await Api.pipelineJobs(this.projectId, this.pipelineId, {
- per_page: 100,
- page,
- });
-
- jobs.forEach(({ artifacts = [] }) => {
- artifacts.forEach(({ file_type }) => {
- if (reportTypesSet.has(file_type)) {
- availableReportTypes.add(file_type);
- }
- });
- });
-
- // If we've found artifacts for all the report types, stop looking!
- if (availableReportTypes.size === reportTypesSet.size) {
- return availableReportTypes;
- }
-
- page = parseIntPagination(normalizeHeaders(headers)).nextPage;
- }
-
- return availableReportTypes;
- },
fetchCounts() {
if (!this.glFeatures.coreSecurityMrWidgetCounts) {
return;
@@ -213,11 +155,6 @@ export default {
this.canShowCounts = true;
}
},
- activatePipelinesTab() {
- if (window.mrTabs) {
- window.mrTabs.tabShown('pipelines');
- }
- },
onCheckingAvailableSecurityReports(availableSecurityReports) {
this.availableSecurityReports = availableSecurityReports;
this.fetchCounts();
@@ -236,12 +173,6 @@ export default {
'SecurityReports|Failed to get security report information. Please reload the page or try again later.',
),
scansHaveRun: s__('SecurityReports|Security scans have run'),
- scansHaveRunWithDownloadGuidance: s__(
- 'SecurityReports|Security scans have run. Go to the %{linkStart}pipelines tab%{linkEnd} to download the security reports',
- ),
- downloadFromPipelineTab: s__(
- 'SecurityReports|Go to the %{linkStart}pipelines tab%{linkEnd} to download the security reports',
- ),
},
summarySlots: [SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR],
};
@@ -265,22 +196,7 @@ export default {
</span>
</template>
- <template v-if="shouldShowDownloadGuidance" #sub-heading>
- <span class="gl-font-sm">
- <gl-sprintf :message="$options.i18n.downloadFromPipelineTab">
- <template #link="{ content }">
- <gl-link
- class="gl-font-sm"
- data-testid="show-pipelines"
- @click="activatePipelinesTab"
- >{{ content }}</gl-link
- >
- </template>
- </gl-sprintf>
- </span>
- </template>
-
- <template v-if="canShowDownloads" #action-buttons>
+ <template #action-buttons>
<security-report-download-dropdown
:artifacts="reportArtifacts"
:loading="isLoadingReportArtifacts"
@@ -298,13 +214,7 @@ export default {
data-testid="security-mr-widget"
>
<template #error>
- <gl-sprintf :message="scansHaveRunMessage">
- <template #link="{ content }">
- <gl-link data-testid="show-pipelines" @click="activatePipelinesTab">{{
- content
- }}</gl-link>
- </template>
- </gl-sprintf>
+ {{ $options.i18n.scansHaveRun }}
<help-icon
:help-path="securityReportsDocsPath"
@@ -312,7 +222,7 @@ export default {
/>
</template>
- <template v-if="canShowDownloads" #action-buttons>
+ <template #action-buttons>
<security-report-download-dropdown
:artifacts="reportArtifacts"
:loading="isLoadingReportArtifacts"
diff --git a/app/assets/javascripts/vue_shared/security_reports/store/getters.js b/app/assets/javascripts/vue_shared/security_reports/store/getters.js
index 443255b0e6a..08f6bcca15b 100644
--- a/app/assets/javascripts/vue_shared/security_reports/store/getters.js
+++ b/app/assets/javascripts/vue_shared/security_reports/store/getters.js
@@ -1,7 +1,7 @@
import { s__, sprintf } from '~/locale';
-import { countVulnerabilities, groupedTextBuilder } from './utils';
import { LOADING, ERROR, SUCCESS } from '~/reports/constants';
import { TRANSLATION_IS_LOADING } from './messages';
+import { countVulnerabilities, groupedTextBuilder } from './utils';
export const summaryCounts = (state) =>
countVulnerabilities(
diff --git a/app/assets/javascripts/vue_shared/security_reports/store/index.js b/app/assets/javascripts/vue_shared/security_reports/store/index.js
index 10705e04a21..164faa86744 100644
--- a/app/assets/javascripts/vue_shared/security_reports/store/index.js
+++ b/app/assets/javascripts/vue_shared/security_reports/store/index.js
@@ -1,9 +1,9 @@
import Vuex from 'vuex';
-import * as getters from './getters';
-import state from './state';
import { MODULE_SAST, MODULE_SECRET_DETECTION } from './constants';
+import * as getters from './getters';
import sast from './modules/sast';
import secretDetection from './modules/secret_detection';
+import state from './state';
export default () =>
new Vuex.Store({
diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/actions.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/actions.js
index 0f26e3c30ef..4f92e181f9f 100644
--- a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/actions.js
+++ b/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/actions.js
@@ -1,5 +1,5 @@
-import * as types from './mutation_types';
import { fetchDiffData } from '../../utils';
+import * as types from './mutation_types';
export const setDiffEndpoint = ({ commit }, path) => commit(types.SET_DIFF_ENDPOINT, path);
diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/index.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/index.js
index 68c81bb4509..1d5af1d4fe5 100644
--- a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/index.js
+++ b/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/index.js
@@ -1,6 +1,6 @@
-import state from './state';
-import mutations from './mutations';
import * as actions from './actions';
+import mutations from './mutations';
+import state from './state';
export default {
namespaced: true,
diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/mutations.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/mutations.js
index 5f6153ca3b1..11aa71d2b6b 100644
--- a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/mutations.js
+++ b/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/mutations.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import * as types from './mutation_types';
import { parseDiff } from '../../utils';
+import * as types from './mutation_types';
export default {
[types.SET_DIFF_ENDPOINT](state, path) {
diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/index.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/index.js
index 68c81bb4509..1d5af1d4fe5 100644
--- a/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/index.js
+++ b/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/index.js
@@ -1,6 +1,6 @@
-import state from './state';
-import mutations from './mutations';
import * as actions from './actions';
+import mutations from './mutations';
+import state from './state';
export default {
namespaced: true,
diff --git a/app/assets/javascripts/vue_shared/security_reports/store/utils.js b/app/assets/javascripts/vue_shared/security_reports/store/utils.js
index fd6613ae11c..458bacce915 100644
--- a/app/assets/javascripts/vue_shared/security_reports/store/utils.js
+++ b/app/assets/javascripts/vue_shared/security_reports/store/utils.js
@@ -1,5 +1,5 @@
-import pollUntilComplete from '~/lib/utils/poll_until_complete';
import axios from '~/lib/utils/axios_utils';
+import pollUntilComplete from '~/lib/utils/poll_until_complete';
import { __, n__, sprintf } from '~/locale';
import { CRITICAL, HIGH } from '~/vulnerabilities/constants';
import {