Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
dfd048606f
commit
7d0b125e88
|
|
@ -7,7 +7,7 @@ export const FORM_SELECTOR = '#js-new-access-token-form';
|
|||
export const INITIAL_PAGE = 1;
|
||||
export const PAGE_SIZE = 100;
|
||||
|
||||
export const FIELDS = [
|
||||
const BASE_FIELDS = [
|
||||
{
|
||||
key: 'name',
|
||||
label: __('Token name'),
|
||||
|
|
@ -31,19 +31,35 @@ export const FIELDS = [
|
|||
label: __('Last Used'),
|
||||
sortable: true,
|
||||
},
|
||||
];
|
||||
|
||||
const ROLE_FIELD = {
|
||||
key: 'role',
|
||||
label: __('Role'),
|
||||
sortable: true,
|
||||
};
|
||||
|
||||
export const FIELDS = [
|
||||
...BASE_FIELDS,
|
||||
{
|
||||
key: 'expiresAt',
|
||||
label: __('Expires'),
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
key: 'role',
|
||||
label: __('Role'),
|
||||
sortable: true,
|
||||
},
|
||||
ROLE_FIELD,
|
||||
{
|
||||
key: 'action',
|
||||
label: __('Action'),
|
||||
tdClass: 'gl-py-3!',
|
||||
},
|
||||
];
|
||||
|
||||
export const INACTIVE_TOKENS_TABLE_FIELDS = [
|
||||
...BASE_FIELDS,
|
||||
{
|
||||
key: 'expiresAt',
|
||||
label: __('Expired'),
|
||||
sortable: true,
|
||||
},
|
||||
ROLE_FIELD,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,125 @@
|
|||
<script>
|
||||
import { GlIcon, GlLink, GlPagination, GlTable, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||
import { __ } from '~/locale';
|
||||
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
import UserDate from '~/vue_shared/components/user_date.vue';
|
||||
import { INACTIVE_TOKENS_TABLE_FIELDS, INITIAL_PAGE, PAGE_SIZE } from './constants';
|
||||
|
||||
export default {
|
||||
PAGE_SIZE,
|
||||
name: 'InactiveAccessTokenTableApp',
|
||||
components: {
|
||||
GlIcon,
|
||||
GlLink,
|
||||
GlPagination,
|
||||
GlTable,
|
||||
TimeAgoTooltip,
|
||||
UserDate,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
lastUsedHelpLink: helpPagePath('/user/profile/personal_access_tokens.md', {
|
||||
anchor: 'view-the-last-time-a-token-was-used',
|
||||
}),
|
||||
i18n: {
|
||||
emptyField: __('Never'),
|
||||
expired: __('Expired'),
|
||||
revoked: __('Revoked'),
|
||||
},
|
||||
INACTIVE_TOKENS_TABLE_FIELDS,
|
||||
inject: [
|
||||
'accessTokenType',
|
||||
'accessTokenTypePlural',
|
||||
'initialInactiveAccessTokens',
|
||||
'noInactiveTokensMessage',
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
inactiveAccessTokens: convertObjectPropsToCamelCase(this.initialInactiveAccessTokens, {
|
||||
deep: true,
|
||||
}),
|
||||
currentPage: INITIAL_PAGE,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
showPagination() {
|
||||
return this.inactiveAccessTokens.length > PAGE_SIZE;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
sortingChanged(aRow, bRow, key) {
|
||||
if (['createdAt', 'lastUsedAt', 'expiresAt'].includes(key)) {
|
||||
// Transform `null` value to the latest possible date
|
||||
// https://stackoverflow.com/a/11526569/18428169
|
||||
const maxEpoch = 8640000000000000;
|
||||
const a = new Date(aRow[key] ?? maxEpoch).getTime();
|
||||
const b = new Date(bRow[key] ?? maxEpoch).getTime();
|
||||
return a - b;
|
||||
}
|
||||
|
||||
// For other columns the default sorting works OK
|
||||
return false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<gl-table
|
||||
data-testid="inactive-access-tokens"
|
||||
:empty-text="noInactiveTokensMessage"
|
||||
:fields="$options.INACTIVE_TOKENS_TABLE_FIELDS"
|
||||
:items="inactiveAccessTokens"
|
||||
:per-page="$options.PAGE_SIZE"
|
||||
:current-page="currentPage"
|
||||
:sort-compare="sortingChanged"
|
||||
show-empty
|
||||
stacked="sm"
|
||||
class="gl-overflow-x-auto"
|
||||
>
|
||||
<template #cell(createdAt)="{ item: { createdAt } }">
|
||||
<user-date :date="createdAt" />
|
||||
</template>
|
||||
|
||||
<template #head(lastUsedAt)="{ label }">
|
||||
<span>{{ label }}</span>
|
||||
<gl-link :href="$options.lastUsedHelpLink"
|
||||
><gl-icon name="question-o" class="gl-ml-2" /><span class="gl-sr-only">{{
|
||||
s__('AccessTokens|The last time a token was used')
|
||||
}}</span></gl-link
|
||||
>
|
||||
</template>
|
||||
|
||||
<template #cell(lastUsedAt)="{ item: { lastUsedAt } }">
|
||||
<time-ago-tooltip v-if="lastUsedAt" :time="lastUsedAt" />
|
||||
<template v-else> {{ $options.i18n.emptyField }}</template>
|
||||
</template>
|
||||
|
||||
<template #cell(expiresAt)="{ item: { expiresAt, revoked } }">
|
||||
<span v-if="revoked" v-gl-tooltip :title="$options.i18n.tokenValidity">{{
|
||||
$options.i18n.revoked
|
||||
}}</span>
|
||||
<template v-else>
|
||||
<span>{{ $options.i18n.expired }}</span>
|
||||
<time-ago-tooltip :time="expiresAt" />
|
||||
</template>
|
||||
</template>
|
||||
</gl-table>
|
||||
<gl-pagination
|
||||
v-if="showPagination"
|
||||
v-model="currentPage"
|
||||
:per-page="$options.PAGE_SIZE"
|
||||
:total-items="inactiveAccessTokens.length"
|
||||
:prev-text="__('Prev')"
|
||||
:next-text="__('Next')"
|
||||
:label-next-page="__('Go to next page')"
|
||||
:label-prev-page="__('Go to previous page')"
|
||||
align="center"
|
||||
class="gl-mt-5"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -5,6 +5,7 @@ import { parseRailsFormFields } from '~/lib/utils/forms';
|
|||
import { __, sprintf } from '~/locale';
|
||||
import Translate from '~/vue_shared/translate';
|
||||
import AccessTokenTableApp from './components/access_token_table_app.vue';
|
||||
import InactiveAccessTokenTableApp from './components/inactive_access_token_table_app.vue';
|
||||
import ExpiresAtField from './components/expires_at_field.vue';
|
||||
import NewAccessTokenApp from './components/new_access_token_app.vue';
|
||||
import TokensApp from './components/tokens_app.vue';
|
||||
|
|
@ -50,6 +51,44 @@ export const initAccessTokenTableApp = () => {
|
|||
});
|
||||
};
|
||||
|
||||
export const initInactiveAccessTokenTableApp = () => {
|
||||
const el = document.querySelector('#js-inactive-access-token-table-app');
|
||||
|
||||
if (!el) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
accessTokenType,
|
||||
accessTokenTypePlural,
|
||||
initialInactiveAccessTokens: initialInactiveAccessTokensJson,
|
||||
noInactiveTokensMessage: noTokensMessage,
|
||||
} = el.dataset;
|
||||
|
||||
// Default values
|
||||
const noInactiveTokensMessage =
|
||||
noTokensMessage ||
|
||||
sprintf(__('This resource has no inactive %{accessTokenTypePlural}.'), {
|
||||
accessTokenTypePlural,
|
||||
});
|
||||
|
||||
const initialInactiveAccessTokens = JSON.parse(initialInactiveAccessTokensJson);
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
name: 'InactiveAccessTokenTableRoot',
|
||||
provide: {
|
||||
accessTokenType,
|
||||
accessTokenTypePlural,
|
||||
initialInactiveAccessTokens,
|
||||
noInactiveTokensMessage,
|
||||
},
|
||||
render(h) {
|
||||
return h(InactiveAccessTokenTableApp);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const initExpiresAtField = () => {
|
||||
const el = document.querySelector('.js-access-tokens-expires-at');
|
||||
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ export default {
|
|||
<gl-icon
|
||||
v-if="upstreamPipeline"
|
||||
:class="$options.arrowStyles"
|
||||
name="long-arrow"
|
||||
name="arrow-right"
|
||||
data-testid="upstream-arrow-icon"
|
||||
/>
|
||||
<legacy-pipeline-stages
|
||||
|
|
@ -75,7 +75,7 @@ export default {
|
|||
<gl-icon
|
||||
v-if="hasDownstreamPipelines"
|
||||
:class="$options.arrowStyles"
|
||||
name="long-arrow"
|
||||
name="arrow-right"
|
||||
data-testid="downstream-arrow-icon"
|
||||
/>
|
||||
<legacy-linked-pipelines-mini-list
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ export default {
|
|||
<gl-icon
|
||||
v-if="upstreamPipeline"
|
||||
:class="$options.arrowStyles"
|
||||
name="long-arrow"
|
||||
name="arrow-right"
|
||||
data-testid="upstream-arrow-icon"
|
||||
/>
|
||||
<pipeline-stages
|
||||
|
|
@ -134,7 +134,7 @@ export default {
|
|||
<gl-icon
|
||||
v-if="hasDownstreamPipelines"
|
||||
:class="$options.arrowStyles"
|
||||
name="long-arrow"
|
||||
name="arrow-right"
|
||||
data-testid="downstream-arrow-icon"
|
||||
/>
|
||||
<downstream-pipelines
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ export default {
|
|||
<transition name="issuable-header-slide">
|
||||
<div
|
||||
v-if="show"
|
||||
class="issue-sticky-header gl-fixed gl-z-3 gl-bg-white gl-border-1 gl-border-b-solid gl-border-b-gray-100 gl-py-3"
|
||||
class="issue-sticky-header gl-fixed gl-z-3 gl-bg-default gl-border-b gl-py-3"
|
||||
data-testid="issue-sticky-header"
|
||||
>
|
||||
<div class="issue-sticky-header-text gl-flex gl-items-center gl-gap-2 gl-mx-auto">
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ export default {
|
|||
@disappear="setStickyHeaderVisible(true)"
|
||||
>
|
||||
<div
|
||||
class="issue-sticky-header merge-request-sticky-header gl-fixed gl-bg-white gl-hidden md:gl-flex gl-flex-col gl-justify-end gl-border-b"
|
||||
class="issue-sticky-header merge-request-sticky-header gl-fixed gl-bg-default gl-hidden md:gl-flex gl-flex-col gl-justify-end gl-border-b"
|
||||
:class="{ 'gl-invisible': !isStickyHeaderVisible }"
|
||||
>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import {
|
||||
initAccessTokenTableApp,
|
||||
initInactiveAccessTokenTableApp,
|
||||
initExpiresAtField,
|
||||
initNewAccessTokenApp,
|
||||
} from '~/access_tokens';
|
||||
|
|
@ -7,3 +8,7 @@ import {
|
|||
initAccessTokenTableApp();
|
||||
initExpiresAtField();
|
||||
initNewAccessTokenApp();
|
||||
|
||||
if (gon.features.retainResourceAccessTokenUserAfterRevoke) {
|
||||
initInactiveAccessTokenTableApp();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import {
|
||||
initAccessTokenTableApp,
|
||||
initInactiveAccessTokenTableApp,
|
||||
initExpiresAtField,
|
||||
initNewAccessTokenApp,
|
||||
} from '~/access_tokens';
|
||||
|
|
@ -7,3 +8,7 @@ import {
|
|||
initAccessTokenTableApp();
|
||||
initExpiresAtField();
|
||||
initNewAccessTokenApp();
|
||||
|
||||
if (gon.features.retainResourceAccessTokenUserAfterRevoke) {
|
||||
initInactiveAccessTokenTableApp();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ export default {
|
|||
<transition name="issuable-header-slide">
|
||||
<div
|
||||
v-if="stickyTitleVisible"
|
||||
class="issue-sticky-header gl-fixed gl-z-3 gl-bg-white gl-border-1 gl-border-b-solid gl-border-b-gray-100 gl-py-3"
|
||||
class="issue-sticky-header gl-fixed gl-z-3 gl-bg-default gl-border-b gl-py-3"
|
||||
data-testid="header"
|
||||
>
|
||||
<div class="issue-sticky-header-text gl-flex gl-items-baseline gl-mx-auto gl-gap-3">
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ export default {
|
|||
<transition name="issuable-header-slide">
|
||||
<div
|
||||
v-if="isStickyHeaderShowing"
|
||||
class="issue-sticky-header gl-fixed gl-bg-white gl-border-b gl-z-3 gl-py-2"
|
||||
class="issue-sticky-header gl-fixed gl-bg-default gl-border-b gl-z-3 gl-py-2"
|
||||
data-testid="work-item-sticky-header"
|
||||
>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ $diff-file-header: 41px;
|
|||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $gray-50;
|
||||
background-color: var(--gl-background-color-strong);
|
||||
}
|
||||
|
||||
svg {
|
||||
|
|
|
|||
|
|
@ -242,7 +242,7 @@ span.idiff {
|
|||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: $gray-10;
|
||||
background-color: var(--gl-background-color-subtle);
|
||||
border-bottom: 1px solid $border-color;
|
||||
padding: $gl-padding-8 $gl-padding;
|
||||
margin: 0;
|
||||
|
|
|
|||
|
|
@ -997,8 +997,8 @@
|
|||
height: var(--mr-review-bar-height);
|
||||
padding-left: $super-sidebar-width;
|
||||
padding-right: $right-sidebar-collapsed-width;
|
||||
background: var(--white, $white);
|
||||
border-top: 1px solid var(--border-color, $border-color);
|
||||
background: var(--gl-background-color-default);
|
||||
border-top: 1px solid var(--gl-border-color-default);
|
||||
@apply gl-transition-padding;
|
||||
|
||||
@media (max-width: map-get($grid-breakpoints, sm)-1) {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ module AccessTokensActions
|
|||
before_action -> { check_permission(:read_resource_access_tokens) }, only: [:index]
|
||||
before_action -> { check_permission(:destroy_resource_access_tokens) }, only: [:revoke]
|
||||
before_action -> { check_permission(:create_resource_access_tokens) }, only: [:create]
|
||||
before_action do
|
||||
push_frontend_feature_flag(:retain_resource_access_token_user_after_revoke, resource.root_ancestor)
|
||||
end
|
||||
end
|
||||
|
||||
# rubocop:disable Gitlab/ModuleWithInstanceVariables
|
||||
|
|
@ -70,6 +73,9 @@ module AccessTokensActions
|
|||
|
||||
@scopes = Gitlab::Auth.available_scopes_for(resource)
|
||||
@active_access_tokens = active_access_tokens
|
||||
if Feature.enabled?(:retain_resource_access_token_user_after_revoke, resource.root_ancestor) # rubocop:disable Style/GuardClause
|
||||
@inactive_access_tokens = inactive_access_tokens
|
||||
end
|
||||
end
|
||||
# rubocop:enable Gitlab/ModuleWithInstanceVariables
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,15 @@ module RenderAccessTokens
|
|||
represent(tokens)
|
||||
end
|
||||
|
||||
def inactive_access_tokens
|
||||
tokens = finder(state: 'inactive', sort: 'updated_at_desc').execute.preload_users
|
||||
|
||||
# We don't call `add_pagination_headers` as this overrides the
|
||||
# pagination of active tokens.
|
||||
|
||||
represent(tokens)
|
||||
end
|
||||
|
||||
def add_pagination_headers(relation)
|
||||
Gitlab::Pagination::OffsetHeaderBuilder.new(
|
||||
request_context: self,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
- type = _('group access token')
|
||||
- type_plural = _('group access tokens')
|
||||
- @force_desktop_expanded_sidebar = true
|
||||
- shared_card_component_classes = "gl-new-card gl-border-b-0 gl-rounded-bottom-left-none gl-rounded-bottom-right-none"
|
||||
|
||||
.settings-section.js-search-settings-section
|
||||
.settings-sticky-header
|
||||
|
|
@ -25,7 +26,7 @@
|
|||
|
||||
#js-new-access-token-app{ data: { access_token_type: type } }
|
||||
|
||||
= render Pajamas::CardComponent.new(card_options: { class: 'gl-new-card gl-border-b-0 gl-rounded-bottom-left-none gl-rounded-bottom-right-none js-toggle-container js-token-card' }, header_options: { class: 'gl-new-card-header' }, body_options: { class: 'gl-new-card-body' }) do |c|
|
||||
= render Pajamas::CardComponent.new(card_options: { class: "#{shared_card_component_classes} js-toggle-container js-token-card" }, header_options: { class: 'gl-new-card-header' }, body_options: { class: 'gl-new-card-body' }) do |c|
|
||||
- c.with_header do
|
||||
.gl-new-card-title-wrapper
|
||||
%h3.gl-new-card-title
|
||||
|
|
@ -54,3 +55,15 @@
|
|||
help_path: help_page_path('user/group/settings/group_access_tokens', anchor: 'scopes-for-a-group-access-token')
|
||||
|
||||
#js-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_active_access_tokens: @active_access_tokens.to_json, no_active_tokens_message: _('This group has no active access tokens.'), show_role: true } }
|
||||
|
||||
- if Feature.enabled?(:retain_resource_access_token_user_after_revoke, @group.root_ancestor)
|
||||
= render Pajamas::CardComponent.new(card_options: { class: shared_card_component_classes }, header_options: { class: 'gl-new-card-header' }, body_options: { class: 'gl-new-card-body gl-px-0 gl-bg-gray-10 gl-border-b gl-rounded-bottom-base' }) do |c|
|
||||
- c.with_header do
|
||||
.gl-new-card-title-wrapper
|
||||
%h3.gl-new-card-title
|
||||
= _('Inactive group access tokens')
|
||||
.gl-new-card-count
|
||||
= sprite_icon('token', css_class: 'gl-mr-2')
|
||||
%span.js-token-count= @inactive_access_tokens.size
|
||||
- c.with_body do
|
||||
#js-inactive-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_inactive_access_tokens: @inactive_access_tokens.to_json, no_inactive_tokens_message: _('This group has no inactive access tokens.')} }
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
- type = _('project access token')
|
||||
- type_plural = _('project access tokens')
|
||||
- @force_desktop_expanded_sidebar = true
|
||||
- shared_card_component_classes = "gl-new-card gl-border-b-0 gl-rounded-bottom-left-none gl-rounded-bottom-right-none"
|
||||
|
||||
.settings-section.js-search-settings-section
|
||||
.settings-sticky-header
|
||||
|
|
@ -24,7 +25,7 @@
|
|||
|
||||
#js-new-access-token-app{ data: { access_token_type: type } }
|
||||
|
||||
= render Pajamas::CardComponent.new(card_options: { class: 'gl-new-card gl-border-b-0 gl-rounded-bottom-left-none gl-rounded-bottom-right-none js-toggle-container js-token-card' }, header_options: { class: 'gl-new-card-header' }, body_options: { class: 'gl-new-card-body' }) do |c|
|
||||
= render Pajamas::CardComponent.new(card_options: { class: "#{shared_card_component_classes} js-toggle-container js-token-card" }, header_options: { class: 'gl-new-card-header' }, body_options: { class: 'gl-new-card-body' }) do |c|
|
||||
- c.with_header do
|
||||
.gl-new-card-title-wrapper
|
||||
%h3.gl-new-card-title
|
||||
|
|
@ -42,3 +43,15 @@
|
|||
= render_if_exists 'projects/settings/access_tokens/form', type: type
|
||||
|
||||
#js-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_active_access_tokens: @active_access_tokens.to_json, no_active_tokens_message: _('This project has no active access tokens.'), show_role: true } }
|
||||
|
||||
- if Feature.enabled?(:retain_resource_access_token_user_after_revoke, @project.root_ancestor)
|
||||
= render Pajamas::CardComponent.new(card_options: { class: shared_card_component_classes }, header_options: { class: 'gl-new-card-header' }, body_options: { class: 'gl-new-card-body gl-px-0 gl-bg-gray-10 gl-border-b gl-rounded-bottom-base' }) do |c|
|
||||
- c.with_header do
|
||||
.gl-new-card-title-wrapper
|
||||
%h3.gl-new-card-title
|
||||
= _('Inactive project access tokens')
|
||||
.gl-new-card-count
|
||||
= sprite_icon('token', css_class: 'gl-mr-2')
|
||||
%span.js-token-count= @inactive_access_tokens.size
|
||||
- c.with_body do
|
||||
#js-inactive-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_inactive_access_tokens: @inactive_access_tokens.to_json, no_inactive_tokens_message: _('This project has no inactive access tokens.')} }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddMemberRoleIdToGroupGroupLinks < Gitlab::Database::Migration[2.2]
|
||||
milestone '17.3'
|
||||
enable_lock_retries!
|
||||
|
||||
def change
|
||||
add_column :group_group_links, :member_role_id, :bigint, if_not_exists: true
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIndexToGroupGroupLinksOnMemberRoleId < Gitlab::Database::Migration[2.2]
|
||||
milestone '17.3'
|
||||
disable_ddl_transaction!
|
||||
|
||||
INDEX_NAME = 'index_group_group_links_on_member_role_id'
|
||||
|
||||
def up
|
||||
add_concurrent_index :group_group_links, :member_role_id, name: INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index_by_name :group_group_links, INDEX_NAME
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddFkToMemberRoleOnGroupGroupLinks < Gitlab::Database::Migration[2.2]
|
||||
milestone '17.3'
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_foreign_key :group_group_links, :member_roles, column: :member_role_id, on_delete: :nullify
|
||||
end
|
||||
|
||||
def down
|
||||
with_lock_retries do
|
||||
remove_foreign_key :group_group_links, column: :member_role_id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
ee3dc82029ae3d9f7ce57e49bba39841872391ae74f7671e5794849a0c4adeb5
|
||||
|
|
@ -0,0 +1 @@
|
|||
6469c09bf160ff6c05e242b63882add76a467642df48424a2a7f3a1baefacb01
|
||||
|
|
@ -0,0 +1 @@
|
|||
ba845f9c8129c6006e7f5c7406befd09c2e4520ed334be658a6bb8334ede42a0
|
||||
|
|
@ -10982,7 +10982,8 @@ CREATE TABLE group_group_links (
|
|||
shared_group_id bigint NOT NULL,
|
||||
shared_with_group_id bigint NOT NULL,
|
||||
expires_at date,
|
||||
group_access smallint DEFAULT 30 NOT NULL
|
||||
group_access smallint DEFAULT 30 NOT NULL,
|
||||
member_role_id bigint
|
||||
);
|
||||
|
||||
CREATE SEQUENCE group_group_links_id_seq
|
||||
|
|
@ -27440,6 +27441,8 @@ CREATE INDEX index_group_deploy_tokens_on_deploy_token_id ON group_deploy_tokens
|
|||
|
||||
CREATE UNIQUE INDEX index_group_deploy_tokens_on_group_and_deploy_token_ids ON group_deploy_tokens USING btree (group_id, deploy_token_id);
|
||||
|
||||
CREATE INDEX index_group_group_links_on_member_role_id ON group_group_links USING btree (member_role_id);
|
||||
|
||||
CREATE UNIQUE INDEX index_group_group_links_on_shared_group_and_shared_with_group ON group_group_links USING btree (shared_group_id, shared_with_group_id);
|
||||
|
||||
CREATE INDEX index_group_group_links_on_shared_with_group_and_group_access ON group_group_links USING btree (shared_with_group_id, group_access);
|
||||
|
|
@ -32210,6 +32213,9 @@ ALTER TABLE ONLY duo_workflows_workflows
|
|||
ALTER TABLE ONLY members
|
||||
ADD CONSTRAINT fk_2f85abf8f1 FOREIGN KEY (member_namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY group_group_links
|
||||
ADD CONSTRAINT fk_2fbc7071a3 FOREIGN KEY (member_role_id) REFERENCES member_roles(id) ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE ONLY zoekt_replicas
|
||||
ADD CONSTRAINT fk_3035f4b498 FOREIGN KEY (zoekt_enabled_namespace_id) REFERENCES zoekt_enabled_namespaces(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
|
|||
|
|
@ -254,11 +254,18 @@ malicious-job:
|
|||
```
|
||||
|
||||
To help reduce the risk of accidentally leaking secrets through scripts like in `accidental-leak-job`,
|
||||
all variables containing sensitive information should be [masked in job logs](#mask-a-cicd-variable).
|
||||
all variables containing sensitive information should always be [masked in job logs](#mask-a-cicd-variable).
|
||||
You can also [limit a variable to protected branches and tags only](#protect-a-cicd-variable).
|
||||
|
||||
Alternatively, use the GitLab [integration with HashiCorp Vault](../secrets/index.md)
|
||||
to store and retrieve secrets.
|
||||
Alternatively, use one of the native GitLab integrations to connect with third party
|
||||
secrets manager providers to store and retrieve secrets:
|
||||
|
||||
- [HashiCorp Vault](../secrets/index.md)
|
||||
- [Azure Key Vault](../secrets/azure_key_vault.md)
|
||||
- [Google Secret Manager](../secrets/gcp_secret_manager.md)
|
||||
|
||||
You can also use [OpenID Connect (OIDC) authentication](../secrets/id_token_authentication.md)
|
||||
for secrets managers which do not have a native integration.
|
||||
|
||||
Malicious scripts like in `malicious-job` must be caught during the review process.
|
||||
Reviewers should never trigger a pipeline when they find code like this, because
|
||||
|
|
@ -272,8 +279,7 @@ valid [secrets file](../../administration/backup_restore/troubleshooting_backup_
|
|||
|
||||
WARNING:
|
||||
Masking a CI/CD variable is not a guaranteed way to prevent malicious users from
|
||||
accessing variable values. The masking feature is "best-effort" and there to
|
||||
help when a variable is accidentally revealed. To make variables more secure,
|
||||
accessing variable values. To ensure security of sensitive information,
|
||||
consider using [external secrets](../secrets/index.md) and [file type variables](#use-file-type-cicd-variables)
|
||||
to prevent commands such as `env`/`printenv` from printing secret variables.
|
||||
|
||||
|
|
@ -295,9 +301,10 @@ To mask a variable:
|
|||
The method used to mask variables [limits what can be included in a masked variable](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/13784#note_106756757).
|
||||
The value of the variable must:
|
||||
|
||||
- Be a single line.
|
||||
- Be a single line with no spaces.
|
||||
- Be 8 characters or longer.
|
||||
- Not match the name of an existing predefined or custom CI/CD variable.
|
||||
- Not include non-alpha-numeric characters other than `@`, `_`, `-`, `:`, or `+`.
|
||||
|
||||
Additionally, if [variable expansion](#prevent-cicd-variable-expansion) is enabled,
|
||||
the value can contain only:
|
||||
|
|
@ -514,7 +521,7 @@ test-job:
|
|||
Variables from `dotenv` reports [take precedence](#cicd-variable-precedence) over
|
||||
certain types of new variable definitions such as job defined variables.
|
||||
|
||||
You can also [pass `dotenv` variables to downstream pipelines](../pipelines/downstream_pipelines.md#pass-dotenv-variables-created-in-a-job)
|
||||
You can also [pass `dotenv` variables to downstream pipelines](../pipelines/downstream_pipelines.md#pass-dotenv-variables-created-in-a-job).
|
||||
|
||||
#### Control which jobs receive `dotenv` variables
|
||||
|
||||
|
|
@ -914,7 +921,7 @@ export CI_PROJECT_TITLE="GitLab"
|
|||
|
||||
WARNING:
|
||||
Debug logging can be a serious security risk. The output contains the content of
|
||||
all variables and other secrets available to the job. The output is uploaded to the
|
||||
all variables available to the job. The output is uploaded to the
|
||||
GitLab server and visible in job logs.
|
||||
|
||||
You can use debug logging to help troubleshoot problems with pipeline configuration
|
||||
|
|
@ -1022,22 +1029,3 @@ WARNING:
|
|||
If you add `CI_DEBUG_TRACE` as a local variable to runners, debug logs generate and are visible
|
||||
to all users with access to job logs. The permission levels are not checked by the runner,
|
||||
so you should only use the variable in GitLab itself.
|
||||
|
||||
## Known issues and workarounds
|
||||
|
||||
These are some known issues with CI/CD variables, and where applicable, known workarounds.
|
||||
|
||||
### "argument list too long"
|
||||
|
||||
This issue occurs when the combined length of all CI/CD variables defined for a job exceeds the limit imposed by the
|
||||
shell where the job executes. This includes the names and values of pre-defined and user defined variables. This limit
|
||||
is typically referred to as `ARG_MAX`, and is shell and operating system dependent. This issue also occurs when the
|
||||
content of a single [File-type](#use-file-type-cicd-variables) variable exceeds `ARG_MAX`.
|
||||
|
||||
For more information, see [issue 392406](https://gitlab.com/gitlab-org/gitlab/-/issues/392406#note_1414219596).
|
||||
|
||||
As a workaround you can either:
|
||||
|
||||
- Use [File-type](#use-file-type-cicd-variables) CI/CD variables for large environment variables where possible.
|
||||
- If a single large variable is larger than `ARG_MAX`, try using [Secure Files](../secure_files/index.md), or
|
||||
bring the file to the job through some other mechanism.
|
||||
|
|
|
|||
|
|
@ -141,6 +141,11 @@ The token generated when you create an agent for Kubernetes. Use **agent access
|
|||
- secret token
|
||||
- authentication token
|
||||
|
||||
## agnostic
|
||||
|
||||
Instead of **agnostic**, use **platform-independent** or **vendor-neutral**.
|
||||
([Vale](../testing/vale.md) rule: [`SubstitutionWarning.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/SubstitutionWarning.yml))
|
||||
|
||||
## AI, artificial intelligence
|
||||
|
||||
Use **AI**. Do not spell out **artificial intelligence**.
|
||||
|
|
|
|||
|
|
@ -153,7 +153,8 @@ module Gitlab
|
|||
target_project_id: target_project_id,
|
||||
remove_source_branch: true,
|
||||
assignee_ids: usernames_to_ids(change.assignees),
|
||||
reviewer_ids: usernames_to_ids(change.reviewers)
|
||||
reviewer_ids: usernames_to_ids(change.reviewers),
|
||||
squash: true
|
||||
})
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -124,7 +124,8 @@ module Gitlab
|
|||
end
|
||||
|
||||
def print_change_details(change, branch_name)
|
||||
base_message = "Merge request URL: #{change.mr_web_url || '(known after create)'}, on branch #{branch_name}."
|
||||
base_message = "Merge request URL: #{change.mr_web_url || '(known after create)'}, on branch #{branch_name}. " \
|
||||
"Squash commits enabled."
|
||||
base_message << " CI skipped." if change.push_options.ci_skip
|
||||
|
||||
@logger.puts base_message.yellowish
|
||||
|
|
|
|||
|
|
@ -343,7 +343,8 @@ RSpec.describe ::Gitlab::Housekeeper::GitlabClient do
|
|||
target_project_id: 456,
|
||||
remove_source_branch: true,
|
||||
assignee_ids: [assignee_id],
|
||||
reviewer_ids: [reviewer_id]
|
||||
reviewer_ids: [reviewer_id],
|
||||
squash: true
|
||||
},
|
||||
headers: {
|
||||
'Content-Type' => 'application/json',
|
||||
|
|
|
|||
|
|
@ -43,12 +43,16 @@ module Gitlab
|
|||
|
||||
module MigratorOverrides
|
||||
def current_version
|
||||
migrations
|
||||
.sort_by(&:version)
|
||||
.reverse
|
||||
reverse_sorted_migrations
|
||||
.find { |m| migrated.include?(m.version) }
|
||||
.try(:version) || 0
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def reverse_sorted_migrations
|
||||
@reverse_sorted_migrations ||= migrations.sort_by(&:version).reverse
|
||||
end
|
||||
end
|
||||
|
||||
def self.patch!
|
||||
|
|
|
|||
|
|
@ -27427,6 +27427,12 @@ msgstr ""
|
|||
msgid "Inactive"
|
||||
msgstr ""
|
||||
|
||||
msgid "Inactive group access tokens"
|
||||
msgstr ""
|
||||
|
||||
msgid "Inactive project access tokens"
|
||||
msgstr ""
|
||||
|
||||
msgid "Incident"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -54310,6 +54316,9 @@ msgstr ""
|
|||
msgid "This group has no active access tokens."
|
||||
msgstr ""
|
||||
|
||||
msgid "This group has no inactive access tokens."
|
||||
msgstr ""
|
||||
|
||||
msgid "This group has no projects yet"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -54603,6 +54612,9 @@ msgstr ""
|
|||
msgid "This project has no active access tokens."
|
||||
msgstr ""
|
||||
|
||||
msgid "This project has no inactive access tokens."
|
||||
msgstr ""
|
||||
|
||||
msgid "This project is %{strongStart}NOT%{strongEnd} a fork, and has the following:"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -54675,6 +54687,9 @@ msgstr ""
|
|||
msgid "This resource has no comments to summarize"
|
||||
msgstr ""
|
||||
|
||||
msgid "This resource has no inactive %{accessTokenTypePlural}."
|
||||
msgstr ""
|
||||
|
||||
msgid "This runner will only run on pipelines triggered on protected branches"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
source 'https://rubygems.org'
|
||||
|
||||
gem 'gitlab-qa', '~> 14', '>= 14.12.0', require: 'gitlab/qa'
|
||||
gem 'gitlab_quality-test_tooling', '~> 1.30.0', require: false
|
||||
gem 'gitlab_quality-test_tooling', '~> 1.31.0', require: false
|
||||
gem 'gitlab-utils', path: '../gems/gitlab-utils'
|
||||
gem 'activesupport', '~> 7.0.8.4' # This should stay in sync with the root's Gemfile
|
||||
gem 'allure-rspec', '~> 2.24.5'
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ GEM
|
|||
rainbow (>= 3, < 4)
|
||||
table_print (= 1.5.7)
|
||||
zeitwerk (>= 2, < 3)
|
||||
gitlab_quality-test_tooling (1.30.0)
|
||||
gitlab_quality-test_tooling (1.31.0)
|
||||
activesupport (>= 7.0, < 7.2)
|
||||
amatch (~> 0.4.1)
|
||||
gitlab (~> 4.19)
|
||||
|
|
@ -405,7 +405,7 @@ DEPENDENCIES
|
|||
gitlab-cng!
|
||||
gitlab-qa (~> 14, >= 14.12.0)
|
||||
gitlab-utils!
|
||||
gitlab_quality-test_tooling (~> 1.30.0)
|
||||
gitlab_quality-test_tooling (~> 1.31.0)
|
||||
googleauth (~> 1.9.0)
|
||||
influxdb-client (~> 3.1)
|
||||
junit_merge (~> 0.1.2)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,180 @@
|
|||
import { GlPagination, GlTable } from '@gitlab/ui';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import InactiveAccessTokenTableApp from '~/access_tokens/components/inactive_access_token_table_app.vue';
|
||||
import { PAGE_SIZE } from '~/access_tokens/components/constants';
|
||||
import { __, s__, sprintf } from '~/locale';
|
||||
|
||||
describe('~/access_tokens/components/inactive_access_token_table_app', () => {
|
||||
let wrapper;
|
||||
|
||||
const accessTokenType = 'access token';
|
||||
const accessTokenTypePlural = 'access tokens';
|
||||
const information = undefined;
|
||||
const noInactiveTokensMessage = 'This resource has no inactive access tokens.';
|
||||
|
||||
const defaultInactiveAccessTokens = [
|
||||
{
|
||||
name: 'a',
|
||||
scopes: ['api'],
|
||||
created_at: '2023-05-01T00:00:00.000Z',
|
||||
last_used_at: null,
|
||||
expired: true,
|
||||
expires_at: '2024-05-01T00:00:00.000Z',
|
||||
revoked: true,
|
||||
role: 'Maintainer',
|
||||
},
|
||||
{
|
||||
name: 'b',
|
||||
scopes: ['api', 'sudo'],
|
||||
created_at: '2024-04-21T00:00:00.000Z',
|
||||
last_used_at: '2024-04-21T00:00:00.000Z',
|
||||
expired: true,
|
||||
expires_at: new Date().toISOString(),
|
||||
revoked: false,
|
||||
role: 'Maintainer',
|
||||
},
|
||||
];
|
||||
|
||||
const createComponent = (props = {}) => {
|
||||
wrapper = mountExtended(InactiveAccessTokenTableApp, {
|
||||
provide: {
|
||||
accessTokenType,
|
||||
accessTokenTypePlural,
|
||||
information,
|
||||
initialInactiveAccessTokens: defaultInactiveAccessTokens,
|
||||
noInactiveTokensMessage,
|
||||
...props,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findTable = () => wrapper.findComponent(GlTable);
|
||||
const findHeaders = () => findTable().findAll('th > div > span');
|
||||
const findCells = () => findTable().findAll('td');
|
||||
const findPagination = () => wrapper.findComponent(GlPagination);
|
||||
|
||||
it('should render an empty table with a default message', () => {
|
||||
createComponent({ initialInactiveAccessTokens: [] });
|
||||
|
||||
const cells = findCells();
|
||||
expect(cells).toHaveLength(1);
|
||||
expect(cells.at(0).text()).toBe(
|
||||
sprintf(__('This resource has no inactive %{accessTokenTypePlural}.'), {
|
||||
accessTokenTypePlural,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should render an empty table with a custom message', () => {
|
||||
const noTokensMessage = 'This group has no inactive access tokens.';
|
||||
createComponent({ initialInactiveAccessTokens: [], noInactiveTokensMessage: noTokensMessage });
|
||||
|
||||
const cells = findCells();
|
||||
expect(cells).toHaveLength(1);
|
||||
expect(cells.at(0).text()).toBe(noTokensMessage);
|
||||
});
|
||||
|
||||
describe('table headers', () => {
|
||||
it('has expected columns', () => {
|
||||
createComponent();
|
||||
|
||||
const headers = findHeaders();
|
||||
expect(headers.wrappers.map((header) => header.text())).toStrictEqual([
|
||||
__('Token name'),
|
||||
__('Scopes'),
|
||||
s__('AccessTokens|Created'),
|
||||
'Last Used',
|
||||
__('Expired'),
|
||||
__('Role'),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('`Last Used` header should contain a link and an assistive message', () => {
|
||||
createComponent();
|
||||
|
||||
const headers = wrapper.findAll('th');
|
||||
const lastUsed = headers.at(3);
|
||||
const anchor = lastUsed.find('a');
|
||||
const assistiveElement = lastUsed.find('.gl-sr-only');
|
||||
expect(anchor.exists()).toBe(true);
|
||||
expect(anchor.attributes('href')).toBe(
|
||||
'/help/user/profile/personal_access_tokens.md#view-the-last-time-a-token-was-used',
|
||||
);
|
||||
expect(assistiveElement.text()).toBe(s__('AccessTokens|The last time a token was used'));
|
||||
});
|
||||
|
||||
it('sorts rows alphabetically', async () => {
|
||||
createComponent();
|
||||
|
||||
const cells = findCells();
|
||||
|
||||
// First and second rows
|
||||
expect(cells.at(0).text()).toBe('a');
|
||||
expect(cells.at(6).text()).toBe('b');
|
||||
|
||||
const headers = findHeaders();
|
||||
await headers.at(0).trigger('click');
|
||||
await headers.at(0).trigger('click');
|
||||
|
||||
// First and second rows have swapped
|
||||
expect(cells.at(0).text()).toBe('b');
|
||||
expect(cells.at(6).text()).toBe('a');
|
||||
});
|
||||
|
||||
it('sorts rows by last used date', async () => {
|
||||
createComponent();
|
||||
|
||||
const cells = findCells();
|
||||
|
||||
// First and second rows
|
||||
expect(cells.at(0).text()).toBe('a');
|
||||
expect(cells.at(6).text()).toBe('b');
|
||||
|
||||
const headers = findHeaders();
|
||||
await headers.at(3).trigger('click');
|
||||
|
||||
// First and second rows have swapped
|
||||
expect(cells.at(0).text()).toBe('b');
|
||||
expect(cells.at(6).text()).toBe('a');
|
||||
});
|
||||
|
||||
it('sorts rows by expiry date', async () => {
|
||||
createComponent();
|
||||
|
||||
const cells = findCells();
|
||||
const headers = findHeaders();
|
||||
await headers.at(4).trigger('click');
|
||||
|
||||
// First and second rows have swapped
|
||||
expect(cells.at(0).text()).toBe('b');
|
||||
expect(cells.at(6).text()).toBe('a');
|
||||
});
|
||||
|
||||
it('shows Revoked in expiry column when revoked', () => {
|
||||
createComponent();
|
||||
|
||||
const cells = findCells();
|
||||
|
||||
// First and second rows
|
||||
expect(cells.at(4).text()).toBe('Revoked');
|
||||
expect(cells.at(10).text()).toBe('Expired just now');
|
||||
});
|
||||
|
||||
describe('pagination', () => {
|
||||
it('does not show pagination component', () => {
|
||||
createComponent({
|
||||
initialInactiveAccessTokens: Array(PAGE_SIZE).fill(defaultInactiveAccessTokens[0]),
|
||||
});
|
||||
|
||||
expect(findPagination().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('shows the pagination component', () => {
|
||||
createComponent({
|
||||
initialInactiveAccessTokens: Array(PAGE_SIZE + 1).fill(defaultInactiveAccessTokens[0]),
|
||||
});
|
||||
expect(findPagination().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -5,9 +5,11 @@ import {
|
|||
initAccessTokenTableApp,
|
||||
initExpiresAtField,
|
||||
initNewAccessTokenApp,
|
||||
initInactiveAccessTokenTableApp,
|
||||
initTokensApp,
|
||||
} from '~/access_tokens';
|
||||
import AccessTokenTableApp from '~/access_tokens/components/access_token_table_app.vue';
|
||||
import InactiveAccessTokenTableApp from '~/access_tokens/components/inactive_access_token_table_app.vue';
|
||||
import ExpiresAtField from '~/access_tokens/components/expires_at_field.vue';
|
||||
import NewAccessTokenApp from '~/access_tokens/components/new_access_token_app.vue';
|
||||
import TokensApp from '~/access_tokens/components/tokens_app.vue';
|
||||
|
|
@ -173,4 +175,81 @@ describe('access tokens', () => {
|
|||
expect(initNewAccessTokenApp()).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('initInactiveAccessTokenTableApp', () => {
|
||||
const accessTokenType = 'group access token';
|
||||
const accessTokenTypePlural = 'group access tokens';
|
||||
const initialInactiveAccessTokens = [
|
||||
{
|
||||
name: 'a',
|
||||
scopes: ['api'],
|
||||
created_at: '2023-05-01T00:00:00.000Z',
|
||||
last_used_at: null,
|
||||
expired: false,
|
||||
expires_at: null,
|
||||
revoked: true,
|
||||
role: 'Maintainer',
|
||||
},
|
||||
];
|
||||
|
||||
it('mounts the component and provides required values', () => {
|
||||
setHTMLFixture(
|
||||
`<div id="js-inactive-access-token-table-app"
|
||||
data-access-token-type="${accessTokenType}"
|
||||
data-access-token-type-plural="${accessTokenTypePlural}"
|
||||
data-initial-inactive-access-tokens=${JSON.stringify(initialInactiveAccessTokens)}
|
||||
>
|
||||
</div>`,
|
||||
);
|
||||
|
||||
const vueInstance = initInactiveAccessTokenTableApp();
|
||||
wrapper = createWrapper(vueInstance);
|
||||
const component = wrapper.findComponent({ name: 'InactiveAccessTokenTableRoot' });
|
||||
|
||||
expect(component.exists()).toBe(true);
|
||||
expect(wrapper.findComponent(InactiveAccessTokenTableApp).vm).toMatchObject({
|
||||
// Required value
|
||||
accessTokenType,
|
||||
accessTokenTypePlural,
|
||||
initialInactiveAccessTokens,
|
||||
|
||||
// Default values
|
||||
noInactiveTokensMessage: sprintf(
|
||||
__('This resource has no inactive %{accessTokenTypePlural}.'),
|
||||
{
|
||||
accessTokenTypePlural,
|
||||
},
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
it('mounts the component and provides all values', () => {
|
||||
const noInactiveTokensMessage = 'This group has no inactive access tokens.';
|
||||
setHTMLFixture(
|
||||
`<div id="js-inactive-access-token-table-app"
|
||||
data-access-token-type="${accessTokenType}"
|
||||
data-access-token-type-plural="${accessTokenTypePlural}"
|
||||
data-initial-inactive-access-tokens=${JSON.stringify(initialInactiveAccessTokens)}
|
||||
data-no-inactive-tokens-message="${noInactiveTokensMessage}"
|
||||
>
|
||||
</div>`,
|
||||
);
|
||||
|
||||
const vueInstance = initInactiveAccessTokenTableApp();
|
||||
wrapper = createWrapper(vueInstance);
|
||||
const component = wrapper.findComponent({ name: 'InactiveAccessTokenTableRoot' });
|
||||
|
||||
expect(component.exists()).toBe(true);
|
||||
expect(component.findComponent(InactiveAccessTokenTableApp).vm).toMatchObject({
|
||||
accessTokenType,
|
||||
accessTokenTypePlural,
|
||||
initialInactiveAccessTokens,
|
||||
noInactiveTokensMessage,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns `null`', () => {
|
||||
expect(initInactiveAccessTokenTableApp()).toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ describe('Legacy Pipeline Mini Graph', () => {
|
|||
it('should render an upstream arrow icon only', () => {
|
||||
expect(findDownstreamArrowIcon().exists()).toBe(false);
|
||||
expect(findUpstreamArrowIcon().exists()).toBe(true);
|
||||
expect(findUpstreamArrowIcon().props('name')).toBe('long-arrow');
|
||||
expect(findUpstreamArrowIcon().props('name')).toBe('arrow-right');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -116,7 +116,7 @@ describe('Legacy Pipeline Mini Graph', () => {
|
|||
it('should render a downstream arrow icon only', () => {
|
||||
expect(findUpstreamArrowIcon().exists()).toBe(false);
|
||||
expect(findDownstreamArrowIcon().exists()).toBe(true);
|
||||
expect(findDownstreamArrowIcon().props('name')).toBe('long-arrow');
|
||||
expect(findDownstreamArrowIcon().props('name')).toBe('arrow-right');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ RSpec.describe Groups::Settings::AccessTokensController, feature_category: :syst
|
|||
it_behaves_like 'feature unavailable'
|
||||
it_behaves_like 'GET resource access tokens available'
|
||||
it_behaves_like 'GET access tokens are paginated and ordered'
|
||||
it_behaves_like 'GET access tokens includes inactive tokens'
|
||||
end
|
||||
|
||||
describe 'POST /:namespace/-/settings/access_tokens' do
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ RSpec.describe Projects::Settings::AccessTokensController, feature_category: :sy
|
|||
it_behaves_like 'feature unavailable'
|
||||
it_behaves_like 'GET resource access tokens available'
|
||||
it_behaves_like 'GET access tokens are paginated and ordered'
|
||||
it_behaves_like 'GET access tokens includes inactive tokens'
|
||||
end
|
||||
|
||||
describe 'POST /:namespace/:project/-/settings/access_tokens' do
|
||||
|
|
|
|||
|
|
@ -129,6 +129,21 @@ RSpec.shared_examples 'inactive resource access tokens' do |no_active_tokens_tex
|
|||
find("[data-testid='active-tokens']")
|
||||
end
|
||||
|
||||
def inactive_access_tokens
|
||||
find("[data-testid='inactive-access-tokens']")
|
||||
end
|
||||
|
||||
context 'when feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(retain_resource_access_token_user_after_revoke: false)
|
||||
end
|
||||
|
||||
it 'does not show inactive tokens' do
|
||||
visit resource_settings_access_tokens_path
|
||||
expect(page).to have_no_selector("[data-testid='inactive-access-tokens']")
|
||||
end
|
||||
end
|
||||
|
||||
it 'allows revocation of an active token' do
|
||||
visit resource_settings_access_tokens_path
|
||||
accept_gl_confirm(button_text: 'Revoke') { click_on 'Revoke' }
|
||||
|
|
@ -141,6 +156,15 @@ RSpec.shared_examples 'inactive resource access tokens' do |no_active_tokens_tex
|
|||
visit resource_settings_access_tokens_path
|
||||
|
||||
expect(active_access_tokens).to have_text(no_active_tokens_text)
|
||||
expect(inactive_access_tokens).to have_text(resource_access_token.name)
|
||||
end
|
||||
|
||||
it 'removes revoked tokens from active section' do
|
||||
resource_access_token.revoke!
|
||||
visit resource_settings_access_tokens_path
|
||||
|
||||
expect(active_access_tokens).to have_text(no_active_tokens_text)
|
||||
expect(inactive_access_tokens).to have_text(resource_access_token.name)
|
||||
end
|
||||
|
||||
context 'when resource access token creation is not allowed' do
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ RSpec.shared_examples 'GET access tokens are paginated and ordered' do
|
|||
end
|
||||
end
|
||||
|
||||
context "when tokens returned are ordered" do
|
||||
context "when active tokens returned are ordered" do
|
||||
let(:expires_1_day_from_now) { 1.day.from_now.to_date }
|
||||
let(:expires_2_day_from_now) { 2.days.from_now.to_date }
|
||||
|
||||
|
|
@ -95,6 +95,33 @@ RSpec.shared_examples 'GET access tokens are paginated and ordered' do
|
|||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'GET access tokens includes inactive tokens' do
|
||||
context "when inactive tokens returned are ordered" do
|
||||
let(:one_day_ago) { 1.day.ago.to_date }
|
||||
let(:two_days_ago) { 2.days.ago.to_date }
|
||||
|
||||
before do
|
||||
create(:personal_access_token, :revoked, user: access_token_user, name: "Token1").update!(updated_at: one_day_ago)
|
||||
create(:personal_access_token, :expired, user: access_token_user,
|
||||
name: "Token2").update!(updated_at: two_days_ago)
|
||||
end
|
||||
|
||||
it "orders token list descending on updated_at" do
|
||||
get_access_tokens
|
||||
|
||||
first_token = assigns(:inactive_access_tokens).first.as_json
|
||||
expect(first_token['name']).to eq("Token1")
|
||||
end
|
||||
end
|
||||
|
||||
context "when there are no inactive tokens" do
|
||||
it "returns an empty array" do
|
||||
get_access_tokens
|
||||
expect(assigns(:inactive_access_tokens)).to eq([])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'POST resource access tokens available' do
|
||||
def created_token
|
||||
PersonalAccessToken.order(:created_at).last
|
||||
|
|
|
|||
Loading…
Reference in New Issue