Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
507c0e71cd
commit
0cbb4a7569
80 changed files with 891 additions and 377 deletions
|
@ -48,3 +48,4 @@ overrides:
|
|||
- '**/spec/**/*'
|
||||
rules:
|
||||
"@gitlab/require-i18n-strings": off
|
||||
"@gitlab/no-runtime-template-compiler": off
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import Vue from 'vue';
|
||||
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||
import UsagePingDisabled from './components/usage_ping_disabled.vue';
|
||||
import AdminUsersApp from './components/app.vue';
|
||||
|
||||
export default function (el = document.querySelector('#js-admin-users-app')) {
|
||||
export const initAdminUsersApp = (el = document.querySelector('#js-admin-users-app')) => {
|
||||
if (!el) {
|
||||
return false;
|
||||
}
|
||||
|
@ -19,4 +20,24 @@ export default function (el = document.querySelector('#js-admin-users-app')) {
|
|||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const initCohortsEmptyState = (el = document.querySelector('#js-cohorts-empty-state')) => {
|
||||
if (!el) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { emptyStateSvgPath, enableUsagePingLink, docsLink } = el.dataset;
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
provide: {
|
||||
svgPath: emptyStateSvgPath,
|
||||
primaryButtonPath: enableUsagePingLink,
|
||||
docsLink,
|
||||
},
|
||||
render(h) {
|
||||
return h(UsagePingDisabled);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
23
app/assets/javascripts/admin/users/tabs.js
Normal file
23
app/assets/javascripts/admin/users/tabs.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { historyPushState } from '~/lib/utils/common_utils';
|
||||
import { mergeUrlParams } from '~/lib/utils/url_utility';
|
||||
|
||||
const COHORTS_PANE = 'cohorts';
|
||||
|
||||
const tabClickHandler = (e) => {
|
||||
const { hash } = e.currentTarget;
|
||||
const tab = hash === `#${COHORTS_PANE}` ? COHORTS_PANE : null;
|
||||
const newUrl = mergeUrlParams({ tab }, window.location.href);
|
||||
historyPushState(newUrl);
|
||||
};
|
||||
|
||||
const initTabs = () => {
|
||||
const tabLinks = document.querySelectorAll('.js-users-tab-item a');
|
||||
|
||||
if (tabLinks.length) {
|
||||
tabLinks.forEach((tabLink) => {
|
||||
tabLink.addEventListener('click', (e) => tabClickHandler(e));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default initTabs;
|
|
@ -1,4 +1,7 @@
|
|||
/* eslint-disable no-new */
|
||||
// This is a true violation of @gitlab/no-runtime-template-compiler, as it
|
||||
// relies on app/views/shared/boards/components/_sidebar.html.haml for its
|
||||
// template.
|
||||
/* eslint-disable no-new, @gitlab/no-runtime-template-compiler */
|
||||
|
||||
import $ from 'jquery';
|
||||
import Vue from 'vue';
|
||||
|
|
52
app/assets/javascripts/boards/components/toggle_focus.vue
Normal file
52
app/assets/javascripts/boards/components/toggle_focus.vue
Normal file
|
@ -0,0 +1,52 @@
|
|||
<script>
|
||||
import { GlIcon } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import { hide } from '~/tooltips';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlIcon,
|
||||
},
|
||||
props: {
|
||||
issueBoardsContentSelector: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isFullscreen: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
toggleFocusMode() {
|
||||
hide(this.$refs.toggleFocusModeButton);
|
||||
|
||||
const issueBoardsContent = document.querySelector(this.issueBoardsContentSelector);
|
||||
issueBoardsContent.classList.toggle('is-focused');
|
||||
|
||||
this.isFullscreen = !this.isFullscreen;
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
toggleFocusMode: __('Toggle focus mode'),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="board-extra-actions">
|
||||
<a
|
||||
ref="toggleFocusModeButton"
|
||||
href="#"
|
||||
class="btn btn-default has-tooltip gl-ml-3 js-focus-mode-btn"
|
||||
data-qa-selector="focus_mode_button"
|
||||
role="button"
|
||||
:aria-label="$options.i18n.toggleFocusMode"
|
||||
:title="$options.i18n.toggleFocusMode"
|
||||
@click="toggleFocusMode"
|
||||
>
|
||||
<gl-icon :name="isFullscreen ? 'minimize' : 'maximize'" />
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
|
@ -1,45 +1,17 @@
|
|||
import $ from 'jquery';
|
||||
import Vue from 'vue';
|
||||
import { GlIcon } from '@gitlab/ui';
|
||||
import { hide } from '~/tooltips';
|
||||
import ToggleFocus from './components/toggle_focus.vue';
|
||||
|
||||
export default (ModalStore, boardsStore) => {
|
||||
const issueBoardsContent = document.querySelector('.content-wrapper > .js-focus-mode-board');
|
||||
export default () => {
|
||||
const issueBoardsContentSelector = '.content-wrapper > .js-focus-mode-board';
|
||||
|
||||
return new Vue({
|
||||
el: document.getElementById('js-toggle-focus-btn'),
|
||||
components: {
|
||||
GlIcon,
|
||||
el: '#js-toggle-focus-btn',
|
||||
render(h) {
|
||||
return h(ToggleFocus, {
|
||||
props: {
|
||||
issueBoardsContentSelector,
|
||||
},
|
||||
});
|
||||
},
|
||||
data: {
|
||||
modal: ModalStore.store,
|
||||
store: boardsStore.state,
|
||||
isFullscreen: false,
|
||||
},
|
||||
methods: {
|
||||
toggleFocusMode() {
|
||||
const $el = $(this.$refs.toggleFocusModeButton);
|
||||
hide($el);
|
||||
|
||||
issueBoardsContent.classList.toggle('is-focused');
|
||||
|
||||
this.isFullscreen = !this.isFullscreen;
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<div class="board-extra-actions">
|
||||
<a
|
||||
href="#"
|
||||
class="btn btn-default has-tooltip gl-ml-3 js-focus-mode-btn"
|
||||
data-qa-selector="focus_mode_button"
|
||||
role="button"
|
||||
aria-label="Toggle focus mode"
|
||||
title="Toggle focus mode"
|
||||
ref="toggleFocusModeButton"
|
||||
@click="toggleFocusMode">
|
||||
<gl-icon :name="isFullscreen ? 'minimize' : 'maximize'" />
|
||||
</a>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
// This is a true violation of @gitlab/no-runtime-template-compiler, as it
|
||||
// relies on app/views/projects/cycle_analytics/show.html.haml for its
|
||||
// template.
|
||||
/* eslint-disable @gitlab/no-runtime-template-compiler */
|
||||
import $ from 'jquery';
|
||||
import Vue from 'vue';
|
||||
import Cookies from 'js-cookie';
|
||||
|
|
|
@ -28,19 +28,18 @@ class RecentSearchesRoot {
|
|||
const { state } = this.store;
|
||||
this.vm = new Vue({
|
||||
el: this.wrapperElement,
|
||||
components: {
|
||||
RecentSearchesDropdownContent,
|
||||
},
|
||||
data() {
|
||||
return state;
|
||||
},
|
||||
template: `
|
||||
<recent-searches-dropdown-content
|
||||
:items="recentSearches"
|
||||
:is-local-storage-available="isLocalStorageAvailable"
|
||||
:allowed-keys="allowedKeys"
|
||||
/>
|
||||
`,
|
||||
render(h) {
|
||||
return h(RecentSearchesDropdownContent, {
|
||||
props: {
|
||||
items: this.recentSearches,
|
||||
isLocalStorageAvailable: this.isLocalStorageAvailable,
|
||||
allowedKeys: this.allowedKeys,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
/* eslint-disable no-param-reassign */
|
||||
// This is a true violation of @gitlab/no-runtime-template-compiler, as it relies on
|
||||
// app/views/projects/merge_requests/conflicts/components/_diff_file_editor.html.haml
|
||||
// for its template.
|
||||
/* eslint-disable no-param-reassign, @gitlab/no-runtime-template-compiler */
|
||||
|
||||
import Vue from 'vue';
|
||||
import { debounce } from 'lodash';
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
/* eslint-disable no-param-reassign */
|
||||
// This is a true violation of @gitlab/no-runtime-template-compiler, as it relies on
|
||||
// app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml
|
||||
// for its template.
|
||||
/* eslint-disable no-param-reassign, @gitlab/no-runtime-template-compiler */
|
||||
|
||||
import Vue from 'vue';
|
||||
import actionsMixin from '../mixins/line_conflict_actions';
|
||||
|
|
|
@ -15,6 +15,9 @@ import utilsMixin from '../mixins/line_conflict_utils';
|
|||
required: true,
|
||||
},
|
||||
},
|
||||
// This is a true violation of @gitlab/no-runtime-template-compiler, as it
|
||||
// has a template string.
|
||||
// eslint-disable-next-line @gitlab/no-runtime-template-compiler
|
||||
template: `
|
||||
<table class="diff-wrap-lines code js-syntax-highlight">
|
||||
<tr class="line_holder parallel" v-for="section in file.parallelLines">
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
// This is a true violation of @gitlab/no-runtime-template-compiler, as it
|
||||
// relies on app/views/projects/merge_requests/conflicts/show.html.haml for its
|
||||
// template.
|
||||
/* eslint-disable @gitlab/no-runtime-template-compiler */
|
||||
import $ from 'jquery';
|
||||
import Vue from 'vue';
|
||||
import FileIcon from '~/vue_shared/components/file_icon.vue';
|
||||
|
|
|
@ -26,7 +26,24 @@ export default (props = {}) => {
|
|||
dashboardProps: { ...dataProps, ...props },
|
||||
};
|
||||
},
|
||||
template: `<router-view :dashboardProps="dashboardProps"/>`,
|
||||
render(h) {
|
||||
return h('RouterView', {
|
||||
// This is attrs rather than props because:
|
||||
// 1. RouterView only actually defines one prop: `name`.
|
||||
// 2. The RouterView [throws away other props][1] given to it, in
|
||||
// favour of those configured in the route config/params.
|
||||
// 3. The Vue template compiler itself in general compiles anything
|
||||
// like <some-component :foo="bar" /> into roughly
|
||||
// h('some-component', { attrs: { foo: bar } }). Then later, Vue
|
||||
// [extract props from attrs and merges them with props][2],
|
||||
// matching them up according to the component's definition.
|
||||
// [1]: https://github.com/vuejs/vue-router/blob/v3.4.9/src/components/view.js#L124
|
||||
// [2]: https://github.com/vuejs/vue/blob/v2.6.12/src/core/vdom/helpers/extract-props.js#L12-L50
|
||||
attrs: {
|
||||
dashboardProps: this.dashboardProps,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -35,16 +35,16 @@ const showPopover = (el, path, footer, options) => {
|
|||
boundary: 'window',
|
||||
html: true,
|
||||
placement: 'top',
|
||||
template: `<div class="popover blue learn-gitlab d-none d-xl-block" role="tooltip">
|
||||
template: `<div class="gl-popover popover blue learn-gitlab d-none d-xl-block" role="tooltip">
|
||||
<div class="arrow"></div>
|
||||
<div class="close cursor-pointer gl-font-base text-white gl-opacity-10 p-2">✕</div>
|
||||
<div class="popover-body gl-font-base gl-line-height-20 pb-0 px-3"></div>
|
||||
<div class="bold text-right text-white p-2">${footer}</div>
|
||||
<div class="js-close-learn-gitlab gl-font-weight-bold gl-line-height-normal float-right gl-cursor-pointer gl-font-base gl-text-white gl-opacity-10 gl-p-3">✕</div>
|
||||
<div class="popover-body gl-font-base"></div>
|
||||
<div class="gl-font-weight-bold gl-text-right gl-text-white gl-p-3 gl-pt-0">${footer}</div>
|
||||
</div>`,
|
||||
};
|
||||
|
||||
// When one of the popovers is dismissed, remove the cookie.
|
||||
const closeButton = () => document.querySelector('.learn-gitlab.popover .close');
|
||||
const closeButton = () => document.querySelector('.js-close-learn-gitlab');
|
||||
|
||||
// We still have to use jQuery, since Bootstrap's Popover is based on jQuery.
|
||||
const jQueryEl = $(el);
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
// This is a true violation of @gitlab/no-runtime-template-compiler, as it
|
||||
// relies on app/views/admin/application_settings/_gitpod.html.haml for its
|
||||
// template.
|
||||
/* eslint-disable @gitlab/no-runtime-template-compiler */
|
||||
import Vue from 'vue';
|
||||
import initUserInternalRegexPlaceholder from '../account_and_limits';
|
||||
import IntegrationHelpText from '~/vue_shared/components/integrations_help_text.vue';
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
import Vue from 'vue';
|
||||
import UsagePingDisabled from '~/admin/cohorts/components/usage_ping_disabled.vue';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const emptyStateContainer = document.getElementById('js-cohorts-empty-state');
|
||||
|
||||
if (!emptyStateContainer) return false;
|
||||
|
||||
const { emptyStateSvgPath, enableUsagePingLink, docsLink } = emptyStateContainer.dataset;
|
||||
|
||||
return new Vue({
|
||||
el: emptyStateContainer,
|
||||
provide: {
|
||||
svgPath: emptyStateSvgPath,
|
||||
primaryButtonPath: enableUsagePingLink,
|
||||
docsLink,
|
||||
},
|
||||
render(h) {
|
||||
return h(UsagePingDisabled);
|
||||
},
|
||||
});
|
||||
});
|
|
@ -4,7 +4,8 @@ import Translate from '~/vue_shared/translate';
|
|||
import ModalManager from './components/user_modal_manager.vue';
|
||||
import csrf from '~/lib/utils/csrf';
|
||||
import initConfirmModal from '~/confirm_modal';
|
||||
import initAdminUsersApp from '~/admin/users';
|
||||
import { initAdminUsersApp, initCohortsEmptyState } from '~/admin/users';
|
||||
import initTabs from '~/admin/users/tabs';
|
||||
|
||||
const MODAL_TEXTS_CONTAINER_SELECTOR = '#js-modal-texts';
|
||||
const MODAL_MANAGER_SELECTOR = '#js-delete-user-modal';
|
||||
|
@ -58,4 +59,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
|
||||
initConfirmModal();
|
||||
initAdminUsersApp();
|
||||
initCohortsEmptyState();
|
||||
initTabs();
|
||||
});
|
||||
|
|
|
@ -7,10 +7,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
remainingTimeElements.forEach(
|
||||
(el) =>
|
||||
new Vue({
|
||||
...GlCountdown,
|
||||
el,
|
||||
propsData: {
|
||||
endDateString: el.dateTime,
|
||||
render(h) {
|
||||
return h(GlCountdown, {
|
||||
props: {
|
||||
endDateString: el.dateTime,
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
// This is a false violation of @gitlab/no-runtime-template-compiler, since it
|
||||
// is simply defining a global Vue mixin.
|
||||
/* eslint-disable @gitlab/no-runtime-template-compiler */
|
||||
const ComponentPerformancePlugin = {
|
||||
install(Vue, options) {
|
||||
Vue.mixin({
|
||||
|
|
|
@ -114,7 +114,7 @@ export default {
|
|||
<div role="rowheader" class="table-mobile-header">{{ __('Status') }}</div>
|
||||
<div class="table-mobile-content text-center">
|
||||
<div
|
||||
class="add-border ci-status-icon d-flex align-items-center justify-content-end justify-content-md-center"
|
||||
class="ci-status-icon d-flex align-items-center justify-content-end justify-content-md-center"
|
||||
:class="`ci-status-icon-${testCase.status}`"
|
||||
>
|
||||
<gl-icon :size="24" :name="testCase.icon" />
|
||||
|
|
|
@ -4,16 +4,16 @@ import { TestStatus } from '../../constants';
|
|||
export function iconForTestStatus(status) {
|
||||
switch (status) {
|
||||
case TestStatus.SUCCESS:
|
||||
return 'status_success_borderless';
|
||||
return 'status_success';
|
||||
case TestStatus.FAILED:
|
||||
return 'status_failed_borderless';
|
||||
return 'status_failed';
|
||||
case TestStatus.ERROR:
|
||||
return 'status_warning_borderless';
|
||||
return 'status_warning';
|
||||
case TestStatus.SKIPPED:
|
||||
return 'status_skipped_borderless';
|
||||
return 'status_skipped';
|
||||
case TestStatus.UNKNOWN:
|
||||
default:
|
||||
return 'status_notfound_borderless';
|
||||
return 'status_notfound';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlBadge, GlIcon, GlLink, GlSprintf, GlTable, GlTooltip } from '@gitlab/ui';
|
||||
import { GlAlert, GlBadge, GlIcon, GlLink, GlSprintf, GlTable, GlTooltip } from '@gitlab/ui';
|
||||
import { s__ } from '~/locale';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
|
||||
|
@ -10,6 +10,7 @@ import timeagoMixin from '~/vue_shared/mixins/timeago';
|
|||
export default {
|
||||
components: {
|
||||
CiBadge,
|
||||
GlAlert,
|
||||
GlBadge,
|
||||
GlIcon,
|
||||
GlLink,
|
||||
|
@ -105,6 +106,7 @@ export default {
|
|||
:items="states"
|
||||
:fields="fields"
|
||||
data-testid="terraform-states-table"
|
||||
details-td-class="gl-p-0!"
|
||||
fixed
|
||||
stacked="md"
|
||||
>
|
||||
|
@ -189,5 +191,21 @@ export default {
|
|||
<template v-if="terraformAdmin" #cell(actions)="{ item }">
|
||||
<state-actions :state="item" />
|
||||
</template>
|
||||
|
||||
<template #row-details="row">
|
||||
<gl-alert
|
||||
data-testid="terraform-states-table-error"
|
||||
variant="danger"
|
||||
@dismiss="row.toggleDetails"
|
||||
>
|
||||
<span
|
||||
v-for="errorMessage in row.item.errorMessages"
|
||||
:key="errorMessage"
|
||||
class="gl-display-flex gl-justify-content-start"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</span>
|
||||
</gl-alert>
|
||||
</template>
|
||||
</gl-table>
|
||||
</template>
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
GlSprintf,
|
||||
} from '@gitlab/ui';
|
||||
import { s__ } from '~/locale';
|
||||
import addDataToState from '../graphql/mutations/add_data_to_state.mutation.graphql';
|
||||
import lockState from '../graphql/mutations/lock_state.mutation.graphql';
|
||||
import unlockState from '../graphql/mutations/unlock_state.mutation.graphql';
|
||||
import removeState from '../graphql/mutations/remove_state.mutation.graphql';
|
||||
|
@ -33,13 +34,13 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
showRemoveModal: false,
|
||||
removeConfirmText: '',
|
||||
};
|
||||
},
|
||||
i18n: {
|
||||
downloadJSON: s__('Terraform|Download JSON'),
|
||||
errorUpdate: s__('Terraform|An error occurred while changing the state file'),
|
||||
lock: s__('Terraform|Lock'),
|
||||
modalBody: s__(
|
||||
'Terraform|You are about to remove the State file %{name}. This will permanently delete all the State versions and history. The infrastructure provisioned previously will remain intact, only the state file with all its versions are to be removed. This action is non-revertible.',
|
||||
|
@ -76,19 +77,37 @@ export default {
|
|||
this.removeConfirmText = '';
|
||||
},
|
||||
lock() {
|
||||
this.stateMutation(lockState);
|
||||
this.stateActionMutation(lockState);
|
||||
},
|
||||
unlock() {
|
||||
this.stateMutation(unlockState);
|
||||
this.stateActionMutation(unlockState);
|
||||
},
|
||||
updateStateCache(newData) {
|
||||
this.$apollo.mutate({
|
||||
mutation: addDataToState,
|
||||
variables: {
|
||||
terraformState: {
|
||||
...this.state,
|
||||
...newData,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
remove() {
|
||||
if (!this.disableModalSubmit) {
|
||||
this.hideModal();
|
||||
this.stateMutation(removeState);
|
||||
this.stateActionMutation(removeState);
|
||||
}
|
||||
},
|
||||
stateMutation(mutation) {
|
||||
this.loading = true;
|
||||
stateActionMutation(mutation) {
|
||||
let errorMessages = [];
|
||||
|
||||
this.updateStateCache({
|
||||
_showDetails: false,
|
||||
errorMessages,
|
||||
loadingActions: true,
|
||||
});
|
||||
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation,
|
||||
|
@ -99,9 +118,22 @@ export default {
|
|||
awaitRefetchQueries: true,
|
||||
notifyOnNetworkStatusChange: true,
|
||||
})
|
||||
.catch(() => {})
|
||||
.then(({ data }) => {
|
||||
errorMessages =
|
||||
data?.terraformStateDelete?.errors ||
|
||||
data?.terraformStateLock?.errors ||
|
||||
data?.terraformStateUnlock?.errors ||
|
||||
[];
|
||||
})
|
||||
.catch(() => {
|
||||
errorMessages = [this.$options.i18n.errorUpdate];
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
this.updateStateCache({
|
||||
_showDetails: Boolean(errorMessages.length),
|
||||
errorMessages,
|
||||
loadingActions: false,
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
|
@ -114,7 +146,7 @@ export default {
|
|||
icon="ellipsis_v"
|
||||
right
|
||||
:data-testid="`terraform-state-actions-${state.name}`"
|
||||
:disabled="loading"
|
||||
:disabled="state.loadingActions"
|
||||
toggle-class="gl-px-3! gl-shadow-none!"
|
||||
>
|
||||
<template #button-content>
|
||||
|
|
|
@ -2,6 +2,10 @@
|
|||
#import "./state_version.fragment.graphql"
|
||||
|
||||
fragment State on TerraformState {
|
||||
_showDetails @client
|
||||
errorMessages @client
|
||||
loadingActions @client
|
||||
|
||||
id
|
||||
name
|
||||
lockedAt
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
mutation addDataToTerraformState($terraformState: State!) {
|
||||
addDataToTerraformState(terraformState: $terraformState) @client
|
||||
}
|
41
app/assets/javascripts/terraform/graphql/resolvers.js
Normal file
41
app/assets/javascripts/terraform/graphql/resolvers.js
Normal file
|
@ -0,0 +1,41 @@
|
|||
import TerraformState from './fragments/state.fragment.graphql';
|
||||
|
||||
export default {
|
||||
TerraformState: {
|
||||
_showDetails: (state) => {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
return state._showDetails || false;
|
||||
},
|
||||
errorMessages: (state) => {
|
||||
return state.errorMessages || [];
|
||||
},
|
||||
loadingActions: (state) => {
|
||||
return state.loadingActions || false;
|
||||
},
|
||||
},
|
||||
Mutation: {
|
||||
addDataToTerraformState: (_, { terraformState }, { client }) => {
|
||||
const fragmentData = {
|
||||
id: terraformState.id,
|
||||
fragment: TerraformState,
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
fragmentName: 'State',
|
||||
};
|
||||
|
||||
const previousTerraformState = client.readFragment(fragmentData);
|
||||
|
||||
if (previousTerraformState) {
|
||||
client.writeFragment({
|
||||
...fragmentData,
|
||||
data: {
|
||||
...previousTerraformState,
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
_showDetails: terraformState._showDetails,
|
||||
errorMessages: terraformState.errorMessages,
|
||||
loadingActions: terraformState.loadingActions,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
|
@ -1,7 +1,9 @@
|
|||
import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
|
||||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import TerraformList from './components/terraform_list.vue';
|
||||
import createDefaultClient from '~/lib/graphql';
|
||||
import resolvers from './graphql/resolvers';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
|
@ -12,7 +14,13 @@ export default () => {
|
|||
return null;
|
||||
}
|
||||
|
||||
const defaultClient = createDefaultClient();
|
||||
const defaultClient = createDefaultClient(resolvers, {
|
||||
cacheConfig: {
|
||||
dataIdFromObject: (object) => {
|
||||
return object.id || defaultDataIdFromObject(object);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { emptyStateImage, projectPath } = el.dataset;
|
||||
|
||||
|
|
|
@ -177,7 +177,7 @@ export default {
|
|||
</template>
|
||||
<template v-else-if="hasPipeline">
|
||||
<a :href="status.details_path" class="align-self-start gl-mr-3">
|
||||
<ci-icon :status="status" :size="24" :borderless="true" class="add-border" />
|
||||
<ci-icon :status="status" :size="24" />
|
||||
</a>
|
||||
<div class="ci-widget-container d-flex">
|
||||
<div class="ci-widget-content">
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
// This is a false violation of @gitlab/no-runtime-template-compiler, since it
|
||||
// creates a new Vue instance by spreading a _valid_ Vue component definition
|
||||
// into the Vue constructor.
|
||||
/* eslint-disable @gitlab/no-runtime-template-compiler */
|
||||
import Vue from 'vue';
|
||||
import MrWidgetOptions from 'ee_else_ce/vue_merge_request_widget/mr_widget_options.vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
// This is a false violation of @gitlab/no-runtime-template-compiler, since it
|
||||
// is simply defining a global Vue mixin.
|
||||
/* eslint-disable @gitlab/no-runtime-template-compiler */
|
||||
export default (Vue) => {
|
||||
Vue.mixin({
|
||||
provide: {
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
// This is a false violation of @gitlab/no-runtime-template-compiler, since it
|
||||
// is simply defining a global Vue mixin.
|
||||
/* eslint-disable @gitlab/no-runtime-template-compiler */
|
||||
import { __, n__, s__, sprintf } from '../locale';
|
||||
|
||||
export default (Vue) => {
|
||||
|
|
|
@ -3,10 +3,6 @@
|
|||
svg {
|
||||
fill: $green-500;
|
||||
}
|
||||
|
||||
&.add-border {
|
||||
@include borderless-status-icon($green-500);
|
||||
}
|
||||
}
|
||||
|
||||
.ci-status-icon-error,
|
||||
|
@ -14,10 +10,6 @@
|
|||
svg {
|
||||
fill: $red-500;
|
||||
}
|
||||
|
||||
&.add-border {
|
||||
@include borderless-status-icon($red-500);
|
||||
}
|
||||
}
|
||||
|
||||
.ci-status-icon-pending,
|
||||
|
@ -27,31 +19,21 @@
|
|||
svg {
|
||||
fill: $orange-500;
|
||||
}
|
||||
|
||||
&.add-border {
|
||||
@include borderless-status-icon($orange-500);
|
||||
}
|
||||
}
|
||||
|
||||
.ci-status-icon-running {
|
||||
svg {
|
||||
fill: $blue-400;
|
||||
}
|
||||
|
||||
&.add-border {
|
||||
@include borderless-status-icon($blue-400);
|
||||
}
|
||||
}
|
||||
|
||||
.ci-status-icon-canceled,
|
||||
.ci-status-icon-disabled {
|
||||
.ci-status-icon-disabled,
|
||||
.ci-status-icon-scheduled,
|
||||
.ci-status-icon-manual {
|
||||
svg {
|
||||
fill: $gl-text-color;
|
||||
}
|
||||
|
||||
&.add-border {
|
||||
@include borderless-status-icon($gl-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
.ci-status-icon-preparing,
|
||||
|
@ -61,17 +43,6 @@
|
|||
svg {
|
||||
fill: var(--gray-400, $gray-400);
|
||||
}
|
||||
|
||||
&.add-border {
|
||||
@include borderless-status-icon(var(--gray-400, $gray-400));
|
||||
}
|
||||
}
|
||||
|
||||
.ci-status-icon-scheduled,
|
||||
.ci-status-icon-manual {
|
||||
svg {
|
||||
fill: $gl-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-link {
|
||||
|
|
|
@ -354,13 +354,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
@mixin borderless-status-icon($color) {
|
||||
svg {
|
||||
border: 1px solid $color;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin emoji-menu-toggle-button {
|
||||
line-height: 1;
|
||||
padding: 0;
|
||||
|
|
|
@ -1,19 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Admin::CohortsController < Admin::ApplicationController
|
||||
include Analytics::UniqueVisitsHelper
|
||||
|
||||
track_unique_visits :index, target_id: 'i_analytics_cohorts'
|
||||
|
||||
feature_category :devops_reports
|
||||
|
||||
# Backwards compatibility. Remove it and routing in 14.0
|
||||
# @see https://gitlab.com/gitlab-org/gitlab/-/issues/299303
|
||||
def index
|
||||
if Gitlab::CurrentSettings.usage_ping_enabled
|
||||
cohorts_results = Rails.cache.fetch('cohorts', expires_in: 1.day) do
|
||||
CohortsService.new.execute
|
||||
end
|
||||
|
||||
@cohorts = CohortsSerializer.new.represent(cohorts_results)
|
||||
end
|
||||
redirect_to admin_users_path(tab: 'cohorts')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
class Admin::UsersController < Admin::ApplicationController
|
||||
include RoutableActions
|
||||
include Analytics::UniqueVisitsHelper
|
||||
|
||||
before_action :user, except: [:index, :new, :create]
|
||||
before_action :check_impersonation_availability, only: :impersonate
|
||||
|
@ -15,6 +16,10 @@ class Admin::UsersController < Admin::ApplicationController
|
|||
@users = @users.includes(:authorized_projects) # rubocop: disable CodeReuse/ActiveRecord
|
||||
@users = @users.sort_by_attribute(@sort = params[:sort])
|
||||
@users = @users.page(params[:page])
|
||||
|
||||
@cohorts = load_cohorts
|
||||
|
||||
track_cohorts_visit if params[:tab] == 'cohorts'
|
||||
end
|
||||
|
||||
def show
|
||||
|
@ -307,6 +312,22 @@ class Admin::UsersController < Admin::ApplicationController
|
|||
def log_impersonation_event
|
||||
Gitlab::AppLogger.info(_("User %{current_user_username} has started impersonating %{username}") % { current_user_username: current_user.username, username: user.username })
|
||||
end
|
||||
|
||||
def load_cohorts
|
||||
if Gitlab::CurrentSettings.usage_ping_enabled
|
||||
cohorts_results = Rails.cache.fetch('cohorts', expires_in: 1.day) do
|
||||
CohortsService.new.execute
|
||||
end
|
||||
|
||||
CohortsSerializer.new.represent(cohorts_results)
|
||||
end
|
||||
end
|
||||
|
||||
def track_cohorts_visit
|
||||
if request.format.html? && request.headers['DNT'] != '1'
|
||||
track_visit('i_analytics_cohorts')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Admin::UsersController.prepend_if_ee('EE::Admin::UsersController')
|
||||
|
|
|
@ -86,7 +86,7 @@ class Projects::ForksController < Projects::ApplicationController
|
|||
|
||||
def fork_service
|
||||
strong_memoize(:fork_service) do
|
||||
::Projects::ForkService.new(project, current_user, namespace: fork_namespace)
|
||||
::Projects::ForkService.new(project, current_user, fork_params)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -96,6 +96,12 @@ class Projects::ForksController < Projects::ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def fork_params
|
||||
params.permit(:path, :name, :description, :visibility).tap do |param|
|
||||
param[:namespace] = fork_namespace
|
||||
end
|
||||
end
|
||||
|
||||
def authorize_fork_namespace!
|
||||
access_denied! unless fork_namespace && fork_service.valid_fork_target?
|
||||
end
|
||||
|
|
|
@ -64,7 +64,7 @@ module NavHelper
|
|||
end
|
||||
|
||||
def admin_analytics_nav_links
|
||||
%w(dev_ops_report cohorts)
|
||||
%w(dev_ops_report)
|
||||
end
|
||||
|
||||
def group_issues_sub_menu_items
|
||||
|
|
|
@ -24,7 +24,9 @@ module Pages
|
|||
@queue.close
|
||||
|
||||
@logger.info("Waiting for threads to finish...")
|
||||
threads.each(&:join)
|
||||
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
|
||||
threads.each(&:join)
|
||||
end
|
||||
|
||||
{ migrated: @migrated, errored: @errored }
|
||||
end
|
||||
|
|
|
@ -43,8 +43,8 @@ module Projects
|
|||
def new_fork_params
|
||||
new_params = {
|
||||
forked_from_project: @project,
|
||||
visibility_level: allowed_visibility_level,
|
||||
description: @project.description,
|
||||
visibility_level: target_visibility_level,
|
||||
description: target_description,
|
||||
name: target_name,
|
||||
path: target_path,
|
||||
shared_runners_enabled: @project.shared_runners_enabled,
|
||||
|
@ -107,6 +107,10 @@ module Projects
|
|||
@target_name ||= @params[:name] || @project.name
|
||||
end
|
||||
|
||||
def target_description
|
||||
@target_description ||= @params[:description] || @project.description
|
||||
end
|
||||
|
||||
def target_namespace
|
||||
@target_namespace ||= @params[:namespace] || current_user.namespace
|
||||
end
|
||||
|
@ -115,8 +119,9 @@ module Projects
|
|||
@skip_disk_validation ||= @params[:skip_disk_validation] || false
|
||||
end
|
||||
|
||||
def allowed_visibility_level
|
||||
def target_visibility_level
|
||||
target_level = [@project.visibility_level, target_namespace.visibility_level].min
|
||||
target_level = [target_level, Gitlab::VisibilityLevel.level_value(params[:visibility])].min if params.key?(:visibility)
|
||||
|
||||
Gitlab::VisibilityLevel.closest_allowed_level(target_level)
|
||||
end
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
- breadcrumb_title _("Cohorts")
|
||||
- page_title _("Cohorts")
|
||||
|
||||
- if @cohorts
|
||||
= render 'cohorts_table'
|
||||
- else
|
90
app/views/admin/users/_users.html.haml
Normal file
90
app/views/admin/users/_users.html.haml
Normal file
|
@ -0,0 +1,90 @@
|
|||
.top-area.scrolling-tabs-container.inner-page-scroll-tabs
|
||||
.fade-left
|
||||
= sprite_icon('chevron-lg-left', size: 12)
|
||||
.fade-right
|
||||
= sprite_icon('chevron-lg-right', size: 12)
|
||||
%ul.nav-links.nav.nav-tabs.scrolling-tabs
|
||||
= nav_link(html_options: { class: active_when(params[:filter].nil?) }) do
|
||||
= link_to admin_users_path do
|
||||
= s_('AdminUsers|Active')
|
||||
%small.badge.badge-pill= limited_counter_with_delimiter(User.active_without_ghosts)
|
||||
= nav_link(html_options: { class: active_when(params[:filter] == 'admins') }) do
|
||||
= link_to admin_users_path(filter: "admins") do
|
||||
= s_('AdminUsers|Admins')
|
||||
%small.badge.badge-pill= limited_counter_with_delimiter(User.admins)
|
||||
= nav_link(html_options: { class: "#{active_when(params[:filter] == 'two_factor_enabled')} filter-two-factor-enabled" }) do
|
||||
= link_to admin_users_path(filter: 'two_factor_enabled') do
|
||||
= s_('AdminUsers|2FA Enabled')
|
||||
%small.badge.badge-pill= limited_counter_with_delimiter(User.with_two_factor)
|
||||
= nav_link(html_options: { class: "#{active_when(params[:filter] == 'two_factor_disabled')} filter-two-factor-disabled" }) do
|
||||
= link_to admin_users_path(filter: 'two_factor_disabled') do
|
||||
= s_('AdminUsers|2FA Disabled')
|
||||
%small.badge.badge-pill= limited_counter_with_delimiter(User.without_two_factor)
|
||||
= nav_link(html_options: { class: active_when(params[:filter] == 'external') }) do
|
||||
= link_to admin_users_path(filter: 'external') do
|
||||
= s_('AdminUsers|External')
|
||||
%small.badge.badge-pill= limited_counter_with_delimiter(User.external)
|
||||
= nav_link(html_options: { class: active_when(params[:filter] == 'blocked') }) do
|
||||
= link_to admin_users_path(filter: "blocked") do
|
||||
= s_('AdminUsers|Blocked')
|
||||
%small.badge.badge-pill= limited_counter_with_delimiter(User.blocked)
|
||||
= nav_link(html_options: { class: "#{active_when(params[:filter] == 'blocked_pending_approval')} filter-blocked-pending-approval" }) do
|
||||
= link_to admin_users_path(filter: "blocked_pending_approval"), data: { qa_selector: 'pending_approval_tab' } do
|
||||
= s_('AdminUsers|Pending approval')
|
||||
%small.badge.badge-pill= limited_counter_with_delimiter(User.blocked_pending_approval)
|
||||
= nav_link(html_options: { class: active_when(params[:filter] == 'deactivated') }) do
|
||||
= link_to admin_users_path(filter: "deactivated") do
|
||||
= s_('AdminUsers|Deactivated')
|
||||
%small.badge.badge-pill= limited_counter_with_delimiter(User.deactivated)
|
||||
= nav_link(html_options: { class: active_when(params[:filter] == 'wop') }) do
|
||||
= link_to admin_users_path(filter: "wop") do
|
||||
= s_('AdminUsers|Without projects')
|
||||
%small.badge.badge-pill= limited_counter_with_delimiter(User.without_projects)
|
||||
.nav-controls
|
||||
= render_if_exists 'admin/users/admin_email_users'
|
||||
= render_if_exists 'admin/users/admin_export_user_permissions'
|
||||
= link_to s_('AdminUsers|New user'), new_admin_user_path, class: 'btn gl-button btn-success btn-search float-right'
|
||||
|
||||
.filtered-search-block.row-content-block.border-top-0
|
||||
= form_tag admin_users_path, method: :get do
|
||||
- if params[:filter].present?
|
||||
= hidden_field_tag "filter", h(params[:filter])
|
||||
.search-holder
|
||||
.search-field-holder.gl-mb-4
|
||||
= search_field_tag :search_query, params[:search_query], placeholder: s_('AdminUsers|Search by name, email or username'), class: 'form-control search-text-input js-search-input', spellcheck: false, data: { qa_selector: 'user_search_field' }
|
||||
- if @sort.present?
|
||||
= hidden_field_tag :sort, @sort
|
||||
= sprite_icon('search', css_class: 'search-icon')
|
||||
= button_tag s_('AdminUsers|Search users') if Rails.env.test?
|
||||
.dropdown.user-sort-dropdown
|
||||
= label_tag 'Sort by', nil, class: 'label-bold'
|
||||
- toggle_text = @sort.present? ? users_sort_options_hash[@sort] : sort_title_name
|
||||
= dropdown_toggle(toggle_text, { toggle: 'dropdown' })
|
||||
%ul.dropdown-menu.dropdown-menu-right
|
||||
%li.dropdown-header
|
||||
= s_('AdminUsers|Sort by')
|
||||
%li
|
||||
- users_sort_options_hash.each do |value, title|
|
||||
= link_to admin_users_path(sort: value, filter: params[:filter], search_query: params[:search_query]) do
|
||||
= title
|
||||
|
||||
- if Feature.enabled?(:vue_admin_users)
|
||||
#js-admin-users-app{ data: admin_users_data_attributes(@users) }
|
||||
.gl-spinner-container.gl-my-7
|
||||
%span.gl-vertical-align-bottom.gl-spinner.gl-spinner-dark.gl-spinner-lg{ aria: { label: _('Loading') } }
|
||||
- elsif @users.empty?
|
||||
.nothing-here-block.border-top-0
|
||||
= s_('AdminUsers|No users found')
|
||||
- else
|
||||
.table-holder
|
||||
.thead-white.text-nowrap.gl-responsive-table-row.table-row-header{ role: 'row' }
|
||||
.table-section.section-40{ role: 'rowheader' }= _('Name')
|
||||
.table-section.section-10{ role: 'rowheader' }= _('Projects')
|
||||
.table-section.section-15{ role: 'rowheader' }= _('Created on')
|
||||
.table-section.section-15{ role: 'rowheader' }= _('Last activity')
|
||||
|
||||
= render partial: 'admin/users/user', collection: @users
|
||||
|
||||
= paginate @users, theme: "gitlab"
|
||||
|
||||
= render partial: 'admin/users/modals'
|
|
@ -1,92 +1,17 @@
|
|||
- page_title _("Users")
|
||||
|
||||
.top-area.scrolling-tabs-container.inner-page-scroll-tabs
|
||||
.fade-left
|
||||
= sprite_icon('chevron-lg-left', size: 12)
|
||||
.fade-right
|
||||
= sprite_icon('chevron-lg-right', size: 12)
|
||||
%ul.nav-links.nav.nav-tabs.scrolling-tabs
|
||||
= nav_link(html_options: { class: active_when(params[:filter].nil?) }) do
|
||||
= link_to admin_users_path do
|
||||
= s_('AdminUsers|Active')
|
||||
%small.badge.badge-pill= limited_counter_with_delimiter(User.active_without_ghosts)
|
||||
= nav_link(html_options: { class: active_when(params[:filter] == 'admins') }) do
|
||||
= link_to admin_users_path(filter: "admins") do
|
||||
= s_('AdminUsers|Admins')
|
||||
%small.badge.badge-pill= limited_counter_with_delimiter(User.admins)
|
||||
= nav_link(html_options: { class: "#{active_when(params[:filter] == 'two_factor_enabled')} filter-two-factor-enabled" }) do
|
||||
= link_to admin_users_path(filter: 'two_factor_enabled') do
|
||||
= s_('AdminUsers|2FA Enabled')
|
||||
%small.badge.badge-pill= limited_counter_with_delimiter(User.with_two_factor)
|
||||
= nav_link(html_options: { class: "#{active_when(params[:filter] == 'two_factor_disabled')} filter-two-factor-disabled" }) do
|
||||
= link_to admin_users_path(filter: 'two_factor_disabled') do
|
||||
= s_('AdminUsers|2FA Disabled')
|
||||
%small.badge.badge-pill= limited_counter_with_delimiter(User.without_two_factor)
|
||||
= nav_link(html_options: { class: active_when(params[:filter] == 'external') }) do
|
||||
= link_to admin_users_path(filter: 'external') do
|
||||
= s_('AdminUsers|External')
|
||||
%small.badge.badge-pill= limited_counter_with_delimiter(User.external)
|
||||
= nav_link(html_options: { class: active_when(params[:filter] == 'blocked') }) do
|
||||
= link_to admin_users_path(filter: "blocked") do
|
||||
= s_('AdminUsers|Blocked')
|
||||
%small.badge.badge-pill= limited_counter_with_delimiter(User.blocked)
|
||||
= nav_link(html_options: { class: "#{active_when(params[:filter] == 'blocked_pending_approval')} filter-blocked-pending-approval" }) do
|
||||
= link_to admin_users_path(filter: "blocked_pending_approval"), data: { qa_selector: 'pending_approval_tab' } do
|
||||
= s_('AdminUsers|Pending approval')
|
||||
%small.badge.badge-pill= limited_counter_with_delimiter(User.blocked_pending_approval)
|
||||
= nav_link(html_options: { class: active_when(params[:filter] == 'deactivated') }) do
|
||||
= link_to admin_users_path(filter: "deactivated") do
|
||||
= s_('AdminUsers|Deactivated')
|
||||
%small.badge.badge-pill= limited_counter_with_delimiter(User.deactivated)
|
||||
= nav_link(html_options: { class: active_when(params[:filter] == 'wop') }) do
|
||||
= link_to admin_users_path(filter: "wop") do
|
||||
= s_('AdminUsers|Without projects')
|
||||
%small.badge.badge-pill= limited_counter_with_delimiter(User.without_projects)
|
||||
.nav-controls
|
||||
= render_if_exists 'admin/users/admin_email_users'
|
||||
= render_if_exists 'admin/users/admin_export_user_permissions'
|
||||
= link_to s_('AdminUsers|New user'), new_admin_user_path, class: 'btn gl-button btn-success btn-search float-right'
|
||||
%ul.nav-links.nav-tabs.nav.js-users-tabs{ role: 'tablist' }
|
||||
%li.nav-item.js-users-tab-item{ role: 'presentation' }
|
||||
%a.nav-link{ href: '#users', class: active_when(params[:tab] != 'cohorts'), data: { toggle: 'tab' }, role: 'tab' }
|
||||
= s_('AdminUsers|Users')
|
||||
%li.nav-item.js-users-tab-item{ role: 'presentation' }
|
||||
%a.nav-link{ href: '#cohorts', class: active_when(params[:tab] == 'cohorts'), data: { toggle: 'tab', track: { event: 'i_analytics_cohorts', action: 'click_tab' } }, role: 'tab' }
|
||||
= s_('AdminUsers|Cohorts')
|
||||
|
||||
.filtered-search-block.row-content-block.border-top-0
|
||||
= form_tag admin_users_path, method: :get do
|
||||
- if params[:filter].present?
|
||||
= hidden_field_tag "filter", h(params[:filter])
|
||||
.search-holder
|
||||
.search-field-holder.gl-mb-4
|
||||
= search_field_tag :search_query, params[:search_query], placeholder: s_('AdminUsers|Search by name, email or username'), class: 'form-control search-text-input js-search-input', spellcheck: false, data: { qa_selector: 'user_search_field' }
|
||||
- if @sort.present?
|
||||
= hidden_field_tag :sort, @sort
|
||||
= sprite_icon('search', css_class: 'search-icon')
|
||||
= button_tag s_('AdminUsers|Search users') if Rails.env.test?
|
||||
.dropdown.user-sort-dropdown
|
||||
= label_tag 'Sort by', nil, class: 'label-bold'
|
||||
- toggle_text = @sort.present? ? users_sort_options_hash[@sort] : sort_title_name
|
||||
= dropdown_toggle(toggle_text, { toggle: 'dropdown' })
|
||||
%ul.dropdown-menu.dropdown-menu-right
|
||||
%li.dropdown-header
|
||||
= s_('AdminUsers|Sort by')
|
||||
%li
|
||||
- users_sort_options_hash.each do |value, title|
|
||||
= link_to admin_users_path(sort: value, filter: params[:filter], search_query: params[:search_query]) do
|
||||
= title
|
||||
.tab-content
|
||||
.tab-pane{ id: 'users', class: ('active' if params[:tab] != 'cohorts') }
|
||||
= render 'users'
|
||||
.tab-pane{ id: 'cohorts', class: ('active' if params[:tab] == 'cohorts') }
|
||||
= render 'cohorts'
|
||||
|
||||
- if Feature.enabled?(:vue_admin_users)
|
||||
#js-admin-users-app{ data: admin_users_data_attributes(@users) }
|
||||
.gl-spinner-container.gl-my-7
|
||||
%span.gl-vertical-align-bottom.gl-spinner.gl-spinner-dark.gl-spinner-lg{ aria: { label: _('Loading') } }
|
||||
- elsif @users.empty?
|
||||
.nothing-here-block.border-top-0
|
||||
= s_('AdminUsers|No users found')
|
||||
- else
|
||||
.table-holder
|
||||
.thead-white.text-nowrap.gl-responsive-table-row.table-row-header{ role: 'row' }
|
||||
.table-section.section-40{ role: 'rowheader' }= _('Name')
|
||||
.table-section.section-10{ role: 'rowheader' }= _('Projects')
|
||||
.table-section.section-15{ role: 'rowheader' }= _('Created on')
|
||||
.table-section.section-15{ role: 'rowheader' }= _('Last activity')
|
||||
|
||||
= render partial: 'admin/users/user', collection: @users
|
||||
|
||||
= paginate @users, theme: "gitlab"
|
||||
|
||||
= render partial: 'admin/users/modals'
|
||||
|
|
|
@ -65,10 +65,6 @@
|
|||
= link_to admin_dev_ops_report_path, title: _('DevOps Report') do
|
||||
%span
|
||||
= _('DevOps Report')
|
||||
= nav_link(controller: :cohorts) do
|
||||
= link_to admin_cohorts_path, title: _('Cohorts') do
|
||||
%span
|
||||
= _('Cohorts')
|
||||
- if Feature.enabled?(:instance_statistics, default_enabled: true)
|
||||
= nav_link(controller: :instance_statistics) do
|
||||
= link_to admin_instance_statistics_path, title: _('Usage Trends') do
|
||||
|
|
5
changelogs/unreleased/15013-fork-to-different-name.yml
Normal file
5
changelogs/unreleased/15013-fork-to-different-name.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Support setting more attributes when forking a project
|
||||
merge_request: 51962
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add unique index on services project_id and type
|
||||
merge_request: 52563
|
||||
author:
|
||||
type: changed
|
5
changelogs/unreleased/281963-move-cohorts-to-users.yml
Normal file
5
changelogs/unreleased/281963-move-cohorts-to-users.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Move Cohorts page to Overiew-Users
|
||||
merge_request: 51707
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: Make CI Icon in merge request pipeline detail consistent with other widget
|
||||
icons
|
||||
merge_request: 52516
|
||||
author:
|
||||
type: fixed
|
5
changelogs/unreleased/terraform-state-errors-graphql.yml
Normal file
5
changelogs/unreleased/terraform-state-errors-graphql.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Display Terraform list errors to user
|
||||
merge_request: 51397
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddUniqueIndexServicesProjectIdAndType < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
INDEX_NAME = 'index_services_on_project_id_and_type_unique'
|
||||
|
||||
def up
|
||||
add_concurrent_index :services, [:project_id, :type], name: INDEX_NAME, unique: true
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index_by_name :services, name: INDEX_NAME
|
||||
end
|
||||
end
|
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoveIndexServicesProjectIdAndType < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
INDEX_NAME = 'index_services_on_project_id_and_type'
|
||||
|
||||
# Replaced by the index added in 20210126091713_add_unique_index_services_project_id_and_type.rb
|
||||
def up
|
||||
remove_concurrent_index_by_name :services, name: INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
add_concurrent_index :services, [:project_id, :type], name: INDEX_NAME
|
||||
end
|
||||
end
|
1
db/schema_migrations/20210126091713
Normal file
1
db/schema_migrations/20210126091713
Normal file
|
@ -0,0 +1 @@
|
|||
3906739d07514e6e59f79a4a81d28859a2481614a299c95ec1b1d9825a07ec64
|
1
db/schema_migrations/20210126092102
Normal file
1
db/schema_migrations/20210126092102
Normal file
|
@ -0,0 +1 @@
|
|||
124c5ae1a1ccade5dec01f72b726e03febc8f56411d7d8990f976bb2a9516037
|
|
@ -23003,7 +23003,7 @@ CREATE INDEX index_service_desk_enabled_projects_on_id_creator_id_created_at ON
|
|||
|
||||
CREATE INDEX index_services_on_inherit_from_id ON services USING btree (inherit_from_id);
|
||||
|
||||
CREATE INDEX index_services_on_project_id_and_type ON services USING btree (project_id, type);
|
||||
CREATE UNIQUE INDEX index_services_on_project_id_and_type_unique ON services USING btree (project_id, type);
|
||||
|
||||
CREATE INDEX index_services_on_template ON services USING btree (template);
|
||||
|
||||
|
|
|
@ -1280,6 +1280,8 @@ POST /projects/:id/fork
|
|||
| `namespace_path` | string | **{dotted-circle}** No | The path of the namespace that the project is forked to. |
|
||||
| `namespace` | integer/string | **{dotted-circle}** No | _(Deprecated)_ The ID or path of the namespace that the project is forked to. |
|
||||
| `path` | string | **{dotted-circle}** No | The path assigned to the resultant project after forking. |
|
||||
| `description` | string | **{dotted-circle}** No | The description assigned to the resultant project after forking. |
|
||||
| `visibility` | string | **{dotted-circle}** No | The [visibility level](#project-visibility-level) assigned to the resultant project after forking. |
|
||||
|
||||
## List Forks of a project
|
||||
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 31 KiB |
BIN
doc/user/admin_area/analytics/img/cohorts_v13_9.png
Normal file
BIN
doc/user/admin_area/analytics/img/cohorts_v13_9.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 207 KiB |
|
@ -9,14 +9,14 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
As a benefit of having the [usage ping active](../settings/usage_statistics.md),
|
||||
you can analyze your users' GitLab activities over time.
|
||||
|
||||
To see user cohorts, go to **Admin Area > Analytics > Cohorts**.
|
||||
To see user cohorts, go to **Admin Area > Overview > Users**.
|
||||
|
||||
## Overview
|
||||
|
||||
How do you interpret the user cohorts table? Let's review an example with the
|
||||
following user cohorts:
|
||||
|
||||
![User cohort example](img/cohorts_v13_4.png)
|
||||
![User cohort example](img/cohorts_v13_9.png)
|
||||
|
||||
For the cohort of March 2020, three users were added to this server and have
|
||||
been active since this month. One month later (April 2020), two users are still
|
||||
|
|
|
@ -260,7 +260,8 @@ For GitLab.com, it is set to 10 MB.
|
|||
|
||||
## Export requirements to a CSV file
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/290813) in GitLab 13.8.
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/290813) in GitLab 13.8.
|
||||
> - Revised CSV column headers [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/299247) in GitLab 13.9.
|
||||
|
||||
You can export GitLab requirements to a
|
||||
[CSV file](https://en.wikipedia.org/wiki/Comma-separated_values) sent to your default notification
|
||||
|
@ -285,11 +286,24 @@ You can preview the exported CSV file in a spreadsheet editor, such as Microsoft
|
|||
OpenOffice Calc, or Google Sheets.
|
||||
<!-- vale gitlab.Spelling = YES -->
|
||||
|
||||
The exported CSV file contains the following columns:
|
||||
The exported CSV file contains the following headers:
|
||||
|
||||
- Requirement ID
|
||||
- Title
|
||||
- Description
|
||||
- Author Username
|
||||
- Latest Test Report State
|
||||
- Latest Test Report Created At (UTC)
|
||||
- In GitLab 13.8:
|
||||
|
||||
- Requirement ID
|
||||
- Title
|
||||
- Description
|
||||
- Author Username
|
||||
- Latest Test Report State
|
||||
- Latest Test Report Created At (UTC)
|
||||
|
||||
- In GitLab 13.9 and later:
|
||||
|
||||
- Requirement ID
|
||||
- Title
|
||||
- Description
|
||||
- Author
|
||||
- Author Username
|
||||
- Created At (UTC)
|
||||
- State
|
||||
- State Updated At (UTC)
|
||||
|
|
|
@ -295,6 +295,8 @@ module API
|
|||
optional :namespace_path, type: String, desc: 'The path of the namespace that the project will be forked into'
|
||||
optional :path, type: String, desc: 'The path that will be assigned to the fork'
|
||||
optional :name, type: String, desc: 'The name that will be assigned to the fork'
|
||||
optional :description, type: String, desc: 'The description that will be assigned to the fork'
|
||||
optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The visibility of the fork'
|
||||
end
|
||||
post ':id/fork', feature_category: :source_code_management do
|
||||
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42284')
|
||||
|
|
|
@ -2203,6 +2203,9 @@ msgstr ""
|
|||
msgid "AdminUsers|Cannot unblock LDAP blocked users"
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminUsers|Cohorts"
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminUsers|Deactivate"
|
||||
msgstr ""
|
||||
|
||||
|
@ -2341,6 +2344,9 @@ msgstr ""
|
|||
msgid "AdminUsers|User will not be able to login"
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminUsers|Users"
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminUsers|When the user logs back in, their account will reactivate as a fully active account"
|
||||
msgstr ""
|
||||
|
||||
|
@ -7063,9 +7069,6 @@ msgstr ""
|
|||
msgid "CodeOwner|Pattern"
|
||||
msgstr ""
|
||||
|
||||
msgid "Cohorts"
|
||||
msgstr ""
|
||||
|
||||
msgid "Cohorts|Inactive users"
|
||||
msgstr ""
|
||||
|
||||
|
@ -27881,6 +27884,9 @@ msgstr ""
|
|||
msgid "Terraform|Actions"
|
||||
msgstr ""
|
||||
|
||||
msgid "Terraform|An error occurred while changing the state file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Terraform|An error occurred while loading your Terraform States"
|
||||
msgstr ""
|
||||
|
||||
|
@ -29937,6 +29943,9 @@ msgstr ""
|
|||
msgid "Toggle emoji award"
|
||||
msgstr ""
|
||||
|
||||
msgid "Toggle focus mode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Toggle navigation"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -163,7 +163,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.10.1",
|
||||
"@gitlab/eslint-plugin": "6.0.0",
|
||||
"@gitlab/eslint-plugin": "7.0.0",
|
||||
"@testing-library/dom": "^7.16.2",
|
||||
"@vue/test-utils": "1.1.2",
|
||||
"acorn": "^6.3.0",
|
||||
|
|
|
@ -6,7 +6,7 @@ module QA
|
|||
module Overview
|
||||
module Users
|
||||
class Index < QA::Page::Base
|
||||
view 'app/views/admin/users/index.html.haml' do
|
||||
view 'app/views/admin/users/_users.html.haml' do
|
||||
element :user_search_field
|
||||
element :pending_approval_tab
|
||||
end
|
||||
|
|
|
@ -39,7 +39,7 @@ module QA
|
|||
element :boards_list
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/boards/toggle_focus.js' do
|
||||
view 'app/assets/javascripts/boards/components/toggle_focus.vue' do
|
||||
element :focus_mode_button
|
||||
end
|
||||
|
||||
|
|
|
@ -3,37 +3,15 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Admin::CohortsController do
|
||||
context 'as admin' do
|
||||
let(:user) { create(:admin) }
|
||||
let(:user) { create(:admin) }
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it 'renders 200' do
|
||||
get :index
|
||||
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
end
|
||||
|
||||
describe 'GET #index' do
|
||||
it_behaves_like 'tracking unique visits', :index do
|
||||
let(:target_id) { 'i_analytics_cohorts' }
|
||||
end
|
||||
end
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
context 'as normal user' do
|
||||
let(:user) { create(:user) }
|
||||
it 'redirects to Overview->Users' do
|
||||
get :index
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it 'renders a 404' do
|
||||
get :index
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
expect(response).to redirect_to(admin_users_path(tab: 'cohorts'))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -29,6 +29,11 @@ RSpec.describe Admin::UsersController do
|
|||
|
||||
expect(assigns(:users).first.association(:authorized_projects)).to be_loaded
|
||||
end
|
||||
|
||||
it_behaves_like 'tracking unique visits', :index do
|
||||
let(:target_id) { 'i_analytics_cohorts' }
|
||||
let(:request_params) { { tab: 'cohorts' } }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET :id' do
|
||||
|
|
|
@ -209,6 +209,13 @@ RSpec.describe Projects::ForksController do
|
|||
}
|
||||
end
|
||||
|
||||
let(:created_project) do
|
||||
Namespace
|
||||
.find_by_id(params[:namespace_key])
|
||||
.projects
|
||||
.find_by_path(params.fetch(:path, project.path))
|
||||
end
|
||||
|
||||
subject do
|
||||
post :create, params: params
|
||||
end
|
||||
|
@ -260,6 +267,21 @@ RSpec.describe Projects::ForksController do
|
|||
expect(response).to redirect_to(namespace_project_import_path(user.namespace, project, continue: continue_params))
|
||||
end
|
||||
end
|
||||
|
||||
context 'custom attributes set' do
|
||||
let(:params) { super().merge(path: 'something_custom', name: 'Something Custom', description: 'Something Custom', visibility: 'private') }
|
||||
|
||||
it 'creates a project with custom values' do
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:found)
|
||||
expect(response).to redirect_to(namespace_project_import_path(user.namespace, params[:path]))
|
||||
expect(created_project.path).to eq(params[:path])
|
||||
expect(created_project.name).to eq(params[:name])
|
||||
expect(created_project.description).to eq(params[:description])
|
||||
expect(created_project.visibility).to eq(params[:visibility])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is not signed in' do
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Cohorts page' do
|
||||
before do
|
||||
admin = create(:admin)
|
||||
sign_in(admin)
|
||||
gitlab_enable_admin_mode_sign_in(admin)
|
||||
end
|
||||
|
||||
context 'with usage ping enabled' do
|
||||
it 'shows users count per month' do
|
||||
stub_application_setting(usage_ping_enabled: true)
|
||||
|
||||
create_list(:user, 2)
|
||||
|
||||
visit admin_cohorts_path
|
||||
|
||||
expect(page).to have_content("#{Time.now.strftime('%b %Y')} 3 0")
|
||||
end
|
||||
end
|
||||
|
||||
context 'with usage ping disabled' do
|
||||
it 'shows empty state', :js do
|
||||
stub_application_setting(usage_ping_enabled: false)
|
||||
|
||||
visit admin_cohorts_path
|
||||
|
||||
expect(page).to have_selector(".js-empty-state")
|
||||
end
|
||||
end
|
||||
end
|
70
spec/features/admin/admin_users_spec.rb
Normal file
70
spec/features/admin/admin_users_spec.rb
Normal file
|
@ -0,0 +1,70 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe "Admin::Users" do
|
||||
let(:current_user) { create(:admin) }
|
||||
|
||||
before do
|
||||
sign_in(current_user)
|
||||
gitlab_enable_admin_mode_sign_in(current_user)
|
||||
end
|
||||
|
||||
describe 'Tabs', :js do
|
||||
let(:tabs_selector) { '.js-users-tabs' }
|
||||
let(:active_tab_selector) { '.nav-link.active' }
|
||||
|
||||
it 'does not add the tab param when the Users tab is selected' do
|
||||
visit admin_users_path
|
||||
|
||||
within tabs_selector do
|
||||
click_link 'Users'
|
||||
end
|
||||
|
||||
expect(page).to have_current_path(admin_users_path)
|
||||
end
|
||||
|
||||
it 'adds the ?tab=cohorts param when the Cohorts tab is selected' do
|
||||
visit admin_users_path
|
||||
|
||||
within tabs_selector do
|
||||
click_link 'Cohorts'
|
||||
end
|
||||
|
||||
expect(page).to have_current_path(admin_users_path(tab: 'cohorts'))
|
||||
end
|
||||
|
||||
it 'shows the cohorts tab when the tab param is set' do
|
||||
visit admin_users_path(tab: 'cohorts')
|
||||
|
||||
within tabs_selector do
|
||||
expect(page).to have_selector active_tab_selector, text: 'Cohorts'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Cohorts tab content' do
|
||||
context 'with usage ping enabled' do
|
||||
it 'shows users count per month' do
|
||||
stub_application_setting(usage_ping_enabled: true)
|
||||
|
||||
create_list(:user, 2)
|
||||
|
||||
visit admin_users_path(tab: 'cohorts')
|
||||
|
||||
expect(page).to have_content("#{Time.now.strftime('%b %Y')} 3 0")
|
||||
end
|
||||
end
|
||||
|
||||
context 'with usage ping disabled' do
|
||||
it 'shows empty state', :js do
|
||||
stub_application_setting(usage_ping_enabled: false)
|
||||
|
||||
visit admin_users_path(tab: 'cohorts')
|
||||
|
||||
expect(page).to have_selector(".js-empty-state")
|
||||
expect(page).to have_content("Activate user activity analysis")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,5 +1,5 @@
|
|||
import { createWrapper } from '@vue/test-utils';
|
||||
import initAdminUsers from '~/admin/users';
|
||||
import { initAdminUsersApp } from '~/admin/users';
|
||||
import AdminUsersApp from '~/admin/users/components/app.vue';
|
||||
import { users, paths } from './mock_data';
|
||||
|
||||
|
@ -16,7 +16,7 @@ describe('initAdminUsersApp', () => {
|
|||
|
||||
document.body.appendChild(el);
|
||||
|
||||
wrapper = createWrapper(initAdminUsers(el));
|
||||
wrapper = createWrapper(initAdminUsersApp(el));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
|
|
@ -1,32 +1,51 @@
|
|||
import Vue from 'vue';
|
||||
import { setHTMLFixture } from 'helpers/fixtures';
|
||||
import RecentSearchesRoot from '~/filtered_search/recent_searches_root';
|
||||
|
||||
jest.mock('vue');
|
||||
const containerId = 'test-container';
|
||||
const dropdownElementId = 'test-dropdown-element';
|
||||
|
||||
describe('RecentSearchesRoot', () => {
|
||||
describe('render', () => {
|
||||
let recentSearchesRoot;
|
||||
let data;
|
||||
let template;
|
||||
let recentSearchesRootMockInstance;
|
||||
let vm;
|
||||
let containerEl;
|
||||
|
||||
beforeEach(() => {
|
||||
recentSearchesRoot = {
|
||||
setHTMLFixture(`
|
||||
<div id="${containerId}">
|
||||
<div id="${dropdownElementId}"></div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
containerEl = document.getElementById(containerId);
|
||||
|
||||
recentSearchesRootMockInstance = {
|
||||
store: {
|
||||
state: 'state',
|
||||
state: {
|
||||
recentSearches: ['foo', 'bar', 'qux'],
|
||||
isLocalStorageAvailable: true,
|
||||
allowedKeys: ['test'],
|
||||
},
|
||||
},
|
||||
wrapperElement: document.getElementById(dropdownElementId),
|
||||
};
|
||||
|
||||
Vue.mockImplementation((options) => {
|
||||
({ data, template } = options);
|
||||
});
|
||||
RecentSearchesRoot.prototype.render.call(recentSearchesRootMockInstance);
|
||||
vm = recentSearchesRootMockInstance.vm;
|
||||
|
||||
RecentSearchesRoot.prototype.render.call(recentSearchesRoot);
|
||||
return vm.$nextTick();
|
||||
});
|
||||
|
||||
it('should instantiate Vue', () => {
|
||||
expect(Vue).toHaveBeenCalled();
|
||||
expect(data()).toBe(recentSearchesRoot.store.state);
|
||||
expect(template).toContain(':is-local-storage-available="isLocalStorageAvailable"');
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
});
|
||||
|
||||
it('should render the recent searches', () => {
|
||||
const { recentSearches } = recentSearchesRootMockInstance.store.state;
|
||||
|
||||
recentSearches.forEach((recentSearch) => {
|
||||
expect(containerEl.textContent).toContain(recentSearch);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -118,7 +118,7 @@ describe('Onboarding Issues Popovers', () => {
|
|||
describe('when dismissing the popover', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(Tracking, 'event');
|
||||
document.querySelector('.learn-gitlab.popover .close').click();
|
||||
document.querySelector('.learn-gitlab.popover .js-close-learn-gitlab').click();
|
||||
});
|
||||
|
||||
it('deletes the cookie', () => {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { GlDropdown, GlModal, GlSprintf } from '@gitlab/ui';
|
||||
import { createLocalVue, shallowMount } from '@vue/test-utils';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import StateActions from '~/terraform/components/states_table_actions.vue';
|
||||
import lockStateMutation from '~/terraform/graphql/mutations/lock_state.mutation.graphql';
|
||||
|
@ -14,6 +15,7 @@ describe('StatesTableActions', () => {
|
|||
let lockResponse;
|
||||
let removeResponse;
|
||||
let unlockResponse;
|
||||
let updateStateResponse;
|
||||
let wrapper;
|
||||
|
||||
const defaultProps = {
|
||||
|
@ -26,7 +28,9 @@ describe('StatesTableActions', () => {
|
|||
};
|
||||
|
||||
const createMockApolloProvider = () => {
|
||||
lockResponse = jest.fn().mockResolvedValue({ data: { terraformStateLock: { errors: [] } } });
|
||||
lockResponse = jest
|
||||
.fn()
|
||||
.mockResolvedValue({ data: { terraformStateLock: { errors: ['There was an error'] } } });
|
||||
|
||||
removeResponse = jest
|
||||
.fn()
|
||||
|
@ -36,11 +40,20 @@ describe('StatesTableActions', () => {
|
|||
.fn()
|
||||
.mockResolvedValue({ data: { terraformStateUnlock: { errors: [] } } });
|
||||
|
||||
return createMockApollo([
|
||||
[lockStateMutation, lockResponse],
|
||||
[removeStateMutation, removeResponse],
|
||||
[unlockStateMutation, unlockResponse],
|
||||
]);
|
||||
updateStateResponse = jest.fn().mockResolvedValue({});
|
||||
|
||||
return createMockApollo(
|
||||
[
|
||||
[lockStateMutation, lockResponse],
|
||||
[removeStateMutation, removeResponse],
|
||||
[unlockStateMutation, unlockResponse],
|
||||
],
|
||||
{
|
||||
Mutation: {
|
||||
addDataToTerraformState: updateStateResponse,
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const createComponent = (propsData = defaultProps) => {
|
||||
|
@ -56,6 +69,7 @@ describe('StatesTableActions', () => {
|
|||
return wrapper.vm.$nextTick();
|
||||
};
|
||||
|
||||
const findActionsDropdown = () => wrapper.find(GlDropdown);
|
||||
const findLockBtn = () => wrapper.find('[data-testid="terraform-state-lock"]');
|
||||
const findUnlockBtn = () => wrapper.find('[data-testid="terraform-state-unlock"]');
|
||||
const findDownloadBtn = () => wrapper.find('[data-testid="terraform-state-download"]');
|
||||
|
@ -70,9 +84,25 @@ describe('StatesTableActions', () => {
|
|||
lockResponse = null;
|
||||
removeResponse = null;
|
||||
unlockResponse = null;
|
||||
updateStateResponse = null;
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('when the state is loading', () => {
|
||||
beforeEach(() => {
|
||||
return createComponent({
|
||||
state: {
|
||||
...defaultProps.state,
|
||||
loadingActions: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('disables the actions dropdown', () => {
|
||||
expect(findActionsDropdown().props('disabled')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('download button', () => {
|
||||
it('displays a download button', () => {
|
||||
expect(findDownloadBtn().text()).toBe('Download JSON');
|
||||
|
@ -104,7 +134,8 @@ describe('StatesTableActions', () => {
|
|||
describe('when clicking the unlock button', () => {
|
||||
beforeEach(() => {
|
||||
findUnlockBtn().vm.$emit('click');
|
||||
return wrapper.vm.$nextTick();
|
||||
|
||||
return waitForPromises();
|
||||
});
|
||||
|
||||
it('calls the unlock mutation', () => {
|
||||
|
@ -137,7 +168,8 @@ describe('StatesTableActions', () => {
|
|||
describe('when clicking the lock button', () => {
|
||||
beforeEach(() => {
|
||||
findLockBtn().vm.$emit('click');
|
||||
return wrapper.vm.$nextTick();
|
||||
|
||||
return waitForPromises();
|
||||
});
|
||||
|
||||
it('calls the lock mutation', () => {
|
||||
|
@ -145,6 +177,42 @@ describe('StatesTableActions', () => {
|
|||
stateID: unlockedProps.state.id,
|
||||
});
|
||||
});
|
||||
|
||||
it('calls mutations to set loading and errors', () => {
|
||||
// loading update
|
||||
expect(updateStateResponse).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
{},
|
||||
{
|
||||
terraformState: {
|
||||
...unlockedProps.state,
|
||||
_showDetails: false,
|
||||
errorMessages: [],
|
||||
loadingActions: true,
|
||||
},
|
||||
},
|
||||
// Apollo fields
|
||||
expect.any(Object),
|
||||
expect.any(Object),
|
||||
);
|
||||
|
||||
// final update
|
||||
expect(updateStateResponse).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
{},
|
||||
{
|
||||
terraformState: {
|
||||
...unlockedProps.state,
|
||||
_showDetails: true,
|
||||
errorMessages: ['There was an error'],
|
||||
loadingActions: false,
|
||||
},
|
||||
},
|
||||
// Apollo fields
|
||||
expect.any(Object),
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -156,7 +224,8 @@ describe('StatesTableActions', () => {
|
|||
describe('when clicking the remove button', () => {
|
||||
beforeEach(() => {
|
||||
findRemoveBtn().vm.$emit('click');
|
||||
return wrapper.vm.$nextTick();
|
||||
|
||||
return waitForPromises();
|
||||
});
|
||||
|
||||
it('displays a remove modal', () => {
|
||||
|
|
|
@ -11,6 +11,8 @@ describe('StatesTable', () => {
|
|||
const defaultProps = {
|
||||
states: [
|
||||
{
|
||||
_showDetails: true,
|
||||
errorMessages: ['State 1 has errored'],
|
||||
name: 'state-1',
|
||||
lockedAt: '2020-10-13T00:00:00Z',
|
||||
lockedByUser: {
|
||||
|
@ -20,6 +22,8 @@ describe('StatesTable', () => {
|
|||
latestVersion: null,
|
||||
},
|
||||
{
|
||||
_showDetails: false,
|
||||
errorMessages: [],
|
||||
name: 'state-2',
|
||||
lockedAt: null,
|
||||
lockedByUser: null,
|
||||
|
@ -27,6 +31,8 @@ describe('StatesTable', () => {
|
|||
latestVersion: null,
|
||||
},
|
||||
{
|
||||
_showDetails: false,
|
||||
errorMessages: [],
|
||||
name: 'state-3',
|
||||
lockedAt: '2020-10-10T00:00:00Z',
|
||||
lockedByUser: {
|
||||
|
@ -54,6 +60,8 @@ describe('StatesTable', () => {
|
|||
},
|
||||
},
|
||||
{
|
||||
_showDetails: true,
|
||||
errorMessages: ['State 4 has errored'],
|
||||
name: 'state-4',
|
||||
lockedAt: '2020-10-10T00:00:00Z',
|
||||
lockedByUser: null,
|
||||
|
@ -154,6 +162,17 @@ describe('StatesTable', () => {
|
|||
expect(findActions().length).toEqual(0);
|
||||
});
|
||||
|
||||
it.each`
|
||||
errorMessage | lineNumber
|
||||
${defaultProps.states[0].errorMessages[0]} | ${0}
|
||||
${defaultProps.states[3].errorMessages[0]} | ${1}
|
||||
`('displays table error message "$errorMessage"', ({ errorMessage, lineNumber }) => {
|
||||
const states = wrapper.findAll('[data-testid="terraform-states-table-error"]');
|
||||
const state = states.at(lineNumber);
|
||||
|
||||
expect(state.text()).toBe(errorMessage);
|
||||
});
|
||||
|
||||
describe('when user is a terraform administrator', () => {
|
||||
beforeEach(() => {
|
||||
return createComponent({
|
||||
|
|
|
@ -27,6 +27,15 @@ describe('TerraformList', () => {
|
|||
},
|
||||
};
|
||||
|
||||
// Override @client _showDetails
|
||||
getStatesQuery.getStates.definitions[1].selectionSet.selections[0].directives = [];
|
||||
|
||||
// Override @client errorMessages
|
||||
getStatesQuery.getStates.definitions[1].selectionSet.selections[1].directives = [];
|
||||
|
||||
// Override @client loadingActions
|
||||
getStatesQuery.getStates.definitions[1].selectionSet.selections[2].directives = [];
|
||||
|
||||
const statsQueryResponse = queryResponse || jest.fn().mockResolvedValue(apolloQueryResponse);
|
||||
const apolloProvider = createMockApollo([[getStatesQuery, statsQueryResponse]]);
|
||||
|
||||
|
@ -52,20 +61,26 @@ describe('TerraformList', () => {
|
|||
describe('when there is a list of terraform states', () => {
|
||||
const states = [
|
||||
{
|
||||
_showDetails: false,
|
||||
errorMessages: [],
|
||||
id: 'gid://gitlab/Terraform::State/1',
|
||||
name: 'state-1',
|
||||
lockedAt: null,
|
||||
updatedAt: null,
|
||||
lockedByUser: null,
|
||||
latestVersion: null,
|
||||
loadingActions: false,
|
||||
lockedAt: null,
|
||||
lockedByUser: null,
|
||||
updatedAt: null,
|
||||
},
|
||||
{
|
||||
_showDetails: false,
|
||||
errorMessages: [],
|
||||
id: 'gid://gitlab/Terraform::State/2',
|
||||
name: 'state-2',
|
||||
lockedAt: null,
|
||||
updatedAt: null,
|
||||
lockedByUser: null,
|
||||
latestVersion: null,
|
||||
loadingActions: false,
|
||||
lockedAt: null,
|
||||
lockedByUser: null,
|
||||
updatedAt: null,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -3328,8 +3328,8 @@ RSpec.describe API::Projects do
|
|||
expect(json_response['message']['path']).to eq(['has already been taken'])
|
||||
end
|
||||
|
||||
it 'accepts a name for the target project' do
|
||||
post api("/projects/#{project.id}/fork", user2), params: { name: 'My Random Project' }
|
||||
it 'accepts custom parameters for the target project' do
|
||||
post api("/projects/#{project.id}/fork", user2), params: { name: 'My Random Project', description: 'A description', visibility: 'private' }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:created)
|
||||
expect(json_response['name']).to eq('My Random Project')
|
||||
|
@ -3337,6 +3337,8 @@ RSpec.describe API::Projects do
|
|||
expect(json_response['owner']['id']).to eq(user2.id)
|
||||
expect(json_response['namespace']['id']).to eq(user2.namespace.id)
|
||||
expect(json_response['forked_from_project']['id']).to eq(project.id)
|
||||
expect(json_response['description']).to eq('A description')
|
||||
expect(json_response['visibility']).to eq('private')
|
||||
expect(json_response['import_status']).to eq('scheduled')
|
||||
expect(json_response).to include("import_error")
|
||||
end
|
||||
|
@ -3368,6 +3370,13 @@ RSpec.describe API::Projects do
|
|||
expect(json_response['message']['path']).to eq(['has already been taken'])
|
||||
expect(json_response['message']['name']).to eq(['has already been taken'])
|
||||
end
|
||||
|
||||
it 'fails to fork with an unknown visibility level' do
|
||||
post api("/projects/#{project.id}/fork", user2), params: { visibility: 'something' }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
expect(json_response['error']).to eq('visibility does not have a valid value')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when unauthenticated' do
|
||||
|
|
|
@ -141,13 +141,6 @@ RSpec.describe Admin::DevOpsReportController, "routing" do
|
|||
end
|
||||
end
|
||||
|
||||
# admin_cohorts GET /admin/cohorts(.:format) admin/cohorst#index
|
||||
RSpec.describe Admin::CohortsController, "routing" do
|
||||
it "to #index" do
|
||||
expect(get("/admin/cohorts")).to route_to('admin/cohorts#index')
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.describe Admin::GroupsController, "routing" do
|
||||
let(:name) { 'complex.group-namegit' }
|
||||
|
||||
|
|
|
@ -323,6 +323,50 @@ RSpec.describe Projects::ForkService do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'fork with optional attributes' do
|
||||
let(:public_project) { create(:project, :public) }
|
||||
|
||||
it 'sets optional attributes to specified values' do
|
||||
forked_project = fork_project(
|
||||
public_project,
|
||||
nil,
|
||||
namespace: public_project.namespace,
|
||||
path: 'forked',
|
||||
name: 'My Fork',
|
||||
description: 'Description',
|
||||
visibility: 'internal',
|
||||
using_service: true
|
||||
)
|
||||
|
||||
expect(forked_project.path).to eq('forked')
|
||||
expect(forked_project.name).to eq('My Fork')
|
||||
expect(forked_project.description).to eq('Description')
|
||||
expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL)
|
||||
end
|
||||
|
||||
it 'sets visibility level to private if an unknown visibility is requested' do
|
||||
forked_project = fork_project(public_project, nil, using_service: true, visibility: 'unknown')
|
||||
|
||||
expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
|
||||
end
|
||||
|
||||
it 'sets visibility level to project visibility level if requested visibility is greater' do
|
||||
private_project = create(:project, :private)
|
||||
|
||||
forked_project = fork_project(private_project, nil, using_service: true, visibility: 'public')
|
||||
|
||||
expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
|
||||
end
|
||||
|
||||
it 'sets visibility level to target namespace visibility level if requested visibility is greater' do
|
||||
private_group = create(:group, :private)
|
||||
|
||||
forked_project = fork_project(public_project, nil, namespace: private_group, using_service: true, visibility: 'public')
|
||||
|
||||
expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a project is already forked' do
|
||||
|
|
|
@ -845,10 +845,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@gitlab/at.js/-/at.js-1.5.7.tgz#1ee6f838cc4410a1d797770934df91d90df8179e"
|
||||
integrity sha512-c6ySRK/Ma7lxwpIVbSAF3P+xiTLrNTGTLRx4/pHK111AdFxwgUwrYF6aVZFXvmG65jHOJHoa0eQQ21RW6rm0Rg==
|
||||
|
||||
"@gitlab/eslint-plugin@6.0.0":
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/eslint-plugin/-/eslint-plugin-6.0.0.tgz#deb18f63808af1cb1cc117a92558f07edb1e2256"
|
||||
integrity sha512-3TihEG0EzbGtc6wxZLANZN1ge2tnAv0qU8w6smUACmPhqFj0/DrCq9V6QKPqAHk/Yn3hrfGk5nznAzzuMEgwDQ==
|
||||
"@gitlab/eslint-plugin@7.0.0":
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/eslint-plugin/-/eslint-plugin-7.0.0.tgz#3c46d88dde2f7aa0be2a7df5af8e593006becea9"
|
||||
integrity sha512-XqISaNqQwJ12jTanESvAFNVAniqFN/UFKj068ESiNumlsxnQA36V945wZ6LnwI7WgSCGQCUfHi9MEgyjUvuvdg==
|
||||
dependencies:
|
||||
babel-eslint "^10.0.3"
|
||||
eslint-config-airbnb-base "^14.0.0"
|
||||
|
|
Loading…
Reference in a new issue