Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/merge_request_templates/New Static Analysis Check.md5
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/monitoring/constants.js2
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue7
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/components/ci_cd_analytics_charts.vue50
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue39
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/constants.js2
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/delete_modal.vue37
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/details_header.vue34
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/status_alert.vue50
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue11
-rw-r--r--app/assets/javascripts/registry/explorer/pages/details.vue59
-rw-r--r--app/controllers/projects/issues_controller.rb4
-rw-r--r--app/policies/project_policy.rb6
-rw-r--r--changelogs/unreleased/216761-add-image-repository-level-delete-functionality-to-the-image-repos.yml5
-rw-r--r--changelogs/unreleased/301140-conditionally-render-test-case-file.yml5
-rw-r--r--changelogs/unreleased/use-segmented-controls-ci-cd-analytics.yml5
-rw-r--r--doc/administration/audit_events.md2
-rw-r--r--doc/administration/logs.md3
-rw-r--r--doc/administration/pages/index.md2
-rw-r--r--doc/api/users.md4
-rw-r--r--doc/development/fe_guide/style/scss.md12
-rw-r--r--doc/user/analytics/code_review_analytics.md3
-rw-r--r--doc/user/analytics/index.md9
-rw-r--r--doc/user/analytics/merge_request_analytics.md5
-rw-r--r--doc/user/packages/dependency_proxy/index.md16
-rw-r--r--locale/gitlab.pot42
-rw-r--r--qa/qa/page/project/commit/show.rb4
-rw-r--r--spec/features/projects/graph_spec.rb6
-rw-r--r--spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_charts_spec.js94
-rw-r--r--spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js19
-rw-r--r--spec/frontend/projects/pipelines/charts/mock_data.js10
-rw-r--r--spec/frontend/registry/explorer/components/details_page/delete_modal_spec.js66
-rw-r--r--spec/frontend/registry/explorer/components/details_page/details_header_spec.js83
-rw-r--r--spec/frontend/registry/explorer/components/details_page/status_alert_spec.js57
-rw-r--r--spec/frontend/registry/explorer/components/details_page/tags_list_spec.js30
-rw-r--r--spec/frontend/registry/explorer/pages/details_spec.js90
37 files changed, 738 insertions, 142 deletions
diff --git a/.gitlab/merge_request_templates/New Static Analysis Check.md b/.gitlab/merge_request_templates/New Static Analysis Check.md
index 5fd2d31767a..66041a784e8 100644
--- a/.gitlab/merge_request_templates/New Static Analysis Check.md
+++ b/.gitlab/merge_request_templates/New Static Analysis Check.md
@@ -1,3 +1,8 @@
+<!--
+When creating a new cop that could be applied to multiple applications,
+we encourage you to add it to https://gitlab.com/gitlab-org/gitlab-styles gem.
+-->
+
## Description of the proposal
<!--
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 22c0679fc86..84ed6bb02a5 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-a3568f3bf5dbe84c238d2666af4ec7cc438faa31
+ad75eb15e8e667620e7cd1386e8361aaef7598d9
diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js
index 81ad3137b8b..060ed896d7c 100644
--- a/app/assets/javascripts/monitoring/constants.js
+++ b/app/assets/javascripts/monitoring/constants.js
@@ -151,7 +151,7 @@ export const linkTypes = {
* chart legend layout.
*
* Currently defined in
- * https://gitlab.com/gitlab-org/gitlab-ui/-/blob/master/src/utils/charts/constants.js
+ * https://gitlab.com/gitlab-org/gitlab-ui/-/blob/main/src/utils/charts/constants.js
*
*/
export const legendLayoutTypes = {
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue b/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
index 6b7381883cc..0fe03b3be6b 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
@@ -97,8 +97,13 @@ export default {
<div class="table-section section-10 section-wrap">
<div role="rowheader" class="table-mobile-header">{{ __('Filename') }}</div>
<div class="table-mobile-content gl-md-pr-2 gl-overflow-wrap-break">
- <gl-friendly-wrap :symbols="$options.wrapSymbols" :text="testCase.file" />
+ <gl-friendly-wrap
+ v-if="testCase.file"
+ :symbols="$options.wrapSymbols"
+ :text="testCase.file"
+ />
<gl-button
+ v-if="testCase.file"
v-gl-tooltip
size="small"
category="tertiary"
diff --git a/app/assets/javascripts/projects/pipelines/charts/components/ci_cd_analytics_charts.vue b/app/assets/javascripts/projects/pipelines/charts/components/ci_cd_analytics_charts.vue
new file mode 100644
index 00000000000..43b36da8b2c
--- /dev/null
+++ b/app/assets/javascripts/projects/pipelines/charts/components/ci_cd_analytics_charts.vue
@@ -0,0 +1,50 @@
+<script>
+import { GlSegmentedControl } from '@gitlab/ui';
+import { s__, sprintf } from '~/locale';
+import CiCdAnalyticsAreaChart from './ci_cd_analytics_area_chart.vue';
+
+export default {
+ components: {
+ GlSegmentedControl,
+ CiCdAnalyticsAreaChart,
+ },
+ props: {
+ charts: {
+ required: true,
+ type: Array,
+ },
+ chartOptions: {
+ required: true,
+ type: Object,
+ },
+ },
+ data() {
+ return {
+ selectedChart: 0,
+ };
+ },
+ computed: {
+ chartRanges() {
+ return this.charts.map(({ title }, index) => ({ text: title, value: index }));
+ },
+ chart() {
+ return this.charts[this.selectedChart];
+ },
+ dateRange() {
+ return sprintf(s__('CiCdAnalytics|Date range: %{range}'), { range: this.chart.range });
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-segmented-control v-model="selectedChart" :options="chartRanges" class="gl-mb-4" />
+ <ci-cd-analytics-area-chart
+ v-if="chart"
+ :chart-data="chart.data"
+ :area-chart-options="chartOptions"
+ >
+ {{ dateRange }}
+ </ci-cd-analytics-area-chart>
+ </div>
+</template>
diff --git a/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue b/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue
index 86433e1b59f..e9ff30008d0 100644
--- a/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue
+++ b/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue
@@ -13,6 +13,7 @@ import {
INNER_CHART_HEIGHT,
ONE_WEEK_AGO_DAYS,
ONE_MONTH_AGO_DAYS,
+ ONE_YEAR_AGO_DAYS,
X_AXIS_LABEL_ROTATION,
X_AXIS_TITLE_OFFSET,
PARSE_FAILURE,
@@ -21,7 +22,7 @@ import {
UNSUPPORTED_DATA,
} from '../constants';
import StatisticsList from './statistics_list.vue';
-import CiCdAnalyticsAreaChart from './ci_cd_analytics_area_chart.vue';
+import CiCdAnalyticsCharts from './ci_cd_analytics_charts.vue';
const defaultAnalyticsValues = {
weekPipelinesTotals: [],
@@ -52,7 +53,7 @@ export default {
GlColumnChart,
GlSkeletonLoader,
StatisticsList,
- CiCdAnalyticsAreaChart,
+ CiCdAnalyticsCharts,
},
inject: {
projectPath: {
@@ -173,10 +174,11 @@ export default {
},
areaCharts() {
const { lastWeek, lastMonth, lastYear } = this.$options.chartTitles;
+ const { lastWeekRange, lastMonthRange, lastYearRange } = this.$options.chartRanges;
const charts = [
- { title: lastWeek, data: this.lastWeekChartData },
- { title: lastMonth, data: this.lastMonthChartData },
- { title: lastYear, data: this.lastYearChartData },
+ { title: lastWeek, range: lastWeekRange, data: this.lastWeekChartData },
+ { title: lastMonth, range: lastMonthRange, data: this.lastMonthChartData },
+ { title: lastYear, range: lastYearRange, data: this.lastYearChartData },
];
let areaChartsData = [];
@@ -209,11 +211,12 @@ export default {
mergeLabelsAndValues(labels, values) {
return labels.map((label, index) => [label, values[index]]);
},
- buildAreaChartData({ title, data }) {
+ buildAreaChartData({ title, data, range }) {
const { labels, totals, success } = data;
return {
title,
+ range,
data: [
{
name: 'all',
@@ -257,20 +260,28 @@ export default {
[PARSE_FAILURE]: s__('PipelineCharts|There was an error parsing the data for the charts.'),
[DEFAULT]: s__('PipelineCharts|An unknown error occurred while processing CI/CD analytics.'),
},
- get chartTitles() {
+ chartTitles: {
+ lastWeek: __('Last week'),
+ lastMonth: __('Last month'),
+ lastYear: __('Last year'),
+ },
+ get chartRanges() {
const today = dateFormat(new Date(), CHART_DATE_FORMAT);
const pastDate = (timeScale) =>
dateFormat(getDateInPast(new Date(), timeScale), CHART_DATE_FORMAT);
return {
- lastWeek: sprintf(__('Pipelines for last week (%{oneWeekAgo} - %{today})'), {
+ lastWeekRange: sprintf(__('%{oneWeekAgo} - %{today}'), {
oneWeekAgo: pastDate(ONE_WEEK_AGO_DAYS),
today,
}),
- lastMonth: sprintf(__('Pipelines for last month (%{oneMonthAgo} - %{today})'), {
+ lastMonthRange: sprintf(__('%{oneMonthAgo} - %{today}'), {
oneMonthAgo: pastDate(ONE_MONTH_AGO_DAYS),
today,
}),
- lastYear: __('Pipelines for last year'),
+ lastYearRange: sprintf(__('%{oneYearAgo} - %{today}'), {
+ oneYearAgo: pastDate(ONE_YEAR_AGO_DAYS),
+ today,
+ }),
};
},
};
@@ -304,13 +315,7 @@ export default {
<template v-if="!loading">
<hr />
<h4 class="gl-my-4">{{ __('Pipelines charts') }}</h4>
- <ci-cd-analytics-area-chart
- v-for="(chart, index) in areaCharts"
- :key="index"
- :chart-data="chart.data"
- :area-chart-options="$options.areaChartOptions"
- >{{ chart.title }}</ci-cd-analytics-area-chart
- >
+ <ci-cd-analytics-charts :charts="areaCharts" :chart-options="$options.areaChartOptions" />
</template>
</div>
</template>
diff --git a/app/assets/javascripts/projects/pipelines/charts/constants.js b/app/assets/javascripts/projects/pipelines/charts/constants.js
index 079e23943c1..41fe81f21ea 100644
--- a/app/assets/javascripts/projects/pipelines/charts/constants.js
+++ b/app/assets/javascripts/projects/pipelines/charts/constants.js
@@ -10,6 +10,8 @@ export const ONE_WEEK_AGO_DAYS = 7;
export const ONE_MONTH_AGO_DAYS = 31;
+export const ONE_YEAR_AGO_DAYS = 365;
+
export const CHART_DATE_FORMAT = 'dd mmm';
export const DEFAULT = 'default';
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/delete_modal.vue b/app/assets/javascripts/registry/explorer/components/details_page/delete_modal.vue
index 96f221bf71d..0432cf1123c 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/delete_modal.vue
+++ b/app/assets/javascripts/registry/explorer/components/details_page/delete_modal.vue
@@ -1,7 +1,12 @@
<script>
import { GlModal, GlSprintf } from '@gitlab/ui';
import { n__ } from '~/locale';
-import { REMOVE_TAG_CONFIRMATION_TEXT, REMOVE_TAGS_CONFIRMATION_TEXT } from '../../constants/index';
+import {
+ REMOVE_TAG_CONFIRMATION_TEXT,
+ REMOVE_TAGS_CONFIRMATION_TEXT,
+ DELETE_IMAGE_CONFIRMATION_TITLE,
+ DELETE_IMAGE_CONFIRMATION_TEXT,
+} from '../../constants';
export default {
components: {
@@ -14,9 +19,17 @@ export default {
required: false,
default: () => [],
},
+ deleteImage: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
},
computed: {
- modalAction() {
+ modalTitle() {
+ if (this.deleteImage) {
+ return DELETE_IMAGE_CONFIRMATION_TITLE;
+ }
return n__(
'ContainerRegistry|Remove tag',
'ContainerRegistry|Remove tags',
@@ -24,14 +37,19 @@ export default {
);
},
modalDescription() {
+ if (this.deleteImage) {
+ return {
+ message: DELETE_IMAGE_CONFIRMATION_TEXT,
+ };
+ }
if (this.itemsToBeDeleted.length > 1) {
return {
message: REMOVE_TAGS_CONFIRMATION_TEXT,
item: this.itemsToBeDeleted.length,
};
}
- const [first] = this.itemsToBeDeleted;
+ const [first] = this.itemsToBeDeleted;
return {
message: REMOVE_TAG_CONFIRMATION_TEXT,
item: first?.path,
@@ -51,16 +69,17 @@ export default {
ref="deleteModal"
modal-id="delete-tag-modal"
ok-variant="danger"
- @ok="$emit('confirmDelete')"
+ :action-primary="{ text: __('Confirm'), attributes: { variant: 'danger' } }"
+ :action-cancel="{ text: __('Cancel') }"
+ @primary="$emit('confirmDelete')"
@cancel="$emit('cancelDelete')"
>
- <template #modal-title>{{ modalAction }}</template>
- <template #modal-ok>{{ modalAction }}</template>
+ <template #modal-title>{{ modalTitle }}</template>
<p v-if="modalDescription" data-testid="description">
<gl-sprintf :message="modalDescription.message">
- <template #item
- ><b>{{ modalDescription.item }}</b></template
- >
+ <template #item>
+ <b>{{ modalDescription.item }}</b>
+ </template>
</gl-sprintf>
</p>
</gl-modal>
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue b/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue
index ed02aa264ed..5f193527a60 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue
+++ b/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue
@@ -1,5 +1,5 @@
<script>
-import { GlSprintf } from '@gitlab/ui';
+import { GlSprintf, GlButton } from '@gitlab/ui';
import { sprintf, n__ } from '~/locale';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
@@ -24,7 +24,7 @@ import {
export default {
name: 'DetailsHeader',
- components: { GlSprintf, TitleArea, MetadataItem },
+ components: { GlSprintf, GlButton, TitleArea, MetadataItem },
mixins: [timeagoMixin],
props: {
image: {
@@ -36,6 +36,11 @@ export default {
required: false,
default: false,
},
+ disabled: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
},
computed: {
visibilityIcon() {
@@ -65,6 +70,9 @@ export default {
[UNFINISHED_STATUS]: { text: CLEANUP_UNFINISHED_TEXT, tooltip: CLEANUP_UNFINISHED_TOOLTIP },
}[this.image?.expirationPolicyCleanupStatus];
},
+ deleteButtonDisabled() {
+ return this.disabled || !this.image.canDelete;
+ },
},
i18n: {
DETAILS_PAGE_TITLE,
@@ -75,11 +83,13 @@ export default {
<template>
<title-area :metadata-loading="metadataLoading">
<template #title>
- <gl-sprintf :message="$options.i18n.DETAILS_PAGE_TITLE">
- <template #imageName>
- {{ image.name }}
- </template>
- </gl-sprintf>
+ <span data-testid="title">
+ <gl-sprintf :message="$options.i18n.DETAILS_PAGE_TITLE">
+ <template #imageName>
+ {{ image.name }}
+ </template>
+ </gl-sprintf>
+ </span>
</template>
<template #metadata-tags-count>
<metadata-item icon="tag" :text="tagCountText" data-testid="tags-count" />
@@ -103,5 +113,15 @@ export default {
data-testid="updated-and-visibility"
/>
</template>
+ <template #right-actions>
+ <gl-button
+ v-if="!metadataLoading"
+ variant="danger"
+ :disabled="deleteButtonDisabled"
+ @click="$emit('delete')"
+ >
+ {{ __('Delete') }}
+ </gl-button>
+ </template>
</title-area>
</template>
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/status_alert.vue b/app/assets/javascripts/registry/explorer/components/details_page/status_alert.vue
new file mode 100644
index 00000000000..fc1504f6c31
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/components/details_page/status_alert.vue
@@ -0,0 +1,50 @@
+<script>
+import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
+import {
+ IMAGE_STATUS_MESSAGES,
+ IMAGE_STATUS_TITLES,
+ IMAGE_STATUS_ALERT_TYPE,
+ PACKAGE_DELETE_HELP_PAGE_PATH,
+} from '../../constants';
+
+export default {
+ components: {
+ GlAlert,
+ GlSprintf,
+ GlLink,
+ },
+ props: {
+ status: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ message() {
+ return IMAGE_STATUS_MESSAGES[this.status];
+ },
+ title() {
+ return IMAGE_STATUS_TITLES[this.status];
+ },
+ variant() {
+ return IMAGE_STATUS_ALERT_TYPE[this.status];
+ },
+ },
+ links: {
+ PACKAGE_DELETE_HELP_PAGE_PATH,
+ },
+};
+</script>
+<template>
+ <gl-alert :title="title" :variant="variant">
+ <span data-testid="message">
+ <gl-sprintf :message="message">
+ <template #link="{ content }">
+ <gl-link :href="$options.links.PACKAGE_DELETE_HELP_PAGE_PATH" target="_blank">{{
+ content
+ }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </span>
+ </gl-alert>
+</template>
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue b/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue
index 9a4ae41d275..bc10246614a 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue
+++ b/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue
@@ -20,6 +20,11 @@ export default {
default: true,
required: false,
},
+ disabled: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
},
i18n: {
REMOVE_TAGS_BUTTON_TITLE,
@@ -37,6 +42,9 @@ export default {
showMultiDeleteButton() {
return this.tags.some((tag) => tag.canDelete) && !this.isMobile;
},
+ multiDeleteButtonIsDisabled() {
+ return !this.hasSelectedItems || this.disabled;
+ },
},
methods: {
updateSelectedItems(name) {
@@ -55,7 +63,7 @@ export default {
<gl-button
v-if="showMultiDeleteButton"
- :disabled="!hasSelectedItems"
+ :disabled="multiDeleteButtonIsDisabled"
category="secondary"
variant="danger"
@click="$emit('delete', selectedItems)"
@@ -70,6 +78,7 @@ export default {
:first="index === 0"
:selected="selectedItems[tag.name]"
:is-mobile="isMobile"
+ :disabled="disabled"
@select="updateSelectedItems(tag.name)"
@delete="$emit('delete', { [tag.name]: true })"
/>
diff --git a/app/assets/javascripts/registry/explorer/pages/details.vue b/app/assets/javascripts/registry/explorer/pages/details.vue
index 0cf83d9c62e..4d992a395af 100644
--- a/app/assets/javascripts/registry/explorer/pages/details.vue
+++ b/app/assets/javascripts/registry/explorer/pages/details.vue
@@ -12,6 +12,8 @@ import DetailsHeader from '../components/details_page/details_header.vue';
import TagsList from '../components/details_page/tags_list.vue';
import TagsLoader from '../components/details_page/tags_loader.vue';
import EmptyState from '../components/details_page/empty_state.vue';
+import StatusAlert from '../components/details_page/status_alert.vue';
+import DeleteImage from '../components/delete_image.vue';
import getContainerRepositoryDetailsQuery from '../graphql/queries/get_container_repository_details.query.graphql';
import deleteContainerRepositoryTagsMutation from '../graphql/mutations/delete_container_repository_tags.mutation.graphql';
@@ -21,6 +23,7 @@ import {
ALERT_DANGER_TAG,
ALERT_SUCCESS_TAGS,
ALERT_DANGER_TAGS,
+ ALERT_DANGER_IMAGE,
GRAPHQL_PAGE_SIZE,
FETCH_IMAGES_LIST_ERROR_MESSAGE,
UNFINISHED_STATUS,
@@ -38,6 +41,8 @@ export default {
TagsList,
TagsLoader,
EmptyState,
+ StatusAlert,
+ DeleteImage,
},
directives: {
GlResizeObserver: GlResizeObserverDirective,
@@ -71,6 +76,7 @@ export default {
mutationLoading: false,
deleteAlertType: null,
hidePartialCleanupWarning: false,
+ deleteImageAlert: false,
};
},
computed: {
@@ -105,6 +111,9 @@ export default {
hasNoTags() {
return this.tags.length === 0;
},
+ pageActionsAreDisabled() {
+ return Boolean(this.image?.status);
+ },
},
methods: {
updateBreadcrumb() {
@@ -112,11 +121,19 @@ export default {
this.breadCrumbState.updateName(name);
},
deleteTags(toBeDeleted) {
+ this.deleteImageAlert = false;
this.itemsToBeDeleted = this.tags.filter((tag) => toBeDeleted[tag.name]);
this.track('click_button');
this.$refs.deleteModal.show();
},
- async handleDelete() {
+ confirmDelete() {
+ if (this.deleteImageAlert) {
+ this.$refs.deleteImage.doDelete();
+ } else {
+ this.handleDeleteTag();
+ }
+ },
+ async handleDeleteTag() {
this.track('confirm_delete');
const { itemsToBeDeleted } = this;
this.itemsToBeDeleted = [];
@@ -184,6 +201,18 @@ export default {
feature_name: this.config.userCalloutId,
});
},
+ deleteImage() {
+ this.deleteImageAlert = true;
+ this.itemsToBeDeleted = [{ path: this.image.path }];
+ this.$refs.deleteModal.show();
+ },
+ deleteImageError() {
+ this.deleteAlertType = ALERT_DANGER_IMAGE;
+ },
+ deleteImageIniit() {
+ this.itemsToBeDeleted = [];
+ this.mutationLoading = true;
+ },
},
};
</script>
@@ -205,13 +234,25 @@ export default {
@dismiss="dismissPartialCleanupWarning"
/>
- <details-header :image="image" :metadata-loading="isLoading" />
+ <status-alert v-if="image.status" :status="image.status" />
+
+ <details-header
+ :image="image"
+ :metadata-loading="isLoading"
+ :disabled="pageActionsAreDisabled"
+ @delete="deleteImage"
+ />
<tags-loader v-if="isLoading" />
<template v-else>
<empty-state v-if="hasNoTags" :no-containers-image="config.noContainersImage" />
<template v-else>
- <tags-list :tags="tags" :is-mobile="isMobile" @delete="deleteTags" />
+ <tags-list
+ :tags="tags"
+ :is-mobile="isMobile"
+ :disabled="pageActionsAreDisabled"
+ @delete="deleteTags"
+ />
<div class="gl-display-flex gl-justify-content-center">
<gl-keyset-pagination
v-if="showPagination"
@@ -225,10 +266,20 @@ export default {
</template>
</template>
+ <delete-image
+ :id="image.id"
+ ref="deleteImage"
+ use-update-fn
+ @start="deleteImageIniit"
+ @error="deleteImageError"
+ @end="mutationLoading = false"
+ />
+
<delete-modal
ref="deleteModal"
:items-to-be-deleted="itemsToBeDeleted"
- @confirmDelete="handleDelete"
+ :delete-image="deleteImageAlert"
+ @confirmDelete="confirmDelete"
@cancel="track('cancel_delete')"
/>
</template>
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 45402964d11..ff8c790f43d 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -130,7 +130,7 @@ class Projects::IssuesController < Projects::ApplicationController
service = ::Issues::CreateService.new(project, current_user, create_params)
@issue = service.execute
- create_vulnerability_issue_link(issue)
+ create_vulnerability_issue_feedback(issue)
if service.discussions_to_resolve.count(&:resolved?) > 0
flash[:notice] = if service.discussion_to_resolve_id
@@ -402,7 +402,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
# Overridden in EE
- def create_vulnerability_issue_link(issue); end
+ def create_vulnerability_issue_feedback(issue); end
end
Projects::IssuesController.prepend_if_ee('EE::Projects::IssuesController')
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 83acf0c12d7..f97d94c14f0 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -82,7 +82,7 @@ class ProjectPolicy < BasePolicy
with_scope :subject
condition(:metrics_dashboard_allowed) do
- feature_available?(:metrics_dashboard)
+ access_allowed_to?(:metrics_dashboard)
end
with_scope :global
@@ -161,7 +161,7 @@ class ProjectPolicy < BasePolicy
features.each do |f|
# these are scored high because they are unlikely
desc "Project has #{f} disabled"
- condition(:"#{f}_disabled", score: 32) { !feature_available?(f.to_sym) }
+ condition(:"#{f}_disabled", score: 32) { !access_allowed_to?(f.to_sym) }
end
# `:read_project` may be prevented in EE, but `:read_project_for_iids` should
@@ -696,7 +696,7 @@ class ProjectPolicy < BasePolicy
project.team.max_member_access(@user.id)
end
- def feature_available?(feature)
+ def access_allowed_to?(feature)
return false unless project.project_feature
case project.project_feature.access_level(feature)
diff --git a/changelogs/unreleased/216761-add-image-repository-level-delete-functionality-to-the-image-repos.yml b/changelogs/unreleased/216761-add-image-repository-level-delete-functionality-to-the-image-repos.yml
new file mode 100644
index 00000000000..950a1b061be
--- /dev/null
+++ b/changelogs/unreleased/216761-add-image-repository-level-delete-functionality-to-the-image-repos.yml
@@ -0,0 +1,5 @@
+---
+title: Add delete functionality to the Image Repository detail view
+merge_request: 51980
+author:
+type: added
diff --git a/changelogs/unreleased/301140-conditionally-render-test-case-file.yml b/changelogs/unreleased/301140-conditionally-render-test-case-file.yml
new file mode 100644
index 00000000000..2c5efb40db6
--- /dev/null
+++ b/changelogs/unreleased/301140-conditionally-render-test-case-file.yml
@@ -0,0 +1,5 @@
+---
+title: Conditionally render test case file
+merge_request: 53497
+author:
+type: fixed
diff --git a/changelogs/unreleased/use-segmented-controls-ci-cd-analytics.yml b/changelogs/unreleased/use-segmented-controls-ci-cd-analytics.yml
new file mode 100644
index 00000000000..61a9a0fc429
--- /dev/null
+++ b/changelogs/unreleased/use-segmented-controls-ci-cd-analytics.yml
@@ -0,0 +1,5 @@
+---
+title: Only Display One Chart at a Time
+merge_request: 52952
+author:
+type: changed
diff --git a/doc/administration/audit_events.md b/doc/administration/audit_events.md
index 6512879e8b8..97222f63e2c 100644
--- a/doc/administration/audit_events.md
+++ b/doc/administration/audit_events.md
@@ -9,7 +9,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
GitLab offers a way to view the changes made within the GitLab server for owners and administrators on a [paid plan](https://about.gitlab.com/pricing/).
GitLab system administrators can also take advantage of the logs located on the
-file system. See [the logs system documentation](logs.md) for more details.
+file system. See [the logs system documentation](logs.md#audit_jsonlog) for more details.
You can generate an [Audit report](audit_reports.md) of audit events.
diff --git a/doc/administration/logs.md b/doc/administration/logs.md
index 36172869493..3ea8efad51a 100644
--- a/doc/administration/logs.md
+++ b/doc/administration/logs.md
@@ -391,7 +391,8 @@ This file lives in `/var/log/gitlab/gitlab-rails/audit_json.log` for
Omnibus GitLab packages or in `/home/git/gitlab/log/audit_json.log` for
installations from source.
-Changes to group or project settings are logged to this file. For example:
+Changes to group or project settings and memberships (`target_details`) are logged to this file.
+For example:
```json
{
diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md
index 2b7a7300ad8..49adaa7eabf 100644
--- a/doc/administration/pages/index.md
+++ b/doc/administration/pages/index.md
@@ -216,7 +216,7 @@ control over how the Pages daemon runs and serves content in your environment.
| Setting | Description |
| ------- | ----------- |
-| `pages_external_url` | The URL where GitLab Pages is accessible, including protocol (HTTP / HTTPS). If `https://` is used, you must also set `gitlab_pages['ssl_certificate']` and `gitlab_pages['ssl_certificate_key']`.
+| `pages_external_url` | The URL where GitLab Pages is accessible, including protocol (HTTP / HTTPS). If `https://` is used, additional configuration is required. See [Wildcard domains with TLS support](#wildcard-domains-with-tls-support) and [Custom domains with TLS support](#custom-domains-with-tls-support) for details.
| `gitlab_pages[]` | |
| `access_control` | Whether to enable [access control](index.md#access-control).
| `api_secret_key` | Full path to file with secret key used to authenticate with the GitLab API. Auto-generated when left unset.
diff --git a/doc/api/users.md b/doc/api/users.md
index 70a7e2a815f..a7714b72b59 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -217,7 +217,9 @@ For example:
GET /users?extern_uid=1234567&provider=github
```
-You can search for users who are external with: `/users?external=true`
+Instance administrators can search for users who are external with: `/users?external=true`
+
+You cannot search for external users if you are not an instance administrator.
You can search users by creation date time range with:
diff --git a/doc/development/fe_guide/style/scss.md b/doc/development/fe_guide/style/scss.md
index a4cae12c4f3..7df16bc70d7 100644
--- a/doc/development/fe_guide/style/scss.md
+++ b/doc/development/fe_guide/style/scss.md
@@ -20,15 +20,17 @@ In order to reduce the generation of more CSS as our site grows, prefer the use
#### Where are utility classes defined?
-Prefer the use of [utility classes defined in GitLab UI](https://gitlab.com/gitlab-org/gitlab-ui/-/blob/master/doc/css.md#utilities). An easy list of classes can also be [seen on Unpkg](https://unpkg.com/browse/@gitlab/ui/src/scss/utilities.scss).
+Prefer the use of [utility classes defined in GitLab UI](https://gitlab.com/gitlab-org/gitlab-ui/-/blob/main/doc/css.md#utilities).
+An easy list of classes can also be [seen on Unpkg](https://unpkg.com/browse/@gitlab/ui/src/scss/utilities.scss).
-Classes in [`utilities.scss`](https://gitlab.com/gitlab-org/gitlab/blob/master/app/assets/stylesheets/utilities.scss) and [`common.scss`](https://gitlab.com/gitlab-org/gitlab/blob/master/app/assets/stylesheets/framework/common.scss) are being deprecated. Classes in [`common.scss`](https://gitlab.com/gitlab-org/gitlab/blob/master/app/assets/stylesheets/framework/common.scss) that use non-design system values should be avoided in favor of conformant values.
+Classes in [`utilities.scss`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/stylesheets/utilities.scss) and [`common.scss`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/stylesheets/framework/common.scss) are being deprecated.
+Classes in [`common.scss`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/stylesheets/framework/common.scss) that use non-design system values should be avoided in favor of conformant values.
Avoid [Bootstrap's Utility Classes](https://getbootstrap.com/docs/4.3/utilities/).
NOTE:
While migrating [Bootstrap's Utility Classes](https://getbootstrap.com/docs/4.3/utilities/)
-to the [GitLab UI](https://gitlab.com/gitlab-org/gitlab-ui/-/blob/master/doc/css.md#utilities)
+to the [GitLab UI](https://gitlab.com/gitlab-org/gitlab-ui/-/blob/main/doc/css.md#utilities)
utility classes, note both the classes for margin and padding differ. The size scale used at
GitLab differs from the scale used in the Bootstrap library. For a Bootstrap padding or margin
utility, you may need to double the size of the applied utility to achieve the same visual
@@ -36,9 +38,9 @@ result (such as `ml-1` becoming `gl-ml-2`).
#### Where should I put new utility classes?
-If a class you need has not been added to GitLab UI, you get to add it! Follow the naming patterns documented in the [utility files](https://gitlab.com/gitlab-org/gitlab-ui/-/tree/master/src/scss/utility-mixins) and refer to [GitLab UI's CSS documentation](https://gitlab.com/gitlab-org/gitlab-ui/-/blob/master/doc/contributing/adding_css.md#adding-utility-mixins) for more details, especially about adding responsive and stateful rules.
+If a class you need has not been added to GitLab UI, you get to add it! Follow the naming patterns documented in the [utility files](https://gitlab.com/gitlab-org/gitlab-ui/-/tree/main/src/scss/utility-mixins) and refer to [GitLab UI's CSS documentation](https://gitlab.com/gitlab-org/gitlab-ui/-/blob/main/doc/contributing/adding_css.md#adding-utility-mixins) for more details, especially about adding responsive and stateful rules.
-If it is not possible to wait for a GitLab UI update (generally one day), add the class to [`utilities.scss`](https://gitlab.com/gitlab-org/gitlab/blob/master/app/assets/stylesheets/utilities.scss) following the same naming conventions documented in GitLab UI. A follow—up issue to backport the class to GitLab UI and delete it from GitLab should be opened.
+If it is not possible to wait for a GitLab UI update (generally one day), add the class to [`utilities.scss`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/stylesheets/utilities.scss) following the same naming conventions documented in GitLab UI. A follow—up issue to backport the class to GitLab UI and delete it from GitLab should be opened.
#### When should I create component classes?
diff --git a/doc/user/analytics/code_review_analytics.md b/doc/user/analytics/code_review_analytics.md
index 7f052aec9cd..8d0d6eb1628 100644
--- a/doc/user/analytics/code_review_analytics.md
+++ b/doc/user/analytics/code_review_analytics.md
@@ -8,7 +8,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Code Review Analytics **(PREMIUM)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/38062) in [GitLab Starter](https://about.gitlab.com/pricing/) 12.7.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/38062) in [GitLab Starter](https://about.gitlab.com/pricing/) 12.7.
+> - Moved to [GitLab Premium](https://about.gitlab.com/pricing/) due to Starter/Bronze being [discontinued](https://about.gitlab.com/blog/2021/01/26/new-gitlab-product-subscription-model/) in 13.9.
Code Review Analytics makes it easy to view the longest-running reviews among open merge requests and
enables you to:
diff --git a/doc/user/analytics/index.md b/doc/user/analytics/index.md
index a186009cc15..f5da373ee6d 100644
--- a/doc/user/analytics/index.md
+++ b/doc/user/analytics/index.md
@@ -40,11 +40,12 @@ in one place.
## Group-level analytics
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/195979) in GitLab 12.8.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/195979) in GitLab 12.8.
+> - Moved to [GitLab Premium](https://about.gitlab.com/pricing/) due to Starter/Bronze being [discontinued](https://about.gitlab.com/blog/2021/01/26/new-gitlab-product-subscription-model/) in 13.9.
The following analytics features are available at the group level:
-- [Contribution](../group/contribution_analytics/index.md). **(STARTER)**
+- [Contribution](../group/contribution_analytics/index.md). **(PREMIUM)**
- [Insights](../group/insights/index.md). **(ULTIMATE)**
- [Issue](../group/issues_analytics/index.md). **(PREMIUM)**
- [Productivity](productivity_analytics.md) **(PREMIUM)**
@@ -56,10 +57,10 @@ The following analytics features are available at the group level:
The following analytics features are available at the project level:
- [CI/CD](ci_cd_analytics.md). **(FREE)**
-- [Code Review](code_review_analytics.md). **(STARTER)**
+- [Code Review](code_review_analytics.md). **(PREMIUM)**
- [Insights](../project/insights/index.md). **(ULTIMATE)**
- [Issue](../group/issues_analytics/index.md). **(PREMIUM)**
- [Merge Request](merge_request_analytics.md), enabled with the `project_merge_request_analytics`
- [feature flag](../../development/feature_flags/development.md#enabling-a-feature-flag-locally-in-development). **(STARTER)**
+ [feature flag](../../development/feature_flags/development.md#enabling-a-feature-flag-locally-in-development). **(PREMIUM)**
- [Repository](repository_analytics.md). **(FREE)**
- [Value Stream](value_stream_analytics.md). **(FREE)**
diff --git a/doc/user/analytics/merge_request_analytics.md b/doc/user/analytics/merge_request_analytics.md
index 6dd50a3be80..3edbe3e8aa4 100644
--- a/doc/user/analytics/merge_request_analytics.md
+++ b/doc/user/analytics/merge_request_analytics.md
@@ -7,7 +7,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Merge Request Analytics **(PREMIUM)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/229045) in [GitLab Starter](https://about.gitlab.com/pricing/) 13.3.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/229045) in [GitLab Starter](https://about.gitlab.com/pricing/) 13.3.
+> - Moved to [GitLab Premium](https://about.gitlab.com/pricing/) due to Starter/Bronze being [discontinued](https://about.gitlab.com/blog/2021/01/26/new-gitlab-product-subscription-model/) in 13.9.
Merge Request Analytics helps you understand the efficiency of your code review process, and the productivity of your team.
@@ -102,5 +103,5 @@ bookmark for those preferred settings in your browser.
The **Merge Request Analytics** feature can be accessed only:
-- On [Starter](https://about.gitlab.com/pricing/) and above.
+- On [Premium](https://about.gitlab.com/pricing/) and above.
- By users with [Reporter access](../permissions.md) and above.
diff --git a/doc/user/packages/dependency_proxy/index.md b/doc/user/packages/dependency_proxy/index.md
index fd75df513c7..878354ab264 100644
--- a/doc/user/packages/dependency_proxy/index.md
+++ b/doc/user/packages/dependency_proxy/index.md
@@ -221,3 +221,19 @@ RateLimit-Remaining: 98;w=21600
```
This example shows the total limit of 100 pulls in six hours, with 98 pulls remaining.
+
+#### Check the rate limit in a CI/CD job
+
+This example shows a GitLab CI/CD job that uses an image with `jq` and `curl` installed:
+
+```yaml
+hub_docker_quota_check:
+ stage: build
+ image: alpine:latest
+ tags:
+ - <optional_runner_tag>
+ before_script: apk add curl jq
+ script:
+ - |
+ TOKEN=$(curl "https://auth.docker.io/token?service=registry.docker.io&scope=repository:ratelimitpreview/test:pull" | jq --raw-output .token) && curl --head --header "Authorization: Bearer $TOKEN" "https://registry-1.docker.io/v2/ratelimitpreview/test/manifests/latest" 2>&1
+```
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index d7f6f5ace0c..90bbdea3cda 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -682,6 +682,15 @@ msgstr[1] ""
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
+msgid "%{oneMonthAgo} - %{today}"
+msgstr ""
+
+msgid "%{oneWeekAgo} - %{today}"
+msgstr ""
+
+msgid "%{oneYearAgo} - %{today}"
+msgstr ""
+
msgid "%{openOrClose} %{noteable}"
msgstr ""
@@ -775,6 +784,9 @@ msgstr ""
msgid "%{spanStart}in%{spanEnd} %{errorFn}"
msgstr ""
+msgid "%{startDate} - %{endDate}"
+msgstr ""
+
msgid "%{start} to %{end}"
msgstr ""
@@ -5700,6 +5712,9 @@ msgstr ""
msgid "Choose your framework"
msgstr ""
+msgid "CiCdAnalytics|Date range: %{range}"
+msgstr ""
+
msgid "CiStatusLabel|canceled"
msgstr ""
@@ -9822,6 +9837,9 @@ msgstr ""
msgid "Deployment Frequency"
msgstr ""
+msgid "DeploymentFrequencyCharts|%{startDate} - %{endDate}"
+msgstr ""
+
msgid "DeploymentFrequencyCharts|Date"
msgstr ""
@@ -9831,15 +9849,6 @@ msgstr ""
msgid "DeploymentFrequencyCharts|Deployments charts"
msgstr ""
-msgid "DeploymentFrequencyCharts|Deployments to production for last month (%{startDate} - %{endDate})"
-msgstr ""
-
-msgid "DeploymentFrequencyCharts|Deployments to production for last week (%{startDate} - %{endDate})"
-msgstr ""
-
-msgid "DeploymentFrequencyCharts|Deployments to production for the last 90 days (%{startDate} - %{endDate})"
-msgstr ""
-
msgid "DeploymentFrequencyCharts|Something went wrong while getting deployment frequency data"
msgstr ""
@@ -17073,6 +17082,9 @@ msgstr ""
msgid "Last item before this page loaded in your browser:"
msgstr ""
+msgid "Last month"
+msgstr ""
+
msgid "Last name"
msgstr ""
@@ -17121,6 +17133,9 @@ msgstr ""
msgid "Last week"
msgstr ""
+msgid "Last year"
+msgstr ""
+
msgid "LastCommit|authored"
msgstr ""
@@ -21446,15 +21461,6 @@ msgstr ""
msgid "Pipelines emails"
msgstr ""
-msgid "Pipelines for last month (%{oneMonthAgo} - %{today})"
-msgstr ""
-
-msgid "Pipelines for last week (%{oneWeekAgo} - %{today})"
-msgstr ""
-
-msgid "Pipelines for last year"
-msgstr ""
-
msgid "Pipelines for merge requests are configured. A detached pipeline runs in the context of the merge request, and not against the merged result. Learn more in the documentation for Pipelines for Merged Results."
msgstr ""
diff --git a/qa/qa/page/project/commit/show.rb b/qa/qa/page/project/commit/show.rb
index ba09dd1b92a..8ece81f7088 100644
--- a/qa/qa/page/project/commit/show.rb
+++ b/qa/qa/page/project/commit/show.rb
@@ -14,12 +14,12 @@ module QA
def select_email_patches
click_element :options_button
- click_element :email_patches
+ visit_link_in_element :email_patches
end
def select_plain_diff
click_element :options_button
- click_element :plain_diff
+ visit_link_in_element :plain_diff
end
def commit_sha
diff --git a/spec/features/projects/graph_spec.rb b/spec/features/projects/graph_spec.rb
index 7b9f79c9f7f..72df84bf905 100644
--- a/spec/features/projects/graph_spec.rb
+++ b/spec/features/projects/graph_spec.rb
@@ -72,9 +72,9 @@ RSpec.describe 'Project Graph', :js do
it 'renders CI graphs' do
expect(page).to have_content 'Overall'
- expect(page).to have_content 'Pipelines for last week'
- expect(page).to have_content 'Pipelines for last month'
- expect(page).to have_content 'Pipelines for last year'
+ expect(page).to have_content 'Last week'
+ expect(page).to have_content 'Last month'
+ expect(page).to have_content 'Last year'
expect(page).to have_content 'Duration for the last 30 commits'
end
end
diff --git a/spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_charts_spec.js b/spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_charts_spec.js
new file mode 100644
index 00000000000..773d7526f12
--- /dev/null
+++ b/spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_charts_spec.js
@@ -0,0 +1,94 @@
+import { nextTick } from 'vue';
+import { shallowMount } from '@vue/test-utils';
+import { GlSegmentedControl } from '@gitlab/ui';
+import CiCdAnalyticsCharts from '~/projects/pipelines/charts/components/ci_cd_analytics_charts.vue';
+import CiCdAnalyticsAreaChart from '~/projects/pipelines/charts/components/ci_cd_analytics_area_chart.vue';
+import { transformedAreaChartData, chartOptions } from '../mock_data';
+
+const DEFAULT_PROPS = {
+ chartOptions,
+ charts: [
+ {
+ range: 'test range 1',
+ title: 'title 1',
+ data: transformedAreaChartData,
+ },
+ {
+ range: 'test range 2',
+ title: 'title 2',
+ data: transformedAreaChartData,
+ },
+ {
+ range: 'test range 3',
+ title: 'title 3',
+ data: transformedAreaChartData,
+ },
+ ],
+};
+
+describe('~/projects/pipelines/charts/components/ci_cd_analytics_charts.vue', () => {
+ let wrapper;
+
+ const createWrapper = (props = {}) =>
+ shallowMount(CiCdAnalyticsCharts, {
+ propsData: {
+ ...DEFAULT_PROPS,
+ ...props,
+ },
+ });
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ wrapper = null;
+ }
+ });
+
+ describe('segmented control', () => {
+ let segmentedControl;
+
+ beforeEach(() => {
+ wrapper = createWrapper();
+ segmentedControl = wrapper.find(GlSegmentedControl);
+ });
+
+ it('should default to the first chart', () => {
+ expect(segmentedControl.props('checked')).toBe(0);
+ });
+
+ it('should use the title and index as values', () => {
+ const options = segmentedControl.props('options');
+ expect(options).toHaveLength(3);
+ expect(options).toEqual([
+ {
+ text: 'title 1',
+ value: 0,
+ },
+ {
+ text: 'title 2',
+ value: 1,
+ },
+ {
+ text: 'title 3',
+ value: 2,
+ },
+ ]);
+ });
+
+ it('should select a different chart on change', async () => {
+ segmentedControl.vm.$emit('input', 1);
+
+ const chart = wrapper.find(CiCdAnalyticsAreaChart);
+
+ await nextTick();
+
+ expect(chart.props('chartData')).toEqual(transformedAreaChartData);
+ expect(chart.text()).toBe('Date range: test range 2');
+ });
+ });
+
+ it('should not display charts if there are no charts', () => {
+ wrapper = createWrapper({ charts: [] });
+ expect(wrapper.find(CiCdAnalyticsAreaChart).exists()).toBe(false);
+ });
+});
diff --git a/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js b/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js
index 3e588d46a4f..9e051550fae 100644
--- a/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js
+++ b/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js
@@ -3,7 +3,7 @@ import VueApollo from 'vue-apollo';
import { GlColumnChart } from '@gitlab/ui/dist/charts';
import createMockApollo from 'helpers/mock_apollo_helper';
import StatisticsList from '~/projects/pipelines/charts/components/statistics_list.vue';
-import CiCdAnalyticsAreaChart from '~/projects/pipelines/charts/components/ci_cd_analytics_area_chart.vue';
+import CiCdAnalyticsCharts from '~/projects/pipelines/charts/components/ci_cd_analytics_charts.vue';
import PipelineCharts from '~/projects/pipelines/charts/components/pipeline_charts.vue';
import getPipelineCountByStatus from '~/projects/pipelines/charts/graphql/queries/get_pipeline_count_by_status.query.graphql';
import getProjectPipelineStatistics from '~/projects/pipelines/charts/graphql/queries/get_project_pipeline_statistics.query.graphql';
@@ -65,20 +65,17 @@ describe('~/projects/pipelines/charts/components/pipeline_charts.vue', () => {
});
describe('pipelines charts', () => {
- it('displays 3 area charts', () => {
- expect(wrapper.findAll(CiCdAnalyticsAreaChart)).toHaveLength(3);
+ it('displays the charts components', () => {
+ expect(wrapper.find(CiCdAnalyticsCharts).exists()).toBe(true);
});
describe('displays individual correctly', () => {
it('renders with the correct data', () => {
- const charts = wrapper.findAll(CiCdAnalyticsAreaChart);
- for (let i = 0; i < charts.length; i += 1) {
- const chart = charts.at(i);
-
- expect(chart.exists()).toBeTruthy();
- expect(chart.props('chartData')).toBe(wrapper.vm.areaCharts[i].data);
- expect(chart.text()).toBe(wrapper.vm.areaCharts[i].title);
- }
+ const charts = wrapper.find(CiCdAnalyticsCharts);
+ expect(charts.props()).toEqual({
+ charts: wrapper.vm.areaCharts,
+ chartOptions: wrapper.vm.$options.areaChartOptions,
+ });
});
});
});
diff --git a/spec/frontend/projects/pipelines/charts/mock_data.js b/spec/frontend/projects/pipelines/charts/mock_data.js
index 3bc09f0b0a0..2e2c594102c 100644
--- a/spec/frontend/projects/pipelines/charts/mock_data.js
+++ b/spec/frontend/projects/pipelines/charts/mock_data.js
@@ -57,6 +57,16 @@ export const mockPipelineCount = {
},
};
+export const chartOptions = {
+ xAxis: {
+ name: 'X axis title',
+ type: 'category',
+ },
+ yAxis: {
+ name: 'Y axis title',
+ },
+};
+
export const mockPipelineStatistics = {
data: {
project: {
diff --git a/spec/frontend/registry/explorer/components/details_page/delete_modal_spec.js b/spec/frontend/registry/explorer/components/details_page/delete_modal_spec.js
index 636e0a285a6..85d7e87b8eb 100644
--- a/spec/frontend/registry/explorer/components/details_page/delete_modal_spec.js
+++ b/spec/frontend/registry/explorer/components/details_page/delete_modal_spec.js
@@ -4,6 +4,8 @@ import component from '~/registry/explorer/components/details_page/delete_modal.
import {
REMOVE_TAG_CONFIRMATION_TEXT,
REMOVE_TAGS_CONFIRMATION_TEXT,
+ DELETE_IMAGE_CONFIRMATION_TITLE,
+ DELETE_IMAGE_CONFIRMATION_TEXT,
} from '~/registry/explorer/constants';
import { GlModal } from '../../stubs';
@@ -35,13 +37,13 @@ describe('Delete Modal', () => {
describe('events', () => {
it.each`
- glEvent | localEvent
- ${'ok'} | ${'confirmDelete'}
- ${'cancel'} | ${'cancelDelete'}
+ glEvent | localEvent
+ ${'primary'} | ${'confirmDelete'}
+ ${'cancel'} | ${'cancelDelete'}
`('GlModal $glEvent emits $localEvent', ({ glEvent, localEvent }) => {
mountComponent();
findModal().vm.$emit(glEvent);
- expect(wrapper.emitted(localEvent)).toBeTruthy();
+ expect(wrapper.emitted(localEvent)).toEqual([[]]);
});
});
@@ -53,27 +55,51 @@ describe('Delete Modal', () => {
});
});
- describe('itemsToBeDeleted contains one element', () => {
- beforeEach(() => {
- mountComponent({ itemsToBeDeleted: [{ path: 'foo' }] });
- });
- it(`has the correct description`, () => {
- expect(findDescription().text()).toBe(REMOVE_TAG_CONFIRMATION_TEXT.replace('%{item}', 'foo'));
+ describe('when we are deleting images', () => {
+ it('has the correct title', () => {
+ mountComponent({ deleteImage: true });
+
+ expect(wrapper.text()).toContain(DELETE_IMAGE_CONFIRMATION_TITLE);
});
- it('has the correct action', () => {
- expect(wrapper.text()).toContain('Remove tag');
+
+ it('has the correct description', () => {
+ mountComponent({ deleteImage: true });
+
+ expect(wrapper.text()).toContain(DELETE_IMAGE_CONFIRMATION_TEXT);
});
});
- describe('itemsToBeDeleted contains more than element', () => {
- beforeEach(() => {
- mountComponent({ itemsToBeDeleted: [{ path: 'foo' }, { path: 'bar' }] });
- });
- it(`has the correct description`, () => {
- expect(findDescription().text()).toBe(REMOVE_TAGS_CONFIRMATION_TEXT.replace('%{item}', '2'));
+ describe('when we are deleting tags', () => {
+ describe('itemsToBeDeleted contains one element', () => {
+ beforeEach(() => {
+ mountComponent({ itemsToBeDeleted: [{ path: 'foo' }] });
+ });
+
+ it(`has the correct description`, () => {
+ expect(findDescription().text()).toBe(
+ REMOVE_TAG_CONFIRMATION_TEXT.replace('%{item}', 'foo'),
+ );
+ });
+
+ it('has the correct title', () => {
+ expect(wrapper.text()).toContain('Remove tag');
+ });
});
- it('has the correct action', () => {
- expect(wrapper.text()).toContain('Remove tags');
+
+ describe('itemsToBeDeleted contains more than element', () => {
+ beforeEach(() => {
+ mountComponent({ itemsToBeDeleted: [{ path: 'foo' }, { path: 'bar' }] });
+ });
+
+ it(`has the correct description`, () => {
+ expect(findDescription().text()).toBe(
+ REMOVE_TAGS_CONFIRMATION_TEXT.replace('%{item}', '2'),
+ );
+ });
+
+ it('has the correct title', () => {
+ expect(wrapper.text()).toContain('Remove tags');
+ });
});
});
});
diff --git a/spec/frontend/registry/explorer/components/details_page/details_header_spec.js b/spec/frontend/registry/explorer/components/details_page/details_header_spec.js
index 337235e3de5..5885537542c 100644
--- a/spec/frontend/registry/explorer/components/details_page/details_header_spec.js
+++ b/spec/frontend/registry/explorer/components/details_page/details_header_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import { GlSprintf } from '@gitlab/ui';
+import { GlSprintf, GlButton } from '@gitlab/ui';
import { useFakeDate } from 'helpers/fake_date';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import component from '~/registry/explorer/components/details_page/details_header.vue';
@@ -23,6 +23,7 @@ describe('Details Header', () => {
name: 'foo',
updatedAt: '2020-11-03T13:29:21Z',
tagsCount: 10,
+ canDelete: true,
project: {
visibility: 'public',
containerExpirationPolicy: {
@@ -36,8 +37,10 @@ describe('Details Header', () => {
const findByTestId = (testId) => wrapper.find(`[data-testid="${testId}"]`);
const findLastUpdatedAndVisibility = () => findByTestId('updated-and-visibility');
+ const findTitle = () => findByTestId('title');
const findTagsCount = () => findByTestId('tags-count');
const findCleanup = () => findByTestId('cleanup');
+ const findDeleteButton = () => wrapper.find(GlButton);
const waitForMetadataItems = async () => {
// Metadata items are printed by a loop in the title-area and it takes two ticks for them to be available
@@ -45,11 +48,9 @@ describe('Details Header', () => {
await wrapper.vm.$nextTick();
};
- const mountComponent = (image = defaultImage) => {
+ const mountComponent = (propsData = { image: defaultImage }) => {
wrapper = shallowMount(component, {
- propsData: {
- image,
- },
+ propsData,
stubs: {
GlSprintf,
TitleArea,
@@ -63,13 +64,65 @@ describe('Details Header', () => {
});
it('has the correct title ', () => {
- mountComponent({ ...defaultImage, name: '' });
- expect(wrapper.text()).toMatchInterpolatedText(DETAILS_PAGE_TITLE);
+ mountComponent({ image: { ...defaultImage, name: '' } });
+ expect(findTitle().text()).toMatchInterpolatedText(DETAILS_PAGE_TITLE);
});
it('shows imageName in the title', () => {
mountComponent();
- expect(wrapper.text()).toContain('foo');
+ expect(findTitle().text()).toContain('foo');
+ });
+
+ describe('delete button', () => {
+ it('exists', () => {
+ mountComponent();
+
+ expect(findDeleteButton().exists()).toBe(true);
+ });
+
+ it('is hidden while loading', () => {
+ mountComponent({ image: defaultImage, metadataLoading: true });
+
+ expect(findDeleteButton().exists()).toBe(false);
+ });
+
+ it('has the correct text', () => {
+ mountComponent();
+
+ expect(findDeleteButton().text()).toBe('Delete');
+ });
+
+ it('has the correct props', () => {
+ mountComponent();
+
+ expect(findDeleteButton().props()).toMatchObject({
+ variant: 'danger',
+ disabled: false,
+ });
+ });
+
+ it('emits the correct event', () => {
+ mountComponent();
+
+ findDeleteButton().vm.$emit('click');
+
+ expect(wrapper.emitted('delete')).toEqual([[]]);
+ });
+
+ it.each`
+ canDelete | disabled | isDisabled
+ ${true} | ${false} | ${false}
+ ${true} | ${true} | ${true}
+ ${false} | ${false} | ${true}
+ ${false} | ${true} | ${true}
+ `(
+ 'when canDelete is $canDelete and disabled is $disabled is $isDisabled that the button is disabled',
+ ({ canDelete, disabled, isDisabled }) => {
+ mountComponent({ image: { ...defaultImage, canDelete }, disabled });
+
+ expect(findDeleteButton().props('disabled')).toBe(isDisabled);
+ },
+ );
});
describe('metadata items', () => {
@@ -82,7 +135,7 @@ describe('Details Header', () => {
});
it('when there is one tag has the correct text', async () => {
- mountComponent({ ...defaultImage, tagsCount: 1 });
+ mountComponent({ image: { ...defaultImage, tagsCount: 1 } });
await waitForMetadataItems();
expect(findTagsCount().props('text')).toBe('1 tag');
@@ -124,10 +177,12 @@ describe('Details Header', () => {
'when the status is $status the text is $text and the tooltip is $tooltip',
async ({ status, text, tooltip }) => {
mountComponent({
- ...defaultImage,
- expirationPolicyCleanupStatus: status,
- project: {
- containerExpirationPolicy: { enabled: true, nextRunAt: '2021-01-03T14:29:21Z' },
+ image: {
+ ...defaultImage,
+ expirationPolicyCleanupStatus: status,
+ project: {
+ containerExpirationPolicy: { enabled: true, nextRunAt: '2021-01-03T14:29:21Z' },
+ },
},
});
await waitForMetadataItems();
@@ -156,7 +211,7 @@ describe('Details Header', () => {
expect(findLastUpdatedAndVisibility().props('icon')).toBe('eye');
});
it('shows an eye slashed when the project is not public', async () => {
- mountComponent({ ...defaultImage, project: { visibility: 'private' } });
+ mountComponent({ image: { ...defaultImage, project: { visibility: 'private' } } });
await waitForMetadataItems();
expect(findLastUpdatedAndVisibility().props('icon')).toBe('eye-slash');
diff --git a/spec/frontend/registry/explorer/components/details_page/status_alert_spec.js b/spec/frontend/registry/explorer/components/details_page/status_alert_spec.js
new file mode 100644
index 00000000000..9bace6d2376
--- /dev/null
+++ b/spec/frontend/registry/explorer/components/details_page/status_alert_spec.js
@@ -0,0 +1,57 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlLink, GlSprintf, GlAlert } from '@gitlab/ui';
+import component from '~/registry/explorer/components/details_page/status_alert.vue';
+import {
+ DELETE_SCHEDULED,
+ DELETE_FAILED,
+ PACKAGE_DELETE_HELP_PAGE_PATH,
+ SCHEDULED_FOR_DELETION_STATUS_TITLE,
+ SCHEDULED_FOR_DELETION_STATUS_MESSAGE,
+ FAILED_DELETION_STATUS_TITLE,
+ FAILED_DELETION_STATUS_MESSAGE,
+} from '~/registry/explorer/constants';
+
+describe('Status Alert', () => {
+ let wrapper;
+
+ const findLink = () => wrapper.find(GlLink);
+ const findAlert = () => wrapper.find(GlAlert);
+ const findMessage = () => wrapper.find('[data-testid="message"]');
+
+ const mountComponent = (propsData) => {
+ wrapper = shallowMount(component, {
+ propsData,
+ stubs: {
+ GlSprintf,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it.each`
+ status | title | variant | message | link
+ ${DELETE_SCHEDULED} | ${SCHEDULED_FOR_DELETION_STATUS_TITLE} | ${'info'} | ${SCHEDULED_FOR_DELETION_STATUS_MESSAGE} | ${PACKAGE_DELETE_HELP_PAGE_PATH}
+ ${DELETE_FAILED} | ${FAILED_DELETION_STATUS_TITLE} | ${'warning'} | ${FAILED_DELETION_STATUS_MESSAGE} | ${''}
+ `(
+ `when the status is $status, title is $title, variant is $variant, message is $message and the link is $link`,
+ ({ status, title, variant, message, link }) => {
+ mountComponent({ status });
+
+ expect(findMessage().text()).toMatchInterpolatedText(message);
+ expect(findAlert().props()).toMatchObject({
+ title,
+ variant,
+ });
+ if (link) {
+ expect(findLink().attributes()).toMatchObject({
+ target: '_blank',
+ href: link,
+ });
+ }
+ },
+ );
+});
diff --git a/spec/frontend/registry/explorer/components/details_page/tags_list_spec.js b/spec/frontend/registry/explorer/components/details_page/tags_list_spec.js
index 413795a7a57..6284eed39eb 100644
--- a/spec/frontend/registry/explorer/components/details_page/tags_list_spec.js
+++ b/spec/frontend/registry/explorer/components/details_page/tags_list_spec.js
@@ -70,18 +70,25 @@ describe('Tags List', () => {
});
});
- it('is disabled when no item is selected', () => {
- mountComponent();
+ it.each`
+ disabled | doSelect | buttonDisabled
+ ${true} | ${false} | ${'true'}
+ ${true} | ${true} | ${'true'}
+ ${false} | ${false} | ${'true'}
+ ${false} | ${true} | ${undefined}
+ `(
+ 'is $buttonDisabled that the button is disabled when the component disabled state is $disabled and is $doSelect that the user selected a tag',
+ async ({ disabled, buttonDisabled, doSelect }) => {
+ mountComponent({ tags, disabled, isMobile: false });
- expect(findDeleteButton().attributes('disabled')).toBe('true');
- });
+ if (doSelect) {
+ findTagsListRow().at(0).vm.$emit('select');
+ await wrapper.vm.$nextTick();
+ }
- it('is enabled when at least one item is selected', async () => {
- mountComponent();
- findTagsListRow().at(0).vm.$emit('select');
- await wrapper.vm.$nextTick();
- expect(findDeleteButton().attributes('disabled')).toBe(undefined);
- });
+ expect(findDeleteButton().attributes('disabled')).toBe(buttonDisabled);
+ },
+ );
it('click event emits a deleted event with selected items', () => {
mountComponent();
@@ -100,12 +107,13 @@ describe('Tags List', () => {
});
it('the correct props are bound to it', () => {
- mountComponent();
+ mountComponent({ tags, disabled: true });
const rows = findTagsListRow();
expect(rows.at(0).attributes()).toMatchObject({
first: 'true',
+ disabled: 'true',
});
});
diff --git a/spec/frontend/registry/explorer/pages/details_spec.js b/spec/frontend/registry/explorer/pages/details_spec.js
index 7106cdae27f..541db5f21c4 100644
--- a/spec/frontend/registry/explorer/pages/details_spec.js
+++ b/spec/frontend/registry/explorer/pages/details_spec.js
@@ -12,11 +12,17 @@ import DetailsHeader from '~/registry/explorer/components/details_page/details_h
import TagsLoader from '~/registry/explorer/components/details_page/tags_loader.vue';
import TagsList from '~/registry/explorer/components/details_page/tags_list.vue';
import EmptyTagsState from '~/registry/explorer/components/details_page/empty_state.vue';
+import StatusAlert from '~/registry/explorer/components/details_page/status_alert.vue';
+import DeleteImage from '~/registry/explorer/components/delete_image.vue';
import getContainerRepositoryDetailsQuery from '~/registry/explorer/graphql/queries/get_container_repository_details.query.graphql';
import deleteContainerRepositoryTagsMutation from '~/registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql';
-import { UNFINISHED_STATUS } from '~/registry/explorer/constants/index';
+import {
+ UNFINISHED_STATUS,
+ DELETE_SCHEDULED,
+ ALERT_DANGER_IMAGE,
+} from '~/registry/explorer/constants';
import {
graphQLImageDetailsMock,
@@ -43,6 +49,8 @@ describe('Details Page', () => {
const findDetailsHeader = () => wrapper.find(DetailsHeader);
const findEmptyState = () => wrapper.find(EmptyTagsState);
const findPartialCleanupAlert = () => wrapper.find(PartialCleanupAlert);
+ const findStatusAlert = () => wrapper.find(StatusAlert);
+ const findDeleteImage = () => wrapper.find(DeleteImage);
const routeId = 1;
@@ -88,6 +96,7 @@ describe('Details Page', () => {
apolloProvider,
stubs: {
DeleteModal,
+ DeleteImage,
},
mocks: {
$route: {
@@ -507,4 +516,83 @@ describe('Details Page', () => {
expect(breadCrumbState.updateName).toHaveBeenCalledWith(containerRepositoryMock.name);
});
});
+
+ describe('when the image has a status different from null', () => {
+ const resolver = jest
+ .fn()
+ .mockResolvedValue(graphQLImageDetailsMock({ status: DELETE_SCHEDULED }));
+ it('disables all the actions', async () => {
+ mountComponent({ resolver });
+
+ await waitForApolloRequestRender();
+
+ expect(findDetailsHeader().props('disabled')).toBe(true);
+ expect(findTagsList().props('disabled')).toBe(true);
+ });
+
+ it('shows a status alert', async () => {
+ mountComponent({ resolver });
+
+ await waitForApolloRequestRender();
+
+ expect(findStatusAlert().exists()).toBe(true);
+ expect(findStatusAlert().props()).toMatchObject({
+ status: DELETE_SCHEDULED,
+ });
+ });
+ });
+
+ describe('delete the image', () => {
+ const mountComponentAndDeleteImage = async () => {
+ mountComponent();
+
+ await waitForApolloRequestRender();
+ findDetailsHeader().vm.$emit('delete');
+
+ await wrapper.vm.$nextTick();
+ };
+
+ it('on delete event it deletes the image', async () => {
+ await mountComponentAndDeleteImage();
+
+ findDeleteModal().vm.$emit('confirmDelete');
+
+ expect(findDeleteImage().emitted('start')).toEqual([[]]);
+ });
+
+ it('binds the correct props to the modal', async () => {
+ await mountComponentAndDeleteImage();
+
+ expect(findDeleteModal().props()).toMatchObject({
+ itemsToBeDeleted: [{ path: 'gitlab-org/gitlab-test/rails-12009' }],
+ deleteImage: true,
+ });
+ });
+
+ it('binds correctly to delete-image start and end events', async () => {
+ mountComponent();
+
+ findDeleteImage().vm.$emit('start');
+
+ await wrapper.vm.$nextTick();
+
+ expect(findTagsLoader().exists()).toBe(true);
+
+ findDeleteImage().vm.$emit('end');
+
+ await wrapper.vm.$nextTick();
+
+ expect(findTagsLoader().exists()).toBe(false);
+ });
+
+ it('binds correctly to delete-image error event', async () => {
+ mountComponent();
+
+ findDeleteImage().vm.$emit('error');
+
+ await wrapper.vm.$nextTick();
+
+ expect(findDeleteAlert().props('deleteAlertType')).toBe(ALERT_DANGER_IMAGE);
+ });
+ });
});