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
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-10-20 18:10:58 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-10-20 18:10:58 +0300
commit049d16d168fdee408b78f5f38619c092fd3b2265 (patch)
tree22d1db5ab4fae0967a4da4b1a6b097ef9e5d7aa2 /app
parentbf18f3295b550c564086efd0a32d9a25435ce216 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/artifacts/components/artifact_row.vue88
-rw-r--r--app/assets/javascripts/artifacts/components/artifacts_table_row_details.vue89
-rw-r--r--app/assets/javascripts/artifacts/components/job_artifacts_table.vue314
-rw-r--r--app/assets/javascripts/artifacts/constants.js47
-rw-r--r--app/assets/javascripts/artifacts/graphql/cache_update.js30
-rw-r--r--app/assets/javascripts/artifacts/graphql/mutations/destroy_artifact.mutation.graphql7
-rw-r--r--app/assets/javascripts/artifacts/graphql/queries/get_job_artifacts.query.graphql56
-rw-r--r--app/assets/javascripts/artifacts/index.js29
-rw-r--r--app/assets/javascripts/artifacts/utils.js26
-rw-r--r--app/assets/javascripts/diffs/components/diff_row_utils.js21
-rw-r--r--app/assets/javascripts/diffs/store/utils.js11
-rw-r--r--app/assets/javascripts/pages/projects/artifacts/index.js3
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue35
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers_inputs.vue34
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js13
-rw-r--r--app/assets/javascripts/sidebar/stores/sidebar_store.js4
-rw-r--r--app/assets/javascripts/users_select/index.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/merge_request_reviewers.subscription.graphql22
-rw-r--r--app/assets/stylesheets/pages/notes.scss2
-rw-r--r--app/controllers/dashboard/projects_controller.rb7
-rw-r--r--app/controllers/projects/merge_requests_controller.rb1
-rw-r--r--app/controllers/users_controller.rb2
-rw-r--r--app/finders/projects_finder.rb7
-rw-r--r--app/graphql/resolvers/concerns/project_search_arguments.rb1
-rw-r--r--app/models/concerns/protected_ref.rb1
-rw-r--r--app/models/group.rb2
-rw-r--r--app/models/project_authorization.rb10
-rw-r--r--app/models/protected_branch.rb16
-rw-r--r--app/models/protected_tag.rb1
-rw-r--r--app/views/admin/application_settings/_account_and_limit.html.haml2
-rw-r--r--app/views/projects/artifacts/_artifact.html.haml61
-rw-r--r--app/views/projects/artifacts/_table.html.haml16
-rw-r--r--app/views/projects/artifacts/index.html.haml13
-rw-r--r--app/views/shared/issuable/_sidebar_reviewers.html.haml9
34 files changed, 872 insertions, 112 deletions
diff --git a/app/assets/javascripts/artifacts/components/artifact_row.vue b/app/assets/javascripts/artifacts/components/artifact_row.vue
new file mode 100644
index 00000000000..92044a3641a
--- /dev/null
+++ b/app/assets/javascripts/artifacts/components/artifact_row.vue
@@ -0,0 +1,88 @@
+<script>
+import { GlButtonGroup, GlButton, GlBadge } from '@gitlab/ui';
+import { numberToHumanSize } from '~/lib/utils/number_utils';
+import { I18N_EXPIRED, I18N_DOWNLOAD, I18N_DELETE } from '../constants';
+
+export default {
+ name: 'ArtifactRow',
+ components: {
+ GlButtonGroup,
+ GlButton,
+ GlBadge,
+ },
+ props: {
+ artifact: {
+ type: Object,
+ required: true,
+ },
+ isLoading: {
+ type: Boolean,
+ required: true,
+ },
+ isLastRow: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ isExpired() {
+ if (!this.artifact.expireAt) {
+ return false;
+ }
+ return Date.now() > new Date(this.artifact.expireAt).getTime();
+ },
+ artifactSize() {
+ return numberToHumanSize(this.artifact.size);
+ },
+ },
+ i18n: {
+ expired: I18N_EXPIRED,
+ download: I18N_DOWNLOAD,
+ delete: I18N_DELETE,
+ },
+};
+</script>
+<template>
+ <div
+ class="gl-py-4"
+ :class="{ 'gl-border-b-solid gl-border-b-1 gl-border-gray-100': !isLastRow }"
+ >
+ <div class="gl-display-inline-flex gl-align-items-center gl-w-full">
+ <span class="gl-w-half gl-pl-8 gl-display-flex" data-testid="job-artifact-row-name">
+ {{ artifact.name }}
+ <gl-badge size="sm" variant="neutral" class="gl-ml-2">
+ {{ artifact.fileType.toLowerCase() }}
+ </gl-badge>
+ <gl-badge v-if="isExpired" size="sm" variant="warning" icon="expire" class="gl-ml-2">
+ {{ $options.i18n.expired }}
+ </gl-badge>
+ </span>
+
+ <span class="gl-w-quarter gl-text-right gl-pr-5" data-testid="job-artifact-row-size">
+ {{ artifactSize }}
+ </span>
+
+ <span class="gl-w-quarter gl-text-right gl-pr-5">
+ <gl-button-group>
+ <gl-button
+ category="tertiary"
+ icon="download"
+ :title="$options.i18n.download"
+ :aria-label="$options.i18n.download"
+ :href="artifact.downloadPath"
+ data-testid="job-artifact-row-download-button"
+ />
+ <gl-button
+ category="tertiary"
+ icon="remove"
+ :title="$options.i18n.delete"
+ :aria-label="$options.i18n.delete"
+ :loading="isLoading"
+ data-testid="job-artifact-row-delete-button"
+ @click="$emit('delete')"
+ />
+ </gl-button-group>
+ </span>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/artifacts/components/artifacts_table_row_details.vue b/app/assets/javascripts/artifacts/components/artifacts_table_row_details.vue
new file mode 100644
index 00000000000..089bfd80222
--- /dev/null
+++ b/app/assets/javascripts/artifacts/components/artifacts_table_row_details.vue
@@ -0,0 +1,89 @@
+<script>
+import { createAlert } from '~/flash';
+import { DynamicScroller, DynamicScrollerItem } from 'vendor/vue-virtual-scroller';
+import getJobArtifactsQuery from '../graphql/queries/get_job_artifacts.query.graphql';
+import destroyArtifactMutation from '../graphql/mutations/destroy_artifact.mutation.graphql';
+import { removeArtifactFromStore } from '../graphql/cache_update';
+import {
+ I18N_DESTROY_ERROR,
+ ARTIFACT_ROW_HEIGHT,
+ ARTIFACTS_SHOWN_WITHOUT_SCROLLING,
+} from '../constants';
+import ArtifactRow from './artifact_row.vue';
+
+export default {
+ name: 'ArtifactsTableRowDetails',
+ components: {
+ DynamicScroller,
+ DynamicScrollerItem,
+ ArtifactRow,
+ },
+ props: {
+ artifacts: {
+ type: Object,
+ required: true,
+ },
+ queryVariables: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ deletingArtifactId: null,
+ };
+ },
+ computed: {
+ scrollContainerStyle() {
+ /*
+ limit the height of the expanded artifacts container to a number of artifacts
+ if a job has more artifacts than ARTIFACTS_SHOWN_WITHOUT_SCROLLING, scroll to see the rest
+ add one pixel to row height to account for borders
+ */
+ return { maxHeight: `${ARTIFACTS_SHOWN_WITHOUT_SCROLLING * (ARTIFACT_ROW_HEIGHT + 1)}px` };
+ },
+ },
+ methods: {
+ isLastRow(index) {
+ return index === this.artifacts.nodes.length - 1;
+ },
+ destroyArtifact(id) {
+ this.deletingArtifactId = id;
+ this.$apollo
+ .mutate({
+ mutation: destroyArtifactMutation,
+ variables: { id },
+ update: (store) => {
+ removeArtifactFromStore(store, id, getJobArtifactsQuery, this.queryVariables);
+ },
+ })
+ .catch(() => {
+ createAlert({
+ message: I18N_DESTROY_ERROR,
+ });
+ this.$emit('refetch');
+ })
+ .finally(() => {
+ this.deletingArtifactId = null;
+ });
+ },
+ },
+ ARTIFACT_ROW_HEIGHT,
+};
+</script>
+<template>
+ <div :style="scrollContainerStyle">
+ <dynamic-scroller :items="artifacts.nodes" :min-item-size="$options.ARTIFACT_ROW_HEIGHT">
+ <template #default="{ item, index, active }">
+ <dynamic-scroller-item :item="item" :active="active" :class="{ active }">
+ <artifact-row
+ :artifact="item"
+ :is-last-row="isLastRow(index)"
+ :is-loading="item.id === deletingArtifactId"
+ @delete="destroyArtifact(item.id)"
+ />
+ </dynamic-scroller-item>
+ </template>
+ </dynamic-scroller>
+ </div>
+</template>
diff --git a/app/assets/javascripts/artifacts/components/job_artifacts_table.vue b/app/assets/javascripts/artifacts/components/job_artifacts_table.vue
new file mode 100644
index 00000000000..7b11e4f17f3
--- /dev/null
+++ b/app/assets/javascripts/artifacts/components/job_artifacts_table.vue
@@ -0,0 +1,314 @@
+<script>
+import {
+ GlLoadingIcon,
+ GlTable,
+ GlLink,
+ GlButtonGroup,
+ GlButton,
+ GlBadge,
+ GlIcon,
+ GlPagination,
+} from '@gitlab/ui';
+import { createAlert } from '~/flash';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import getJobArtifactsQuery from '../graphql/queries/get_job_artifacts.query.graphql';
+import { totalArtifactsSizeForJob, mapArchivesToJobNodes, mapBooleansToJobNodes } from '../utils';
+import {
+ STATUS_BADGE_VARIANTS,
+ I18N_DOWNLOAD,
+ I18N_BROWSE,
+ I18N_DELETE,
+ I18N_EXPIRED,
+ I18N_DESTROY_ERROR,
+ I18N_FETCH_ERROR,
+ I18N_ARTIFACTS,
+ I18N_JOB,
+ I18N_SIZE,
+ I18N_CREATED,
+ I18N_ARTIFACTS_COUNT,
+ INITIAL_CURRENT_PAGE,
+ INITIAL_PREVIOUS_PAGE_CURSOR,
+ INITIAL_NEXT_PAGE_CURSOR,
+ JOBS_PER_PAGE,
+ INITIAL_LAST_PAGE_SIZE,
+} from '../constants';
+import ArtifactsTableRowDetails from './artifacts_table_row_details.vue';
+
+const INITIAL_PAGINATION_STATE = {
+ currentPage: INITIAL_CURRENT_PAGE,
+ prevPageCursor: INITIAL_PREVIOUS_PAGE_CURSOR,
+ nextPageCursor: INITIAL_NEXT_PAGE_CURSOR,
+ firstPageSize: JOBS_PER_PAGE,
+ lastPageSize: INITIAL_LAST_PAGE_SIZE,
+};
+
+export default {
+ name: 'JobArtifactsTable',
+ components: {
+ GlLoadingIcon,
+ GlTable,
+ GlLink,
+ GlButtonGroup,
+ GlButton,
+ GlBadge,
+ GlIcon,
+ GlPagination,
+ CiIcon,
+ TimeAgo,
+ ArtifactsTableRowDetails,
+ },
+ inject: ['projectPath'],
+ apollo: {
+ jobArtifacts: {
+ query: getJobArtifactsQuery,
+ variables() {
+ return this.queryVariables;
+ },
+ update({ project: { jobs: { nodes = [], pageInfo = {}, count = 0 } = {} } }) {
+ return {
+ nodes: nodes.map(mapArchivesToJobNodes).map(mapBooleansToJobNodes),
+ count,
+ pageInfo,
+ };
+ },
+ error() {
+ createAlert({
+ message: I18N_FETCH_ERROR,
+ });
+ },
+ },
+ },
+ data() {
+ return {
+ jobArtifacts: {
+ nodes: [],
+ count: 0,
+ pageInfo: {},
+ },
+ pagination: INITIAL_PAGINATION_STATE,
+ };
+ },
+ computed: {
+ queryVariables() {
+ return {
+ projectPath: this.projectPath,
+ firstPageSize: this.pagination.firstPageSize,
+ lastPageSize: this.pagination.lastPageSize,
+ prevPageCursor: this.pagination.prevPageCursor,
+ nextPageCursor: this.pagination.nextPageCursor,
+ };
+ },
+ showPagination() {
+ return this.jobArtifacts.count > JOBS_PER_PAGE;
+ },
+ prevPage() {
+ return Number(this.jobArtifacts.pageInfo.hasPreviousPage);
+ },
+ nextPage() {
+ return Number(this.jobArtifacts.pageInfo.hasNextPage);
+ },
+ },
+ methods: {
+ refetchArtifacts() {
+ this.$apollo.queries.jobArtifacts.refetch();
+ },
+ artifactsSize(item) {
+ return totalArtifactsSizeForJob(item);
+ },
+ pipelineId(item) {
+ const id = getIdFromGraphQLId(item.pipeline.id);
+ return `#${id}`;
+ },
+ handlePageChange(page) {
+ const { startCursor, endCursor } = this.jobArtifacts.pageInfo;
+
+ if (page > this.pagination.currentPage) {
+ this.pagination = {
+ ...INITIAL_PAGINATION_STATE,
+ nextPageCursor: endCursor,
+ currentPage: page,
+ };
+ } else {
+ this.pagination = {
+ lastPageSize: JOBS_PER_PAGE,
+ firstPageSize: null,
+ prevPageCursor: startCursor,
+ currentPage: page,
+ };
+ }
+ },
+ handleRowToggle(toggleDetails, hasArtifacts) {
+ if (!hasArtifacts) return;
+ toggleDetails();
+ },
+ },
+ fields: [
+ {
+ key: 'artifacts',
+ label: I18N_ARTIFACTS,
+ thClass: 'gl-w-quarter',
+ },
+ {
+ key: 'job',
+ label: I18N_JOB,
+ thClass: 'gl-w-35p',
+ },
+ {
+ key: 'size',
+ label: I18N_SIZE,
+ thClass: 'gl-w-15p gl-text-right',
+ tdClass: 'gl-text-right',
+ },
+ {
+ key: 'created',
+ label: I18N_CREATED,
+ thClass: 'gl-w-eighth gl-text-center',
+ tdClass: 'gl-text-center',
+ },
+ {
+ key: 'actions',
+ label: '',
+ thClass: 'gl-w-eighth',
+ tdClass: 'gl-text-right',
+ },
+ ],
+ STATUS_BADGE_VARIANTS,
+ i18n: {
+ download: I18N_DOWNLOAD,
+ browse: I18N_BROWSE,
+ delete: I18N_DELETE,
+ expired: I18N_EXPIRED,
+ destroyArtifactError: I18N_DESTROY_ERROR,
+ fetchArtifactsError: I18N_FETCH_ERROR,
+ artifactsLabel: I18N_ARTIFACTS,
+ jobLabel: I18N_JOB,
+ sizeLabel: I18N_SIZE,
+ createdLabel: I18N_CREATED,
+ artifactsCount: I18N_ARTIFACTS_COUNT,
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-table
+ :items="jobArtifacts.nodes"
+ :fields="$options.fields"
+ :busy="$apollo.queries.jobArtifacts.loading"
+ stacked="sm"
+ details-td-class="gl-bg-gray-10! gl-p-0! gl-overflow-auto"
+ >
+ <template #table-busy>
+ <gl-loading-icon size="lg" />
+ </template>
+ <template
+ #cell(artifacts)="{ item: { artifacts, hasArtifacts }, toggleDetails, detailsShowing }"
+ >
+ <span
+ :class="{ 'gl-cursor-pointer': hasArtifacts }"
+ data-testid="job-artifacts-count"
+ @click="handleRowToggle(toggleDetails, hasArtifacts)"
+ >
+ <gl-icon
+ v-if="hasArtifacts"
+ :name="detailsShowing ? 'chevron-down' : 'chevron-right'"
+ class="gl-mr-2"
+ />
+ <strong>
+ {{ $options.i18n.artifactsCount(artifacts.nodes.length) }}
+ </strong>
+ </span>
+ </template>
+ <template #cell(job)="{ item }">
+ <span class="gl-display-inline-flex gl-align-items-center gl-w-full gl-mb-4">
+ <span data-testid="job-artifacts-job-status">
+ <ci-icon v-if="item.succeeded" :status="item.detailedStatus" class="gl-mr-3" />
+ <gl-badge
+ v-else
+ :icon="item.detailedStatus.icon"
+ :variant="$options.STATUS_BADGE_VARIANTS[item.detailedStatus.group]"
+ class="gl-mr-3"
+ >
+ {{ item.detailedStatus.label }}
+ </gl-badge>
+ </span>
+ <gl-link :href="item.webPath" class="gl-font-weight-bold">
+ {{ item.name }}
+ </gl-link>
+ </span>
+ <span class="gl-display-inline-flex">
+ <gl-icon name="pipeline" class="gl-mr-2" />
+ <gl-link
+ :href="item.pipeline.path"
+ class="gl-text-black-normal gl-text-decoration-underline gl-mr-4"
+ >
+ {{ pipelineId(item) }}
+ </gl-link>
+ <gl-icon name="branch" class="gl-mr-2" />
+ <gl-link
+ :href="item.refPath"
+ class="gl-text-black-normal gl-text-decoration-underline gl-mr-4"
+ >
+ {{ item.refName }}
+ </gl-link>
+ <gl-icon name="commit" class="gl-mr-2" />
+ <gl-link
+ :href="item.commitPath"
+ class="gl-text-black-normal gl-text-decoration-underline gl-mr-4"
+ >
+ {{ item.shortSha }}
+ </gl-link>
+ </span>
+ </template>
+ <template #cell(size)="{ item }">
+ <span data-testid="job-artifacts-size">{{ artifactsSize(item) }}</span>
+ </template>
+ <template #cell(created)="{ item }">
+ <time-ago data-testid="job-artifacts-created" :time="item.finishedAt" />
+ </template>
+ <template #cell(actions)="{ item }">
+ <gl-button-group>
+ <gl-button
+ icon="download"
+ :disabled="!item.archive.downloadPath"
+ :href="item.archive.downloadPath"
+ :title="$options.i18n.download"
+ :aria-label="$options.i18n.download"
+ data-testid="job-artifacts-download-button"
+ />
+ <gl-button
+ icon="folder-open"
+ :title="$options.i18n.browse"
+ :aria-label="$options.i18n.browse"
+ data-testid="job-artifacts-browse-button"
+ disabled
+ />
+ <gl-button
+ icon="remove"
+ :title="$options.i18n.delete"
+ :aria-label="$options.i18n.delete"
+ data-testid="job-artifacts-delete-button"
+ disabled
+ />
+ </gl-button-group>
+ </template>
+ <template #row-details="{ item: { artifacts } }">
+ <artifacts-table-row-details
+ :artifacts="artifacts"
+ :query-variables="queryVariables"
+ @refetch="refetchArtifacts"
+ />
+ </template>
+ </gl-table>
+ <gl-pagination
+ v-if="showPagination"
+ :value="pagination.currentPage"
+ :prev-page="prevPage"
+ :next-page="nextPage"
+ align="center"
+ class="gl-mt-3"
+ @input="handlePageChange"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/artifacts/constants.js b/app/assets/javascripts/artifacts/constants.js
new file mode 100644
index 00000000000..9ed0821ac2d
--- /dev/null
+++ b/app/assets/javascripts/artifacts/constants.js
@@ -0,0 +1,47 @@
+import { __, s__, n__ } from '~/locale';
+
+export const JOB_STATUS_GROUP_SUCCESS = 'success';
+
+export const STATUS_BADGE_VARIANTS = {
+ success: 'success',
+ passed: 'success',
+ error: 'danger',
+ failed: 'danger',
+ pending: 'warning',
+ 'waiting-for-resource': 'warning',
+ 'failed-with-warnings': 'warning',
+ 'success-with-warnings': 'warning',
+ running: 'info',
+ canceled: 'neutral',
+ disabled: 'neutral',
+ scheduled: 'neutral',
+ manual: 'neutral',
+ notification: 'muted',
+ preparing: 'muted',
+ created: 'muted',
+ skipped: 'muted',
+ notfound: 'muted',
+};
+
+export const I18N_DOWNLOAD = __('Download');
+export const I18N_BROWSE = s__('Artifacts|Browse');
+export const I18N_DELETE = __('Delete');
+export const I18N_EXPIRED = __('Expired');
+export const I18N_DESTROY_ERROR = s__('Artifacts|An error occurred while deleting the artifact');
+export const I18N_FETCH_ERROR = s__('Artifacts|An error occurred while retrieving job artifacts');
+export const I18N_ARTIFACTS = __('Artifacts');
+export const I18N_JOB = __('Job');
+export const I18N_SIZE = __('Size');
+export const I18N_CREATED = __('Created');
+export const I18N_ARTIFACTS_COUNT = (count) => n__('%d file', '%d files', count);
+
+export const INITIAL_CURRENT_PAGE = 1;
+export const INITIAL_PREVIOUS_PAGE_CURSOR = '';
+export const INITIAL_NEXT_PAGE_CURSOR = '';
+export const JOBS_PER_PAGE = 20;
+export const INITIAL_LAST_PAGE_SIZE = null;
+
+export const ARCHIVE_FILE_TYPE = 'ARCHIVE';
+
+export const ARTIFACT_ROW_HEIGHT = 56;
+export const ARTIFACTS_SHOWN_WITHOUT_SCROLLING = 4;
diff --git a/app/assets/javascripts/artifacts/graphql/cache_update.js b/app/assets/javascripts/artifacts/graphql/cache_update.js
new file mode 100644
index 00000000000..c620e03c80d
--- /dev/null
+++ b/app/assets/javascripts/artifacts/graphql/cache_update.js
@@ -0,0 +1,30 @@
+import produce from 'immer';
+
+export const hasErrors = ({ errors = [] }) => errors?.length;
+
+export function removeArtifactFromStore(store, deletedArtifactId, query, variables) {
+ if (!hasErrors(deletedArtifactId)) {
+ const sourceData = store.readQuery({
+ query,
+ variables,
+ });
+
+ const data = produce(sourceData, (draftData) => {
+ draftData.project.jobs.nodes = draftData.project.jobs.nodes.map((jobNode) => {
+ return {
+ ...jobNode,
+ artifacts: {
+ ...jobNode.artifacts,
+ nodes: jobNode.artifacts.nodes.filter(({ id }) => id !== deletedArtifactId),
+ },
+ };
+ });
+ });
+
+ store.writeQuery({
+ query,
+ variables,
+ data,
+ });
+ }
+}
diff --git a/app/assets/javascripts/artifacts/graphql/mutations/destroy_artifact.mutation.graphql b/app/assets/javascripts/artifacts/graphql/mutations/destroy_artifact.mutation.graphql
new file mode 100644
index 00000000000..529224b47e6
--- /dev/null
+++ b/app/assets/javascripts/artifacts/graphql/mutations/destroy_artifact.mutation.graphql
@@ -0,0 +1,7 @@
+mutation destroyArtifact($id: CiJobArtifactID!) {
+ artifactDestroy(input: { id: $id }) {
+ artifact {
+ id
+ }
+ }
+}
diff --git a/app/assets/javascripts/artifacts/graphql/queries/get_job_artifacts.query.graphql b/app/assets/javascripts/artifacts/graphql/queries/get_job_artifacts.query.graphql
new file mode 100644
index 00000000000..685196e28d5
--- /dev/null
+++ b/app/assets/javascripts/artifacts/graphql/queries/get_job_artifacts.query.graphql
@@ -0,0 +1,56 @@
+#import "~/graphql_shared/fragments/page_info.fragment.graphql"
+
+query getJobArtifacts(
+ $projectPath: ID!
+ $firstPageSize: Int
+ $lastPageSize: Int
+ $prevPageCursor: String = ""
+ $nextPageCursor: String = ""
+) {
+ project(fullPath: $projectPath) {
+ id
+ jobs(
+ statuses: [SUCCESS, FAILED]
+ first: $firstPageSize
+ last: $lastPageSize
+ after: $nextPageCursor
+ before: $prevPageCursor
+ ) {
+ count
+ nodes {
+ id
+ name
+ webPath
+ detailedStatus {
+ id
+ group
+ icon
+ label
+ }
+ pipeline {
+ id
+ iid
+ path
+ }
+ refName
+ refPath
+ shortSha
+ commitPath
+ finishedAt
+ artifacts {
+ nodes {
+ id
+ name
+ fileType
+ downloadPath
+ size
+ expireAt
+ }
+ }
+ }
+ pageInfo {
+ ...PageInfo
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/artifacts/index.js b/app/assets/javascripts/artifacts/index.js
new file mode 100644
index 00000000000..b5146e0f0e9
--- /dev/null
+++ b/app/assets/javascripts/artifacts/index.js
@@ -0,0 +1,29 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import JobArtifactsTable from './components/job_artifacts_table.vue';
+
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+});
+
+export const initArtifactsTable = () => {
+ const el = document.querySelector('#js-artifact-management');
+
+ if (!el) {
+ return false;
+ }
+
+ const { projectPath } = el.dataset;
+
+ return new Vue({
+ el,
+ apolloProvider,
+ provide: {
+ projectPath,
+ },
+ render: (createElement) => createElement(JobArtifactsTable),
+ });
+};
diff --git a/app/assets/javascripts/artifacts/utils.js b/app/assets/javascripts/artifacts/utils.js
new file mode 100644
index 00000000000..ebcf0af8d2a
--- /dev/null
+++ b/app/assets/javascripts/artifacts/utils.js
@@ -0,0 +1,26 @@
+import { numberToHumanSize } from '~/lib/utils/number_utils';
+import { ARCHIVE_FILE_TYPE, JOB_STATUS_GROUP_SUCCESS } from './constants';
+
+export const totalArtifactsSizeForJob = (job) =>
+ numberToHumanSize(
+ job.artifacts.nodes
+ .map((artifact) => artifact.size)
+ .reduce((total, artifact) => total + artifact, 0),
+ );
+
+export const mapArchivesToJobNodes = (jobNode) => {
+ return {
+ archive: {
+ ...jobNode.artifacts.nodes.find((artifact) => artifact.fileType === ARCHIVE_FILE_TYPE),
+ },
+ ...jobNode,
+ };
+};
+
+export const mapBooleansToJobNodes = (jobNode) => {
+ return {
+ succeeded: jobNode.detailedStatus.group === JOB_STATUS_GROUP_SUCCESS,
+ hasArtifacts: jobNode.artifacts.nodes.length > 0,
+ ...jobNode,
+ };
+};
diff --git a/app/assets/javascripts/diffs/components/diff_row_utils.js b/app/assets/javascripts/diffs/components/diff_row_utils.js
index 7732badde34..e0749b63021 100644
--- a/app/assets/javascripts/diffs/components/diff_row_utils.js
+++ b/app/assets/javascripts/diffs/components/diff_row_utils.js
@@ -57,21 +57,32 @@ export const classNameMapCell = ({ line, hll, isLoggedIn, isHover }) => {
export const addCommentTooltip = (line) => {
let tooltip;
- if (!line) return tooltip;
+ if (!line) {
+ return tooltip;
+ }
tooltip = __('Add a comment to this line or drag for multiple lines');
- const brokenSymlinks = line.commentsDisabled;
- if (brokenSymlinks) {
- if (brokenSymlinks.wasSymbolic || brokenSymlinks.isSymbolic) {
+ if (!line.problems) {
+ return tooltip;
+ }
+
+ const { brokenSymlink, brokenLineCode, fileOnlyMoved } = line.problems;
+
+ if (brokenSymlink) {
+ if (brokenSymlink.wasSymbolic || brokenSymlink.isSymbolic) {
tooltip = __(
'Commenting on symbolic links that replace or are replaced by files is currently not supported.',
);
- } else if (brokenSymlinks.wasReal || brokenSymlinks.isReal) {
+ } else if (brokenSymlink.wasReal || brokenSymlink.isReal) {
tooltip = __(
'Commenting on files that replace or are replaced by symbolic links is currently not supported.',
);
}
+ } else if (fileOnlyMoved) {
+ tooltip = __('Commenting on files that are only moved or renamed is currently not supported');
+ } else if (brokenLineCode) {
+ tooltip = __('Commenting on this line is currently not supported');
}
return tooltip;
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
index cf86ebea4a9..0519ca3d715 100644
--- a/app/assets/javascripts/diffs/store/utils.js
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -324,15 +324,24 @@ function cleanRichText(text) {
}
function prepareLine(line, file) {
+ const problems = {
+ brokenSymlink: file.brokenSymlink,
+ brokenLineCode: !line.line_code,
+ fileOnlyMoved: file.renamed_file && file.added_lines === 0 && file.removed_lines === 0,
+ };
+
if (!line.alreadyPrepared) {
Object.assign(line, {
- commentsDisabled: file.brokenSymlink,
+ commentsDisabled: Boolean(
+ problems.brokenSymlink || problems.fileOnlyMoved || problems.brokenLineCode,
+ ),
rich_text: cleanRichText(line.rich_text),
discussionsExpanded: true,
discussions: [],
hasForm: false,
text: undefined,
alreadyPrepared: true,
+ problems,
});
}
}
diff --git a/app/assets/javascripts/pages/projects/artifacts/index.js b/app/assets/javascripts/pages/projects/artifacts/index.js
new file mode 100644
index 00000000000..4aa9b225790
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/artifacts/index.js
@@ -0,0 +1,3 @@
+import { initArtifactsTable } from '~/artifacts/index';
+
+initArtifactsTable();
diff --git a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue
index ad061dd2e6b..5f1350690eb 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue
@@ -9,6 +9,8 @@ import eventHub from '~/sidebar/event_hub';
import Store from '~/sidebar/stores/sidebar_store';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import getMergeRequestReviewersQuery from '~/vue_shared/components/sidebar/queries/get_merge_request_reviewers.query.graphql';
+import mergeRequestReviewersUpdatedSubscription from '~/vue_shared/components/sidebar/queries/merge_request_reviewers.subscription.graphql';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import ReviewerTitle from './reviewer_title.vue';
import Reviewers from './reviewers.vue';
@@ -66,6 +68,36 @@ export default {
error() {
createAlert({ message: __('An error occurred while fetching reviewers.') });
},
+ subscribeToMore: {
+ document() {
+ return mergeRequestReviewersUpdatedSubscription;
+ },
+ variables() {
+ return {
+ issuableId: this.issuable?.id,
+ };
+ },
+ skip() {
+ return !this.issuable?.id || !this.isRealtimeEnabled;
+ },
+ updateQuery(
+ _,
+ {
+ subscriptionData: {
+ data: { mergeRequestReviewersUpdated },
+ },
+ },
+ ) {
+ if (mergeRequestReviewersUpdated) {
+ this.store.setReviewersFromRealtime(
+ mergeRequestReviewersUpdated.reviewers.nodes.map((r) => ({
+ ...r,
+ id: getIdFromGraphQLId(r.id),
+ })),
+ );
+ }
+ },
+ },
},
},
data() {
@@ -87,6 +119,9 @@ export default {
canUpdate() {
return this.issuable.userPermissions?.adminMergeRequest || false;
},
+ isRealtimeEnabled() {
+ return this.glFeatures.realtimeReviewers;
+ },
},
created() {
this.store = new Store();
diff --git a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers_inputs.vue b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers_inputs.vue
new file mode 100644
index 00000000000..a135dfdca72
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers_inputs.vue
@@ -0,0 +1,34 @@
+<script>
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { state } from './sidebar_reviewers.vue';
+
+export default {
+ data() {
+ return state;
+ },
+ computed: {
+ reviewers() {
+ return this.issuable?.reviewers?.nodes || [];
+ },
+ },
+ methods: {
+ getIdFromGraphQLId,
+ },
+};
+</script>
+
+<template>
+ <div>
+ <input
+ v-for="reviewer in reviewers"
+ :key="reviewer.id"
+ type="hidden"
+ name="merge_request[reviewer_ids][]"
+ :value="getIdFromGraphQLId(reviewer.id)"
+ :data-avatar-url="reviewer.avatarUrl"
+ :data-name="reviewer.name"
+ :data-username="reviewer.username"
+ :data-can-merge="reviewer.mergeRequestInteraction.canMerge"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 9b5bad710dd..1d9ddfe6bfd 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -33,6 +33,7 @@ import CopyEmailToClipboard from './components/copy_email_to_clipboard.vue';
import SidebarEscalationStatus from './components/incidents/sidebar_escalation_status.vue';
import IssuableLockForm from './components/lock/issuable_lock_form.vue';
import SidebarReviewers from './components/reviewers/sidebar_reviewers.vue';
+import SidebarReviewersInputs from './components/reviewers/sidebar_reviewers_inputs.vue';
import SidebarSeverity from './components/severity/sidebar_severity.vue';
import SidebarSubscriptionsWidget from './components/subscriptions/sidebar_subscriptions_widget.vue';
import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking.vue';
@@ -210,6 +211,18 @@ function mountReviewersComponent(mediator) {
}),
});
+ const reviewersInputEl = document.querySelector('.js-reviewers-inputs');
+
+ if (reviewersInputEl) {
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: reviewersInputEl,
+ render(createElement) {
+ return createElement(SidebarReviewersInputs);
+ },
+ });
+ }
+
const reviewerDropdown = document.querySelector('.js-sidebar-reviewer-dropdown');
if (reviewerDropdown) {
diff --git a/app/assets/javascripts/sidebar/stores/sidebar_store.js b/app/assets/javascripts/sidebar/stores/sidebar_store.js
index e2581a8f30e..baf906bb96c 100644
--- a/app/assets/javascripts/sidebar/stores/sidebar_store.js
+++ b/app/assets/javascripts/sidebar/stores/sidebar_store.js
@@ -138,6 +138,10 @@ export default class SidebarStore {
this.assignees = data;
}
+ setReviewersFromRealtime(data) {
+ this.reviewers = data;
+ }
+
setAutocompleteProjects(projects) {
this.autocompleteProjects = projects;
}
diff --git a/app/assets/javascripts/users_select/index.js b/app/assets/javascripts/users_select/index.js
index bd425bdc2a8..8fc5c354802 100644
--- a/app/assets/javascripts/users_select/index.js
+++ b/app/assets/javascripts/users_select/index.js
@@ -431,6 +431,10 @@ function UsersSelect(currentUser, els, options = {}) {
hidden() {
if ($dropdown.hasClass('js-multiselect')) {
if ($dropdown.hasClass(elsClassName)) {
+ if (window.gon?.features?.realtimeReviewers) {
+ $dropdown.data('deprecatedJQueryDropdown').clearMenu();
+ $dropdown.closest('.selectbox').children('input[type="hidden"]').remove();
+ }
emitSidebarEvent('sidebar.saveReviewers');
} else {
emitSidebarEvent('sidebar.saveAssignees');
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/merge_request_reviewers.subscription.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/merge_request_reviewers.subscription.graphql
new file mode 100644
index 00000000000..a1b16b378b3
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/merge_request_reviewers.subscription.graphql
@@ -0,0 +1,22 @@
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+#import "~/graphql_shared/fragments/user_availability.fragment.graphql"
+
+subscription mergeRequestReviewersUpdated($issuableId: IssuableID!) {
+ mergeRequestReviewersUpdated(issuableId: $issuableId) {
+ ... on MergeRequest {
+ id
+ reviewers {
+ nodes {
+ ...User
+ ...UserAvailability
+ mergeRequestInteraction {
+ canMerge
+ canUpdate
+ approved
+ reviewed
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 438b7b1afa6..955dcdc1c0f 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -968,7 +968,7 @@ $system-note-svg-size: 1rem;
height: 12px;
}
- &:hover,
+ &:hover:not([disabled]),
&.inverted {
&::before {
background-color: $white;
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index 0e4592259d8..89d362c88a4 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -66,11 +66,10 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
end
def load_projects(finder_params)
- @total_user_projects_count = ProjectsFinder.new(params: { non_public: true, without_deleted: true, not_aimed_for_deletion: true }, current_user: current_user).execute
- @total_starred_projects_count = ProjectsFinder.new(params: { starred: true, without_deleted: true, not_aimed_for_deletion: true }, current_user: current_user).execute
+ @total_user_projects_count = ProjectsFinder.new(params: { non_public: true, not_aimed_for_deletion: true }, current_user: current_user).execute
+ @total_starred_projects_count = ProjectsFinder.new(params: { starred: true, not_aimed_for_deletion: true }, current_user: current_user).execute
finder_params[:use_cte] = true if use_cte_for_finder?
- finder_params[:without_deleted] = true
projects = ProjectsFinder.new(params: finder_params, current_user: current_user).execute
@@ -93,7 +92,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
def load_events
projects = ProjectsFinder
- .new(params: params.merge(non_public: true, without_deleted: true), current_user: current_user)
+ .new(params: params.merge(non_public: true, not_aimed_for_deletion: true), current_user: current_user)
.execute
@events = EventCollection
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 9c139733248..36c050be76b 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -44,6 +44,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:paginated_mr_discussions, project)
push_frontend_feature_flag(:mr_review_submit_comment, project)
push_frontend_feature_flag(:mr_experience_survey, project)
+ push_frontend_feature_flag(:realtime_reviewers, project)
end
before_action do
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index c35aa8e4346..0f03333d793 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -274,8 +274,6 @@ class UsersController < ApplicationController
def finder_params
{
- # don't display projects pending deletion
- without_deleted: true,
# don't display projects marked for deletion
not_aimed_for_deletion: true
}
diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb
index 6bfe730ebc9..126687ae41f 100644
--- a/app/finders/projects_finder.rb
+++ b/app/finders/projects_finder.rb
@@ -27,7 +27,6 @@
# last_activity_after: datetime
# last_activity_before: datetime
# repository_storage: string
-# without_deleted: boolean
# not_aimed_for_deletion: boolean
#
class ProjectsFinder < UnionFinder
@@ -76,6 +75,7 @@ class ProjectsFinder < UnionFinder
# EE would override this to add more filters
def filter_projects(collection)
+ collection = collection.without_deleted
collection = by_ids(collection)
collection = by_personal(collection)
collection = by_starred(collection)
@@ -86,7 +86,6 @@ class ProjectsFinder < UnionFinder
collection = by_search(collection)
collection = by_archived(collection)
collection = by_custom_attributes(collection)
- collection = by_deleted_status(collection)
collection = by_not_aimed_for_deletion(collection)
collection = by_last_activity_after(collection)
collection = by_last_activity_before(collection)
@@ -212,10 +211,6 @@ class ProjectsFinder < UnionFinder
items.optionally_search(params[:search], include_namespace: params[:search_namespaces].present?)
end
- def by_deleted_status(items)
- params[:without_deleted].present? ? items.without_deleted : items
- end
-
def by_not_aimed_for_deletion(items)
params[:not_aimed_for_deletion].present? ? items.not_aimed_for_deletion : items
end
diff --git a/app/graphql/resolvers/concerns/project_search_arguments.rb b/app/graphql/resolvers/concerns/project_search_arguments.rb
index 7e03963f412..faf3b85fc14 100644
--- a/app/graphql/resolvers/concerns/project_search_arguments.rb
+++ b/app/graphql/resolvers/concerns/project_search_arguments.rb
@@ -25,7 +25,6 @@ module ProjectSearchArguments
def project_finder_params(params)
{
- without_deleted: true,
non_public: params[:membership],
search: params[:search],
search_namespaces: params[:search_namespaces],
diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb
index ec56f4a32af..7e1ebd1eba3 100644
--- a/app/models/concerns/protected_ref.rb
+++ b/app/models/concerns/protected_ref.rb
@@ -7,7 +7,6 @@ module ProtectedRef
belongs_to :project, touch: true
validates :name, presence: true
- validates :project, presence: true
delegate :matching, :matches?, :wildcard?, to: :ref_matcher
diff --git a/app/models/group.rb b/app/models/group.rb
index 38623d91705..708fe83a7e5 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -119,6 +119,8 @@ class Group < Namespace
has_many :group_callouts, class_name: 'Users::GroupCallout', foreign_key: :group_id
+ has_many :protected_branches, inverse_of: :group
+
has_one :group_feature, inverse_of: :group, class_name: 'Groups::FeatureSetting'
delegate :prevent_sharing_groups_outside_hierarchy, :new_user_signups_cap, :setup_for_company, :jobs_to_be_done, to: :namespace_settings
diff --git a/app/models/project_authorization.rb b/app/models/project_authorization.rb
index 8b43e5e5d63..3623b3be20d 100644
--- a/app/models/project_authorization.rb
+++ b/app/models/project_authorization.rb
@@ -31,6 +31,7 @@ class ProjectAuthorization < ApplicationRecord
def self.insert_all_in_batches(attributes, per_batch = BATCH_SIZE)
add_delay = add_delay_between_batches?(entire_size: attributes.size, batch_size: per_batch)
+ log_details(entire_size: attributes.size) if add_delay
attributes.each_slice(per_batch) do |attributes_batch|
insert_all(attributes_batch)
@@ -40,6 +41,7 @@ class ProjectAuthorization < ApplicationRecord
def self.delete_all_in_batches_for_project(project:, user_ids:, per_batch: BATCH_SIZE)
add_delay = add_delay_between_batches?(entire_size: user_ids.size, batch_size: per_batch)
+ log_details(entire_size: user_ids.size) if add_delay
user_ids.each_slice(per_batch) do |user_ids_batch|
project.project_authorizations.where(user_id: user_ids_batch).delete_all
@@ -49,6 +51,7 @@ class ProjectAuthorization < ApplicationRecord
def self.delete_all_in_batches_for_user(user:, project_ids:, per_batch: BATCH_SIZE)
add_delay = add_delay_between_batches?(entire_size: project_ids.size, batch_size: per_batch)
+ log_details(entire_size: project_ids.size) if add_delay
project_ids.each_slice(per_batch) do |project_ids_batch|
user.project_authorizations.where(project_id: project_ids_batch).delete_all
@@ -65,6 +68,13 @@ class ProjectAuthorization < ApplicationRecord
Feature.enabled?(:enable_minor_delay_during_project_authorizations_refresh)
end
+ private_class_method def self.log_details(entire_size:)
+ Gitlab::AppLogger.info(
+ entire_size: entire_size,
+ message: 'Project authorizations refresh performed with delay'
+ )
+ end
+
private_class_method def self.perform_delay
sleep(SLEEP_DELAY)
end
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index dfd5c315f6e..80967c1b072 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -4,6 +4,10 @@ class ProtectedBranch < ApplicationRecord
include ProtectedRef
include Gitlab::SQL::Pattern
+ belongs_to :group, foreign_key: :namespace_id, touch: true, inverse_of: :protected_branches
+
+ validate :validate_either_project_or_top_group
+
scope :requiring_code_owner_approval,
-> { where(code_owner_approval_required: true) }
@@ -99,6 +103,18 @@ class ProtectedBranch < ApplicationRecord
def default_branch?
name == project.default_branch
end
+
+ private
+
+ def validate_either_project_or_top_group
+ if !project && !group
+ errors.add(:base, _('must be associated with a Group or a Project'))
+ elsif project && group
+ errors.add(:base, _('cannot be associated with both a Group and a Project'))
+ elsif group && group.root_ancestor != group
+ errors.add(:base, _('cannot be associated with a subgroup'))
+ end
+ end
end
ProtectedBranch.prepend_mod_with('ProtectedBranch')
diff --git a/app/models/protected_tag.rb b/app/models/protected_tag.rb
index 5b2467daddc..e89cb3aabc7 100644
--- a/app/models/protected_tag.rb
+++ b/app/models/protected_tag.rb
@@ -4,6 +4,7 @@ class ProtectedTag < ApplicationRecord
include ProtectedRef
validates :name, uniqueness: { scope: :project_id }
+ validates :project, presence: true
protected_ref_access_levels :create
diff --git a/app/views/admin/application_settings/_account_and_limit.html.haml b/app/views/admin/application_settings/_account_and_limit.html.haml
index c091a2180c5..0f7b10f822d 100644
--- a/app/views/admin/application_settings/_account_and_limit.html.haml
+++ b/app/views/admin/application_settings/_account_and_limit.html.haml
@@ -69,4 +69,6 @@
= render 'admin/application_settings/invitation_flow_enforcement', form: f
= render 'admin/application_settings/user_restrictions', form: f
= render_if_exists 'admin/application_settings/availability_on_namespace_setting', form: f
+ -# This is added for Jihu edition which should not be deleted without notifying Jihu
+ = render_if_exists 'admin/application_settings/password_expiration_setting', form: f
= f.submit _('Save changes'), pajamas_button: true, data: { qa_selector: 'save_changes_button' }
diff --git a/app/views/projects/artifacts/_artifact.html.haml b/app/views/projects/artifacts/_artifact.html.haml
deleted file mode 100644
index 9e548582396..00000000000
--- a/app/views/projects/artifacts/_artifact.html.haml
+++ /dev/null
@@ -1,61 +0,0 @@
-.gl-responsive-table-row.px-md-3
- .table-section.section-25.section-wrap.commit
- .table-mobile-header{ role: 'rowheader' }= _('Job')
- .table-mobile-content
- .branch-commit.cgray
- - if can?(current_user, :read_build, @project)
- = link_to project_job_path(@project, artifact.job) do
- %span.build-link ##{artifact.job_id}
- - else
- %span.build-link ##{artifact.job_id}
-
- - if artifact.job.ref
- .icon-container.gl-display-inline-block{ "aria-label" => artifact.job.tag? ? _('Tag') : _('Branch') }
- = artifact.job.tag? ? sprite_icon('tag', css_class: 'sprite') : sprite_icon('branch', css_class: 'sprite')
- = link_to artifact.job.ref, project_ref_path(@project, artifact.job.ref), class: 'ref-name'
- - else
- .light= _('none')
- .icon-container.commit-icon{ "aria-label" => _('Commit') }
- = sprite_icon('commit')
-
- - if artifact.job.sha
- = link_to artifact.job.short_sha, project_commit_path(@project, artifact.job.sha), class: 'commit-sha mr-0'
-
- .table-section.section-15.section-wrap
- .table-mobile-header{ role: 'rowheader' }= _('Name')
- .table-mobile-content
- = artifact.job.name
-
- .table-section.section-20
- .table-mobile-header{ role: 'rowheader' }= _('Creation date')
- .table-mobile-content
- %p.finished-at
- = sprite_icon("calendar")
- %span= time_ago_with_tooltip(artifact.created_at)
-
- .table-section.section-20
- .table-mobile-header{ role: 'rowheader' }= _('Expiration date')
- .table-mobile-content
- - if artifact.expire_at
- %p.finished-at
- = sprite_icon("calendar")
- %span= time_ago_with_tooltip(artifact.expire_at)
-
- .table-section.section-10
- .table-mobile-header{ role: 'rowheader' }= _('Size')
- .table-mobile-content
- = number_to_human_size(artifact.size, precision: 2)
-
- .table-section.table-button-footer.section-10
- .table-action-buttons
- .btn-group
- - if can?(current_user, :read_build, @project)
- = link_to download_project_job_artifacts_path(@project, artifact.job), rel: 'nofollow', download: '', title: _('Download artifacts'), data: { placement: 'top', container: 'body' }, ref: 'tooltip', aria: { label: _('Download artifacts') }, class: 'gl-button btn btn-default btn-icon has-tooltip' do
- = sprite_icon('download', css_class: 'gl-icon')
-
- = link_to browse_project_job_artifacts_path(@project, artifact.job), rel: 'nofollow', title: _('Browse artifacts'), data: { placement: 'top', container: 'body' }, ref: 'tooltip', aria: { label: _('Browse artifacts') }, class: 'gl-button btn btn-default btn-icon has-tooltip' do
- = sprite_icon('folder-open', css_class: 'gl-icon')
-
- - if can?(current_user, :destroy_artifacts, @project)
- = link_to project_artifact_path(@project, artifact), data: { placement: 'top', container: 'body', confirm: _('Are you sure you want to delete these artifacts?'), confirm_btn_variant: "danger" }, method: :delete, title: _('Delete artifacts'), ref: 'tooltip', aria: { label: _('Delete artifacts') }, class: 'gl-button btn btn-danger btn-icon has-tooltip' do
- = sprite_icon('remove', css_class: 'gl-icon')
diff --git a/app/views/projects/artifacts/_table.html.haml b/app/views/projects/artifacts/_table.html.haml
deleted file mode 100644
index 1963449d704..00000000000
--- a/app/views/projects/artifacts/_table.html.haml
+++ /dev/null
@@ -1,16 +0,0 @@
-- if artifacts.blank?
- .nothing-here-block= _('No jobs to show')
-- else
- .table-holder
- .ci-table
- .gl-responsive-table-row.table-row-header.px-md-3{ role: 'row' }
- .table-section.section-25{ role: 'rowheader' }= _('Job')
- .table-section.section-15{ role: 'rowheader' }= _('Name')
- .table-section.section-20{ role: 'rowheader' }= _('Creation date')
- .table-section.section-20{ role: 'rowheader' }= _('Expiration date')
- .table-section.section-10{ role: 'rowheader' }= _('Size')
- .table-section.section-10{ role: 'rowheader' }
-
- = render partial: 'artifact', collection: artifacts, as: :artifact
-
- = paginate artifacts, theme: "gitlab", total_pages: @total_pages
diff --git a/app/views/projects/artifacts/index.html.haml b/app/views/projects/artifacts/index.html.haml
index 1ab3e8e67d8..9cbc149177c 100644
--- a/app/views/projects/artifacts/index.html.haml
+++ b/app/views/projects/artifacts/index.html.haml
@@ -1,10 +1,9 @@
-- @no_container = true
- page_title _('Artifacts')
%div{ class: container_class }
- .top-area.py-3
- .align-self-center
- = _('Total artifacts size: %{total_size}') % { total_size: number_to_human_size(@total_size, precicion: 2) }
-
- .content-list.builds-content-list
- = render "table", artifacts: @artifacts, project: @project
+ %h1.page-title.gl-font-size-h-display.gl-mb-0
+ = s_('Artifacts|Artifacts')
+ .gl-mb-6
+ %strong= s_('Artifacts|Total artifacts size')
+ = number_to_human_size(@total_size, precicion: 2)
+ #js-artifact-management{ data: { "project-path" => @project.full_path } }
diff --git a/app/views/shared/issuable/_sidebar_reviewers.html.haml b/app/views/shared/issuable/_sidebar_reviewers.html.haml
index 771db8af6a8..c764cfff2fd 100644
--- a/app/views/shared/issuable/_sidebar_reviewers.html.haml
+++ b/app/views/shared/issuable/_sidebar_reviewers.html.haml
@@ -6,11 +6,7 @@
= gl_loading_icon(inline: true)
.selectbox.hide-collapsed
- - if reviewers.none?
- = hidden_field_tag "#{issuable_type}[reviewer_ids][]", 0, id: nil
- - else
- - reviewers.each do |reviewer|
- = hidden_field_tag "#{issuable_type}[reviewer_ids][]", reviewer.id, id: nil, data: reviewer_sidebar_data(reviewer, merge_request: @merge_request)
+ .js-reviewers-inputs
- options = { toggle_class: 'js-reviewer-search js-author-search',
title: _('Request review from'),
@@ -32,8 +28,7 @@
- dropdown_options = reviewers_dropdown_options(issuable_type)
- title = dropdown_options[:title]
- options[:toggle_class] += ' js-multiselect js-save-user-data'
- - data = { field_name: "#{issuable_type}[reviewer_ids][]" }
- - data[:multi_select] = true
+ - data = { multi_select: true }
- data['dropdown-title'] = title
- data['dropdown-header'] = dropdown_options[:data][:'dropdown-header']
- data[:suggested_reviewers_header] = dropdown_options[:data][:suggested_reviewers_header]