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>2020-02-28 21:09:07 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-02-28 21:09:07 +0300
commit1c8fa70f9d0818e2a82089c8643a6e455bca47fd (patch)
treef339f97de0425270bdd909e2f4d378927b6e0a18 /app
parent736d36d8597d0d1ec1b47644e6d091c3f4a78f45 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js4
-rw-r--r--app/assets/javascripts/registry/explorer/pages/details.vue260
-rw-r--r--app/assets/javascripts/registry/explorer/pages/list.vue141
-rw-r--r--app/assets/javascripts/registry/explorer/stores/actions.js13
-rw-r--r--app/assets/javascripts/registry/explorer/stores/getters.js6
-rw-r--r--app/assets/javascripts/registry/explorer/stores/index.js2
-rw-r--r--app/assets/stylesheets/utilities.scss2
-rw-r--r--app/controllers/groups/deploy_tokens_controller.rb12
-rw-r--r--app/controllers/groups/settings/ci_cd_controller.rb29
-rw-r--r--app/controllers/projects/deploy_tokens_controller.rb2
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb23
-rw-r--r--app/controllers/projects/settings/repository_controller.rb20
-rw-r--r--app/helpers/ci_variables_helper.rb16
-rw-r--r--app/models/group.rb3
-rw-r--r--app/models/group_deploy_token.rb2
-rw-r--r--app/models/project.rb8
-rw-r--r--app/models/snippet_repository.rb65
-rw-r--r--app/services/concerns/deploy_token_methods.rb11
-rw-r--r--app/services/deploy_tokens/create_service.rb11
-rw-r--r--app/services/groups/deploy_tokens/create_service.rb13
-rw-r--r--app/services/projects/deploy_tokens/create_service.rb13
-rw-r--r--app/services/snippets/create_service.rb51
-rw-r--r--app/views/ci/variables/_index.html.haml2
-rw-r--r--app/views/groups/settings/ci_cd/show.html.haml3
-rw-r--r--app/views/import/shared/_new_project_form.html.haml2
-rw-r--r--app/views/projects/deploy_tokens/_index.html.haml17
-rw-r--r--app/views/projects/registry/repositories/index.html.haml1
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml3
-rw-r--r--app/views/projects/settings/repository/show.html.haml1
-rw-r--r--app/views/shared/deploy_tokens/_form.html.haml (renamed from app/views/projects/deploy_tokens/_form.html.haml)4
-rw-r--r--app/views/shared/deploy_tokens/_index.html.haml18
-rw-r--r--app/views/shared/deploy_tokens/_new_deploy_token.html.haml (renamed from app/views/projects/deploy_tokens/_new_deploy_token.html.haml)0
-rw-r--r--app/views/shared/deploy_tokens/_revoke_modal.html.haml (renamed from app/views/projects/deploy_tokens/_revoke_modal.html.haml)8
-rw-r--r--app/views/shared/deploy_tokens/_table.html.haml (renamed from app/views/projects/deploy_tokens/_table.html.haml)8
35 files changed, 510 insertions, 267 deletions
diff --git a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
index 479c82265f2..1ef18b356f2 100644
--- a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
+++ b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
@@ -1,10 +1,13 @@
import initSettingsPanels from '~/settings_panels';
import AjaxVariableList from '~/ci_variable_list/ajax_variable_list';
import initVariableList from '~/ci_variable_list';
+import DueDateSelectors from '~/due_date_select';
document.addEventListener('DOMContentLoaded', () => {
// Initialize expandable settings panels
initSettingsPanels();
+ // eslint-disable-next-line no-new
+ new DueDateSelectors();
if (gon.features.newVariablesUi) {
initVariableList();
diff --git a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
index e08d0407245..c83e2bdbf38 100644
--- a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
@@ -3,6 +3,7 @@ import SecretValues from '~/behaviors/secret_values';
import AjaxVariableList from '~/ci_variable_list/ajax_variable_list';
import registrySettingsApp from '~/registry/settings/registry_settings_bundle';
import initVariableList from '~/ci_variable_list';
+import DueDateSelectors from '~/due_date_select';
document.addEventListener('DOMContentLoaded', () => {
// Initialize expandable settings panels
@@ -39,5 +40,8 @@ document.addEventListener('DOMContentLoaded', () => {
autoDevOpsExtraSettings.classList.toggle('hidden', !target.checked);
});
+ // eslint-disable-next-line no-new
+ new DueDateSelectors();
+
registrySettingsApp();
});
diff --git a/app/assets/javascripts/registry/explorer/pages/details.vue b/app/assets/javascripts/registry/explorer/pages/details.vue
index bfb9b0f4688..0f4ed1550ce 100644
--- a/app/assets/javascripts/registry/explorer/pages/details.vue
+++ b/app/assets/javascripts/registry/explorer/pages/details.vue
@@ -1,5 +1,5 @@
<script>
-import { mapState, mapActions } from 'vuex';
+import { mapState, mapActions, mapGetters } from 'vuex';
import {
GlTable,
GlFormCheckbox,
@@ -8,10 +8,10 @@ import {
GlTooltipDirective,
GlPagination,
GlModal,
- GlLoadingIcon,
GlSprintf,
GlEmptyState,
GlResizeObserverDirective,
+ GlSkeletonLoader,
} from '@gitlab/ui';
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
import { n__, s__ } from '~/locale';
@@ -42,7 +42,7 @@ export default {
ClipboardButton,
GlPagination,
GlModal,
- GlLoadingIcon,
+ GlSkeletonLoader,
GlSprintf,
GlEmptyState,
},
@@ -51,6 +51,11 @@ export default {
GlResizeObserver: GlResizeObserverDirective,
},
mixins: [timeagoMixin, Tracking.mixin()],
+ loader: {
+ repeat: 10,
+ width: 1000,
+ height: 40,
+ },
data() {
return {
selectedItems: [],
@@ -61,15 +66,16 @@ export default {
};
},
computed: {
- ...mapState(['tags', 'tagsPagination', 'isLoading', 'config']),
+ ...mapGetters(['tags']),
+ ...mapState(['tagsPagination', 'isLoading', 'config']),
imageName() {
const { name } = decodeAndParse(this.$route.params.id);
return name;
},
fields() {
return [
- { key: LIST_KEY_CHECKBOX, label: '' },
- { key: LIST_KEY_TAG, label: LIST_LABEL_TAG },
+ { key: LIST_KEY_CHECKBOX, label: '', class: 'gl-w-16' },
+ { key: LIST_KEY_TAG, label: LIST_LABEL_TAG, class: 'w-25' },
{ key: LIST_KEY_IMAGE_ID, label: LIST_LABEL_IMAGE_ID },
{ key: LIST_KEY_SIZE, label: LIST_LABEL_SIZE },
{ key: LIST_KEY_LAST_UPDATED, label: LIST_LABEL_LAST_UPDATED },
@@ -209,122 +215,142 @@ export default {
</gl-sprintf>
</h4>
</div>
- <gl-loading-icon v-if="isLoading" />
- <template v-else-if="tags.length > 0">
- <gl-table :items="tags" :fields="fields" :stacked="!isDesktop">
- <template v-if="isDesktop" #head(checkbox)>
- <gl-form-checkbox
- ref="mainCheckbox"
- :checked="selectAllChecked"
- @change="onSelectAllChange"
- />
- </template>
- <template #head(actions)>
- <gl-button
- ref="bulkDeleteButton"
- v-gl-tooltip
- :disabled="!selectedItems || selectedItems.length === 0"
- class="float-right"
- variant="danger"
- :title="s__('ContainerRegistry|Remove selected tags')"
- :aria-label="s__('ContainerRegistry|Remove selected tags')"
- @click="deleteMultipleItems()"
- >
- <gl-icon name="remove" />
- </gl-button>
- </template>
- <template #cell(checkbox)="{index}">
- <gl-form-checkbox
- ref="rowCheckbox"
- class="js-row-checkbox"
- :checked="selectedItems.includes(index)"
- @change="updateSelectedItems(index)"
- />
- </template>
- <template #cell(name)="{item}">
- <span ref="rowName">
- {{ item.name }}
- </span>
- <clipboard-button
- v-if="item.location"
- ref="rowClipboardButton"
- :title="item.location"
- :text="item.location"
- css-class="btn-default btn-transparent btn-clipboard"
- />
- </template>
- <template #cell(short_revision)="{value}">
- <span ref="rowShortRevision">
- {{ value }}
- </span>
- </template>
- <template #cell(total_size)="{item}">
- <span ref="rowSize">
- {{ formatSize(item.total_size) }}
- <template v-if="item.total_size && item.layers">
- &middot;
- </template>
- {{ layers(item.layers) }}
- </span>
- </template>
- <template #cell(created_at)="{value}">
- <span ref="rowTime">
- {{ timeFormatted(value) }}
- </span>
- </template>
- <template #cell(actions)="{index, item}">
- <gl-button
- ref="singleDeleteButton"
- :title="s__('ContainerRegistry|Remove tag')"
- :aria-label="s__('ContainerRegistry|Remove tag')"
- :disabled="!item.destroy_path"
- variant="danger"
- :class="['js-delete-registry float-right btn-inverted btn-border-color btn-icon']"
- @click="deleteSingleItem(index)"
+ <gl-table :items="tags" :fields="fields" :stacked="!isDesktop" show-empty>
+ <template v-if="isDesktop" #head(checkbox)>
+ <gl-form-checkbox
+ ref="mainCheckbox"
+ :checked="selectAllChecked"
+ @change="onSelectAllChange"
+ />
+ </template>
+ <template #head(actions)>
+ <gl-button
+ ref="bulkDeleteButton"
+ v-gl-tooltip
+ :disabled="!selectedItems || selectedItems.length === 0"
+ class="float-right"
+ variant="danger"
+ :title="s__('ContainerRegistry|Remove selected tags')"
+ :aria-label="s__('ContainerRegistry|Remove selected tags')"
+ @click="deleteMultipleItems()"
+ >
+ <gl-icon name="remove" />
+ </gl-button>
+ </template>
+
+ <template #cell(checkbox)="{index}">
+ <gl-form-checkbox
+ ref="rowCheckbox"
+ class="js-row-checkbox"
+ :checked="selectedItems.includes(index)"
+ @change="updateSelectedItems(index)"
+ />
+ </template>
+ <template #cell(name)="{item}">
+ <span ref="rowName">
+ {{ item.name }}
+ </span>
+ <clipboard-button
+ v-if="item.location"
+ ref="rowClipboardButton"
+ :title="item.location"
+ :text="item.location"
+ css-class="btn-default btn-transparent btn-clipboard"
+ />
+ </template>
+ <template #cell(short_revision)="{value}">
+ <span ref="rowShortRevision">
+ {{ value }}
+ </span>
+ </template>
+ <template #cell(total_size)="{item}">
+ <span ref="rowSize">
+ {{ formatSize(item.total_size) }}
+ <template v-if="item.total_size && item.layers">
+ &middot;
+ </template>
+ {{ layers(item.layers) }}
+ </span>
+ </template>
+ <template #cell(created_at)="{value}">
+ <span ref="rowTime">
+ {{ timeFormatted(value) }}
+ </span>
+ </template>
+ <template #cell(actions)="{index, item}">
+ <gl-button
+ ref="singleDeleteButton"
+ :title="s__('ContainerRegistry|Remove tag')"
+ :aria-label="s__('ContainerRegistry|Remove tag')"
+ :disabled="!item.destroy_path"
+ variant="danger"
+ class="js-delete-registry float-right btn-inverted btn-border-color btn-icon"
+ @click="deleteSingleItem(index)"
+ >
+ <gl-icon name="remove" />
+ </gl-button>
+ </template>
+
+ <template #empty>
+ <template v-if="isLoading">
+ <gl-skeleton-loader
+ v-for="index in $options.loader.repeat"
+ :key="index"
+ :width="$options.loader.width"
+ :height="$options.loader.height"
+ preserve-aspect-ratio="xMinYMax meet"
>
- <gl-icon name="remove" />
- </gl-button>
+ <rect width="15" x="0" y="12.5" height="15" rx="4" />
+ <rect width="250" x="25" y="10" height="20" rx="4" />
+ <circle cx="290" cy="20" r="10" />
+ <rect width="100" x="315" y="10" height="20" rx="4" />
+ <rect width="100" x="500" y="10" height="20" rx="4" />
+ <rect width="100" x="630" y="10" height="20" rx="4" />
+ <rect x="960" y="0" width="40" height="40" rx="4" />
+ </gl-skeleton-loader>
</template>
- </gl-table>
- <gl-pagination
- ref="pagination"
- v-model="currentPage"
- :per-page="tagsPagination.perPage"
- :total-items="tagsPagination.total"
- align="center"
- class="w-100"
- />
- <gl-modal
- ref="deleteModal"
- modal-id="delete-tag-modal"
- ok-variant="danger"
- @ok="onDeletionConfirmed"
- @cancel="track('cancel_delete')"
- >
- <template #modal-title>{{ modalAction }}</template>
- <template #modal-ok>{{ modalAction }}</template>
- <p v-if="modalDescription">
- <gl-sprintf :message="modalDescription.message">
- <template #item>
- <b>{{ modalDescription.item }}</b>
- </template>
- </gl-sprintf>
- </p>
- </gl-modal>
- </template>
- <gl-empty-state
- v-else
- :title="s__('ContainerRegistry|This image has no active tags')"
- :svg-path="config.noContainersImage"
- :description="
- s__(
- `ContainerRegistry|The last tag related to this image was recently removed.
+ <gl-empty-state
+ v-else
+ :title="s__('ContainerRegistry|This image has no active tags')"
+ :svg-path="config.noContainersImage"
+ :description="
+ s__(
+ `ContainerRegistry|The last tag related to this image was recently removed.
This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process.
If you have any questions, contact your administrator.`,
- )
- "
- class="mx-auto my-0"
+ )
+ "
+ class="mx-auto my-0"
+ />
+ </template>
+ </gl-table>
+
+ <gl-pagination
+ ref="pagination"
+ v-model="currentPage"
+ :per-page="tagsPagination.perPage"
+ :total-items="tagsPagination.total"
+ align="center"
+ class="w-100"
/>
+
+ <gl-modal
+ ref="deleteModal"
+ modal-id="delete-tag-modal"
+ ok-variant="danger"
+ @ok="onDeletionConfirmed"
+ @cancel="track('cancel_delete')"
+ >
+ <template #modal-title>{{ modalAction }}</template>
+ <template #modal-ok>{{ modalAction }}</template>
+ <p v-if="modalDescription">
+ <gl-sprintf :message="modalDescription.message">
+ <template #item>
+ <b>{{ modalDescription.item }}</b>
+ </template>
+ </gl-sprintf>
+ </p>
+ </gl-modal>
</div>
</template>
diff --git a/app/assets/javascripts/registry/explorer/pages/list.vue b/app/assets/javascripts/registry/explorer/pages/list.vue
index 5f8f4d8df1e..4e9f0a83501 100644
--- a/app/assets/javascripts/registry/explorer/pages/list.vue
+++ b/app/assets/javascripts/registry/explorer/pages/list.vue
@@ -1,7 +1,6 @@
<script>
import { mapState, mapActions } from 'vuex';
import {
- GlLoadingIcon,
GlEmptyState,
GlPagination,
GlTooltipDirective,
@@ -10,6 +9,7 @@ import {
GlModal,
GlSprintf,
GlLink,
+ GlSkeletonLoader,
} from '@gitlab/ui';
import Tracking from '~/tracking';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
@@ -20,7 +20,6 @@ export default {
name: 'RegistryListApp',
components: {
GlEmptyState,
- GlLoadingIcon,
GlPagination,
ProjectEmptyState,
GroupEmptyState,
@@ -30,11 +29,17 @@ export default {
GlModal,
GlSprintf,
GlLink,
+ GlSkeletonLoader,
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [Tracking.mixin()],
+ loader: {
+ repeat: 10,
+ width: 1000,
+ height: 40,
+ },
data() {
return {
itemToDelete: {},
@@ -104,74 +109,81 @@ export default {
</gl-empty-state>
<template v-else>
- <gl-loading-icon v-if="isLoading" size="md" class="prepend-top-16" />
-
- <template v-else>
- <div v-if="images.length" ref="imagesList">
- <h4>{{ s__('ContainerRegistry|Container Registry') }}</h4>
- <p>
- <gl-sprintf
- :message="
- s__(`ContainerRegistry|With the Docker Container Registry integrated into GitLab, every
+ <div>
+ <h4>{{ s__('ContainerRegistry|Container Registry') }}</h4>
+ <p>
+ <gl-sprintf
+ :message="
+ s__(`ContainerRegistry|With the Docker Container Registry integrated into GitLab, every
project can have its own space to store its Docker images.
%{docLinkStart}More Information%{docLinkEnd}`)
- "
- >
- <template #docLink="{content}">
- <gl-link :href="config.helpPagePath" target="_blank">
- {{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
- </p>
+ "
+ >
+ <template #docLink="{content}">
+ <gl-link :href="config.helpPagePath" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ </div>
- <div class="d-flex flex-column">
+ <div v-if="isLoading" class="mt-2">
+ <gl-skeleton-loader
+ v-for="index in $options.loader.repeat"
+ :key="index"
+ :width="$options.loader.width"
+ :height="$options.loader.height"
+ preserve-aspect-ratio="xMinYMax meet"
+ >
+ <rect width="500" x="10" y="10" height="20" rx="4" />
+ <circle cx="525" cy="20" r="10" />
+ <rect x="960" y="0" width="40" height="40" rx="4" />
+ </gl-skeleton-loader>
+ </div>
+ <template v-else>
+ <div v-if="images.length" ref="imagesList" class="d-flex flex-column">
+ <div
+ v-for="(listItem, index) in images"
+ :key="index"
+ ref="rowItem"
+ :class="{ 'border-top': index === 0 }"
+ class="d-flex justify-content-between align-items-center py-2 border-bottom"
+ >
+ <div>
+ <router-link
+ ref="detailsLink"
+ :to="{ name: 'details', params: { id: encodeListItem(listItem) } }"
+ >
+ {{ listItem.path }}
+ </router-link>
+ <clipboard-button
+ v-if="listItem.location"
+ ref="clipboardButton"
+ :text="listItem.location"
+ :title="listItem.location"
+ css-class="btn-default btn-transparent btn-clipboard"
+ />
+ </div>
<div
- v-for="(listItem, index) in images"
- :key="index"
- ref="rowItem"
- :class="[
- 'd-flex justify-content-between align-items-center py-2 border-bottom',
- { 'border-top': index === 0 },
- ]"
+ v-gl-tooltip="{ disabled: listItem.destroy_path }"
+ class="d-none d-sm-block"
+ :title="
+ s__('ContainerRegistry|Missing or insufficient permission, delete button disabled')
+ "
>
- <div>
- <router-link
- ref="detailsLink"
- :to="{ name: 'details', params: { id: encodeListItem(listItem) } }"
- >
- {{ listItem.path }}
- </router-link>
- <clipboard-button
- v-if="listItem.location"
- ref="clipboardButton"
- :text="listItem.location"
- :title="listItem.location"
- css-class="btn-default btn-transparent btn-clipboard"
- />
- </div>
- <div
- v-gl-tooltip="{ disabled: listItem.destroy_path }"
- class="d-none d-sm-block"
- :title="
- s__(
- 'ContainerRegistry|Missing or insufficient permission, delete button disabled',
- )
- "
+ <gl-button
+ ref="deleteImageButton"
+ v-gl-tooltip
+ :disabled="!listItem.destroy_path"
+ :title="s__('ContainerRegistry|Remove repository')"
+ :aria-label="s__('ContainerRegistry|Remove repository')"
+ class="btn-inverted"
+ variant="danger"
+ @click="deleteImage(listItem)"
>
- <gl-button
- ref="deleteImageButton"
- v-gl-tooltip
- :disabled="!listItem.destroy_path"
- :title="s__('ContainerRegistry|Remove repository')"
- :aria-label="s__('ContainerRegistry|Remove repository')"
- class="btn-inverted"
- variant="danger"
- @click="deleteImage(listItem)"
- >
- <gl-icon name="remove" />
- </gl-button>
- </div>
+ <gl-icon name="remove" />
+ </gl-button>
</div>
</div>
<gl-pagination
@@ -182,6 +194,7 @@ export default {
class="w-100 mt-2"
/>
</div>
+
<template v-else>
<project-empty-state v-if="!config.isGroupPage" />
<group-empty-state v-else />
diff --git a/app/assets/javascripts/registry/explorer/stores/actions.js b/app/assets/javascripts/registry/explorer/stores/actions.js
index 86d00d4fca9..7d8201949f4 100644
--- a/app/assets/javascripts/registry/explorer/stores/actions.js
+++ b/app/assets/javascripts/registry/explorer/stores/actions.js
@@ -68,31 +68,28 @@ export const requestDeleteTag = ({ commit, dispatch, state }, { tag, params }) =
.delete(tag.destroy_path)
.then(() => {
createFlash(DELETE_TAG_SUCCESS_MESSAGE, 'success');
- dispatch('requestTagsList', { pagination: state.tagsPagination, params });
+ return dispatch('requestTagsList', { pagination: state.tagsPagination, params });
})
.catch(() => {
createFlash(DELETE_TAG_ERROR_MESSAGE);
- })
- .finally(() => {
commit(types.SET_MAIN_LOADING, false);
});
};
export const requestDeleteTags = ({ commit, dispatch, state }, { ids, params }) => {
commit(types.SET_MAIN_LOADING, true);
- const { id } = decodeAndParse(params);
- const url = `/${state.config.projectPath}/registry/repository/${id}/tags/bulk_destroy`;
+ const { tags_path } = decodeAndParse(params);
+
+ const url = tags_path.replace('?format=json', '/bulk_destroy');
return axios
.delete(url, { params: { ids } })
.then(() => {
createFlash(DELETE_TAGS_SUCCESS_MESSAGE, 'success');
- dispatch('requestTagsList', { pagination: state.tagsPagination, params });
+ return dispatch('requestTagsList', { pagination: state.tagsPagination, params });
})
.catch(() => {
createFlash(DELETE_TAGS_ERROR_MESSAGE);
- })
- .finally(() => {
commit(types.SET_MAIN_LOADING, false);
});
};
diff --git a/app/assets/javascripts/registry/explorer/stores/getters.js b/app/assets/javascripts/registry/explorer/stores/getters.js
new file mode 100644
index 00000000000..5619b73d495
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/stores/getters.js
@@ -0,0 +1,6 @@
+// eslint-disable-next-line import/prefer-default-export
+export const tags = state => {
+ // to show the loader inside the table we need to pass an empty array to gl-table whenever the table is loading
+ // this is to take in account isLoading = true and state.tags =[1,2,3] during pagination and delete
+ return state.isLoading ? [] : state.tags;
+};
diff --git a/app/assets/javascripts/registry/explorer/stores/index.js b/app/assets/javascripts/registry/explorer/stores/index.js
index 91a35aac149..b3ff2e6e002 100644
--- a/app/assets/javascripts/registry/explorer/stores/index.js
+++ b/app/assets/javascripts/registry/explorer/stores/index.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
+import * as getters from './getters';
import mutations from './mutations';
import state from './state';
@@ -9,6 +10,7 @@ Vue.use(Vuex);
export const createStore = () =>
new Vuex.Store({
state,
+ getters,
actions,
mutations,
});
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index dabbcf0eac1..2a92b271ed0 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -54,7 +54,7 @@
.mh-50vh { max-height: 50vh; }
.font-size-inherit { font-size: inherit; }
-
+.gl-w-16 { width: px-to-rem($grid-size * 2); }
.gl-w-64 { width: px-to-rem($grid-size * 8); }
.gl-h-32 { height: px-to-rem($grid-size * 4); }
.gl-h-64 { height: px-to-rem($grid-size * 8); }
diff --git a/app/controllers/groups/deploy_tokens_controller.rb b/app/controllers/groups/deploy_tokens_controller.rb
new file mode 100644
index 00000000000..a765922fc54
--- /dev/null
+++ b/app/controllers/groups/deploy_tokens_controller.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class Groups::DeployTokensController < Groups::ApplicationController
+ before_action :authorize_admin_group!
+
+ def revoke
+ @token = @group.deploy_tokens.find(params[:id])
+ @token.revoke!
+
+ redirect_to group_settings_ci_cd_path(@group, anchor: 'js-deploy-tokens')
+ end
+end
diff --git a/app/controllers/groups/settings/ci_cd_controller.rb b/app/controllers/groups/settings/ci_cd_controller.rb
index 3c1f020702f..ffa3f2c3364 100644
--- a/app/controllers/groups/settings/ci_cd_controller.rb
+++ b/app/controllers/groups/settings/ci_cd_controller.rb
@@ -7,11 +7,11 @@ module Groups
before_action :authorize_admin_group!
before_action :authorize_update_max_artifacts_size!, only: [:update]
before_action do
- push_frontend_feature_flag(:new_variables_ui, @group, default_enabled: true)
+ push_frontend_feature_flag(:new_variables_ui, @group)
end
+ before_action :define_variables, only: [:show, :create_deploy_token]
def show
- define_ci_variables
end
def update
@@ -41,8 +41,23 @@ module Groups
redirect_to group_settings_ci_cd_path
end
+ def create_deploy_token
+ @new_deploy_token = Groups::DeployTokens::CreateService.new(@group, current_user, deploy_token_params).execute
+
+ if @new_deploy_token.persisted?
+ flash.now[:notice] = s_('DeployTokens|Your new group deploy token has been created.')
+ end
+
+ render 'show'
+ end
+
private
+ def define_variables
+ define_ci_variables
+ define_deploy_token_variables
+ end
+
def define_ci_variables
@variable = Ci::GroupVariable.new(group: group)
.present(current_user: current_user)
@@ -50,6 +65,12 @@ module Groups
.map { |variable| variable.present(current_user: current_user) }
end
+ def define_deploy_token_variables
+ @deploy_tokens = @group.deploy_tokens.active
+
+ @new_deploy_token = DeployToken.new
+ end
+
def authorize_admin_group!
return render_404 unless can?(current_user, :admin_group, group)
end
@@ -73,6 +94,10 @@ module Groups
def update_group_params
params.require(:group).permit(:max_artifacts_size)
end
+
+ def deploy_token_params
+ params.require(:deploy_token).permit(:name, :expires_at, :read_repository, :read_registry, :username)
+ end
end
end
end
diff --git a/app/controllers/projects/deploy_tokens_controller.rb b/app/controllers/projects/deploy_tokens_controller.rb
index 830b1f4fe4a..4a70424ec01 100644
--- a/app/controllers/projects/deploy_tokens_controller.rb
+++ b/app/controllers/projects/deploy_tokens_controller.rb
@@ -7,6 +7,6 @@ class Projects::DeployTokensController < Projects::ApplicationController
@token = @project.deploy_tokens.find(params[:id])
@token.revoke!
- redirect_to project_settings_repository_path(project, anchor: 'js-deploy-tokens')
+ redirect_to project_settings_ci_cd_path(project, anchor: 'js-deploy-tokens')
end
end
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index 5097b6b8c8c..ed42fb55223 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -6,7 +6,7 @@ module Projects
before_action :authorize_admin_pipeline!
before_action :define_variables
before_action do
- push_frontend_feature_flag(:new_variables_ui, @project, default_enabled: true)
+ push_frontend_feature_flag(:new_variables_ui, @project)
end
def show
@@ -46,6 +46,16 @@ module Projects
redirect_to namespace_project_settings_ci_cd_path
end
+ def create_deploy_token
+ @new_deploy_token = Projects::DeployTokens::CreateService.new(@project, current_user, deploy_token_params).execute
+
+ if @new_deploy_token.persisted?
+ flash.now[:notice] = s_('DeployTokens|Your new project deploy token has been created.')
+ end
+
+ render 'show'
+ end
+
private
def update_params
@@ -64,6 +74,10 @@ module Projects
end
end
+ def deploy_token_params
+ params.require(:deploy_token).permit(:name, :expires_at, :read_repository, :read_registry, :username)
+ end
+
def run_autodevops_pipeline(service)
return unless service.run_auto_devops_pipeline?
@@ -83,6 +97,7 @@ module Projects
def define_variables
define_runners_variables
define_ci_variables
+ define_deploy_token_variables
define_triggers_variables
define_badges_variables
define_auto_devops_variables
@@ -132,6 +147,12 @@ module Projects
def define_auto_devops_variables
@auto_devops = @project.auto_devops || ProjectAutoDevops.new
end
+
+ def define_deploy_token_variables
+ @deploy_tokens = @project.deploy_tokens.active
+
+ @new_deploy_token = DeployToken.new
+ end
end
end
end
diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb
index 63f5d5073a7..28db3024dc4 100644
--- a/app/controllers/projects/settings/repository_controller.rb
+++ b/app/controllers/projects/settings/repository_controller.rb
@@ -10,16 +10,6 @@ module Projects
render_show
end
- def create_deploy_token
- @new_deploy_token = DeployTokens::CreateService.new(@project, current_user, deploy_token_params).execute
-
- if @new_deploy_token.persisted?
- flash.now[:notice] = s_('DeployTokens|Your new project deploy token has been created.')
- end
-
- render_show
- end
-
def cleanup
cleanup_params = params.require(:project).permit(:bfg_object_map)
result = Projects::UpdateService.new(project, current_user, cleanup_params).execute
@@ -38,9 +28,7 @@ module Projects
def render_show
@deploy_keys = DeployKeysPresenter.new(@project, current_user: current_user)
- @deploy_tokens = @project.deploy_tokens.active
- define_deploy_token
define_protected_refs
remote_mirror
@@ -93,14 +81,6 @@ module Projects
gon.push(protectable_branches_for_dropdown)
gon.push(access_levels_options)
end
-
- def define_deploy_token
- @new_deploy_token ||= DeployToken.new
- end
-
- def deploy_token_params
- params.require(:deploy_token).permit(:name, :expires_at, :read_repository, :read_registry, :username)
- end
end
end
end
diff --git a/app/helpers/ci_variables_helper.rb b/app/helpers/ci_variables_helper.rb
index fc51f00d052..3f4c04070b5 100644
--- a/app/helpers/ci_variables_helper.rb
+++ b/app/helpers/ci_variables_helper.rb
@@ -5,6 +5,22 @@ module CiVariablesHelper
Gitlab::CurrentSettings.current_application_settings.protected_ci_variables
end
+ def create_deploy_token_path(entity, opts = {})
+ if entity.is_a?(Group)
+ create_deploy_token_group_settings_ci_cd_path(entity, opts)
+ else
+ create_deploy_token_project_settings_repository_path(entity, opts)
+ end
+ end
+
+ def revoke_deploy_token_path(entity, token)
+ if entity.is_a?(Group)
+ revoke_group_deploy_token_path(entity, token)
+ else
+ revoke_project_deploy_token_path(entity, token)
+ end
+ end
+
def ci_variable_protected?(variable, only_key_value)
if variable && !only_key_value
variable.protected
diff --git a/app/models/group.rb b/app/models/group.rb
index ea5d46e23f4..a5337f19b38 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -59,6 +59,9 @@ class Group < Namespace
has_many :import_failures, inverse_of: :group
+ has_many :group_deploy_tokens
+ has_many :deploy_tokens, through: :group_deploy_tokens
+
accepts_nested_attributes_for :variables, allow_destroy: true
validate :visibility_level_allowed_by_projects
diff --git a/app/models/group_deploy_token.rb b/app/models/group_deploy_token.rb
index 221a7d768ae..d4ad29ddabb 100644
--- a/app/models/group_deploy_token.rb
+++ b/app/models/group_deploy_token.rb
@@ -9,7 +9,7 @@ class GroupDeployToken < ApplicationRecord
validates :deploy_token_id, uniqueness: { scope: [:group_id] }
def has_access_to?(requested_project)
- return false unless Feature.enabled?(:allow_group_deploy_token, default: true)
+ return false unless Feature.enabled?(:allow_group_deploy_token, default_enabled: true)
requested_project_group = requested_project&.group
return false unless requested_project_group
diff --git a/app/models/project.rb b/app/models/project.rb
index 41c56fe6931..5ec43de21fe 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -2343,6 +2343,14 @@ class Project < ApplicationRecord
Gitlab::CurrentSettings.self_monitoring_project_id == id
end
+ def deploy_token_create_url(opts = {})
+ Gitlab::Routing.url_helpers.create_deploy_token_project_settings_ci_cd_path(self, opts)
+ end
+
+ def deploy_token_revoke_url_for(token)
+ Gitlab::Routing.url_helpers.revoke_project_deploy_token_path(self, token)
+ end
+
private
def closest_namespace_setting(name)
diff --git a/app/models/snippet_repository.rb b/app/models/snippet_repository.rb
index ba2a061a5f4..10580c51098 100644
--- a/app/models/snippet_repository.rb
+++ b/app/models/snippet_repository.rb
@@ -3,11 +3,76 @@
class SnippetRepository < ApplicationRecord
include Shardable
+ DEFAULT_EMPTY_FILE_NAME = 'snippetfile'
+ EMPTY_FILE_PATTERN = /^#{DEFAULT_EMPTY_FILE_NAME}(\d)\.txt$/.freeze
+
+ CommitError = Class.new(StandardError)
+
belongs_to :snippet, inverse_of: :snippet_repository
+ delegate :repository, to: :snippet
+
class << self
def find_snippet(disk_path)
find_by(disk_path: disk_path)&.snippet
end
end
+
+ def multi_files_action(user, files = [], **options)
+ return if files.nil? || files.empty?
+
+ lease_key = "multi_files_action:#{snippet_id}"
+
+ lease = Gitlab::ExclusiveLease.new(lease_key, timeout: 120)
+ raise CommitError, 'Snippet is already being updated' unless uuid = lease.try_obtain
+
+ options[:actions] = transform_file_entries(files)
+
+ capture_git_error { repository.multi_action(user, **options) }
+ ensure
+ Gitlab::ExclusiveLease.cancel(lease_key, uuid)
+ end
+
+ private
+
+ def capture_git_error(&block)
+ yield block
+ rescue Gitlab::Git::Index::IndexError,
+ Gitlab::Git::CommitError,
+ Gitlab::Git::PreReceiveError,
+ Gitlab::Git::CommandError => e
+ raise CommitError, e.message
+ end
+
+ def transform_file_entries(files)
+ last_index = get_last_empty_file_index
+
+ files.each do |file_entry|
+ file_entry[:action] = infer_action(file_entry) unless file_entry[:action]
+
+ if file_entry[:file_path].blank?
+ file_entry[:file_path] = build_empty_file_name(last_index)
+ last_index += 1
+ end
+ end
+ end
+
+ def infer_action(file_entry)
+ return :create if file_entry[:previous_path].blank?
+
+ file_entry[:previous_path] != file_entry[:file_path] ? :move : :update
+ end
+
+ def get_last_empty_file_index
+ last_file = repository.ls_files(nil)
+ .map! { |file| file.match(EMPTY_FILE_PATTERN) }
+ .compact
+ .max_by { |element| element[1] }
+
+ last_file ? (last_file[1].to_i + 1) : 1
+ end
+
+ def build_empty_file_name(index)
+ "#{DEFAULT_EMPTY_FILE_NAME}#{index}.txt"
+ end
end
diff --git a/app/services/concerns/deploy_token_methods.rb b/app/services/concerns/deploy_token_methods.rb
new file mode 100644
index 00000000000..c0208b16623
--- /dev/null
+++ b/app/services/concerns/deploy_token_methods.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module DeployTokenMethods
+ def create_deploy_token_for(entity, params)
+ params[:deploy_token_type] = DeployToken.deploy_token_types["#{entity.class.name.downcase}_type".to_sym]
+
+ entity.deploy_tokens.create(params) do |deploy_token|
+ deploy_token.username = params[:username].presence
+ end
+ end
+end
diff --git a/app/services/deploy_tokens/create_service.rb b/app/services/deploy_tokens/create_service.rb
deleted file mode 100644
index 327a1dbf408..00000000000
--- a/app/services/deploy_tokens/create_service.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-
-module DeployTokens
- class CreateService < BaseService
- def execute
- @project.deploy_tokens.create(params) do |deploy_token|
- deploy_token.username = params[:username].presence
- end
- end
- end
-end
diff --git a/app/services/groups/deploy_tokens/create_service.rb b/app/services/groups/deploy_tokens/create_service.rb
new file mode 100644
index 00000000000..8c42b56ebb0
--- /dev/null
+++ b/app/services/groups/deploy_tokens/create_service.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Groups
+ module DeployTokens
+ class CreateService < BaseService
+ include DeployTokenMethods
+
+ def execute
+ create_deploy_token_for(@group, params)
+ end
+ end
+ end
+end
diff --git a/app/services/projects/deploy_tokens/create_service.rb b/app/services/projects/deploy_tokens/create_service.rb
new file mode 100644
index 00000000000..51cb68dfb10
--- /dev/null
+++ b/app/services/projects/deploy_tokens/create_service.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Projects
+ module DeployTokens
+ class CreateService < BaseService
+ include DeployTokenMethods
+
+ def execute
+ create_deploy_token_for(@project, params)
+ end
+ end
+ end
+end
diff --git a/app/services/snippets/create_service.rb b/app/services/snippets/create_service.rb
index 7ded185a6f9..cc645c514b7 100644
--- a/app/services/snippets/create_service.rb
+++ b/app/services/snippets/create_service.rb
@@ -4,6 +4,8 @@ module Snippets
class CreateService < Snippets::BaseService
include SpamCheckMethods
+ CreateRepositoryError = Class.new(StandardError)
+
def execute
filter_spam_check_params
@@ -23,13 +25,7 @@ module Snippets
spam_check(snippet, current_user)
- snippet_saved = snippet.with_transaction_returning_status do
- (snippet.save && snippet.store_mentions!).tap do |saved|
- create_repository_for(snippet, current_user) if saved
- end
- end
-
- if snippet_saved
+ if save_and_commit(snippet)
UserAgentDetailService.new(snippet, @request).create
Gitlab::UsageDataCounters::SnippetCounter.count(:create)
@@ -41,8 +37,45 @@ module Snippets
private
- def create_repository_for(snippet, user)
- snippet.create_repository if Feature.enabled?(:version_snippets, user)
+ def save_and_commit(snippet)
+ snippet.with_transaction_returning_status do
+ (snippet.save && snippet.store_mentions!).tap do |saved|
+ break false unless saved
+
+ if Feature.enabled?(:version_snippets, current_user)
+ create_repository_for(snippet)
+ create_commit(snippet)
+ end
+ end
+ rescue => e # Rescuing all because we can receive Creation exceptions, GRPC exceptions, Git exceptions, ...
+ snippet.errors.add(:base, e.message)
+
+ # If the commit action failed we need to remove the repository if exists
+ if snippet.repository_exists?
+ Repositories::DestroyService.new(snippet.repository).execute
+ end
+
+ false
+ end
+ end
+
+ def create_repository_for(snippet)
+ snippet.create_repository
+
+ raise CreateRepositoryError, 'Repository could not be created' unless snippet.repository_exists?
+ end
+
+ def create_commit(snippet)
+ commit_attrs = {
+ branch_name: 'master',
+ message: 'Initial commit'
+ }
+
+ snippet.snippet_repository.multi_files_action(current_user, snippet_files, commit_attrs)
+ end
+
+ def snippet_files
+ [{ file_path: params[:file_name], content: params[:content] }]
end
end
end
diff --git a/app/views/ci/variables/_index.html.haml b/app/views/ci/variables/_index.html.haml
index aadb2c62d83..f11c730eba6 100644
--- a/app/views/ci/variables/_index.html.haml
+++ b/app/views/ci/variables/_index.html.haml
@@ -5,7 +5,7 @@
- link_start = '<a href="%{url}">'.html_safe % { url: help_page_path('ci/variables/README', anchor: 'protected-variables') }
= s_('Environment variables are configured by your administrator to be %{link_start}protected%{link_end} by default').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
-- if Feature.enabled?(:new_variables_ui, @project || @group, default_enabled: true)
+- if Feature.enabled?(:new_variables_ui, @project || @group)
- is_group = !@group.nil?
#js-ci-project-variables{ data: { endpoint: save_endpoint, project_id: @project&.id || '', group: is_group.to_s, maskable_regex: ci_variable_maskable_regex} }
diff --git a/app/views/groups/settings/ci_cd/show.html.haml b/app/views/groups/settings/ci_cd/show.html.haml
index 8c9b859e127..4aef30622cd 100644
--- a/app/views/groups/settings/ci_cd/show.html.haml
+++ b/app/views/groups/settings/ci_cd/show.html.haml
@@ -3,6 +3,7 @@
- expanded = expanded_by_default?
- general_expanded = @group.errors.empty? ? expanded : true
+- deploy_token_description = s_('DeployTokens|Group deploy tokens allow read-only access to the repositories and registry images within the group.')
-# Given we only have one field in this form which is also admin-only,
-# we don't want to show an empty section to non-admin users,
@@ -24,6 +25,8 @@
.settings-content
= render 'ci/variables/index', save_endpoint: group_variables_path
+= render "shared/deploy_tokens/index", group_or_project: @group, description: deploy_token_description
+
%section.settings#runners-settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
diff --git a/app/views/import/shared/_new_project_form.html.haml b/app/views/import/shared/_new_project_form.html.haml
index 35059229a55..a558b21b461 100644
--- a/app/views/import/shared/_new_project_form.html.haml
+++ b/app/views/import/shared/_new_project_form.html.haml
@@ -15,7 +15,7 @@
.input-group-prepend.static-namespace.has-tooltip{ title: user_url(current_user.username) + '/' }
.input-group-text.border-0
#{user_url(current_user.username)}/
- = hidden_field_tag :namespace_id, value: current_user.namespace_id
+ = hidden_field_tag :namespace_id, current_user.namespace_id
.form-group.col-12.col-sm-6.project-path
= label_tag :path, _('Project slug'), class: 'label-bold'
= text_field_tag :path, @path, placeholder: "my-awesome-project", class: "js-path-name form-control", tabindex: 2, required: true
diff --git a/app/views/projects/deploy_tokens/_index.html.haml b/app/views/projects/deploy_tokens/_index.html.haml
deleted file mode 100644
index 4619522cfaf..00000000000
--- a/app/views/projects/deploy_tokens/_index.html.haml
+++ /dev/null
@@ -1,17 +0,0 @@
-- expanded = expand_deploy_tokens_section?(@new_deploy_token)
-
-%section.qa-deploy-tokens-settings.settings.no-animate#js-deploy-tokens{ class: ('expanded' if expanded) }
- .settings-header
- %h4= s_('DeployTokens|Deploy Tokens')
- %button.btn.js-settings-toggle.qa-expand-deploy-keys{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
- %p
- = s_('DeployTokens|Deploy tokens allow read-only access to your repository and registry images.')
- .settings-content
- - if @new_deploy_token.persisted?
- = render 'projects/deploy_tokens/new_deploy_token', deploy_token: @new_deploy_token
- %h5.prepend-top-0
- = s_('DeployTokens|Add a deploy token')
- = render 'projects/deploy_tokens/form', project: @project, token: @new_deploy_token, presenter: @deploy_tokens
- %hr
- = render 'projects/deploy_tokens/table', project: @project, active_tokens: @deploy_tokens
diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml
index 247cf021cc7..62b744b5095 100644
--- a/app/views/projects/registry/repositories/index.html.haml
+++ b/app/views/projects/registry/repositories/index.html.haml
@@ -6,7 +6,6 @@
.col-12
- if Feature.enabled?(:vue_container_registry_explorer, @project.group)
#js-container-registry{ data: { endpoint: project_container_registry_index_path(@project),
- project_path: @project.full_path,
"help_page_path" => help_page_path('user/packages/container_registry/index'),
"two_factor_auth_help_link" => help_page_path('user/profile/account/two_factor_authentication'),
"personal_access_tokens_help_link" => help_page_path('user/profile/personal_access_tokens'),
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index 1358077f2b2..9e47e380266 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -4,6 +4,7 @@
- expanded = expanded_by_default?
- general_expanded = @project.errors.empty? ? expanded : true
+- deploy_token_description = s_('DeployTokens|Deploy tokens allow read-only access to your repository and registry images.')
%section.settings#js-general-pipeline-settings.no-animate{ class: ('expanded' if general_expanded) }
.settings-header
@@ -51,6 +52,8 @@
.settings-content
= render 'ci/variables/index', save_endpoint: project_variables_path(@project)
+= render "shared/deploy_tokens/index", group_or_project: @project, description: deploy_token_description
+
%section.settings.no-animate#js-pipeline-triggers{ class: ('expanded' if expanded) }
.settings-header
%h4
diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml
index ff30cc4f6db..3d1eb85da0d 100644
--- a/app/views/projects/settings/repository/show.html.haml
+++ b/app/views/projects/settings/repository/show.html.haml
@@ -13,7 +13,6 @@
= render "projects/settings/repository/protected_branches"
= render @deploy_keys
-= render "projects/deploy_tokens/index"
= render "projects/cleanup/show"
= render_if_exists 'shared/promotions/promote_repository_features'
diff --git a/app/views/projects/deploy_tokens/_form.html.haml b/app/views/shared/deploy_tokens/_form.html.haml
index f846dbd3763..99e259ba944 100644
--- a/app/views/projects/deploy_tokens/_form.html.haml
+++ b/app/views/shared/deploy_tokens/_form.html.haml
@@ -1,7 +1,7 @@
%p.profile-settings-content
= s_("DeployTokens|Pick a name for the application, and we'll give you a unique deploy token.")
-= form_for token, url: create_deploy_token_namespace_project_settings_repository_path(project.namespace, project, anchor: 'js-deploy-tokens'), method: :post do |f|
+= form_for token, url: create_deploy_token_path(group_or_project, anchor: 'js-deploy-tokens'), method: :post do |f|
= form_errors(token)
.form-group
@@ -24,7 +24,7 @@
= label_tag ("deploy_token_read_repository"), 'read_repository', class: 'label-bold form-check-label'
.text-secondary= s_('DeployTokens|Allows read-only access to the repository')
- - if container_registry_enabled?(project)
+ - if container_registry_enabled?(group_or_project)
%fieldset.form-group.form-check
= f.check_box :read_registry, class: 'form-check-input qa-deploy-token-read-registry'
= label_tag ("deploy_token_read_registry"), 'read_registry', class: 'label-bold form-check-label'
diff --git a/app/views/shared/deploy_tokens/_index.html.haml b/app/views/shared/deploy_tokens/_index.html.haml
new file mode 100644
index 00000000000..b0c9c72dfaa
--- /dev/null
+++ b/app/views/shared/deploy_tokens/_index.html.haml
@@ -0,0 +1,18 @@
+- expanded = expand_deploy_tokens_section?(@new_deploy_token)
+
+%section.qa-deploy-tokens-settings.settings.no-animate#js-deploy-tokens{ class: ('expanded' if expanded), data: { qa_selector: 'deploy_tokens_settings' } }
+ .settings-header
+ %h4= s_('DeployTokens|Deploy Tokens')
+ %button.btn.js-settings-toggle.qa-expand-deploy-keys{ type: 'button' }
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = description
+ .settings-content
+ - if @new_deploy_token.persisted?
+ = render 'shared/deploy_tokens/new_deploy_token', deploy_token: @new_deploy_token
+ %h5.prepend-top-0
+ = s_('DeployTokens|Add a deploy token')
+ = render 'shared/deploy_tokens/form', group_or_project: group_or_project, token: @new_deploy_token, presenter: @deploy_tokens
+ %hr
+ = render 'shared/deploy_tokens/table', group_or_project: group_or_project, active_tokens: @deploy_tokens
+
diff --git a/app/views/projects/deploy_tokens/_new_deploy_token.html.haml b/app/views/shared/deploy_tokens/_new_deploy_token.html.haml
index f295fa82192..f295fa82192 100644
--- a/app/views/projects/deploy_tokens/_new_deploy_token.html.haml
+++ b/app/views/shared/deploy_tokens/_new_deploy_token.html.haml
diff --git a/app/views/projects/deploy_tokens/_revoke_modal.html.haml b/app/views/shared/deploy_tokens/_revoke_modal.html.haml
index 35eacae2c2e..5a3759ef755 100644
--- a/app/views/projects/deploy_tokens/_revoke_modal.html.haml
+++ b/app/views/shared/deploy_tokens/_revoke_modal.html.haml
@@ -3,15 +3,13 @@
.modal-content
.modal-header
%h4.modal-title
- = s_('DeployTokens|Revoke')
- %b #{token.name}?
+ = s_('DeployTokens|Revoke %{b_start}%{name}%{b_end}?').html_safe % { b_start: '<b>'.html_safe, name: token.name, b_end: '</b>'.html_safe }
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
%span{ "aria-hidden": true } &times;
.modal-body
%p
- = s_('DeployTokens|You are about to revoke')
- %b #{token.name}.
+ = s_('DeployTokens|You are about to revoke %{b_start}%{name}%{b_end}.').html_safe % { b_start: '<b>'.html_safe, name: token.name, b_end: '</b>'.html_safe }
= s_('DeployTokens|This action cannot be undone.')
.modal-footer
%a{ href: '#', data: { dismiss: 'modal' }, class: 'btn btn-default' }= _('Cancel')
- = link_to s_('DeployTokens|Revoke %{name}') % { name: token.name }, revoke_project_deploy_token_path(project, token), method: :put, class: 'btn btn-danger'
+ = link_to s_('DeployTokens|Revoke %{name}') % { name: token.name }, revoke_deploy_token_path(group_or_project, token), method: :put, class: 'btn btn-danger'
diff --git a/app/views/projects/deploy_tokens/_table.html.haml b/app/views/shared/deploy_tokens/_table.html.haml
index 91466a6736b..d4e20805a2a 100644
--- a/app/views/projects/deploy_tokens/_table.html.haml
+++ b/app/views/shared/deploy_tokens/_table.html.haml
@@ -22,10 +22,10 @@
%span{ class: ('text-warning' if token.expires_soon?) }
In #{distance_of_time_in_words_to_now(token.expires_at)}
- else
- %span.token-never-expires-label Never
- %td= token.scopes.present? ? token.scopes.join(", ") : "<no scopes selected>"
+ %span.token-never-expires-label= _('Never')
+ %td= token.scopes.present? ? token.scopes.join(", ") : _('<no scopes selected>')
%td= link_to s_('DeployTokens|Revoke'), "#", class: "btn btn-danger float-right", data: { toggle: "modal", target: "#revoke-modal-#{token.id}"}
- = render 'projects/deploy_tokens/revoke_modal', token: token, project: project
+ = render 'shared/deploy_tokens/revoke_modal', token: token, group_or_project: group_or_project
- else
.settings-message.text-center
- = s_('DeployTokens|This project has no active Deploy Tokens.')
+ = s_('DeployTokens|This %{entity_type} has no active Deploy Tokens.') % { entity_type: group_or_project.class.name.downcase }