Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
24cae4b6d5
commit
fab43fda65
|
|
@ -1 +1 @@
|
|||
2da899b99c7bc3536b1658f54ed1e8fdb6e02f23
|
||||
5460b4217be7746352cb9e8350fc7e12db27d4ba
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import {
|
||||
GlAlert,
|
||||
GlButton,
|
||||
GlDrawer,
|
||||
GlFormCheckbox,
|
||||
|
|
@ -16,11 +17,14 @@ import { __, s__ } from '~/locale';
|
|||
import { DRAWER_Z_INDEX } from '~/lib/utils/constants';
|
||||
import { getContentWrapperHeight } from '~/lib/utils/dom_utils';
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
import Tracking from '~/tracking';
|
||||
import {
|
||||
allEnvironments,
|
||||
defaultVariableState,
|
||||
DRAWER_EVENT_LABEL,
|
||||
EDIT_VARIABLE_ACTION,
|
||||
ENVIRONMENT_SCOPE_LINK_TITLE,
|
||||
EVENT_ACTION,
|
||||
EXPANDED_VARIABLES_NOTE,
|
||||
FLAG_LINK_TITLE,
|
||||
VARIABLE_ACTIONS,
|
||||
|
|
@ -29,6 +33,8 @@ import {
|
|||
import CiEnvironmentsDropdown from './ci_environments_dropdown.vue';
|
||||
import { awsTokenList } from './ci_variable_autocomplete_tokens';
|
||||
|
||||
const trackingMixin = Tracking.mixin({ label: DRAWER_EVENT_LABEL });
|
||||
|
||||
export const i18n = {
|
||||
addVariable: s__('CiVariables|Add Variable'),
|
||||
cancel: __('Cancel'),
|
||||
|
|
@ -49,14 +55,27 @@ export const i18n = {
|
|||
protectedDescription: s__(
|
||||
'CiVariables|Export variable to pipelines running on protected branches and tags only.',
|
||||
),
|
||||
valueFeedback: {
|
||||
rawHelpText: s__('CiVariables|Variable value will be evaluated as raw string.'),
|
||||
maskedReqsNotMet: s__(
|
||||
'CiVariables|This variable value does not meet the masking requirements.',
|
||||
),
|
||||
},
|
||||
variableReferenceTitle: s__('CiVariables|Value might contain a variable reference'),
|
||||
variableReferenceDescription: s__(
|
||||
'CiVariables|Unselect "Expand variable reference" if you want to use the variable value as a raw string.',
|
||||
),
|
||||
type: __('Type'),
|
||||
value: __('Value'),
|
||||
};
|
||||
|
||||
const VARIABLE_REFERENCE_REGEX = /\$/;
|
||||
|
||||
export default {
|
||||
DRAWER_Z_INDEX,
|
||||
components: {
|
||||
CiEnvironmentsDropdown,
|
||||
GlAlert,
|
||||
GlButton,
|
||||
GlDrawer,
|
||||
GlFormCheckbox,
|
||||
|
|
@ -69,7 +88,8 @@ export default {
|
|||
GlLink,
|
||||
GlSprintf,
|
||||
},
|
||||
inject: ['environmentScopeLink', 'isProtectedByDefault'],
|
||||
mixins: [trackingMixin],
|
||||
inject: ['environmentScopeLink', 'isProtectedByDefault', 'maskableRawRegex', 'maskableRegex'],
|
||||
props: {
|
||||
areEnvironmentsLoading: {
|
||||
type: Boolean,
|
||||
|
|
@ -106,22 +126,59 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
variable: { ...defaultVariableState, ...this.selectedVariable },
|
||||
trackedValidationErrorProperty: undefined,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isValueMaskable() {
|
||||
return this.variable.masked && !this.isValueMasked;
|
||||
},
|
||||
isValueMasked() {
|
||||
const regex = RegExp(this.maskedRegexToUse);
|
||||
return regex.test(this.variable.value);
|
||||
},
|
||||
canSubmit() {
|
||||
return this.variable.key.length > 0 && this.isValueValid;
|
||||
},
|
||||
getDrawerHeaderHeight() {
|
||||
return getContentWrapperHeight();
|
||||
},
|
||||
hasVariableReference() {
|
||||
return this.isExpanded && VARIABLE_REFERENCE_REGEX.test(this.variable.value);
|
||||
},
|
||||
isExpanded() {
|
||||
return !this.variable.raw;
|
||||
},
|
||||
isMaskedReqsMet() {
|
||||
return !this.variable.masked || this.isValueMasked;
|
||||
},
|
||||
isValueEmpty() {
|
||||
return this.variable.value === '';
|
||||
},
|
||||
isValueValid() {
|
||||
return this.isValueEmpty || this.isMaskedReqsMet;
|
||||
},
|
||||
isEditing() {
|
||||
return this.mode === EDIT_VARIABLE_ACTION;
|
||||
},
|
||||
maskedRegexToUse() {
|
||||
return this.variable.raw ? this.maskableRawRegex : this.maskableRegex;
|
||||
},
|
||||
maskedReqsNotMetText() {
|
||||
return !this.isMaskedReqsMet ? this.$options.i18n.valueFeedback.maskedReqsNotMet : '';
|
||||
},
|
||||
modalActionText() {
|
||||
return this.isEditing ? this.$options.i18n.editVariable : this.$options.i18n.addVariable;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
variable: {
|
||||
handler() {
|
||||
this.trackVariableValidationErrors();
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.isProtectedByDefault && !this.isEditing) {
|
||||
this.variable = { ...this.variable, protected: true };
|
||||
|
|
@ -131,6 +188,22 @@ export default {
|
|||
close() {
|
||||
this.$emit('close-form');
|
||||
},
|
||||
getTrackingErrorProperty() {
|
||||
if (this.isValueEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let property;
|
||||
if (this.isValueMaskable) {
|
||||
const supportedChars = this.maskedRegexToUse.replace('^', '').replace(/{(\d,)}\$/, '');
|
||||
const regex = new RegExp(supportedChars, 'g');
|
||||
property = this.variable.value.replace(regex, '');
|
||||
} else if (this.hasVariableReference) {
|
||||
property = '$';
|
||||
}
|
||||
|
||||
return property;
|
||||
},
|
||||
setRaw(expanded) {
|
||||
this.variable = { ...this.variable, raw: !expanded };
|
||||
},
|
||||
|
|
@ -138,6 +211,13 @@ export default {
|
|||
this.$emit(this.isEditing ? 'update-variable' : 'add-variable', this.variable);
|
||||
this.close();
|
||||
},
|
||||
trackVariableValidationErrors() {
|
||||
const property = this.getTrackingErrorProperty();
|
||||
if (property && !this.trackedValidationErrorProperty) {
|
||||
this.track(EVENT_ACTION, { property });
|
||||
this.trackedValidationErrorProperty = property;
|
||||
}
|
||||
},
|
||||
},
|
||||
awsTokenList,
|
||||
flagLink: helpPagePath('ci/variables/index', {
|
||||
|
|
@ -256,24 +336,41 @@ export default {
|
|||
:token-list="$options.awsTokenList"
|
||||
:label-text="$options.i18n.key"
|
||||
class="gl-border-none gl-pb-0! gl-mb-n5"
|
||||
data-testid="pipeline-form-ci-variable-key"
|
||||
data-testid="ci-variable-key"
|
||||
data-qa-selector="ci_variable_key_field"
|
||||
/>
|
||||
<gl-form-group
|
||||
:label="$options.i18n.value"
|
||||
label-for="ci-variable-value"
|
||||
class="gl-border-none gl-mb-n2"
|
||||
data-testid="ci-variable-value-label"
|
||||
:invalid-feedback="maskedReqsNotMetText"
|
||||
:state="isValueValid"
|
||||
>
|
||||
<gl-form-textarea
|
||||
id="ci-variable-value"
|
||||
v-model="variable.value"
|
||||
class="gl-border-none gl-font-monospace!"
|
||||
rows="3"
|
||||
max-rows="10"
|
||||
data-testid="pipeline-form-ci-variable-value"
|
||||
data-testid="ci-variable-value"
|
||||
data-qa-selector="ci_variable_value_field"
|
||||
spellcheck="false"
|
||||
/>
|
||||
<p v-if="variable.raw" class="gl-mt-2 gl-mb-0 text-secondary" data-testid="raw-variable-tip">
|
||||
{{ $options.i18n.valueFeedback.rawHelpText }}
|
||||
</p>
|
||||
</gl-form-group>
|
||||
<gl-alert
|
||||
v-if="hasVariableReference"
|
||||
:title="$options.i18n.variableReferenceTitle"
|
||||
:dismissible="false"
|
||||
variant="warning"
|
||||
class="gl-mx-4 gl-pl-9! gl-border-bottom-0"
|
||||
data-testid="has-variable-reference-alert"
|
||||
>
|
||||
{{ $options.i18n.variableReferenceDescription }}
|
||||
</gl-alert>
|
||||
<div class="gl-display-flex gl-justify-content-end">
|
||||
<gl-button category="secondary" class="gl-mr-3" data-testid="cancel-button" @click="close"
|
||||
>{{ $options.i18n.cancel }}
|
||||
|
|
@ -281,6 +378,7 @@ export default {
|
|||
<gl-button
|
||||
category="primary"
|
||||
variant="confirm"
|
||||
:disabled="!canSubmit"
|
||||
data-testid="ci-variable-confirm-btn"
|
||||
@click="submit"
|
||||
>{{ modalActionText }}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ export const AWS_TIP_MESSAGE = s__(
|
|||
);
|
||||
|
||||
export const EVENT_LABEL = 'ci_variable_modal';
|
||||
export const DRAWER_EVENT_LABEL = 'ci_variable_drawer';
|
||||
export const EVENT_ACTION = 'validation_error';
|
||||
|
||||
// AWS TOKEN CONSTANTS
|
||||
|
|
|
|||
|
|
@ -243,10 +243,6 @@ export default {
|
|||
eventHub.$on('openModal', (options) => {
|
||||
this.openModal(options);
|
||||
});
|
||||
|
||||
if (this.tasksToBeDoneEnabled) {
|
||||
this.openModal({ source: 'in_product_marketing_email' });
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showInvalidFeedbackMessage(response) {
|
||||
|
|
@ -317,7 +313,7 @@ export default {
|
|||
const { error, message } = responseFromSuccess(response);
|
||||
|
||||
if (error) {
|
||||
this.showMemberErrors(message);
|
||||
this.showErrors(message);
|
||||
} else {
|
||||
this.onInviteSuccess();
|
||||
}
|
||||
|
|
@ -327,9 +323,13 @@ export default {
|
|||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
showMemberErrors(message) {
|
||||
this.invalidMembers = message;
|
||||
this.$refs.alerts.focus();
|
||||
showErrors(message) {
|
||||
if (isString(message)) {
|
||||
this.invalidFeedbackMessage = message;
|
||||
} else {
|
||||
this.invalidMembers = message;
|
||||
this.$refs.alerts.focus();
|
||||
}
|
||||
},
|
||||
tokenName(username) {
|
||||
// initial token creation hits this and nothing is found... so safe navigation
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { getParameterValues } from '~/lib/utils/url_utility';
|
||||
|
||||
export function memberName(member) {
|
||||
// user defined tokens(invites by email) will have email in `name` and will not contain `username`
|
||||
return member.username || member.name;
|
||||
|
|
@ -10,5 +8,5 @@ export function triggerExternalAlert() {
|
|||
}
|
||||
|
||||
export function qualifiesForTasksToBeDone() {
|
||||
return getParameterValues('open_modal')[0] === 'invite_members_for_task';
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import Vue from 'vue';
|
|||
import VueApollo from 'vue-apollo';
|
||||
import VueRouter from 'vue-router';
|
||||
import createDefaultClient from '~/lib/graphql';
|
||||
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||
import { ORGANIZATION_ROOT_ROUTE_NAME } from '../constants';
|
||||
import resolvers from '../shared/graphql/resolvers';
|
||||
import App from './components/app.vue';
|
||||
|
|
@ -23,6 +24,16 @@ export const initOrganizationsGroupsAndProjects = () => {
|
|||
|
||||
if (!el) return false;
|
||||
|
||||
const {
|
||||
dataset: { appData },
|
||||
} = el;
|
||||
const {
|
||||
projectsEmptyStateSvgPath,
|
||||
groupsEmptyStateSvgPath,
|
||||
newGroupPath,
|
||||
newProjectPath,
|
||||
} = convertObjectPropsToCamelCase(JSON.parse(appData));
|
||||
|
||||
Vue.use(VueRouter);
|
||||
const apolloProvider = new VueApollo({
|
||||
defaultClient: createDefaultClient(resolvers),
|
||||
|
|
@ -34,6 +45,12 @@ export const initOrganizationsGroupsAndProjects = () => {
|
|||
name: 'OrganizationsGroupsAndProjects',
|
||||
apolloProvider,
|
||||
router,
|
||||
provide: {
|
||||
projectsEmptyStateSvgPath,
|
||||
groupsEmptyStateSvgPath,
|
||||
newGroupPath,
|
||||
newProjectPath,
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(App);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import { GlLoadingIcon, GlEmptyState } from '@gitlab/ui';
|
||||
import { createAlert } from '~/alert';
|
||||
import { s__ } from '~/locale';
|
||||
import { s__, __ } from '~/locale';
|
||||
import GroupsList from '~/vue_shared/components/groups_list/groups_list.vue';
|
||||
import groupsQuery from '../graphql/queries/groups.query.graphql';
|
||||
import { formatGroups } from '../utils';
|
||||
|
|
@ -11,8 +11,28 @@ export default {
|
|||
errorMessage: s__(
|
||||
'Organization|An error occurred loading the groups. Please refresh the page to try again.',
|
||||
),
|
||||
emptyState: {
|
||||
title: s__("Organization|You don't have any groups yet."),
|
||||
description: s__(
|
||||
'Organization|A group is a collection of several projects. If you organize your projects under a group, it works like a folder.',
|
||||
),
|
||||
primaryButtonText: __('New group'),
|
||||
},
|
||||
},
|
||||
components: { GlLoadingIcon, GlEmptyState, GroupsList },
|
||||
inject: {
|
||||
groupsEmptyStateSvgPath: {},
|
||||
newGroupPath: {
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
shouldShowEmptyStateButtons: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
components: { GlLoadingIcon, GroupsList },
|
||||
data() {
|
||||
return {
|
||||
groups: [],
|
||||
|
|
@ -33,11 +53,30 @@ export default {
|
|||
isLoading() {
|
||||
return this.$apollo.queries.groups.loading;
|
||||
},
|
||||
emptyStateProps() {
|
||||
const baseProps = {
|
||||
svgHeight: 144,
|
||||
svgPath: this.groupsEmptyStateSvgPath,
|
||||
title: this.$options.i18n.emptyState.title,
|
||||
description: this.$options.i18n.emptyState.description,
|
||||
};
|
||||
|
||||
if (this.shouldShowEmptyStateButtons && this.newGroupPath) {
|
||||
return {
|
||||
...baseProps,
|
||||
primaryButtonLink: this.newGroupPath,
|
||||
primaryButtonText: this.$options.i18n.emptyState.primaryButtonText,
|
||||
};
|
||||
}
|
||||
|
||||
return baseProps;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-loading-icon v-if="isLoading" class="gl-mt-5" size="md" />
|
||||
<groups-list v-else :groups="groups" show-group-icon />
|
||||
<groups-list v-else-if="groups.length" :groups="groups" show-group-icon />
|
||||
<gl-empty-state v-else v-bind="emptyStateProps" />
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import { s__ } from '~/locale';
|
||||
import { GlLoadingIcon, GlEmptyState } from '@gitlab/ui';
|
||||
import { s__, __ } from '~/locale';
|
||||
import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue';
|
||||
import { createAlert } from '~/alert';
|
||||
import projectsQuery from '../graphql/queries/projects.query.graphql';
|
||||
|
|
@ -11,10 +11,31 @@ export default {
|
|||
errorMessage: s__(
|
||||
'Organization|An error occurred loading the projects. Please refresh the page to try again.',
|
||||
),
|
||||
emptyState: {
|
||||
title: s__("Organization|You don't have any projects yet."),
|
||||
description: s__(
|
||||
'GroupsEmptyState|Projects are where you can store your code, access issues, wiki, and other features of Gitlab.',
|
||||
),
|
||||
primaryButtonText: __('New project'),
|
||||
},
|
||||
},
|
||||
components: {
|
||||
ProjectsList,
|
||||
GlLoadingIcon,
|
||||
GlEmptyState,
|
||||
},
|
||||
inject: {
|
||||
projectsEmptyStateSvgPath: {},
|
||||
newProjectPath: {
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
shouldShowEmptyStateButtons: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -36,11 +57,30 @@ export default {
|
|||
isLoading() {
|
||||
return this.$apollo.queries.projects.loading;
|
||||
},
|
||||
emptyStateProps() {
|
||||
const baseProps = {
|
||||
svgHeight: 144,
|
||||
svgPath: this.projectsEmptyStateSvgPath,
|
||||
title: this.$options.i18n.emptyState.title,
|
||||
description: this.$options.i18n.emptyState.description,
|
||||
};
|
||||
|
||||
if (this.shouldShowEmptyStateButtons && this.newProjectPath) {
|
||||
return {
|
||||
...baseProps,
|
||||
primaryButtonLink: this.newProjectPath,
|
||||
primaryButtonText: this.$options.i18n.emptyState.primaryButtonText,
|
||||
};
|
||||
}
|
||||
|
||||
return baseProps;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-loading-icon v-if="isLoading" class="gl-mt-5" size="md" />
|
||||
<projects-list v-else :projects="projects" show-project-icon />
|
||||
<projects-list v-else-if="projects.length" :projects="projects" show-project-icon />
|
||||
<gl-empty-state v-else v-bind="emptyStateProps" />
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -105,6 +105,6 @@ export default {
|
|||
$options.i18n.viewAll
|
||||
}}</gl-link>
|
||||
</div>
|
||||
<component :is="routerView" class="gl-mt-5" />
|
||||
<component :is="routerView" should-show-empty-state-buttons class="gl-mt-5" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -27,9 +27,14 @@ export const initOrganizationsShow = () => {
|
|||
const {
|
||||
dataset: { appData },
|
||||
} = el;
|
||||
const { organization, groupsAndProjectsOrganizationPath } = convertObjectPropsToCamelCase(
|
||||
JSON.parse(appData),
|
||||
);
|
||||
const {
|
||||
organization,
|
||||
groupsAndProjectsOrganizationPath,
|
||||
projectsEmptyStateSvgPath,
|
||||
groupsEmptyStateSvgPath,
|
||||
newGroupPath,
|
||||
newProjectPath,
|
||||
} = convertObjectPropsToCamelCase(JSON.parse(appData));
|
||||
|
||||
Vue.use(VueRouter);
|
||||
const router = createRouter();
|
||||
|
|
@ -42,6 +47,12 @@ export const initOrganizationsShow = () => {
|
|||
name: 'OrganizationShowRoot',
|
||||
apolloProvider,
|
||||
router,
|
||||
provide: {
|
||||
projectsEmptyStateSvgPath,
|
||||
groupsEmptyStateSvgPath,
|
||||
newGroupPath,
|
||||
newProjectPath,
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(App, {
|
||||
props: { organization, groupsAndProjectsOrganizationPath },
|
||||
|
|
|
|||
|
|
@ -102,8 +102,14 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="user-bar">
|
||||
<div class="gl-display-flex gl-align-items-center gl-px-3 gl-py-2">
|
||||
<div
|
||||
class="user-bar gl-display-flex gl-p-3 gl-gap-1"
|
||||
:class="{ 'gl-flex-direction-column gl-gap-3': sidebarData.is_logged_in }"
|
||||
>
|
||||
<div
|
||||
v-if="hasCollapseButton || sidebarData.is_logged_in"
|
||||
class="gl-display-flex gl-align-items-center gl-gap-1"
|
||||
>
|
||||
<template v-if="sidebarData.is_logged_in">
|
||||
<brand-logo :logo-url="sidebarData.logo_url" />
|
||||
<gl-badge
|
||||
|
|
@ -111,7 +117,6 @@ export default {
|
|||
variant="success"
|
||||
:href="sidebarData.canary_toggle_com_url"
|
||||
size="sm"
|
||||
class="gl-ml-2"
|
||||
>
|
||||
{{ $options.NEXT_LABEL }}
|
||||
</gl-badge>
|
||||
|
|
@ -146,7 +151,7 @@ export default {
|
|||
</div>
|
||||
<div
|
||||
v-if="sidebarData.is_logged_in"
|
||||
class="gl-display-flex gl-justify-content-space-between gl-px-3 gl-pt-2 gl-pb-3 gl-gap-2"
|
||||
class="gl-display-flex gl-justify-content-space-between gl-gap-2"
|
||||
>
|
||||
<counter
|
||||
v-gl-tooltip:super-sidebar.hover.noninteractive.ds500.bottom="$options.i18n.issues"
|
||||
|
|
@ -193,18 +198,16 @@ export default {
|
|||
data-track-property="nav_core_menu"
|
||||
/>
|
||||
</div>
|
||||
<div class="gl-px-3 gl-pb-3">
|
||||
<button
|
||||
id="super-sidebar-search"
|
||||
v-gl-tooltip.bottom.hover.noninteractive.ds500.html="searchTooltip"
|
||||
v-gl-modal="$options.SEARCH_MODAL_ID"
|
||||
class="counter gl-display-block gl-py-3 gl-bg-gray-10 gl-rounded-base gl-text-gray-900 gl-border-none gl-inset-border-1-gray-a-08 gl-line-height-1 gl-focus--focus gl-w-full"
|
||||
data-testid="super-sidebar-search-button"
|
||||
>
|
||||
<gl-icon name="search" />
|
||||
{{ $options.i18n.searchBtnText }}
|
||||
</button>
|
||||
<search-modal @shown="hideSearchTooltip" @hidden="showSearchTooltip" />
|
||||
</div>
|
||||
<button
|
||||
id="super-sidebar-search"
|
||||
v-gl-tooltip.bottom.hover.noninteractive.ds500.html="searchTooltip"
|
||||
v-gl-modal="$options.SEARCH_MODAL_ID"
|
||||
class="counter gl-display-block gl-py-3 gl-bg-gray-10 gl-rounded-base gl-text-gray-900 gl-border-none gl-inset-border-1-gray-a-08 gl-line-height-1 gl-focus--focus gl-w-full"
|
||||
data-testid="super-sidebar-search-button"
|
||||
>
|
||||
<gl-icon name="search" />
|
||||
{{ $options.i18n.searchBtnText }}
|
||||
</button>
|
||||
<search-modal @shown="hideSearchTooltip" @hidden="showSearchTooltip" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Clusters
|
||||
module Agents
|
||||
class DashboardController < ApplicationController
|
||||
include KasCookie
|
||||
|
||||
before_action :check_feature_flag!
|
||||
before_action :find_agent
|
||||
before_action :authorize_read_cluster_agent!
|
||||
before_action :set_kas_cookie, only: [:show], if: -> { current_user }
|
||||
|
||||
feature_category :deployment_management
|
||||
|
||||
def show
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_agent
|
||||
@agent = ::Clusters::Agent.find(params[:agent_id])
|
||||
end
|
||||
|
||||
def check_feature_flag!
|
||||
not_found unless ::Feature.enabled?(:k8s_dashboard, current_user)
|
||||
end
|
||||
|
||||
def authorize_read_cluster_agent!
|
||||
not_found unless can?(current_user, :read_cluster_agent, @agent)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -6,7 +6,22 @@ module Organizations
|
|||
{
|
||||
organization: organization.slice(:id, :name),
|
||||
groups_and_projects_organization_path: groups_and_projects_organization_path(organization)
|
||||
}.to_json
|
||||
}.merge(shared_groups_and_projects_app_data).to_json
|
||||
end
|
||||
|
||||
def organization_groups_and_projects_app_data
|
||||
shared_groups_and_projects_app_data.to_json
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def shared_groups_and_projects_app_data
|
||||
{
|
||||
projects_empty_state_svg_path: image_path('illustrations/empty-state/empty-projects-md.svg'),
|
||||
groups_empty_state_svg_path: image_path('illustrations/empty-state/empty-groups-md.svg'),
|
||||
new_group_path: new_group_path,
|
||||
new_project_path: new_project_path
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
- page_title _('Groups and projects')
|
||||
|
||||
#js-organizations-groups-and-projects
|
||||
#js-organizations-groups-and-projects{ data: { app_data: organization_groups_and_projects_app_data } }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: k8s_dashboard
|
||||
introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/130953"
|
||||
rollout_issue_url: "https://gitlab.com/gitlab-org/gitlab/-/issues/424237"
|
||||
milestone: '16.4'
|
||||
type: development
|
||||
group: group::environments
|
||||
default_enabled: false
|
||||
|
|
@ -18,7 +18,6 @@ options:
|
|||
- g_edit_by_web_ide
|
||||
- g_edit_by_sfe
|
||||
- g_edit_by_snippet_ide
|
||||
- g_edit_by_live_preview
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ options:
|
|||
- g_edit_by_web_ide
|
||||
- g_edit_by_sfe
|
||||
- g_edit_by_snippet_ide
|
||||
- g_edit_by_live_preview
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ product_section: dev
|
|||
product_stage: create
|
||||
product_group: ide
|
||||
value_type: number
|
||||
status: active
|
||||
status: removed
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85420
|
||||
time_frame: 28d
|
||||
data_source: redis_hll
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ options:
|
|||
- g_edit_by_web_ide
|
||||
- g_edit_by_sfe
|
||||
- g_edit_by_snippet_ide
|
||||
- g_edit_by_live_preview
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
|
|
|
|||
|
|
@ -117,6 +117,12 @@ InitializerConnections.raise_if_new_database_connection do
|
|||
get 'offline' => "pwa#offline"
|
||||
get 'manifest' => "pwa#manifest", constraints: lambda { |req| req.format == :json }
|
||||
|
||||
scope module: 'clusters' do
|
||||
scope module: 'agents' do
|
||||
get '/kubernetes/:agent_id', to: 'dashboard#show', as: 'kubernetes_dashboard'
|
||||
end
|
||||
end
|
||||
|
||||
# '/-/health' implemented by BasicHealthCheck middleware
|
||||
get 'liveness' => 'health#liveness'
|
||||
get 'readiness' => 'health#readiness'
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 42 KiB |
|
|
@ -22,7 +22,7 @@ Load the `.drawio.png` or `.drawio.svg` file directly into **draw.io**, which yo
|
|||
To create a diagram from a file:
|
||||
|
||||
1. Copy existing file and rename it. Ensure that the extension is `.drawio.png` or `.drawio.svg`.
|
||||
1. Edit the diagram.
|
||||
1. Edit the diagram.
|
||||
1. Save the file.
|
||||
|
||||
To create a diagram from scratch using [draw.io desktop](https://github.com/jgraph/drawio-desktop/releases):
|
||||
|
|
|
|||
|
|
@ -51,4 +51,4 @@ Today we can have multiple catalogs based on the number of namespaces, making it
|
|||
|
||||
### 4.2. Cons
|
||||
|
||||
- A separate catalog that surfaces components across Organizations would need to be implemented to serve the wider community (community catalog). This catalog will be required for GitLab.com only, later on we can evaluate if a similar catalog is needed for self-managed customers.
|
||||
- A separate catalog that surfaces components across Organizations would need to be implemented to serve the wider community (community catalog). This catalog will be required for GitLab.com only, later on we can evaluate if a similar catalog is needed for self-managed customers.
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ This is especially the case with the merge request, as it is one of the most com
|
|||
|
||||
Today personal Namespaces serve two purposes that are mostly non-overlapping:
|
||||
|
||||
1. They provide a place for users to create personal Projects
|
||||
1. They provide a place for users to create personal Projects
|
||||
that aren't expected to receive contributions from other people. This use case saves them from having to create a Group just for themselves.
|
||||
1. They provide a default place for a user to put any forks they
|
||||
create when contributing to Projects where they don't have permission to push a branch. This again saves them from needing to create a Group just to store these forks. But the primary user need here is because they can't push branches to the upstream Project so they create a fork and contribute merge requests from the fork.
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ quick reference for what features we have now, are planning, their statuses, and
|
|||
an excutive summary of the overall state of the migration experience.
|
||||
This could be advertised to self-managed users via a simple chart, allowing them
|
||||
to tell at a glance the status of this project and determine if it is feature-
|
||||
complete enough for their needs and level of risk tolerance.
|
||||
complete enough for their needs and level of risk tolerance.
|
||||
|
||||
This should be documented in the container registry administration documentation,
|
||||
rather than in this blueprint. Providing this information there will place it in
|
||||
|
|
@ -86,7 +86,7 @@ For example:
|
|||
|
||||
The metadata database is in early beta for self-managed users. The core migration
|
||||
process for existing registries has been implemented, and online garbage collection
|
||||
is fully implemented. Certain database enabled features are only enabled for GitLab.com
|
||||
is fully implemented. Certain database enabled features are only enabled for GitLab.com
|
||||
and automatic database provisioning for the registry database is not available.
|
||||
Please see the table below for the status of features related to the container
|
||||
registry database.
|
||||
|
|
|
|||
|
|
@ -256,7 +256,7 @@ In our component, we then listen on the `aiCompletionResponse` using the `userId
|
|||
```graphql
|
||||
subscription aiCompletionResponse($userId: UserID, $resourceId: AiModelID, $clientSubscriptionId: String) {
|
||||
aiCompletionResponse(userId: $userId, resourceId: $resourceId, clientSubscriptionId: $clientSubscriptionId) {
|
||||
responseBody
|
||||
content
|
||||
errors
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
## Code Suggestions development setup
|
||||
|
||||
The recommended setup for locally developing and debugging Code Suggestions is to have all 3 different components running:
|
||||
The recommended setup for locally developing and debugging Code Suggestions is to have all 3 different components running:
|
||||
|
||||
- IDE Extension (e.g. VSCode Extension)
|
||||
- Main application configured correctly
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 9.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 29 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 55 KiB |
|
|
@ -13,7 +13,7 @@ You can update your preferences to change the look and feel of GitLab.
|
|||
|
||||
You can change the color theme of the GitLab UI. These colors are displayed on the left sidebar.
|
||||
Using individual color themes might help you differentiate between your different
|
||||
GitLab instances.
|
||||
GitLab instances.
|
||||
|
||||
To change the color theme:
|
||||
|
||||
|
|
@ -25,7 +25,7 @@ To change the color theme:
|
|||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28252) in GitLab 13.1 as an [Experiment](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28252).
|
||||
|
||||
Dark mode makes elements on the GitLab UI stand out on a dark background.
|
||||
Dark mode makes elements on the GitLab UI stand out on a dark background.
|
||||
|
||||
- To turn on Dark mode, Select **Preferences > Color theme > Dark Mode**.
|
||||
|
||||
|
|
@ -44,11 +44,11 @@ To change the syntax highlighting theme:
|
|||
1. In the **Syntax highlighting theme** section, select a theme.
|
||||
1. Select **Save changes**.
|
||||
|
||||
To view the updated syntax highlighting theme, refresh your project's page.
|
||||
To view the updated syntax highlighting theme, refresh your project's page.
|
||||
|
||||
To customize the syntax highlighting theme, you can also [use the Application settings API](../../api/settings.md#list-of-settings-that-can-be-accessed-via-api-calls). Use `default_syntax_highlighting_theme` to change the syntax highlighting colors on a more granular level.
|
||||
|
||||
If these steps do not work, your programming language might not be supported by the syntax highlighters.
|
||||
If these steps do not work, your programming language might not be supported by the syntax highlighters.
|
||||
For more information, view [Rouge Ruby Library](https://github.com/rouge-ruby/rouge) for guidance on code files and Snippets. View [Moncaco Editor](https://microsoft.github.io/monaco-editor/) and [Monarch](https://microsoft.github.io/monaco-editor/monarch.html) for guidance on the Web IDE.
|
||||
|
||||
## Change the diff colors
|
||||
|
|
@ -59,7 +59,7 @@ To change the diff colors:
|
|||
|
||||
1. On the left sidebar, select your avatar.
|
||||
1. Select **Preferences**.
|
||||
1. Go to the **Diff colors** section.
|
||||
1. Go to the **Diff colors** section.
|
||||
1. Select a color or enter a color code.
|
||||
1. Select **Save changes**.
|
||||
|
||||
|
|
@ -142,7 +142,7 @@ To render whitespace in the Web IDE:
|
|||
1. Select the **Render whitespace characters in the Web IDE** checkbox.
|
||||
1. Select **Save changes**.
|
||||
|
||||
You can view changes to whitespace in diffs.
|
||||
You can view changes to whitespace in diffs.
|
||||
|
||||
To view diffs on the Web IDE, follow these steps:
|
||||
|
||||
|
|
@ -315,7 +315,7 @@ To integrate with Gitpod:
|
|||
|
||||
### Integrate your GitLab instance with Sourcegraph
|
||||
|
||||
GitLab supports Sourcegraph integration for all public projects on GitLab.
|
||||
GitLab supports Sourcegraph integration for all public projects on GitLab.
|
||||
|
||||
To integrate with Sourcegraph:
|
||||
|
||||
|
|
|
|||
|
|
@ -58,7 +58,6 @@ You can upload files of the following types as designs:
|
|||
- JPEG
|
||||
- JPG
|
||||
- PNG
|
||||
- SVG
|
||||
- TIFF
|
||||
- WEBP
|
||||
|
||||
|
|
|
|||
|
|
@ -3028,7 +3028,7 @@ msgstr ""
|
|||
msgid "AddMember|Invite email is invalid"
|
||||
msgstr ""
|
||||
|
||||
msgid "AddMember|Invite limit of %{daily_invites} per day exceeded"
|
||||
msgid "AddMember|Invite limit of %{daily_invites} per day exceeded."
|
||||
msgstr ""
|
||||
|
||||
msgid "AddMember|Invites cannot be blank"
|
||||
|
|
@ -10275,12 +10275,24 @@ msgstr ""
|
|||
msgid "CiVariables|This %{entity} has %{currentVariableCount} defined CI/CD variables. The maximum number of variables per %{entity} is %{maxVariableLimit}. To add new variables, you must reduce the number of defined variables."
|
||||
msgstr ""
|
||||
|
||||
msgid "CiVariables|This variable value does not meet the masking requirements."
|
||||
msgstr ""
|
||||
|
||||
msgid "CiVariables|Type"
|
||||
msgstr ""
|
||||
|
||||
msgid "CiVariables|Unselect \"Expand variable reference\" if you want to use the variable value as a raw string."
|
||||
msgstr ""
|
||||
|
||||
msgid "CiVariables|Value"
|
||||
msgstr ""
|
||||
|
||||
msgid "CiVariables|Value might contain a variable reference"
|
||||
msgstr ""
|
||||
|
||||
msgid "CiVariables|Variable value will be evaluated as raw string."
|
||||
msgstr ""
|
||||
|
||||
msgid "CiVariables|Variable will be masked in job logs. Requires values to meet regular expression requirements."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -32821,6 +32833,9 @@ msgstr ""
|
|||
msgid "Organizations"
|
||||
msgstr ""
|
||||
|
||||
msgid "Organization|A group is a collection of several projects. If you organize your projects under a group, it works like a folder."
|
||||
msgstr ""
|
||||
|
||||
msgid "Organization|An error occurred loading the groups. Please refresh the page to try again."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -32860,6 +32875,12 @@ msgstr ""
|
|||
msgid "Organization|View all"
|
||||
msgstr ""
|
||||
|
||||
msgid "Organization|You don't have any groups yet."
|
||||
msgstr ""
|
||||
|
||||
msgid "Organization|You don't have any projects yet."
|
||||
msgstr ""
|
||||
|
||||
msgid "Orphaned member"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -416,7 +416,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
|
|||
before do
|
||||
detached_merge_request_pipeline.reload.succeed!
|
||||
|
||||
refresh
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
it 'merges the merge request' do
|
||||
|
|
|
|||
|
|
@ -1,24 +1,36 @@
|
|||
import { GlDrawer, GlFormInput, GlFormSelect } from '@gitlab/ui';
|
||||
import { GlDrawer, GlFormCombobox, GlFormInput, GlFormSelect } from '@gitlab/ui';
|
||||
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import CiEnvironmentsDropdown from '~/ci/ci_variable_list/components/ci_environments_dropdown.vue';
|
||||
import CiVariableDrawer, { i18n } from '~/ci/ci_variable_list/components/ci_variable_drawer.vue';
|
||||
import CiVariableDrawer from '~/ci/ci_variable_list/components/ci_variable_drawer.vue';
|
||||
import { awsTokenList } from '~/ci/ci_variable_list/components/ci_variable_autocomplete_tokens';
|
||||
import {
|
||||
ADD_VARIABLE_ACTION,
|
||||
DRAWER_EVENT_LABEL,
|
||||
EDIT_VARIABLE_ACTION,
|
||||
EVENT_ACTION,
|
||||
variableOptions,
|
||||
projectString,
|
||||
variableTypes,
|
||||
} from '~/ci/ci_variable_list/constants';
|
||||
import { mockTracking } from 'helpers/tracking_helper';
|
||||
import { mockVariablesWithScopes } from '../mocks';
|
||||
|
||||
describe('CI Variable Drawer', () => {
|
||||
let wrapper;
|
||||
let trackingSpy;
|
||||
|
||||
const mockProjectVariable = mockVariablesWithScopes(projectString)[0];
|
||||
const mockProjectVariableFileType = mockVariablesWithScopes(projectString)[1];
|
||||
const mockEnvScope = 'staging';
|
||||
const mockEnvironments = ['*', 'dev', 'staging', 'production'];
|
||||
|
||||
// matches strings that contain at least 8 consecutive characters consisting of only
|
||||
// letters (both uppercase and lowercase), digits, or the specified special characters
|
||||
const maskableRegex = '^[a-zA-Z0-9_+=/@:.~-]{8,}$';
|
||||
|
||||
// matches strings that consist of at least 8 or more non-whitespace characters
|
||||
const maskableRawRegex = '^\\S{8,}$';
|
||||
|
||||
const defaultProps = {
|
||||
areEnvironmentsLoading: false,
|
||||
areScopedVariablesAvailable: true,
|
||||
|
|
@ -31,6 +43,8 @@ describe('CI Variable Drawer', () => {
|
|||
const defaultProvide = {
|
||||
isProtectedByDefault: true,
|
||||
environmentScopeLink: '/help/environments',
|
||||
maskableRawRegex,
|
||||
maskableRegex,
|
||||
};
|
||||
|
||||
const createComponent = ({
|
||||
|
|
@ -57,8 +71,11 @@ describe('CI Variable Drawer', () => {
|
|||
const findDrawer = () => wrapper.findComponent(GlDrawer);
|
||||
const findEnvironmentScopeDropdown = () => wrapper.findComponent(CiEnvironmentsDropdown);
|
||||
const findExpandedCheckbox = () => wrapper.findByTestId('ci-variable-expanded-checkbox');
|
||||
const findKeyField = () => wrapper.findComponent(GlFormCombobox);
|
||||
const findMaskedCheckbox = () => wrapper.findByTestId('ci-variable-masked-checkbox');
|
||||
const findProtectedCheckbox = () => wrapper.findByTestId('ci-variable-protected-checkbox');
|
||||
const findValueField = () => wrapper.findByTestId('ci-variable-value');
|
||||
const findValueLabel = () => wrapper.findByTestId('ci-variable-value-label');
|
||||
const findTitle = () => findDrawer().find('h2');
|
||||
const findTypeDropdown = () => wrapper.findComponent(GlFormSelect);
|
||||
|
||||
|
|
@ -201,12 +218,131 @@ describe('CI Variable Drawer', () => {
|
|||
});
|
||||
|
||||
it("sets the variable's raw value", async () => {
|
||||
await findKeyField().vm.$emit('input', 'NEW_VARIABLE');
|
||||
await findExpandedCheckbox().vm.$emit('change');
|
||||
await findConfirmBtn().vm.$emit('click');
|
||||
|
||||
const sentRawValue = wrapper.emitted('add-variable')[0][0].raw;
|
||||
expect(sentRawValue).toBe(!defaultProps.raw);
|
||||
});
|
||||
|
||||
it('shows help text when variable is not expanded (will be evaluated as raw)', async () => {
|
||||
expect(findExpandedCheckbox().attributes('checked')).toBeDefined();
|
||||
expect(findDrawer().text()).not.toContain(
|
||||
'Variable value will be evaluated as raw string.',
|
||||
);
|
||||
|
||||
await findExpandedCheckbox().vm.$emit('change');
|
||||
|
||||
expect(findExpandedCheckbox().attributes('checked')).toBeUndefined();
|
||||
expect(findDrawer().text()).toContain('Variable value will be evaluated as raw string.');
|
||||
});
|
||||
|
||||
it('shows help text when variable is expanded and contains the $ character', async () => {
|
||||
expect(findDrawer().text()).not.toContain(
|
||||
'Unselect "Expand variable reference" if you want to use the variable value as a raw string.',
|
||||
);
|
||||
|
||||
await findValueField().vm.$emit('input', '$NEW_VALUE');
|
||||
|
||||
expect(findDrawer().text()).toContain(
|
||||
'Unselect "Expand variable reference" if you want to use the variable value as a raw string.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('key', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('prompts AWS tokens as options', () => {
|
||||
expect(findKeyField().props('tokenList')).toBe(awsTokenList);
|
||||
});
|
||||
|
||||
it('cannot submit with empty key', async () => {
|
||||
expect(findConfirmBtn().attributes('disabled')).toBeDefined();
|
||||
|
||||
await findKeyField().vm.$emit('input', 'NEW_VARIABLE');
|
||||
|
||||
expect(findConfirmBtn().attributes('disabled')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('value', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('can submit empty value', async () => {
|
||||
await findKeyField().vm.$emit('input', 'NEW_VARIABLE');
|
||||
|
||||
// value is empty by default
|
||||
expect(findConfirmBtn().attributes('disabled')).toBeUndefined();
|
||||
});
|
||||
|
||||
describe.each`
|
||||
value | canSubmit | trackingErrorProperty
|
||||
${'secretValue'} | ${true} | ${null}
|
||||
${'~v@lid:symbols.'} | ${true} | ${null}
|
||||
${'short'} | ${false} | ${null}
|
||||
${'multiline\nvalue'} | ${false} | ${'\n'}
|
||||
${'dollar$ign'} | ${false} | ${'$'}
|
||||
${'unsupported|char'} | ${false} | ${'|'}
|
||||
`('masking requirements', ({ value, canSubmit, trackingErrorProperty }) => {
|
||||
beforeEach(async () => {
|
||||
createComponent();
|
||||
|
||||
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
|
||||
await findKeyField().vm.$emit('input', 'NEW_VARIABLE');
|
||||
await findValueField().vm.$emit('input', value);
|
||||
await findMaskedCheckbox().vm.$emit('input', true);
|
||||
});
|
||||
|
||||
it(`${
|
||||
canSubmit ? 'can submit' : 'shows validation errors and disables submit button'
|
||||
} when value is '${value}'`, () => {
|
||||
if (canSubmit) {
|
||||
expect(findValueLabel().attributes('invalid-feedback')).toBe('');
|
||||
expect(findConfirmBtn().attributes('disabled')).toBeUndefined();
|
||||
} else {
|
||||
expect(findValueLabel().attributes('invalid-feedback')).toBe(
|
||||
'This variable value does not meet the masking requirements.',
|
||||
);
|
||||
expect(findConfirmBtn().attributes('disabled')).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
it(`${
|
||||
trackingErrorProperty ? 'sends the correct' : 'does not send the'
|
||||
} variable validation tracking event when value is '${value}'`, () => {
|
||||
const trackingEventSent = trackingErrorProperty ? 1 : 0;
|
||||
expect(trackingSpy).toHaveBeenCalledTimes(trackingEventSent);
|
||||
|
||||
if (trackingErrorProperty) {
|
||||
expect(trackingSpy).toHaveBeenCalledWith(undefined, EVENT_ACTION, {
|
||||
label: DRAWER_EVENT_LABEL,
|
||||
property: trackingErrorProperty,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('only sends the tracking event once', async () => {
|
||||
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
|
||||
await findKeyField().vm.$emit('input', 'NEW_VARIABLE');
|
||||
await findMaskedCheckbox().vm.$emit('input', true);
|
||||
|
||||
expect(trackingSpy).toHaveBeenCalledTimes(0);
|
||||
|
||||
await findValueField().vm.$emit('input', 'unsupported|char');
|
||||
|
||||
expect(trackingSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
await findValueField().vm.$emit('input', 'dollar$ign');
|
||||
|
||||
expect(trackingSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -227,8 +363,8 @@ describe('CI Variable Drawer', () => {
|
|||
});
|
||||
|
||||
it('title and confirm button renders the correct text', () => {
|
||||
expect(findTitle().text()).toBe(i18n.addVariable);
|
||||
expect(findConfirmBtn().text()).toBe(i18n.addVariable);
|
||||
expect(findTitle().text()).toBe('Add Variable');
|
||||
expect(findConfirmBtn().text()).toBe('Add Variable');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -241,8 +377,8 @@ describe('CI Variable Drawer', () => {
|
|||
});
|
||||
|
||||
it('title and confirm button renders the correct text', () => {
|
||||
expect(findTitle().text()).toBe(i18n.editVariable);
|
||||
expect(findConfirmBtn().text()).toBe(i18n.editVariable);
|
||||
expect(findTitle().text()).toBe('Edit Variable');
|
||||
expect(findConfirmBtn().text()).toBe('Edit Variable');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { GlLink, GlModal, GlSprintf, GlFormGroup, GlCollapse, GlIcon } from '@gitlab/ui';
|
||||
import { GlModal, GlSprintf, GlFormGroup, GlCollapse, GlIcon } from '@gitlab/ui';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { nextTick } from 'vue';
|
||||
import { stubComponent } from 'helpers/stub_component';
|
||||
|
|
@ -12,7 +12,6 @@ import ModalConfetti from '~/invite_members/components/confetti.vue';
|
|||
import MembersTokenSelect from '~/invite_members/components/members_token_select.vue';
|
||||
import UserLimitNotification from '~/invite_members/components/user_limit_notification.vue';
|
||||
import {
|
||||
INVITE_MEMBERS_FOR_TASK,
|
||||
MEMBERS_MODAL_CELEBRATE_INTRO,
|
||||
MEMBERS_MODAL_CELEBRATE_TITLE,
|
||||
MEMBERS_PLACEHOLDER,
|
||||
|
|
@ -31,7 +30,6 @@ import {
|
|||
HTTP_STATUS_CREATED,
|
||||
HTTP_STATUS_INTERNAL_SERVER_ERROR,
|
||||
} from '~/lib/utils/http_status';
|
||||
import { getParameterValues } from '~/lib/utils/url_utility';
|
||||
import {
|
||||
displaySuccessfulInvitationAlert,
|
||||
reloadOnInvitationSuccess,
|
||||
|
|
@ -54,10 +52,6 @@ import {
|
|||
|
||||
jest.mock('~/invite_members/utils/trigger_successful_invite_alert');
|
||||
jest.mock('~/experimentation/experiment_tracking');
|
||||
jest.mock('~/lib/utils/url_utility', () => ({
|
||||
...jest.requireActual('~/lib/utils/url_utility'),
|
||||
getParameterValues: jest.fn(() => []),
|
||||
}));
|
||||
|
||||
describe('InviteMembersModal', () => {
|
||||
let wrapper;
|
||||
|
|
@ -129,7 +123,6 @@ describe('InviteMembersModal', () => {
|
|||
});
|
||||
|
||||
const findModal = () => wrapper.findComponent(GlModal);
|
||||
const findBase = () => wrapper.findComponent(InviteModalBase);
|
||||
const findIntroText = () => wrapper.findByTestId('modal-base-intro-text').text();
|
||||
const findEmptyInvitesAlert = () => wrapper.findByTestId('empty-invites-alert');
|
||||
const findMemberErrorAlert = () => wrapper.findByTestId('alert-member-error');
|
||||
|
|
@ -155,10 +148,6 @@ describe('InviteMembersModal', () => {
|
|||
findMembersFormGroup().attributes('invalid-feedback');
|
||||
const membersFormGroupDescription = () => findMembersFormGroup().attributes('description');
|
||||
const findMembersSelect = () => wrapper.findComponent(MembersTokenSelect);
|
||||
const findTasksToBeDone = () => wrapper.findByTestId('invite-members-modal-tasks-to-be-done');
|
||||
const findTasks = () => wrapper.findByTestId('invite-members-modal-tasks');
|
||||
const findProjectSelect = () => wrapper.findByTestId('invite-members-modal-project-select');
|
||||
const findNoProjectsAlert = () => wrapper.findByTestId('invite-members-modal-no-projects-alert');
|
||||
const findCelebrationEmoji = () => wrapper.findComponent(GlEmoji);
|
||||
const triggerOpenModal = async ({ mode = 'default', source } = {}) => {
|
||||
eventHub.$emit('openModal', { mode, source });
|
||||
|
|
@ -168,131 +157,11 @@ describe('InviteMembersModal', () => {
|
|||
findMembersSelect().vm.$emit('input', val);
|
||||
await nextTick();
|
||||
};
|
||||
const triggerTasks = async (val) => {
|
||||
findTasks().vm.$emit('input', val);
|
||||
await nextTick();
|
||||
};
|
||||
const triggerAccessLevel = async (val) => {
|
||||
findBase().vm.$emit('access-level', val);
|
||||
await nextTick();
|
||||
};
|
||||
const removeMembersToken = async (val) => {
|
||||
findMembersSelect().vm.$emit('token-remove', val);
|
||||
await nextTick();
|
||||
};
|
||||
|
||||
describe('rendering the tasks to be done', () => {
|
||||
const setupComponent = async (props = {}, urlParameter = ['invite_members_for_task']) => {
|
||||
getParameterValues.mockImplementation(() => urlParameter);
|
||||
createComponent(props);
|
||||
|
||||
await triggerAccessLevel(30);
|
||||
};
|
||||
|
||||
const setupComponentWithTasks = async (...args) => {
|
||||
await setupComponent(...args);
|
||||
await triggerTasks(['ci', 'code']);
|
||||
};
|
||||
|
||||
afterAll(() => {
|
||||
getParameterValues.mockImplementation(() => []);
|
||||
});
|
||||
|
||||
it('renders the tasks to be done', async () => {
|
||||
await setupComponent();
|
||||
|
||||
expect(findTasksToBeDone().exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('when the selected access level is lower than 30', () => {
|
||||
it('does not render the tasks to be done', async () => {
|
||||
await setupComponent();
|
||||
await triggerAccessLevel(20);
|
||||
|
||||
expect(findTasksToBeDone().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the url does not contain the parameter `open_modal=invite_members_for_task`', () => {
|
||||
it('does not render the tasks to be done', async () => {
|
||||
await setupComponent({}, []);
|
||||
|
||||
expect(findTasksToBeDone().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('rendering the tasks', () => {
|
||||
it('renders the tasks', async () => {
|
||||
await setupComponent();
|
||||
|
||||
expect(findTasks().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not render an alert', async () => {
|
||||
await setupComponent();
|
||||
|
||||
expect(findNoProjectsAlert().exists()).toBe(false);
|
||||
});
|
||||
|
||||
describe('when there are no projects passed in the data', () => {
|
||||
it('does not render the tasks', async () => {
|
||||
await setupComponent({ projects: [] });
|
||||
|
||||
expect(findTasks().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('renders an alert with a link to the new projects path', async () => {
|
||||
await setupComponent({ projects: [] });
|
||||
|
||||
expect(findNoProjectsAlert().exists()).toBe(true);
|
||||
expect(findNoProjectsAlert().findComponent(GlLink).attributes('href')).toBe(
|
||||
newProjectPath,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('rendering the project dropdown', () => {
|
||||
it('renders the project select', async () => {
|
||||
await setupComponentWithTasks();
|
||||
|
||||
expect(findProjectSelect().exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('when the modal is shown for a project', () => {
|
||||
it('does not render the project select', async () => {
|
||||
await setupComponentWithTasks({ isProject: true });
|
||||
|
||||
expect(findProjectSelect().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when no tasks are selected', () => {
|
||||
it('does not render the project select', async () => {
|
||||
await setupComponent();
|
||||
|
||||
expect(findProjectSelect().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('tracking events', () => {
|
||||
it('tracks the submit for invite_members_for_task', async () => {
|
||||
await setupComponentWithTasks();
|
||||
|
||||
await triggerMembersTokenSelect([user1]);
|
||||
|
||||
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
|
||||
|
||||
clickInviteButton();
|
||||
|
||||
expectTracking(INVITE_MEMBERS_FOR_TASK.submit, 'selected_tasks_to_be_done', 'ci,code');
|
||||
|
||||
unmockTracking();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('rendering with tracking considerations', () => {
|
||||
describe('when inviting to a project', () => {
|
||||
describe('when inviting members', () => {
|
||||
|
|
@ -624,6 +493,18 @@ describe('InviteMembersModal', () => {
|
|||
expect(membersFormGroupInvalidFeedback()).toBe('');
|
||||
expect(findMembersSelect().props('exceptionState')).not.toBe(false);
|
||||
});
|
||||
|
||||
it('displays invite limit error message', async () => {
|
||||
mockInvitationsApi(HTTP_STATUS_CREATED, invitationsApiResponse.INVITE_LIMIT);
|
||||
|
||||
clickInviteButton();
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(membersFormGroupInvalidFeedback()).toBe(
|
||||
invitationsApiResponse.INVITE_LIMIT.message,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -47,6 +47,11 @@ const EMAIL_TAKEN = {
|
|||
status: 'error',
|
||||
};
|
||||
|
||||
const INVITE_LIMIT = {
|
||||
message: 'Invite limit of 5 per day exceeded.',
|
||||
status: 'error',
|
||||
};
|
||||
|
||||
export const GROUPS_INVITATIONS_PATH = '/api/v4/groups/1/invitations';
|
||||
|
||||
export const invitationsApiResponse = {
|
||||
|
|
@ -56,6 +61,7 @@ export const invitationsApiResponse = {
|
|||
MULTIPLE_RESTRICTED,
|
||||
EMAIL_TAKEN,
|
||||
EXPANDED_RESTRICTED,
|
||||
INVITE_LIMIT,
|
||||
};
|
||||
|
||||
export const IMPORT_PROJECT_MEMBERS_PATH = '/api/v4/projects/1/import_project_members/2';
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@ import {
|
|||
triggerExternalAlert,
|
||||
qualifiesForTasksToBeDone,
|
||||
} from '~/invite_members/utils/member_utils';
|
||||
import setWindowLocation from 'helpers/set_window_location_helper';
|
||||
import { getParameterValues } from '~/lib/utils/url_utility';
|
||||
|
||||
jest.mock('~/lib/utils/url_utility');
|
||||
|
||||
|
|
@ -26,15 +24,7 @@ describe('Trigger External Alert', () => {
|
|||
});
|
||||
|
||||
describe('Qualifies For Tasks To Be Done', () => {
|
||||
it.each([
|
||||
['invite_members_for_task', true],
|
||||
['blah', false],
|
||||
])(`returns name from supplied member token: %j`, (value, result) => {
|
||||
setWindowLocation(`blah/blah?open_modal=${value}`);
|
||||
getParameterValues.mockImplementation(() => {
|
||||
return [value];
|
||||
});
|
||||
|
||||
expect(qualifiesForTasksToBeDone()).toBe(result);
|
||||
it('returns false', () => {
|
||||
expect(qualifiesForTasksToBeDone()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import VueApollo from 'vue-apollo';
|
||||
import Vue from 'vue';
|
||||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
|
||||
import GroupsView from '~/organizations/shared/components/groups_view.vue';
|
||||
import { formatGroups } from '~/organizations/shared/utils';
|
||||
import resolvers from '~/organizations/shared/graphql/resolvers';
|
||||
|
|
@ -20,10 +20,19 @@ describe('GroupsView', () => {
|
|||
let wrapper;
|
||||
let mockApollo;
|
||||
|
||||
const createComponent = ({ mockResolvers = resolvers } = {}) => {
|
||||
const defaultProvide = {
|
||||
groupsEmptyStateSvgPath: 'illustrations/empty-state/empty-groups-md.svg',
|
||||
newGroupPath: '/groups/new',
|
||||
};
|
||||
|
||||
const createComponent = ({ mockResolvers = resolvers, propsData = {} } = {}) => {
|
||||
mockApollo = createMockApollo([], mockResolvers);
|
||||
|
||||
wrapper = shallowMountExtended(GroupsView, { apolloProvider: mockApollo });
|
||||
wrapper = shallowMountExtended(GroupsView, {
|
||||
apolloProvider: mockApollo,
|
||||
provide: defaultProvide,
|
||||
propsData,
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
|
|
@ -47,17 +56,66 @@ describe('GroupsView', () => {
|
|||
});
|
||||
|
||||
describe('when API call is successful', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
describe('when there are no groups', () => {
|
||||
it('renders empty state without buttons by default', async () => {
|
||||
const mockResolvers = {
|
||||
Query: {
|
||||
organization: jest.fn().mockResolvedValueOnce({
|
||||
groups: { nodes: [] },
|
||||
}),
|
||||
},
|
||||
};
|
||||
createComponent({ mockResolvers });
|
||||
|
||||
jest.runAllTimers();
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({
|
||||
title: "You don't have any groups yet.",
|
||||
description:
|
||||
'A group is a collection of several projects. If you organize your projects under a group, it works like a folder.',
|
||||
svgHeight: 144,
|
||||
svgPath: defaultProvide.groupsEmptyStateSvgPath,
|
||||
primaryButtonLink: null,
|
||||
primaryButtonText: null,
|
||||
});
|
||||
});
|
||||
|
||||
describe('when `shouldShowEmptyStateButtons` is `true` and `groupsEmptyStateSvgPath` is set', () => {
|
||||
it('renders empty state with buttons', async () => {
|
||||
const mockResolvers = {
|
||||
Query: {
|
||||
organization: jest.fn().mockResolvedValueOnce({
|
||||
groups: { nodes: [] },
|
||||
}),
|
||||
},
|
||||
};
|
||||
createComponent({ mockResolvers, propsData: { shouldShowEmptyStateButtons: true } });
|
||||
|
||||
jest.runAllTimers();
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({
|
||||
primaryButtonLink: defaultProvide.newGroupPath,
|
||||
primaryButtonText: 'New group',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('renders `GroupsList` component and passes correct props', async () => {
|
||||
jest.runAllTimers();
|
||||
await waitForPromises();
|
||||
describe('when there are groups', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
expect(wrapper.findComponent(GroupsList).props()).toEqual({
|
||||
groups: formatGroups(organizationGroups.nodes),
|
||||
showGroupIcon: true,
|
||||
it('renders `GroupsList` component and passes correct props', async () => {
|
||||
jest.runAllTimers();
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.findComponent(GroupsList).props()).toEqual({
|
||||
groups: formatGroups(organizationGroups.nodes),
|
||||
showGroupIcon: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import VueApollo from 'vue-apollo';
|
||||
import Vue from 'vue';
|
||||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import { GlLoadingIcon, GlEmptyState } from '@gitlab/ui';
|
||||
import ProjectsView from '~/organizations/shared/components/projects_view.vue';
|
||||
import { formatProjects } from '~/organizations/shared/utils';
|
||||
import resolvers from '~/organizations/shared/graphql/resolvers';
|
||||
|
|
@ -20,10 +20,19 @@ describe('ProjectsView', () => {
|
|||
let wrapper;
|
||||
let mockApollo;
|
||||
|
||||
const createComponent = ({ mockResolvers = resolvers } = {}) => {
|
||||
const defaultProvide = {
|
||||
projectsEmptyStateSvgPath: 'illustrations/empty-state/empty-projects-md.svg',
|
||||
newProjectPath: '/projects/new',
|
||||
};
|
||||
|
||||
const createComponent = ({ mockResolvers = resolvers, propsData = {} } = {}) => {
|
||||
mockApollo = createMockApollo([], mockResolvers);
|
||||
|
||||
wrapper = shallowMountExtended(ProjectsView, { apolloProvider: mockApollo });
|
||||
wrapper = shallowMountExtended(ProjectsView, {
|
||||
apolloProvider: mockApollo,
|
||||
provide: defaultProvide,
|
||||
propsData,
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
|
|
@ -47,17 +56,66 @@ describe('ProjectsView', () => {
|
|||
});
|
||||
|
||||
describe('when API call is successful', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
describe('when there are no projects', () => {
|
||||
it('renders empty state without buttons by default', async () => {
|
||||
const mockResolvers = {
|
||||
Query: {
|
||||
organization: jest.fn().mockResolvedValueOnce({
|
||||
projects: { nodes: [] },
|
||||
}),
|
||||
},
|
||||
};
|
||||
createComponent({ mockResolvers });
|
||||
|
||||
jest.runAllTimers();
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({
|
||||
title: "You don't have any projects yet.",
|
||||
description:
|
||||
'Projects are where you can store your code, access issues, wiki, and other features of Gitlab.',
|
||||
svgHeight: 144,
|
||||
svgPath: defaultProvide.projectsEmptyStateSvgPath,
|
||||
primaryButtonLink: null,
|
||||
primaryButtonText: null,
|
||||
});
|
||||
});
|
||||
|
||||
describe('when `shouldShowEmptyStateButtons` is `true` and `projectsEmptyStateSvgPath` is set', () => {
|
||||
it('renders empty state with buttons', async () => {
|
||||
const mockResolvers = {
|
||||
Query: {
|
||||
organization: jest.fn().mockResolvedValueOnce({
|
||||
projects: { nodes: [] },
|
||||
}),
|
||||
},
|
||||
};
|
||||
createComponent({ mockResolvers, propsData: { shouldShowEmptyStateButtons: true } });
|
||||
|
||||
jest.runAllTimers();
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({
|
||||
primaryButtonLink: defaultProvide.newProjectPath,
|
||||
primaryButtonText: 'New project',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('renders `ProjectsList` component and passes correct props', async () => {
|
||||
jest.runAllTimers();
|
||||
await waitForPromises();
|
||||
describe('when there are projects', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
expect(wrapper.findComponent(ProjectsList).props()).toEqual({
|
||||
projects: formatProjects(organizationProjects.nodes),
|
||||
showProjectIcon: true,
|
||||
it('renders `ProjectsList` component and passes correct props', async () => {
|
||||
jest.runAllTimers();
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.findComponent(ProjectsList).props()).toEqual({
|
||||
projects: formatProjects(organizationProjects.nodes),
|
||||
showProjectIcon: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -72,7 +72,9 @@ describe('OrganizationShowGroupsAndProjects', () => {
|
|||
});
|
||||
|
||||
it('renders expected view', () => {
|
||||
expect(wrapper.findComponent(expectedViewComponent).exists()).toBe(true);
|
||||
expect(
|
||||
wrapper.findComponent(expectedViewComponent).props('shouldShowEmptyStateButtons'),
|
||||
).toBe(true);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,17 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do
|
||||
let_it_be(:organization) { build_stubbed(:organization) }
|
||||
let_it_be(:new_group_path) { '/groups/new' }
|
||||
let_it_be(:new_project_path) { '/projects/new' }
|
||||
let_it_be(:groups_empty_state_svg_path) { 'illustrations/empty-state/empty-groups-md.svg' }
|
||||
let_it_be(:projects_empty_state_svg_path) { 'illustrations/empty-state/empty-projects-md.svg' }
|
||||
|
||||
before do
|
||||
allow(helper).to receive(:new_group_path).and_return(new_group_path)
|
||||
allow(helper).to receive(:new_project_path).and_return(new_project_path)
|
||||
allow(helper).to receive(:image_path).with(groups_empty_state_svg_path).and_return(groups_empty_state_svg_path)
|
||||
allow(helper).to receive(:image_path).with(projects_empty_state_svg_path).and_return(projects_empty_state_svg_path)
|
||||
end
|
||||
|
||||
describe '#organization_show_app_data' do
|
||||
before do
|
||||
|
|
@ -20,7 +31,28 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do
|
|||
).to eq(
|
||||
{
|
||||
'organization' => { 'id' => organization.id, 'name' => organization.name },
|
||||
'groups_and_projects_organization_path' => '/-/organizations/default/groups_and_projects'
|
||||
'groups_and_projects_organization_path' => '/-/organizations/default/groups_and_projects',
|
||||
'new_group_path' => new_group_path,
|
||||
'new_project_path' => new_project_path,
|
||||
'groups_empty_state_svg_path' => groups_empty_state_svg_path,
|
||||
'projects_empty_state_svg_path' => projects_empty_state_svg_path
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#organization_groups_and_projects_app_data' do
|
||||
it 'returns expected json' do
|
||||
expect(
|
||||
Gitlab::Json.parse(
|
||||
helper.organization_groups_and_projects_app_data
|
||||
)
|
||||
).to eq(
|
||||
{
|
||||
'new_group_path' => new_group_path,
|
||||
'new_project_path' => new_project_path,
|
||||
'groups_empty_state_svg_path' => groups_empty_state_svg_path,
|
||||
'projects_empty_state_svg_path' => projects_empty_state_svg_path
|
||||
}
|
||||
)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,76 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Clusters::Agents::DashboardController, feature_category: :deployment_management do
|
||||
describe 'GET show' do
|
||||
let_it_be(:organization) { create(:group) }
|
||||
let_it_be(:agent_management_project) { create(:project, group: organization) }
|
||||
let_it_be(:agent) { create(:cluster_agent, project: agent_management_project) }
|
||||
let_it_be(:deployment_project) { create(:project, group: organization) }
|
||||
let(:user) { create(:user) }
|
||||
let(:stub_ff) { true }
|
||||
|
||||
before do
|
||||
allow(::Gitlab::Kas).to receive(:enabled?).and_return(true)
|
||||
end
|
||||
|
||||
context 'with authorized user' do
|
||||
let!(:authorization) do
|
||||
create(
|
||||
:agent_user_access_project_authorization,
|
||||
agent: agent,
|
||||
project: deployment_project
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
stub_feature_flags(k8s_dashboard: stub_ff)
|
||||
deployment_project.add_member(user, :developer)
|
||||
sign_in(user)
|
||||
get kubernetes_dashboard_path(agent.id)
|
||||
end
|
||||
|
||||
it 'sets the kas cookie' do
|
||||
expect(
|
||||
request.env['action_dispatch.cookies'][Gitlab::Kas::COOKIE_KEY]
|
||||
).to be_present
|
||||
end
|
||||
|
||||
it 'returns not found' do
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
|
||||
context 'with k8s_dashboard feature flag disabled' do
|
||||
let(:stub_ff) { false }
|
||||
|
||||
it 'does not set the kas cookie' do
|
||||
expect(
|
||||
request.env['action_dispatch.cookies'][Gitlab::Kas::COOKIE_KEY]
|
||||
).not_to be_present
|
||||
end
|
||||
|
||||
it 'returns not found' do
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with unauthorized user' do
|
||||
before do
|
||||
sign_in(user)
|
||||
get kubernetes_dashboard_path(agent.id)
|
||||
end
|
||||
|
||||
it 'does not set the kas cookie' do
|
||||
expect(
|
||||
request.env['action_dispatch.cookies'][Gitlab::Kas::COOKIE_KEY]
|
||||
).not_to be_present
|
||||
end
|
||||
|
||||
it 'returns not found' do
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Reference in New Issue