summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-03-06 03:10:24 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2023-03-06 03:10:24 +0000
commit66089ccf1ab842eb5f7608502eaf8d2642e350b5 (patch)
treeac7da536104695d325c2fe9764b24524d3431213
parent5eac1a5d627e9cdfa02b4dfd9d39d693c5ce65b8 (diff)
downloadgitlab-ce-66089ccf1ab842eb5f7608502eaf8d2642e350b5.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/components/base.vue19
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/utils.js1
-rw-r--r--app/assets/javascripts/analytics/shared/components/value_stream_metrics.vue18
-rw-r--r--app/assets/javascripts/analytics/shared/components/value_streams_dashboard_link.vue30
-rw-r--r--app/assets/javascripts/analytics/shared/utils.js19
-rw-r--r--app/controllers/projects/cycle_analytics_controller.rb1
-rw-r--r--doc/user/compliance/license_scanning_of_cyclonedx_files/index.md4
-rw-r--r--locale/gitlab.pot6
-rw-r--r--spec/frontend/analytics/cycle_analytics/base_spec.js21
-rw-r--r--spec/frontend/analytics/cycle_analytics/utils_spec.js2
-rw-r--r--spec/frontend/analytics/cycle_analytics/value_stream_metrics_spec.js21
-rw-r--r--spec/frontend/analytics/shared/utils_spec.js28
-rw-r--r--workhorse/go.mod2
-rw-r--r--workhorse/go.sum4
14 files changed, 167 insertions, 9 deletions
diff --git a/app/assets/javascripts/analytics/cycle_analytics/components/base.vue b/app/assets/javascripts/analytics/cycle_analytics/components/base.vue
index a688e2f497b..a0416100e49 100644
--- a/app/assets/javascripts/analytics/cycle_analytics/components/base.vue
+++ b/app/assets/javascripts/analytics/cycle_analytics/components/base.vue
@@ -4,7 +4,7 @@ import { mapActions, mapState, mapGetters } from 'vuex';
import { getCookie, setCookie } from '~/lib/utils/common_utils';
import ValueStreamMetrics from '~/analytics/shared/components/value_stream_metrics.vue';
import { VSA_METRICS_GROUPS } from '~/analytics/shared/constants';
-import { toYmd } from '~/analytics/shared/utils';
+import { toYmd, generateValueStreamsDashboardLink } from '~/analytics/shared/utils';
import PathNavigation from '~/analytics/cycle_analytics/components/path_navigation.vue';
import StageTable from '~/analytics/cycle_analytics/components/stage_table.vue';
import ValueStreamFilters from '~/analytics/cycle_analytics/components/value_stream_filters.vue';
@@ -98,8 +98,22 @@ export default {
}
return 0;
},
+ hasCycleAnalyticsForGroups() {
+ return this.features?.cycleAnalyticsForGroups;
+ },
metricsRequests() {
- return this.features?.cycleAnalyticsForGroups ? METRICS_REQUESTS : SUMMARY_METRICS_REQUEST;
+ return this.hasCycleAnalyticsForGroups ? METRICS_REQUESTS : SUMMARY_METRICS_REQUEST;
+ },
+ showLinkToDashboard() {
+ return this.hasCycleAnalyticsForGroups && this.features?.groupAnalyticsDashboardsPage;
+ },
+ dashboardsPath() {
+ const {
+ endpoints: { groupPath, fullPath },
+ } = this;
+ return this.showLinkToDashboard
+ ? generateValueStreamsDashboardLink(groupPath, [fullPath])
+ : null;
},
query() {
return {
@@ -173,6 +187,7 @@ export default {
:request-params="filterParams"
:requests="metricsRequests"
:group-by="$options.VSA_METRICS_GROUPS"
+ :dashboards-path="dashboardsPath"
/>
<gl-loading-icon v-if="isLoading" size="lg" />
<stage-table
diff --git a/app/assets/javascripts/analytics/cycle_analytics/utils.js b/app/assets/javascripts/analytics/cycle_analytics/utils.js
index 428bb11b950..0326aa1e70e 100644
--- a/app/assets/javascripts/analytics/cycle_analytics/utils.js
+++ b/app/assets/javascripts/analytics/cycle_analytics/utils.js
@@ -78,6 +78,7 @@ export const filterStagesByHiddenStatus = (stages = [], isHidden = true) =>
const extractFeatures = (gon) => ({
cycleAnalyticsForGroups: Boolean(gon?.licensed_features?.cycleAnalyticsForGroups),
+ groupAnalyticsDashboardsPage: Boolean(gon?.features?.groupAnalyticsDashboardsPage),
});
/**
diff --git a/app/assets/javascripts/analytics/shared/components/value_stream_metrics.vue b/app/assets/javascripts/analytics/shared/components/value_stream_metrics.vue
index cc7b554f32c..f917248cd13 100644
--- a/app/assets/javascripts/analytics/shared/components/value_stream_metrics.vue
+++ b/app/assets/javascripts/analytics/shared/components/value_stream_metrics.vue
@@ -4,6 +4,7 @@ import { isEqual, keyBy } from 'lodash';
import { createAlert } from '~/flash';
import { sprintf, s__ } from '~/locale';
import { fetchMetricsData, removeFlash } from '../utils';
+import ValueStreamsDashboardLink from './value_streams_dashboard_link.vue';
import MetricTile from './metric_tile.vue';
const extractMetricsGroupData = (keyList = [], data = []) => {
@@ -28,6 +29,7 @@ export default {
components: {
GlSkeletonLoader,
MetricTile,
+ ValueStreamsDashboardLink,
},
props: {
requestPath: {
@@ -52,6 +54,11 @@ export default {
required: false,
default: () => [],
},
+ dashboardsPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
data() {
return {
@@ -76,6 +83,10 @@ export default {
this.fetchData();
},
methods: {
+ shouldDisplayDashboardLink(index) {
+ // When we have groups of metrics, we should only display the link for the first group
+ return index === 0 && this.dashboardsPath;
+ },
fetchData() {
removeFlash();
this.isLoading = true;
@@ -110,7 +121,7 @@ export default {
<template v-else>
<div v-if="hasGroupedMetrics" class="gl-flex-direction-column">
<div
- v-for="group in groupedMetrics"
+ v-for="(group, groupIndex) in groupedMetrics"
:key="group.key"
class="gl-mb-7"
data-testid="vsa-metrics-group"
@@ -123,6 +134,11 @@ export default {
:metric="metric"
class="gl-mt-5 gl-pr-10"
/>
+ <value-streams-dashboard-link
+ v-if="shouldDisplayDashboardLink(groupIndex)"
+ class="gl-mt-5"
+ :request-path="dashboardsPath"
+ />
</div>
</div>
</div>
diff --git a/app/assets/javascripts/analytics/shared/components/value_streams_dashboard_link.vue b/app/assets/javascripts/analytics/shared/components/value_streams_dashboard_link.vue
new file mode 100644
index 00000000000..59c14f5e2ac
--- /dev/null
+++ b/app/assets/javascripts/analytics/shared/components/value_streams_dashboard_link.vue
@@ -0,0 +1,30 @@
+<script>
+import { GlIcon, GlLink } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export default {
+ name: 'ValueStreamsDashboardLink',
+ components: { GlIcon, GlLink },
+ props: {
+ requestPath: {
+ type: String,
+ required: true,
+ },
+ },
+ i18n: {
+ title: __('Related'),
+ linkText: __('Value Streams Dashboard | DORA'),
+ },
+};
+</script>
+<template>
+ <div class="gl-display-flex gl-flex-direction-column">
+ <div class="gl-display-flex gl-mb-2">
+ <span>{{ $options.i18n.title }}</span>
+ </div>
+ <div class="gl-display-flex gl-align-items-baseline">
+ <gl-link :href="requestPath">{{ $options.i18n.linkText }}</gl-link
+ >&nbsp;<gl-icon name="dashboard" />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/analytics/shared/utils.js b/app/assets/javascripts/analytics/shared/utils.js
index aafbf642766..701cdcf0a2d 100644
--- a/app/assets/javascripts/analytics/shared/utils.js
+++ b/app/assets/javascripts/analytics/shared/utils.js
@@ -1,6 +1,7 @@
import { flatten } from 'lodash';
import dateFormat from '~/lib/dateformat';
import { slugify } from '~/lib/utils/text_utility';
+import { joinPaths } from '~/lib/utils/url_utility';
import { urlQueryToFilter } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
import { dateFormats, METRICS_POPOVER_CONTENT } from './constants';
@@ -119,3 +120,21 @@ export const fetchMetricsData = (requests = [], requestPath, params) => {
prepareTimeMetricsData(flatten(responses), METRICS_POPOVER_CONTENT),
);
};
+
+/**
+ * Generates a URL link to the VSD dashboard based on the group
+ * and project paths passed into the method.
+ *
+ * @param {String} groupPath - Path of the specified group
+ * @param {Array} projectPaths - Array of project paths to include in the `query` parameter
+ * @returns a URL or blank string if there is no groupPath set
+ */
+export const generateValueStreamsDashboardLink = (groupPath, projectPaths = []) => {
+ if (groupPath.length) {
+ const query = projectPaths.length ? `?query=${projectPaths.join(',')}` : '';
+ const dashboardsSlug = '/-/analytics/dashboards';
+ const segments = [gon.relative_url_root || '', '/groups', groupPath, dashboardsSlug];
+ return joinPaths(...segments).concat(query);
+ }
+ return '';
+};
diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb
index dd2b3b8a067..e6f15eb05a5 100644
--- a/app/controllers/projects/cycle_analytics_controller.rb
+++ b/app/controllers/projects/cycle_analytics_controller.rb
@@ -22,6 +22,7 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
before_action do
push_licensed_feature(:cycle_analytics_for_groups) if project.licensed_feature_available?(:cycle_analytics_for_groups)
+ push_frontend_feature_flag(:group_analytics_dashboards_page)
end
def show
diff --git a/doc/user/compliance/license_scanning_of_cyclonedx_files/index.md b/doc/user/compliance/license_scanning_of_cyclonedx_files/index.md
index 483c15d648c..6a41128a8d4 100644
--- a/doc/user/compliance/license_scanning_of_cyclonedx_files/index.md
+++ b/doc/user/compliance/license_scanning_of_cyclonedx_files/index.md
@@ -16,8 +16,8 @@ To detect the licenses in use, License Compliance relies on running the
[Dependency Scanning CI Jobs](../../application_security/dependency_scanning/index.md),
and analyzing the [CycloneDX](https://cyclonedx.org/) Software Bill of Materials (SBOM) generated by those jobs.
Other 3rd party scanners may also be used as long as they produce a CycloneDX file with a list of dependencies for [one of our supported languages](#supported-languages-and-package-managers).
-This method of scanning is also capable of parsing and identifying over 500 different types of licenses
-and can extract license information from packages that are dual-licensed or have multiple different licenses that apply.
+This method of scanning is also capable of parsing and identifying over 500 different types of licenses, as defined in [the SPDX list](https://spdx.org/licenses/).
+Licenses not in the SPDX list are reported as "Unknown". License information can also be extracted from packages that are dual-licensed, or have multiple different licenses that apply.
To enable license detection using Dependency Scanning in a project,
include the `Jobs/Dependency-Scanning.yml` template in its CI configuration,
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index d2d43c5a255..53b08377674 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -35719,6 +35719,9 @@ msgstr ""
msgid "Relate to %{issuable_type} %{add_related_issue_link}"
msgstr ""
+msgid "Related"
+msgstr ""
+
msgid "Related feature flags"
msgstr ""
@@ -47182,6 +47185,9 @@ msgstr ""
msgid "Value Streams Dashboard (Beta)"
msgstr ""
+msgid "Value Streams Dashboard | DORA"
+msgstr ""
+
msgid "Value might contain a variable reference"
msgstr ""
diff --git a/spec/frontend/analytics/cycle_analytics/base_spec.js b/spec/frontend/analytics/cycle_analytics/base_spec.js
index 58588ff49ce..f27c3746c76 100644
--- a/spec/frontend/analytics/cycle_analytics/base_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/base_spec.js
@@ -157,6 +157,10 @@ describe('Value stream analytics component', () => {
expect(findPagination().exists()).toBe(true);
});
+ it('does not render a link to the value streams dashboard', () => {
+ expect(findOverviewMetrics().props('dashboardsPath')).toBeNull();
+ });
+
describe('with `cycleAnalyticsForGroups=true` license', () => {
beforeEach(() => {
wrapper = createComponent({ initialState: { features: { cycleAnalyticsForGroups: true } } });
@@ -167,6 +171,23 @@ describe('Value stream analytics component', () => {
});
});
+ describe('with `groupAnalyticsDashboardsPage=true` and `cycleAnalyticsForGroups=true` license', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ initialState: {
+ features: { groupAnalyticsDashboardsPage: true, cycleAnalyticsForGroups: true },
+ },
+ });
+ });
+
+ it('renders a link to the value streams dashboard', () => {
+ expect(findOverviewMetrics().props('dashboardsPath')).toBeDefined();
+ expect(findOverviewMetrics().props('dashboardsPath')).toBe(
+ '/groups/foo/-/analytics/dashboards?query=full/path/to/foo',
+ );
+ });
+ });
+
describe('isLoading = true', () => {
beforeEach(() => {
wrapper = createComponent({
diff --git a/spec/frontend/analytics/cycle_analytics/utils_spec.js b/spec/frontend/analytics/cycle_analytics/utils_spec.js
index fe412bf7498..a79abff1dff 100644
--- a/spec/frontend/analytics/cycle_analytics/utils_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/utils_spec.js
@@ -164,7 +164,7 @@ describe('Value stream analytics utils', () => {
...rawData,
gon: { licensed_features: fakeFeatures },
});
- expect(res.features).toEqual(fakeFeatures);
+ expect(res.features).toMatchObject(fakeFeatures);
});
});
});
diff --git a/spec/frontend/analytics/cycle_analytics/value_stream_metrics_spec.js b/spec/frontend/analytics/cycle_analytics/value_stream_metrics_spec.js
index 948dc5c9be2..b96580eeb2d 100644
--- a/spec/frontend/analytics/cycle_analytics/value_stream_metrics_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/value_stream_metrics_spec.js
@@ -8,6 +8,7 @@ import { METRIC_TYPE_SUMMARY } from '~/api/analytics_api';
import { VSA_METRICS_GROUPS, METRICS_POPOVER_CONTENT } from '~/analytics/shared/constants';
import { prepareTimeMetricsData } from '~/analytics/shared/utils';
import MetricTile from '~/analytics/shared/components/metric_tile.vue';
+import ValueStreamsDashboardLink from '~/analytics/shared/components/value_streams_dashboard_link.vue';
import { createAlert } from '~/flash';
import { group } from './mock_data';
@@ -37,6 +38,7 @@ describe('ValueStreamMetrics', () => {
});
};
+ const findVSDLink = () => wrapper.findComponent(ValueStreamsDashboardLink);
const findMetrics = () => wrapper.findAllComponents(MetricTile);
const findMetricsGroups = () => wrapper.findAllByTestId('vsa-metrics-group');
@@ -168,6 +170,25 @@ describe('ValueStreamMetrics', () => {
});
});
+ describe('Value Streams Dashboard Link', () => {
+ it('will render when a dashboardsPath is set', async () => {
+ wrapper = createComponent({ groupBy: VSA_METRICS_GROUPS, dashboardsPath: 'fake-group-path' });
+ await waitForPromises();
+
+ const vsdLink = findVSDLink();
+
+ expect(vsdLink.exists()).toBe(true);
+ expect(vsdLink.props()).toEqual({ requestPath: 'fake-group-path' });
+ });
+
+ it('does not render without a dashboardsPath', async () => {
+ wrapper = createComponent({ groupBy: VSA_METRICS_GROUPS });
+ await waitForPromises();
+
+ expect(findVSDLink().exists()).toBe(false);
+ });
+ });
+
describe('with a request failing', () => {
beforeEach(async () => {
mockGetValueStreamSummaryMetrics = jest.fn().mockRejectedValue();
diff --git a/spec/frontend/analytics/shared/utils_spec.js b/spec/frontend/analytics/shared/utils_spec.js
index b48e2d971b5..0b4346de9dc 100644
--- a/spec/frontend/analytics/shared/utils_spec.js
+++ b/spec/frontend/analytics/shared/utils_spec.js
@@ -5,6 +5,7 @@ import {
extractPaginationQueryParameters,
getDataZoomOption,
prepareTimeMetricsData,
+ generateValueStreamsDashboardLink,
} from '~/analytics/shared/utils';
import { slugify } from '~/lib/utils/text_utility';
import { objectToQuery } from '~/lib/utils/url_utility';
@@ -212,3 +213,30 @@ describe('prepareTimeMetricsData', () => {
]);
});
});
+
+describe('generateValueStreamsDashboardLink', () => {
+ it.each`
+ groupPath | projectPaths | result
+ ${''} | ${[]} | ${''}
+ ${'fake-group'} | ${[]} | ${'/groups/fake-group/-/analytics/dashboards'}
+ ${'fake-group'} | ${['fake-path/project_1']} | ${'/groups/fake-group/-/analytics/dashboards?query=fake-path/project_1'}
+ ${'fake-group'} | ${['fake-path/project_1', 'fake-path/project_2']} | ${'/groups/fake-group/-/analytics/dashboards?query=fake-path/project_1,fake-path/project_2'}
+ `(
+ 'generates the dashboard link when groupPath=$groupPath and projectPaths=$projectPaths',
+ ({ groupPath, projectPaths, result }) => {
+ expect(generateValueStreamsDashboardLink(groupPath, projectPaths)).toBe(result);
+ },
+ );
+
+ describe('with a relative url rool set', () => {
+ beforeEach(() => {
+ gon.relative_url_root = '/foobar';
+ });
+
+ it('with includes a relative path if one is set', () => {
+ expect(generateValueStreamsDashboardLink('fake-path', ['project_1'])).toBe(
+ '/foobar/groups/fake-path/-/analytics/dashboards?query=project_1',
+ );
+ });
+ });
+});
diff --git a/workhorse/go.mod b/workhorse/go.mod
index c584ce4b310..6d0af2c69fe 100644
--- a/workhorse/go.mod
+++ b/workhorse/go.mod
@@ -7,7 +7,7 @@ require (
github.com/BurntSushi/toml v1.2.1
github.com/FZambia/sentinel v1.1.1
github.com/alecthomas/chroma/v2 v2.5.0
- github.com/aws/aws-sdk-go v1.44.208
+ github.com/aws/aws-sdk-go v1.44.209
github.com/disintegration/imaging v1.6.2
github.com/getsentry/raven-go v0.2.0
github.com/golang-jwt/jwt/v4 v4.5.0
diff --git a/workhorse/go.sum b/workhorse/go.sum
index db8289b93fc..2773a57eeed 100644
--- a/workhorse/go.sum
+++ b/workhorse/go.sum
@@ -544,8 +544,8 @@ github.com/aws/aws-sdk-go v1.43.11/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4
github.com/aws/aws-sdk-go v1.43.31/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
github.com/aws/aws-sdk-go v1.44.128/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
github.com/aws/aws-sdk-go v1.44.151/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
-github.com/aws/aws-sdk-go v1.44.208 h1:xk1E2zWAIskrOP+huXuCYFR9ZdQWfTVid8Cjiwj2H1o=
-github.com/aws/aws-sdk-go v1.44.208/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
+github.com/aws/aws-sdk-go v1.44.209 h1:wZuiaA4eaqYZmoZXqGgNHqVD7y7kUGFvACDGBgowTps=
+github.com/aws/aws-sdk-go v1.44.209/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
github.com/aws/aws-sdk-go-v2 v1.17.1 h1:02c72fDJr87N8RAC2s3Qu0YuvMRZKNZJ9F+lAehCazk=
github.com/aws/aws-sdk-go-v2 v1.17.1/go.mod h1:JLnGeGONAyi2lWXI1p0PCIOIy333JMVK1U7Hf0aRFLw=