...
 
Commits (194)
......@@ -17,6 +17,7 @@ eslint-report.html
.rbx/
/.ruby-gemset
/.ruby-version
/.tool-versions
/.rvmrc
.sass-cache/
/.secret
......
......@@ -224,7 +224,7 @@ gitlab:setup:
rspec:coverage:
extends:
- .rails-job-base
- .rails:rules:ee-mr-and-master-only
- .rails:rules:rspec-coverage
stage: post-test
# We cannot use needs since it would mean needing 84 jobs (since most are parallelized)
# so we use `dependencies` here.
......
This diff is collapsed.
......@@ -390,9 +390,7 @@
rules:
- <<: *if-not-ee
when: never
- <<: *if-dot-com-gitlab-org-master
changes: *code-backstage-qa-patterns
when: on_success
- <<: *if-master-schedule-2-hourly
############
# QA rules #
......@@ -545,6 +543,12 @@
- <<: *if-merge-request
changes: *code-backstage-patterns
.rails:rules:rspec-coverage:
rules:
- <<: *if-not-ee
when: never
- <<: *if-master-schedule-2-hourly
##################
# Releases rules #
##################
......
......@@ -43,7 +43,14 @@ https://about.gitlab.com/handbook/engineering/ux/ux-research-training/user-story
### Permissions and Security
<!-- What permissions are required to perform the described actions? Are they consistent with the existing permissions as documented for users, groups, and projects as appropriate? Is the proposed behavior consistent between the UI, API, and other access methods (e.g. email replies)?-->
<!-- What permissions are required to perform the described actions? Are they consistent with the existing permissions as documented for users, groups, and projects as appropriate? Is the proposed behavior consistent between the UI, API, and other access methods (e.g. email replies)?
Consider adding checkboxes and expectations of users with certain levels of membership https://docs.gitlab.com/ee/user/permissions.html
* [ ] Add expected impact to members with no access (0)
* [ ] Add expected impact to Guest (10) members
* [ ] Add expected impact to Reporter (20) members
* [ ] Add expected impact to Developer (30) members
* [ ] Add expected impact to Maintainer (40) members
* [ ] Add expected impact to Owner (50) members -->
### Documentation
......
......@@ -66,7 +66,7 @@ gem 'u2f', '~> 0.2.1'
gem 'validates_hostname', '~> 1.0.10'
gem 'rubyzip', '~> 2.0.0', require: 'zip'
# GitLab Pages letsencrypt support
gem 'acme-client', '~> 2.0.5'
gem 'acme-client', '~> 2.0', '>= 2.0.6'
# Browser detection
gem 'browser', '~> 2.5'
......
......@@ -4,8 +4,8 @@ GEM
RedCloth (4.3.2)
abstract_type (0.0.7)
ace-rails-ap (4.1.2)
acme-client (2.0.5)
faraday (~> 0.9, >= 0.9.1)
acme-client (2.0.6)
faraday (>= 0.17, < 2.0.0)
actioncable (6.0.3.1)
actionpack (= 6.0.3.1)
nio4r (~> 2.0)
......@@ -1169,7 +1169,7 @@ PLATFORMS
DEPENDENCIES
RedCloth (~> 4.3.2)
ace-rails-ap (~> 4.1.0)
acme-client (~> 2.0.5)
acme-client (~> 2.0, >= 2.0.6)
activerecord-explain-analyze (~> 0.1)
acts-as-taggable-on (~> 6.0)
addressable (~> 2.7)
......
......@@ -12,13 +12,15 @@ import {
GlTable,
} from '@gitlab/ui';
import { s__ } from '~/locale';
import query from '../graphql/queries/details.query.graphql';
import alertQuery from '../graphql/queries/details.query.graphql';
import sidebarStatusQuery from '../graphql/queries/sidebar_status.query.graphql';
import { fetchPolicies } from '~/lib/graphql';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user';
import initUserPopovers from '~/user_popovers';
import { ALERTS_SEVERITY_LABELS, trackAlertsDetailsViewsOptions } from '../constants';
import createIssueQuery from '../graphql/mutations/create_issue_from_alert.graphql';
import createIssueMutation from '../graphql/mutations/create_issue_from_alert.graphql';
import toggleSidebarStatusMutation from '../graphql/mutations/toggle_sidebar_status.mutation.graphql';
import { visitUrl, joinPaths } from '~/lib/utils/url_utility';
import Tracking from '~/tracking';
import { toggleContainerClasses } from '~/lib/utils/dom_utils';
......@@ -52,28 +54,27 @@ export default {
AlertSidebar,
SystemNote,
},
props: {
inject: {
projectPath: {
default: '',
},
alertId: {
type: String,
required: true,
default: '',
},
projectId: {
type: String,
required: true,
},
projectPath: {
type: String,
required: true,
default: '',
},
projectIssuesPath: {
type: String,
required: true,
default: '',
},
},
apollo: {
alert: {
fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
query,
query: alertQuery,
variables() {
return {
fullPath: this.projectPath,
......@@ -88,15 +89,18 @@ export default {
Sentry.captureException(error);
},
},
sidebarStatus: {
query: sidebarStatusQuery,
},
},
data() {
return {
alert: null,
errored: false,
sidebarStatus: false,
isErrorDismissed: false,
createIssueError: '',
issueCreationInProgress: false,
sidebarCollapsed: false,
sidebarErrorMessage: '',
};
},
......@@ -132,10 +136,10 @@ export default {
this.sidebarErrorMessage = '';
},
toggleSidebar() {
this.sidebarCollapsed = !this.sidebarCollapsed;
this.$apollo.mutate({ mutation: toggleSidebarStatusMutation });
toggleContainerClasses(containerEl, {
'right-sidebar-collapsed': this.sidebarCollapsed,
'right-sidebar-expanded': !this.sidebarCollapsed,
'right-sidebar-collapsed': !this.sidebarStatus,
'right-sidebar-expanded': this.sidebarStatus,
});
},
handleAlertSidebarError(errorMessage) {
......@@ -147,7 +151,7 @@ export default {
this.$apollo
.mutate({
mutation: createIssueQuery,
mutation: createIssueMutation,
variables: {
iid: this.alert.iid,
projectPath: this.projectPath,
......@@ -197,7 +201,7 @@ export default {
<div
v-if="alert"
class="alert-management-details gl-relative"
:class="{ 'pr-sm-8': sidebarCollapsed }"
:class="{ 'pr-sm-8': sidebarStatus }"
>
<div
class="gl-display-flex gl-justify-content-space-between gl-align-items-baseline gl-px-1 py-3 py-md-4 gl-border-b-1 gl-border-b-gray-200 gl-border-b-solid flex-column flex-sm-row"
......@@ -330,10 +334,7 @@ export default {
</gl-tab>
</gl-tabs>
<alert-sidebar
:project-path="projectPath"
:project-id="projectId"
:alert="alert"
:sidebar-collapsed="sidebarCollapsed"
@alert-refresh="alertRefresh"
@toggle-sidebar="toggleSidebar"
@alert-error="handleAlertSidebarError"
......
......@@ -4,6 +4,8 @@ import SidebarTodo from './sidebar/sidebar_todo.vue';
import SidebarStatus from './sidebar/sidebar_status.vue';
import SidebarAssignees from './sidebar/sidebar_assignees.vue';
import sidebarStatusQuery from '../graphql/queries/sidebar_status.query.graphql';
export default {
components: {
SidebarAssignees,
......@@ -11,27 +13,34 @@ export default {
SidebarTodo,
SidebarStatus,
},
props: {
sidebarCollapsed: {
type: Boolean,
required: true,
inject: {
projectPath: {
default: '',
},
projectId: {
type: String,
required: true,
},
projectPath: {
type: String,
required: true,
default: '',
},
},
props: {
alert: {
type: Object,
required: true,
},
},
apollo: {
sidebarStatus: {
query: sidebarStatusQuery,
},
},
data() {
return {
sidebarStatus: false,
};
},
computed: {
sidebarCollapsedClass() {
return this.sidebarCollapsed ? 'right-sidebar-collapsed' : 'right-sidebar-expanded';
return this.sidebarStatus ? 'right-sidebar-collapsed' : 'right-sidebar-expanded';
},
},
};
......@@ -41,10 +50,10 @@ export default {
<aside :class="sidebarCollapsedClass" class="right-sidebar alert-sidebar">
<div class="issuable-sidebar js-issuable-update">
<sidebar-header
:sidebar-collapsed="sidebarCollapsed"
:sidebar-collapsed="sidebarStatus"
@toggle-sidebar="$emit('toggle-sidebar')"
/>
<sidebar-todo v-if="sidebarCollapsed" :sidebar-collapsed="sidebarCollapsed" />
<sidebar-todo v-if="sidebarStatus" :sidebar-collapsed="sidebarStatus" />
<sidebar-status
:project-path="projectPath"
:alert="alert"
......@@ -55,7 +64,7 @@ export default {
:project-path="projectPath"
:project-id="projectId"
:alert="alert"
:sidebar-collapsed="sidebarCollapsed"
:sidebar-collapsed="sidebarStatus"
@alert-refresh="$emit('alert-refresh')"
@toggle-sidebar="$emit('toggle-sidebar')"
@alert-error="$emit('alert-error', $event)"
......
......@@ -3,6 +3,7 @@ import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
import AlertDetails from './components/alert_details.vue';
import sidebarStatusQuery from './graphql/queries/sidebar_status.query.graphql';
Vue.use(VueApollo);
......@@ -10,39 +11,51 @@ export default selector => {
const domEl = document.querySelector(selector);
const { alertId, projectPath, projectIssuesPath, projectId } = domEl.dataset;
const resolvers = {
Mutation: {
toggleSidebarStatus: (_, __, { cache }) => {
const data = cache.readQuery({ query: sidebarStatusQuery });
data.sidebarStatus = !data.sidebarStatus;
cache.writeQuery({ query: sidebarStatusQuery, data });
},
},
};
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(
{},
{
cacheConfig: {
dataIdFromObject: object => {
// eslint-disable-next-line no-underscore-dangle
if (object.__typename === 'AlertManagementAlert') {
return object.iid;
}
return defaultDataIdFromObject(object);
},
defaultClient: createDefaultClient(resolvers, {
cacheConfig: {
dataIdFromObject: object => {
// eslint-disable-next-line no-underscore-dangle
if (object.__typename === 'AlertManagementAlert') {
return object.iid;
}
return defaultDataIdFromObject(object);
},
},
),
}),
});
apolloProvider.clients.defaultClient.cache.writeData({
data: {
sidebarStatus: false,
},
});
// eslint-disable-next-line no-new
new Vue({
el: selector,
provide: {
projectPath,
alertId,
projectIssuesPath,
projectId,
},
apolloProvider,
components: {
AlertDetails,
},
render(createElement) {
return createElement('alert-details', {
props: {
alertId,
projectPath,
projectId,
projectIssuesPath,
},
});
return createElement('alert-details', {});
},
});
};
mutation($projectPath: ID!, $assigneeUsernames: [String!]!, $iid: String!) {
mutation alertSetAssignees($projectPath: ID!, $assigneeUsernames: [String!]!, $iid: String!) {
alertSetAssignees(
input: { iid: $iid, assigneeUsernames: $assigneeUsernames, projectPath: $projectPath }
) {
......
mutation ($projectPath: ID!, $iid: String!) {
mutation createAlertIssue($projectPath: ID!, $iid: String!) {
createAlertIssue(input: { iid: $iid, projectPath: $projectPath }) {
errors
issue {
......
mutation ($projectPath: ID!, $status: AlertManagementStatus!, $iid: String!) {
mutation updateAlertStatus($projectPath: ID!, $status: AlertManagementStatus!, $iid: String!) {
updateAlertStatus(input: { iid: $iid, status: $status, projectPath: $projectPath }) {
errors
alert {
......
<script>
import * as Sentry from '@sentry/browser';
import { mapState, mapActions } from 'vuex';
import {
GlDeprecatedBadge as GlBadge,
......@@ -88,7 +87,7 @@ export default {
this.fetchClusters();
},
methods: {
...mapActions(['fetchClusters', 'setPage']),
...mapActions(['fetchClusters', 'reportSentryError', 'setPage']),
k8sQuantityToGb(quantity) {
if (!quantity) {
return 0;
......@@ -150,7 +149,7 @@ export default {
};
}
} catch (error) {
Sentry.captureException(error);
this.reportSentryError({ error, tag: 'totalMemoryAndUsageError' });
}
return { totalMemory: null, freeSpacePercentage: null };
......@@ -183,7 +182,7 @@ export default {
};
}
} catch (error) {
Sentry.captureException(error);
this.reportSentryError({ error, tag: 'totalCpuAndUsageError' });
}
return { totalCpu: null, freeSpacePercentage: null };
......
......@@ -16,7 +16,14 @@ const allNodesPresent = (clusters, retryCount) => {
return retryCount > MAX_REQUESTS || clusters.every(cluster => cluster.nodes != null);
};
export const fetchClusters = ({ state, commit }) => {
export const reportSentryError = (_store, { error, tag }) => {
Sentry.withScope(scope => {
scope.setTag('javascript_clusters_list', tag);
Sentry.captureException(error);
});
};
export const fetchClusters = ({ state, commit, dispatch }) => {
let retryCount = 0;
commit(types.SET_LOADING_NODES, true);
......@@ -49,10 +56,7 @@ export const fetchClusters = ({ state, commit }) => {
commit(types.SET_LOADING_CLUSTERS, false);
commit(types.SET_LOADING_NODES, false);
Sentry.withScope(scope => {
scope.setTag('javascript_clusters_list', 'fetchClustersSuccessCallback');
Sentry.captureException(error);
});
dispatch('reportSentryError', { error, tag: 'fetchClustersSuccessCallback' });
}
},
errorCallback: response => {
......@@ -62,10 +66,7 @@ export const fetchClusters = ({ state, commit }) => {
commit(types.SET_LOADING_NODES, false);
flash(__('Clusters|An error occurred while loading clusters'));
Sentry.withScope(scope => {
scope.setTag('javascript_clusters_list', 'fetchClustersErrorCallback');
Sentry.captureException(response);
});
dispatch('reportSentryError', { error: response, tag: 'fetchClustersErrorCallback' });
},
});
......
......@@ -13,13 +13,14 @@ export default {
type: Array,
required: true,
},
},
inject: {
projectPath: {
type: String,
required: true,
default: '',
},
iid: {
type: String,
required: true,
from: 'issueIid',
defaut: '',
},
},
computed: {
......
......@@ -60,7 +60,7 @@ export default {
},
mounted() {
if (this.isNoteLinked) {
this.$refs.anchor.$el.scrollIntoView({ behavior: 'smooth', inline: 'start' });
this.$el.scrollIntoView({ behavior: 'smooth', inline: 'start' });
}
},
methods: {
......@@ -80,7 +80,7 @@ export default {
</script>
<template>
<timeline-entry-item :id="`note_${noteAnchorId}`" ref="anchor" class="design-note note-form">
<timeline-entry-item :id="`note_${noteAnchorId}`" class="design-note note-form">
<user-avatar-link
:link-href="author.webUrl"
:img-src="author.avatarUrl"
......
......@@ -6,7 +6,6 @@ import timeagoMixin from '~/vue_shared/mixins/timeago';
import Pagination from './pagination.vue';
import DeleteButton from '../delete_button.vue';
import permissionsQuery from '../../graphql/queries/design_permissions.query.graphql';
import appDataQuery from '../../graphql/queries/app_data.query.graphql';
import { DESIGNS_ROUTE_NAME } from '../../router/constants';
export default {
......@@ -55,19 +54,17 @@ export default {
permissions: {
createDesign: false,
},
projectPath: '',
issueIid: null,
};
},
apollo: {
appData: {
query: appDataQuery,
manual: true,
result({ data: { projectPath, issueIid } }) {
this.projectPath = projectPath;
this.issueIid = issueIid;
},
inject: {
projectPath: {
default: '',
},
issueIid: {
default: '',
},
},
apollo: {
permissions: {
query: permissionsQuery,
variables() {
......@@ -102,6 +99,7 @@ export default {
query: $route.query,
}"
:aria-label="s__('DesignManagement|Go back to designs')"
data-testid="close-design"
class="mr-3 text-plain d-flex justify-content-center align-items-center"
>
<icon :size="18" name="close" />
......
query projectFullPath {
projectPath @client
issueIid @client
}
import $ from 'jquery';
import Vue from 'vue';
import createRouter from './router';
import App from './components/app.vue';
import apolloProvider from './graphql';
import getDesignListQuery from './graphql/queries/get_design_list.query.graphql';
import { DESIGNS_ROUTE_NAME, ROOT_ROUTE_NAME } from './router/constants';
export default () => {
const el = document.querySelector('.js-design-management-new');
const badge = document.querySelector('.js-designs-count');
const { issueIid, projectPath, issuePath } = el.dataset;
const router = createRouter(issuePath);
$('.js-issue-tabs').on('shown.bs.tab', ({ target: { id } }) => {
if (id === 'designs' && router.currentRoute.name === ROOT_ROUTE_NAME) {
router.push({ name: DESIGNS_ROUTE_NAME });
} else if (id === 'discussion') {
router.push({ name: ROOT_ROUTE_NAME });
}
});
apolloProvider.clients.defaultClient.cache.writeData({
data: {
projectPath,
issueIid,
activeDiscussion: {
__typename: 'ActiveDiscussion',
id: null,
......@@ -32,25 +18,14 @@ export default () => {
},
});
apolloProvider.clients.defaultClient
.watchQuery({
query: getDesignListQuery,
variables: {
fullPath: projectPath,
iid: issueIid,
atVersion: null,
},
})
.subscribe(({ data }) => {
if (badge) {
badge.textContent = data.project.issue.designCollection.designs.edges.length;
}
});
return new Vue({
el,
router,
apolloProvider,
provide: {
projectPath,
issueIid,
},
render(createElement) {
return createElement(App);
},
......
import getDesignListQuery from '../graphql/queries/get_design_list.query.graphql';
import appDataQuery from '../graphql/queries/app_data.query.graphql';
import { findVersionId } from '../utils/design_management_utils';
export default {
apollo: {
appData: {
query: appDataQuery,
manual: true,
result({ data: { projectPath, issueIid } }) {
this.projectPath = projectPath;
this.issueIid = issueIid;
},
},
allVersions: {
query: getDesignListQuery,
variables() {
......@@ -24,6 +15,14 @@ export default {
update: data => data.project.issue.designCollection.versions.edges,
},
},
inject: {
projectPath: {
default: '',
},
issueIid: {
default: '',
},
},
computed: {
hasValidVersion() {
return (
......@@ -55,8 +54,6 @@ export default {
data() {
return {
allVersions: [],
projectPath: '',
issueIid: null,
};
},
};
......@@ -12,7 +12,6 @@ import DesignPresentation from '../../components/design_presentation.vue';
import DesignReplyForm from '../../components/design_notes/design_reply_form.vue';
import DesignSidebar from '../../components/design_sidebar.vue';
import getDesignQuery from '../../graphql/queries/get_design.query.graphql';
import appDataQuery from '../../graphql/queries/app_data.query.graphql';
import createImageDiffNoteMutation from '../../graphql/mutations/create_image_diff_note.mutation.graphql';
import updateImageDiffNoteMutation from '../../graphql/mutations/update_image_diff_note.mutation.graphql';
import updateActiveDiscussionMutation from '../../graphql/mutations/update_active_discussion.mutation.graphql';
......@@ -62,22 +61,12 @@ export default {
design: {},
comment: '',
annotationCoordinates: null,
projectPath: '',
errorMessage: '',
issueIid: '',
scale: 1,
resolvedDiscussionsExpanded: false,
};
},
apollo: {
appData: {
query: appDataQuery,
manual: true,
result({ data: { projectPath, issueIid } }) {
this.projectPath = projectPath;
this.issueIid = issueIid;
},
},
design: {
query: getDesignQuery,
// We want to see cached design version if we have one, and fetch newer version on the background to update discussions
......
......@@ -259,7 +259,7 @@ export default {
</script>
<template>
<div>
<div data-testid="designs-root">
<header v-if="showToolbar" class="row-content-block border-top-0 p-2 d-flex">
<div class="d-flex justify-content-between align-items-center w-100">
<design-version-dropdown />
......@@ -274,8 +274,6 @@ export default {
<design-destroyer
#default="{ mutate, loading }"
:filenames="selectedDesigns"
:project-path="projectPath"
:iid="issueIid"
@done="onDesignDelete"
@error="onDesignDeleteError"
>
......
export const ROOT_ROUTE_NAME = 'root';
export const DESIGNS_ROUTE_NAME = 'designs';
export const DESIGN_ROUTE_NAME = 'design';
import $ from 'jquery';
import Vue from 'vue';
import VueRouter from 'vue-router';
import routes from './routes';
......@@ -16,9 +15,7 @@ export default function createRouter(base) {
});
const pageEl = getPageLayoutElement();
router.beforeEach(({ meta: { el }, name }, _, next) => {
$(`#${el}`).tab('show');
router.beforeEach(({ name }, _, next) => {
// apply a fullscreen layout style in Design View (a.k.a design detail)
if (pageEl) {
if (name === DESIGN_ROUTE_NAME) {
......
import Home from '../pages/index.vue';
import DesignDetail from '../pages/design/index.vue';
import { ROOT_ROUTE_NAME, DESIGNS_ROUTE_NAME, DESIGN_ROUTE_NAME } from './constants';
import { DESIGNS_ROUTE_NAME, DESIGN_ROUTE_NAME } from './constants';
export default [
{
name: ROOT_ROUTE_NAME,
name: DESIGNS_ROUTE_NAME,
path: '/',
component: Home,
meta: {
el: 'discussion',
},
},
{
name: DESIGNS_ROUTE_NAME,
path: '/designs',
component: Home,
meta: {
el: 'designs',
},
children: [
name: DESIGN_ROUTE_NAME,
path: '/designs/:id',
component: DesignDetail,
beforeEnter(
{
name: DESIGN_ROUTE_NAME,
path: ':id',
component: DesignDetail,
meta: {
el: 'designs',
},
beforeEnter(
{
params: { id },
},
from,
next,
) {
if (typeof id === 'string') {
next();
}
},
props: ({ params: { id } }) => ({ id }),
params: { id },
},
],
from,
next,
) {
if (typeof id === 'string') {
next();
}
},
props: ({ params: { id } }) => ({ id }),
},
];
......@@ -16,6 +16,7 @@ export const getMergeRequestsForBranch = (
.getProjectMergeRequests(`${projectId}`, {
source_branch: branchId,
source_project_id: state.projects[projectId].id,
state: 'opened',
order_by: 'created_at',
per_page: 1,
})
......
......@@ -70,6 +70,7 @@ export default {
>
<gl-form-select
id="jira-project-select"
data-qa-selector="jira_project_dropdown"
class="mb-2"
:options="jiraProjects"
:state="selectState"
......@@ -135,7 +136,13 @@ export default {
</gl-form-group>
<div class="footer-block row-content-block d-flex justify-content-between">
<gl-button type="submit" category="primary" variant="success" class="js-no-auto-disable">
<gl-button
type="submit"
category="primary"
variant="success"
class="js-no-auto-disable"
data-qa-selector="jira_issues_import_button"
>
{{ __('Next') }}
</gl-button>
<gl-button :href="issuesPath">{{ __('Cancel') }}</gl-button>
......
......@@ -32,7 +32,7 @@ export default {
block: !isLastBlock,
}"
>
<p class="append-bottom-5">
<p class="gl-mb-2">
<span class="font-weight-bold">{{ __('Commit') }}</span>
<gl-link :href="commit.commit_path" class="js-commit-sha commit-sha link-commit">
......
......@@ -46,7 +46,7 @@ export default {
<p
v-if="trigger.short_token"
class="js-short-token"
:class="{ 'append-bottom-5': hasVariables, 'gl-mb-0': !hasVariables }"
:class="{ 'gl-mb-2': hasVariables, 'gl-mb-0': !hasVariables }"
>
<span class="font-weight-bold">{{ __('Trigger token:') }}</span> {{ trigger.short_token }}
</p>
......
......@@ -20,18 +20,22 @@ export const toNounSeriesText = items => {
if (items.length === 0) {
return '';
} else if (items.length === 1) {
return items[0];
return sprintf(s__(`nounSeries|%{item}`), { item: items[0] }, false);
} else if (items.length === 2) {
return sprintf(s__('nounSeries|%{firstItem} and %{lastItem}'), {
firstItem: items[0],
lastItem: items[1],
});
return sprintf(
s__('nounSeries|%{firstItem} and %{lastItem}'),
{
firstItem: items[0],
lastItem: items[1],
},
false,
);
}
return items.reduce((item, nextItem, idx) =>
idx === items.length - 1
? sprintf(s__('nounSeries|%{item}, and %{lastItem}'), { item, lastItem: nextItem })
: sprintf(s__('nounSeries|%{item}, %{nextItem}'), { item, nextItem }),
? sprintf(s__('nounSeries|%{item}, and %{lastItem}'), { item, lastItem: nextItem }, false)
: sprintf(s__('nounSeries|%{item}, %{nextItem}'), { item, nextItem }, false),
);
};
......
......@@ -353,7 +353,10 @@ export default {
</gl-deprecated-button>
</div>
<div v-if="externalDashboardUrl.length" class="mb-2 mr-2 d-flex d-sm-block">
<div
v-if="externalDashboardUrl && externalDashboardUrl.length"
class="mb-2 mr-2 d-flex d-sm-block"
>
<gl-deprecated-button
class="flex-grow-1 js-external-dashboard-link"
variant="primary"
......
......@@ -208,6 +208,14 @@ export const annotationsSymbolIcon = 'path://m5 229 5 8h-10z';
*/
export const DEFAULT_DASHBOARD_PATH = 'config/prometheus/common_metrics.yml';
/**
* GitLab provide metrics dashboards that are available to a user once
* the Prometheus managed app has been installed, without any extra setup
* required. These "out of the box" dashboards are defined under the
* `config/prometheus` path.
*/
export const OUT_OF_THE_BOX_DASHBOARDS_PATH_PREFIX = 'config/prometheus/';
export const OPERATORS = {
greaterThan: '>',
equalTo: '==',
......
import Vue from 'vue';
import { GlToast } from '@gitlab/ui';
import { parseBoolean } from '~/lib/utils/common_utils';
import { getParameterValues } from '~/lib/utils/url_utility';
import { createStore } from './stores';
import createRouter from './router';
import { stateAndPropsFromDataset } from './utils';
Vue.use(GlToast);
......@@ -12,37 +12,10 @@ export default (props = {}) => {
if (el && el.dataset) {
const [currentDashboard] = getParameterValues('dashboard');
const { metricsDashboardBasePath, ...dataset } = el.dataset;
const {
deploymentsEndpoint,
dashboardEndpoint,
dashboardsEndpoint,
projectPath,
logsPath,
currentEnvironmentName,
dashboardTimezone,
metricsDashboardBasePath,
customDashboardBasePath,
...dataProps
} = el.dataset;
const store = createStore({
currentDashboard,
deploymentsEndpoint,
dashboardEndpoint,
dashboardsEndpoint,
dashboardTimezone,
projectPath,
logsPath,
currentEnvironmentName,
customDashboardBasePath,
});
// HTML attributes are always strings, parse other types.
dataProps.hasMetrics = parseBoolean(dataProps.hasMetrics);
dataProps.customMetricsAvailable = parseBoolean(dataProps.customMetricsAvailable);
dataProps.prometheusAlertsAvailable = parseBoolean(dataProps.prometheusAlertsAvailable);
const { initState, dataProps } = stateAndPropsFromDataset({ currentDashboard, ...dataset });
const store = createStore(initState);
const router = createRouter(metricsDashboardBasePath);
// eslint-disable-next-line no-new
......