Skip to content
Snippets Groups Projects
Verified Commit 6fede435 authored by Ezekiel Kigbo's avatar Ezekiel Kigbo :two: Committed by GitLab
Browse files

Merge branch '498179-add-legacy-value-stream-metrics-component' into 'master'

Add LegacyValueStreamMetrics component for graphql migration

See merge request !173072



Merged-by: default avatarEzekiel Kigbo <3397881-ekigbo@users.noreply.gitlab.com>
Approved-by: default avatarJulie Huang <julhuang@gitlab.com>
Reviewed-by: default avatarEzekiel Kigbo <3397881-ekigbo@users.noreply.gitlab.com>
Reviewed-by: default avatarJulie Huang <julhuang@gitlab.com>
Co-authored-by: default avatarEzekiel Kigbo <ekigbo@gitlab.com>
parents a7453d0c 8d154dbf
No related branches found
No related tags found
No related merge requests found
Pipeline #1555766817 passed with warnings
Pipeline: E2E GDK

#1555781850

    Pipeline: E2E CNG

    #1555776705

      Pipeline: Ruby 3.2.5 as-if-foss

      #1555768798

        Showing
        with 423 additions and 156 deletions
        ......@@ -3,7 +3,7 @@ import { GlLoadingIcon } from '@gitlab/ui';
        // eslint-disable-next-line no-restricted-imports
        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 LegacyValueStreamMetrics from '~/analytics/shared/components/legacy_value_stream_metrics.vue';
        import { VSA_METRICS_GROUPS } from '~/analytics/shared/constants';
        import { toYmd, generateValueStreamsDashboardLink } from '~/analytics/shared/utils';
        import PathNavigation from '~/analytics/cycle_analytics/components/path_navigation.vue';
        ......@@ -24,7 +24,7 @@ export default {
        PathNavigation,
        StageTable,
        ValueStreamFilters,
        ValueStreamMetrics,
        LegacyValueStreamMetrics,
        UrlSync,
        },
        props: {
        ......@@ -188,7 +188,7 @@ export default {
        @selected="onSelectStage"
        />
        </div>
        <value-stream-metrics
        <legacy-value-stream-metrics
        :request-path="namespace.fullPath"
        :request-params="filterParams"
        :requests="metricsRequests"
        ......
        <script>
        // NOTE: the API requests for this component are being migrated to graphql
        // related issue: https://gitlab.com/gitlab-org/gitlab/-/issues/498179
        import { GlSkeletonLoader } from '@gitlab/ui';
        import { isEqual, keyBy } from 'lodash';
        import { createAlert } from '~/alert';
        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 = []) => {
        if (!keyList.length || !data.length) return [];
        const kv = keyBy(data, 'identifier');
        return keyList.map((id) => kv[id] || null).filter((obj) => Boolean(obj));
        };
        const groupRawMetrics = (groups = [], rawData = []) => {
        return groups.map((curr) => {
        const { keys, ...rest } = curr;
        return {
        data: extractMetricsGroupData(keys, rawData),
        keys,
        ...rest,
        };
        });
        };
        export default {
        name: 'LegacyValueStreamMetrics',
        components: {
        GlSkeletonLoader,
        MetricTile,
        ValueStreamsDashboardLink,
        },
        props: {
        requestPath: {
        type: String,
        required: true,
        },
        requestParams: {
        type: Object,
        required: true,
        },
        requests: {
        type: Array,
        required: true,
        },
        filterFn: {
        type: Function,
        required: false,
        default: null,
        },
        groupBy: {
        type: Array,
        required: false,
        default: () => [],
        },
        dashboardsPath: {
        type: String,
        required: false,
        default: null,
        },
        },
        data() {
        return {
        metrics: [],
        groupedMetrics: [],
        isLoading: false,
        };
        },
        computed: {
        hasGroupedMetrics() {
        return Boolean(this.groupBy.length);
        },
        },
        watch: {
        requestParams(newVal, oldVal) {
        if (!isEqual(newVal, oldVal)) {
        this.fetchData();
        }
        },
        },
        mounted() {
        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;
        return fetchMetricsData(this.requests, this.requestPath, this.requestParams)
        .then((data) => {
        this.metrics = this.filterFn ? this.filterFn(data) : data;
        if (this.hasGroupedMetrics) {
        this.groupedMetrics = groupRawMetrics(this.groupBy, this.metrics);
        }
        this.isLoading = false;
        })
        .catch((err) => {
        const message = sprintf(
        s__(
        'ValueStreamAnalytics|There was an error while fetching value stream analytics %{requestTypeName} data.',
        ),
        { requestTypeName: err.message },
        );
        createAlert({ message });
        this.isLoading = false;
        });
        },
        },
        };
        </script>
        <template>
        <div class="gl-flex" data-testid="vsa-metrics" :class="isLoading ? 'gl-my-6' : 'gl-mt-6'">
        <gl-skeleton-loader v-if="isLoading" />
        <template v-else>
        <div v-if="hasGroupedMetrics" class="gl-flex-col">
        <div
        v-for="(group, groupIndex) in groupedMetrics"
        :key="group.key"
        class="gl-mb-7"
        data-testid="vsa-metrics-group"
        >
        <h4 class="gl-my-0">{{ group.title }}</h4>
        <div class="gl-flex gl-flex-wrap">
        <metric-tile
        v-for="metric in group.data"
        :key="metric.identifier"
        :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>
        <div v-else class="gl-mb-7 gl-flex gl-flex-wrap">
        <metric-tile
        v-for="metric in metrics"
        :key="metric.identifier"
        :metric="metric"
        class="gl-mt-5 gl-pr-10"
        />
        </div>
        </template>
        </div>
        </template>
        <script>
        import { GlSkeletonLoader } from '@gitlab/ui';
        import { isEqual, keyBy } from 'lodash';
        import { isEqual } from 'lodash';
        import { createAlert } from '~/alert';
        import { sprintf, s__ } from '~/locale';
        import { fetchMetricsData, removeFlash } from '../utils';
        import { s__ } from '~/locale';
        import {
        DORA_METRICS_QUERY_TYPE,
        FLOW_METRICS_QUERY_TYPE,
        ALL_METRICS_QUERY_TYPE,
        VALUE_STREAM_METRIC_TILE_METADATA,
        } from '../constants';
        import { rawMetricToMetricTile, extractQueryResponseFromNamespace } from '../utils';
        import { BUCKETING_INTERVAL_ALL } from '../graphql/constants';
        import FlowMetricsQuery from '../graphql/flow_metrics.query.graphql';
        import DoraMetricsQuery from '../graphql/dora_metrics.query.graphql';
        import ValueStreamsDashboardLink from './value_streams_dashboard_link.vue';
        import MetricTile from './metric_tile.vue';
        const extractMetricsGroupData = (keyList = [], data = []) => {
        if (!keyList.length || !data.length) return [];
        const kv = keyBy(data, 'identifier');
        return keyList.map((id) => kv[id] || null).filter((obj) => Boolean(obj));
        const dataKeys = data.map(({ identifier }) => identifier);
        if (!keyList.length || !dataKeys.some((key) => keyList.includes(key))) return [];
        return keyList.reduce((acc, curr) => {
        const metric = data.find((item) => item.identifier === curr);
        return metric ? [...acc, metric] : acc;
        }, []);
        };
        const groupRawMetrics = (groups = [], rawData = []) => {
        ......@@ -40,9 +53,10 @@ export default {
        type: Object,
        required: true,
        },
        requests: {
        type: Array,
        required: true,
        queryType: {
        type: String,
        required: false,
        default: ALL_METRICS_QUERY_TYPE,
        },
        filterFn: {
        type: Function,
        ......@@ -62,56 +76,111 @@ export default {
        },
        data() {
        return {
        metrics: [],
        groupedMetrics: [],
        isLoading: false,
        flowMetrics: [],
        doraMetrics: [],
        };
        },
        computed: {
        hasGroupedMetrics() {
        return Boolean(this.groupBy.length);
        },
        isLoading() {
        return Boolean(
        this.$apollo.queries.doraMetrics.loading || this.$apollo.queries.flowMetrics.loading,
        );
        },
        groupedMetrics() {
        return groupRawMetrics(this.groupBy, this.metrics);
        },
        isFlowMetricsQuery() {
        return [ALL_METRICS_QUERY_TYPE, FLOW_METRICS_QUERY_TYPE].includes(this.queryType);
        },
        isDoraMetricsQuery() {
        return [ALL_METRICS_QUERY_TYPE, DORA_METRICS_QUERY_TYPE].includes(this.queryType);
        },
        displayableMetrics() {
        // NOTE: workaround while the flowMetrics/doraMetrics dont support including/excluding unwanted metrics from the response
        return Object.keys(VALUE_STREAM_METRIC_TILE_METADATA);
        },
        metrics() {
        const combined = [...this.flowMetrics, ...this.doraMetrics].filter(({ identifier }) =>
        this.displayableMetrics.includes(identifier),
        );
        const filtered = this.filterFn ? this.filterFn(combined) : combined;
        return filtered.map((metric) => rawMetricToMetricTile(metric));
        },
        },
        watch: {
        requestParams(newVal, oldVal) {
        async requestParams(newVal, oldVal) {
        if (!isEqual(newVal, oldVal)) {
        this.fetchData();
        await Promise.all([
        this.$apollo.queries.doraMetrics.refetch(),
        this.$apollo.queries.flowMetrics.refetch(),
        ]);
        }
        },
        },
        mounted() {
        this.fetchData();
        apollo: {
        flowMetrics: {
        query: FlowMetricsQuery,
        variables() {
        const { created_after: startDate, created_before: endDate } = this.requestParams;
        return { startDate, endDate, fullPath: this.requestPath };
        },
        skip() {
        return !this.isFlowMetricsQuery;
        },
        update(data) {
        const metrics = extractQueryResponseFromNamespace({
        result: { data },
        resultKey: 'flowMetrics',
        });
        return Object.values(metrics).filter((metric) => metric?.identifier);
        },
        error() {
        createAlert({
        message: s__('ValueStreamAnalytics|There was an error while fetching flow metrics data.'),
        });
        },
        },
        doraMetrics: {
        query: DoraMetricsQuery,
        variables() {
        const { created_after: startDate, created_before: endDate } = this.requestParams;
        return {
        fullPath: this.requestPath,
        interval: BUCKETING_INTERVAL_ALL,
        startDate,
        endDate,
        };
        },
        skip() {
        return !this.isDoraMetricsQuery;
        },
        update(data) {
        const responseData = extractQueryResponseFromNamespace({
        result: { data },
        resultKey: 'dora',
        });
        const [rawMetrics] = responseData.metrics;
        return Object.entries(rawMetrics).reduce((acc, [identifier, value]) => {
        return [...acc, { identifier, value }];
        }, []);
        },
        error() {
        createAlert({
        message: s__('ValueStreamAnalytics|There was an error while fetching DORA metrics data.'),
        });
        },
        },
        },
        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;
        return fetchMetricsData(this.requests, this.requestPath, this.requestParams)
        .then((data) => {
        this.metrics = this.filterFn ? this.filterFn(data) : data;
        if (this.hasGroupedMetrics) {
        this.groupedMetrics = groupRawMetrics(this.groupBy, this.metrics);
        }
        this.isLoading = false;
        })
        .catch((err) => {
        const message = sprintf(
        s__(
        'ValueStreamAnalytics|There was an error while fetching value stream analytics %{requestTypeName} data.',
        ),
        { requestTypeName: err.message },
        );
        createAlert({ message });
        this.isLoading = false;
        });
        },
        },
        };
        </script>
        ......
        ......@@ -93,6 +93,10 @@ export const METRIC_POPOVER_LABEL = s__('ValueStreamAnalytics|View details');
        export const ISSUES_COMPLETED_TYPE = 'issues_completed';
        export const ALL_METRICS_QUERY_TYPE = 'ALL_METRICS_QUERY_TYPE';
        export const DORA_METRICS_QUERY_TYPE = 'DORA_METRICS_QUERY_TYPE';
        export const FLOW_METRICS_QUERY_TYPE = 'FLOW_METRICS_QUERY_TYPE';
        export const FLOW_METRICS = {
        LEAD_TIME: 'lead_time',
        CYCLE_TIME: 'cycle_time',
        ......@@ -110,13 +114,19 @@ export const DORA_METRICS = {
        CHANGE_FAILURE_RATE: 'change_failure_rate',
        };
        const VSA_FLOW_METRICS_GROUP = {
        key: 'lifecycle_metrics',
        title: s__('ValueStreamAnalytics|Lifecycle metrics'),
        keys: Object.values(FLOW_METRICS),
        };
        export const VSA_METRICS_GROUPS = [VSA_FLOW_METRICS_GROUP];
        export const VSA_METRICS_GROUPS = [
        {
        key: 'lifecycle_metrics',
        title: s__('ValueStreamAnalytics|Lifecycle metrics'),
        keys: [
        FLOW_METRICS.LEAD_TIME,
        FLOW_METRICS.CYCLE_TIME,
        FLOW_METRICS.ISSUES,
        FLOW_METRICS.COMMITS,
        FLOW_METRICS.DEPLOYS,
        ],
        },
        ];
        export const VULNERABILITY_CRITICAL_TYPE = 'vulnerability_critical';
        export const VULNERABILITY_HIGH_TYPE = 'vulnerability_high';
        ......@@ -151,6 +161,9 @@ export const VALUE_STREAM_METRIC_DISPLAY_UNITS = {
        [UNITS.PERCENT]: '%',
        };
        // NOTE: ideally we would return these fields in the metrics queries
        // the flow metrics query returns some but not all fields we need
        // while the DORA query do not return any.
        export const VALUE_STREAM_METRIC_TILE_METADATA = {
        [DORA_METRICS.DEPLOYMENT_FREQUENCY]: {
        label: s__('DORA4Metrics|Deployment frequency'),
        ......@@ -215,7 +228,7 @@ export const VALUE_STREAM_METRIC_TILE_METADATA = {
        unit: UNITS.DAYS,
        },
        [FLOW_METRICS.ISSUES]: {
        label: s__('DORA4Metrics|Issues created'),
        label: s__('DORA4Metrics|New issues'),
        unit: UNITS.COUNT,
        description: s__('ValueStreamAnalytics|Number of new issues created.'),
        groupLink: '-/issues_analytics',
        ......
        import { flatten } from 'lodash';
        import dateFormat from '~/lib/dateformat';
        import { SECONDS_IN_DAY } from '~/lib/utils/datetime_utility';
        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, VALUE_STREAM_METRIC_METADATA } from './constants';
        import {
        dateFormats,
        FLOW_METRICS,
        MAX_METRIC_PRECISION,
        UNITS,
        VALUE_STREAM_METRIC_DISPLAY_UNITS,
        VALUE_STREAM_METRIC_TILE_METADATA,
        } from './constants';
        export const filterBySearchTerm = (data = [], searchTerm = '', filterByKey = 'name') => {
        if (!searchTerm?.length) return data;
        ......@@ -117,10 +125,76 @@ const requestData = ({ request, endpoint, requestPath, params, name }) => {
        export const fetchMetricsData = (requests = [], requestPath, params) => {
        const promises = requests.map((r) => requestData({ ...r, requestPath, params }));
        return Promise.all(promises).then((responses) =>
        prepareTimeMetricsData(flatten(responses), VALUE_STREAM_METRIC_METADATA),
        prepareTimeMetricsData(flatten(responses), VALUE_STREAM_METRIC_TILE_METADATA),
        );
        };
        /**
        * Formats any valid number as percentage
        *
        * @param {number|string} decimalValue Decimal value between 0 and 1 to be converted to a percentage
        * @param {number} precision The number of decimal places to round to
        *
        * @returns {string} Returns a formatted string multiplied by 100
        */
        export const formatAsPercentageWithoutSymbol = (decimalValue = 0, precision = 1) => {
        const parsed = Number.isNaN(Number(decimalValue)) ? 0 : decimalValue;
        return (parsed * 100).toFixed(precision);
        };
        /**
        * Converts a time in seconds to number of days, with variable precision
        *
        * @param {Number} seconds Time in seconds
        * @param {Number} precision Specifies the number of digits after the decimal
        *
        * @returns {Float} The number of days
        */
        export const secondsToDays = (seconds, precision = 1) =>
        (seconds / SECONDS_IN_DAY).toFixed(precision);
        export const scaledValueForDisplay = (value, units, precision = MAX_METRIC_PRECISION) => {
        switch (units) {
        case UNITS.PERCENT:
        return formatAsPercentageWithoutSymbol(value);
        case UNITS.DAYS:
        return secondsToDays(value, precision);
        default:
        return value;
        }
        };
        const prepareMetricValue = ({ identifier, value, unit }) => {
        // NOTE: the flow metrics graphql endpoint returns values already scaled for display
        if (!value) {
        // ensures we return `-` for 0/null etc
        return '-';
        }
        return Object.values(FLOW_METRICS).includes(identifier)
        ? value
        : scaledValueForDisplay(value, unit);
        };
        /**
        * Prepares metric data to be rendered in the metric_tile component
        *
        * @param {MetricData[]} data - The metric data to be rendered
        * @returns {TransformedMetricData[]} An array of metrics ready to render in the metric_tile
        */
        export const rawMetricToMetricTile = (metric) => {
        const { identifier, value, ...metricRest } = metric;
        const { unit, label, ...metadataRest } = VALUE_STREAM_METRIC_TILE_METADATA[identifier];
        return {
        ...metadataRest,
        ...metricRest,
        title: label,
        identifier,
        label,
        unit: VALUE_STREAM_METRIC_DISPLAY_UNITS[unit],
        value: prepareMetricValue({ value, unit, identifier }),
        };
        };
        /**
        * Generates a URL link to the VSD dashboard based on the group
        * and project paths passed into the method.
        ......@@ -149,3 +223,24 @@ export const extractVSAFeaturesFromGON = () => ({
        cycleAnalyticsForProjects: Boolean(gon?.licensed_features?.cycleAnalyticsForProjects),
        groupLevelAnalyticsDashboard: Boolean(gon?.licensed_features?.groupLevelAnalyticsDashboard),
        });
        /**
        * Takes a raw GraphQL response which could contain data for a group or project namespace,
        * and returns the data for the namespace which is present in the response.
        *
        * @param {Object} params
        * @param {string} params.resultKey - The data to be extracted from the namespace.
        * @param {Object} params.result
        * @param {Object} params.result.data
        * @param {Object} params.result.data.group - The group GraphQL response.
        * @param {Object} params.result.data.project - The project GraphQL response.
        * @returns {Object} The data extracted from either group[resultKey] or project[resultKey].
        */
        export const extractQueryResponseFromNamespace = ({ result, resultKey }) => {
        const { group = null, project = null } = result.data;
        if (group || project) {
        const namespace = group ?? project;
        return namespace[resultKey] || {};
        }
        return {};
        };
        ......@@ -5,10 +5,6 @@ import {
        AI_IMPACT_OVER_TIME_METRICS_TOOLTIPS,
        } from 'ee/analytics/dashboards/ai_impact/constants';
        import { calculateRate } from 'ee/analytics/dashboards/ai_impact/utils';
        import {
        extractQueryResponseFromNamespace,
        scaledValueForDisplay,
        } from 'ee/analytics/dashboards/api';
        import {
        LAST_30_DAYS,
        LAST_180_DAYS,
        ......@@ -16,6 +12,7 @@ import {
        startOfTomorrow,
        } from 'ee/dora/components/static_data/shared';
        import { AI_METRICS } from '~/analytics/shared/constants';
        import { scaledValueForDisplay, extractQueryResponseFromNamespace } from '~/analytics/shared/utils';
        import { defaultClient } from '../graphql/client';
        const DATE_RANGE_TITLES = { [LAST_30_DAYS]: sprintf(__('Last %{days} days'), { days: 30 }) };
        ......
        import { BUCKETING_INTERVAL_ALL } from '~/analytics/shared/graphql/constants';
        import DoraMetricsQuery from '~/analytics/shared/graphql/dora_metrics.query.graphql';
        import { extractQueryResponseFromNamespace, scaledValueForDisplay } from '~/analytics/shared/utils';
        import { TABLE_METRICS } from 'ee/analytics/dashboards/constants';
        import {
        extractQueryResponseFromNamespace,
        scaledValueForDisplay,
        } from 'ee/analytics/dashboards/api';
        import {
        LAST_180_DAYS,
        DORA_METRIC_QUERY_RANGES,
        ......
        ......@@ -5,7 +5,7 @@ import { GlEmptyState } from '@gitlab/ui';
        import { refreshCurrentPage } from '~/lib/utils/url_utility';
        import { VSA_METRICS_GROUPS } from '~/analytics/shared/constants';
        import { generateValueStreamsDashboardLink } from '~/analytics/shared/utils';
        import ValueStreamMetrics from '~/analytics/shared/components/value_stream_metrics.vue';
        import LegacyValueStreamMetrics from '~/analytics/shared/components/legacy_value_stream_metrics.vue';
        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';
        ......@@ -34,7 +34,7 @@ export default {
        ValueStreamAggregatingWarning,
        ValueStreamEmptyState,
        ValueStreamFilters,
        ValueStreamMetrics,
        LegacyValueStreamMetrics,
        ValueStreamSelect,
        UrlSync,
        DurationOverviewChart,
        ......@@ -260,7 +260,7 @@ export default {
        "
        />
        <template v-else>
        <value-stream-metrics
        <legacy-value-stream-metrics
        v-if="isOverviewStageSelected"
        :request-path="namespacePath"
        :request-params="cycleAnalyticsRequestParams"
        ......
        ......@@ -9,7 +9,7 @@ import {
        GlLink,
        } from '@gitlab/ui';
        import { GlSparklineChart } from '@gitlab/ui/dist/charts';
        import { toYmd } from '~/analytics/shared/utils';
        import { toYmd, extractQueryResponseFromNamespace } from '~/analytics/shared/utils';
        import { AI_METRICS, UNITS } from '~/analytics/shared/constants';
        import { BUCKETING_INTERVAL_ALL } from '~/analytics/shared/graphql/constants';
        import { dasherize } from '~/lib/utils/text_utility';
        ......@@ -57,7 +57,6 @@ import {
        extractGraphqlVulnerabilitiesData,
        extractGraphqlDoraData,
        extractGraphqlFlowData,
        extractQueryResponseFromNamespace,
        } from '../../api';
        import { extractGraphqlAiData } from '../api';
        ......
        import { formatAsPercentageWithoutSymbol, secondsToDays } from 'ee/dora/components/util';
        import {
        CONTRIBUTOR_METRICS,
        VULNERABILITY_METRICS,
        UNITS,
        MAX_METRIC_PRECISION,
        } from '~/analytics/shared/constants';
        import { CONTRIBUTOR_METRICS, VULNERABILITY_METRICS } from '~/analytics/shared/constants';
        import { scaledValueForDisplay } from '~/analytics/shared/utils';
        import { TABLE_METRICS } from './constants';
        /**
        ......@@ -49,17 +44,6 @@ export const extractGraphqlVulnerabilitiesData = (rawVulnerabilityData = []) =>
        };
        };
        export const scaledValueForDisplay = (value, units, precision = MAX_METRIC_PRECISION) => {
        switch (units) {
        case UNITS.PERCENT:
        return formatAsPercentageWithoutSymbol(value);
        case UNITS.DAYS:
        return secondsToDays(value, precision);
        default:
        return value;
        }
        };
        /**
        * @typedef {Object} DoraMetricItem
        * @property {String} date - ISO 8601 date
        ......@@ -240,24 +224,3 @@ export const fetchMetricsForTimePeriods = async (timePeriods, queryFn, queryPara
        return Promise.all(promises);
        };
        /**
        * Takes a raw GraphQL response which could contain data for a group or project namespace,
        * and returns the data for the namespace which is present in the response.
        *
        * @param {Object} params
        * @param {string} params.resultKey - The data to be extracted from the namespace.
        * @param {Object} params.result
        * @param {Object} params.result.data
        * @param {Object} params.result.data.group - The group GraphQL response.
        * @param {Object} params.result.data.project - The project GraphQL response.
        * @returns {Object} The data extracted from either group[resultKey] or project[resultKey].
        */
        export const extractQueryResponseFromNamespace = ({ result, resultKey }) => {
        const { group = null, project = null } = result.data;
        if (group || project) {
        const namespace = group ?? project;
        return namespace[resultKey] || {};
        }
        return {};
        };
        <script>
        import { uniq } from 'lodash';
        import * as Sentry from '~/sentry/sentry_browser_wrapper';
        import { toYmd } from '~/analytics/shared/utils';
        import { toYmd, extractQueryResponseFromNamespace } from '~/analytics/shared/utils';
        import { CONTRIBUTOR_METRICS } from '~/analytics/shared/constants';
        import { BUCKETING_INTERVAL_ALL } from '~/analytics/shared/graphql/constants';
        import FlowMetricsQuery from '~/analytics/shared/graphql/flow_metrics.query.graphql';
        ......@@ -28,7 +28,6 @@ import {
        extractGraphqlFlowData,
        extractGraphqlMergeRequestsData,
        extractGraphqlContributorCountData,
        extractQueryResponseFromNamespace,
        } from '../api';
        import {
        generateSkeletonTableData,
        ......
        <script>
        import * as Sentry from '~/sentry/sentry_browser_wrapper';
        import * as DoraApi from 'ee/api/dora_api';
        import ValueStreamMetrics from '~/analytics/shared/components/value_stream_metrics.vue';
        import LegacyValueStreamMetrics from '~/analytics/shared/components/legacy_value_stream_metrics.vue';
        import { toYmd } from '~/analytics/shared/utils';
        import { createAlert } from '~/alert';
        import { s__, sprintf } from '~/locale';
        ......@@ -42,7 +42,7 @@ export default {
        CiCdAnalyticsCharts,
        DoraChartHeader,
        ChartTooltipText,
        ValueStreamMetrics,
        LegacyValueStreamMetrics,
        },
        inject: {
        projectPath: {
        ......@@ -192,7 +192,7 @@ export default {
        :format-tooltip-text="formatTooltipText"
        >
        <template #metrics="{ selectedChart }">
        <value-stream-metrics
        <legacy-value-stream-metrics
        :request-path="metricsRequestPath"
        :requests="$options.metricsRequest"
        :request-params="getMetricsRequestParams(selectedChart)"
        ......
        ......@@ -5,7 +5,7 @@ import * as Sentry from '~/sentry/sentry_browser_wrapper';
        import { BASE_FORECAST_SERIES_OPTIONS } from 'ee/analytics/shared/constants';
        import * as DoraApi from 'ee/api/dora_api';
        import SafeHtml from '~/vue_shared/directives/safe_html';
        import ValueStreamMetrics from '~/analytics/shared/components/value_stream_metrics.vue';
        import LegacyValueStreamMetrics from '~/analytics/shared/components/legacy_value_stream_metrics.vue';
        import { toYmd } from '~/analytics/shared/utils';
        import { createAlert } from '~/alert';
        import { __, s__, sprintf } from '~/locale';
        ......@@ -50,7 +50,7 @@ export default {
        components: {
        CiCdAnalyticsCharts,
        DoraChartHeader,
        ValueStreamMetrics,
        LegacyValueStreamMetrics,
        GlToggle,
        GlBadge,
        GlAlert,
        ......@@ -431,7 +431,7 @@ export default {
        {{ forecastRequestErrorMessage }}
        </template>
        </gl-alert>
        <value-stream-metrics
        <legacy-value-stream-metrics
        :request-path="metricsRequestPath"
        :requests="$options.metricsRequest"
        :request-params="getMetricsRequestParams(selectedChart)"
        ......
        import { helpPagePath } from '~/helpers/help_page_helper';
        import { s__ } from '~/locale';
        import { secondsToDays } from '../util';
        import { secondsToDays } from '~/analytics/shared/utils';
        export * from './shared';
        ......
        <script>
        import * as Sentry from '~/sentry/sentry_browser_wrapper';
        import * as DoraApi from 'ee/api/dora_api';
        import ValueStreamMetrics from '~/analytics/shared/components/value_stream_metrics.vue';
        import LegacyValueStreamMetrics from '~/analytics/shared/components/legacy_value_stream_metrics.vue';
        import { toYmd } from '~/analytics/shared/utils';
        import { createAlert } from '~/alert';
        import { s__, sprintf } from '~/locale';
        ......@@ -37,7 +37,7 @@ export default {
        CiCdAnalyticsCharts,
        DoraChartHeader,
        ChartTooltipText,
        ValueStreamMetrics,
        LegacyValueStreamMetrics,
        },
        inject: {
        projectPath: {
        ......@@ -183,7 +183,7 @@ export default {
        :format-tooltip-text="formatTooltipText"
        >
        <template #metrics="{ selectedChart }">
        <value-stream-metrics
        <legacy-value-stream-metrics
        :request-path="metricsRequestPath"
        :requests="$options.metricsRequest"
        :request-params="getMetricsRequestParams(selectedChart)"
        ......
        ......@@ -5,10 +5,10 @@ import {
        nDaysAfter,
        getStartOfDay,
        humanizeTimeInterval,
        SECONDS_IN_DAY,
        } from '~/lib/utils/datetime_utility';
        import { median } from '~/lib/utils/number_utils';
        import { dateFormats } from '~/analytics/shared/constants';
        import { formatAsPercentageWithoutSymbol } from '~/analytics/shared/utils';
        import { linearRegression } from 'ee/analytics/shared/utils';
        import { buildForecast } from '../graphql/api';
        ......@@ -97,17 +97,6 @@ export const seriesToMedianSeries = (chartSeriesData, seriesName) => {
        };
        };
        /**
        * Converts a time in seconds to number of days, with variable precision
        *
        * @param {Number} seconds Time in seconds
        * @param {Number} precision Specifies the number of digits after the decimal
        *
        * @returns {Float} The number of days
        */
        export const secondsToDays = (seconds, precision = 1) =>
        (seconds / SECONDS_IN_DAY).toFixed(precision);
        /**
        * Generates the tooltip text and value for time interval series
        *
        ......@@ -151,19 +140,6 @@ export const extractTimeSeriesTooltip = (params, seriesName, formatter = humaniz
        };
        };
        /**
        * Formats any valid number as percentage
        *
        * @param {number|string} decimalValue Decimal value between 0 and 1 to be converted to a percentage
        * @param {number} precision The number of decimal places to round to
        *
        * @returns {string} Returns a formatted string multiplied by 100
        */
        export const formatAsPercentageWithoutSymbol = (decimalValue = 0, precision = 1) => {
        const parsed = Number.isNaN(Number(decimalValue)) ? 0 : decimalValue;
        return (parsed * 100).toFixed(precision);
        };
        /**
        * Formats any valid number as percentage
        *
        ......
        import { mockDoraMetricsResponseData } from 'ee_jest/analytics/dashboards/mock_data';
        import { mockDoraMetricsResponseData } from 'jest/analytics/shared/mock_data';
        import fetch from 'ee/analytics/analytics_dashboards/data_sources/dora_metrics_over_time';
        import { defaultClient } from 'ee/analytics/analytics_dashboards/graphql/client';
        import { LAST_WEEK, LAST_180_DAYS } from 'ee/dora/components/static_data/shared';
        ......
        ......@@ -23,7 +23,7 @@ import {
        initialPaginationQuery,
        selectedProjects as rawSelectedProjects,
        } from 'jest/analytics/cycle_analytics/mock_data';
        import ValueStreamMetrics from '~/analytics/shared/components/value_stream_metrics.vue';
        import LegacyValueStreamMetrics from '~/analytics/shared/components/legacy_value_stream_metrics.vue';
        import { toYmd } from '~/analytics/shared/utils';
        import PathNavigation from '~/analytics/cycle_analytics/components/path_navigation.vue';
        import StageTable from '~/analytics/cycle_analytics/components/stage_table.vue';
        ......@@ -184,7 +184,7 @@ describe('EE Value Stream Analytics component', () => {
        const findAggregationStatus = () => wrapper.findComponent(ValueStreamAggregationStatus);
        const findPathNavigation = () => wrapper.findComponent(PathNavigation);
        const findStageTable = () => wrapper.findComponent(StageTable);
        const findOverviewMetrics = () => wrapper.findComponent(ValueStreamMetrics);
        const findOverviewMetrics = () => wrapper.findComponent(LegacyValueStreamMetrics);
        const findFilterBar = () => wrapper.findComponent(ValueStreamFilters);
        const findDurationChart = () => wrapper.findComponent(DurationChart);
        const findDurationOverviewChart = () => wrapper.findComponent(DurationOverviewChart);
        ......
        import MockAdapter from 'axios-mock-adapter';
        import axios from '~/lib/utils/axios_utils';
        import { UNITS } from '~/analytics/shared/constants';
        import { extractQueryResponseFromNamespace, scaledValueForDisplay } from '~/analytics/shared/utils';
        import {
        extractGraphqlDoraData,
        extractGraphqlFlowData,
        extractGraphqlVulnerabilitiesData,
        extractGraphqlMergeRequestsData,
        scaledValueForDisplay,
        extractGraphqlContributorCountData,
        extractQueryResponseFromNamespace,
        } from 'ee/analytics/dashboards/api';
        import {
        mockDoraMetricsResponseData,
        mockLastVulnerabilityCountData,
        mockFlowMetricsResponseData,
        } from 'jest/analytics/shared/mock_data';
        import {
        mockLastVulnerabilityCountData,
        mockMergeRequestsResponseData,
        mockContributorCountResponseData,
        } from './mock_data';
        ......
        ......@@ -20,13 +20,19 @@ import FlowMetricsQuery from '~/analytics/shared/graphql/flow_metrics.query.grap
        import DoraMetricsQuery from '~/analytics/shared/graphql/dora_metrics.query.graphql';
        import createMockApollo from 'helpers/mock_apollo_helper';
        import waitForPromises from 'helpers/wait_for_promises';
        import {
        mockDoraMetricsResponseData,
        mockFlowMetricsResponseData,
        } from 'jest/analytics/shared/mock_data';
        import {
        mockGraphqlDoraMetricsResponse,
        mockGraphqlFlowMetricsResponse,
        } from 'jest/analytics/shared/helpers';
        import {
        doraMetricsParamsHelper,
        flowMetricsParamsHelper,
        vulnerabilityParamsHelper,
        mergeRequestsParamsHelper,
        mockGraphqlFlowMetricsResponse,
        mockGraphqlDoraMetricsResponse,
        mockGraphqlVulnerabilityResponse,
        mockGraphqlMergeRequestsResponse,
        expectTimePeriodRequests,
        ......@@ -38,8 +44,6 @@ import {
        MOCK_CHART_TIME_PERIODS,
        mockComparativeTableData,
        mockLastVulnerabilityCountData,
        mockDoraMetricsResponseData,
        mockFlowMetricsResponseData,
        mockMergeRequestsResponseData,
        mockContributorCountResponseData,
        } from '../mock_data';
        ......@@ -60,9 +64,6 @@ const allTimePeriods = [...MOCK_TABLE_TIME_PERIODS, ...MOCK_CHART_TIME_PERIODS];
        jest.mock('~/sentry/sentry_browser_wrapper');
        jest.mock('~/alert');
        jest.mock('~/analytics/shared/utils', () => ({
        toYmd: jest.requireActual('~/analytics/shared/utils').toYmd,
        }));
        Vue.use(VueApollo);
        ......
        0% Loading or .
        You are about to add 0 people to the discussion. Proceed with caution.
        Finish editing this message first!
        Please register or to comment