diff options
Diffstat (limited to 'app/assets/javascripts/vue_shared')
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 { |