Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
329f63356a
commit
92bcd7dce0
|
|
@ -73,6 +73,18 @@ export default {
|
|||
value: { data: queryStringValue, operator: OPERATOR_IS },
|
||||
},
|
||||
];
|
||||
case 'name':
|
||||
if (!this.glFeatures.populateAndUseBuildNamesTable) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
return [
|
||||
...acc,
|
||||
{
|
||||
type: 'filtered-search-term',
|
||||
value: { data: queryStringValue },
|
||||
},
|
||||
];
|
||||
default:
|
||||
return acc;
|
||||
}
|
||||
|
|
@ -91,6 +103,16 @@ export default {
|
|||
|
||||
<template>
|
||||
<gl-filtered-search
|
||||
v-if="glFeatures.populateAndUseBuildNamesTable"
|
||||
:placeholder="s__('Jobs|Search or filter jobs...')"
|
||||
:available-tokens="tokens"
|
||||
:value="filteredSearchValue"
|
||||
:search-text-option-label="__('Search for this text')"
|
||||
terms-as-tokens
|
||||
@submit="onSubmit"
|
||||
/>
|
||||
<gl-filtered-search
|
||||
v-else
|
||||
:placeholder="s__('Jobs|Filter jobs')"
|
||||
:available-tokens="tokens"
|
||||
:value="filteredSearchValue"
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ export const validateQueryString = (queryStringObj) => {
|
|||
const runnerTypesValueValid = jobRunnerTypeValues.includes(runnerTypesValue);
|
||||
return runnerTypesValueValid ? { ...acc, runnerTypes: runnerTypesValue } : acc;
|
||||
}
|
||||
case 'name': {
|
||||
return { ...acc, name: queryStringValue };
|
||||
}
|
||||
default:
|
||||
return acc;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
query getJobs($fullPath: ID!, $after: String, $first: Int = 30, $statuses: [CiJobStatus!]) {
|
||||
query getJobs(
|
||||
$fullPath: ID!
|
||||
$after: String
|
||||
$first: Int = 30
|
||||
$statuses: [CiJobStatus!]
|
||||
$name: String
|
||||
) {
|
||||
project(fullPath: $fullPath) {
|
||||
id
|
||||
jobs(after: $after, first: $first, statuses: $statuses) {
|
||||
jobs(after: $after, first: $first, statuses: $statuses, name: $name) {
|
||||
pageInfo {
|
||||
endCursor
|
||||
hasNextPage
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
query getJobsCount($fullPath: ID!, $statuses: [CiJobStatus!]) {
|
||||
query getJobsCount($fullPath: ID!, $statuses: [CiJobStatus!], $name: String) {
|
||||
project(fullPath: $fullPath) {
|
||||
id
|
||||
jobs(statuses: $statuses) {
|
||||
jobs(statuses: $statuses, name: $name) {
|
||||
count
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { setUrlParams, updateHistory, queryToObject } from '~/lib/utils/url_util
|
|||
import JobsSkeletonLoader from '~/ci/admin/jobs_table/components/jobs_skeleton_loader.vue';
|
||||
import JobsFilteredSearch from '~/ci/common/private/jobs_filtered_search/app.vue';
|
||||
import { validateQueryString } from '~/ci/common/private/jobs_filtered_search/utils';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import GetJobs from './graphql/queries/get_jobs.query.graphql';
|
||||
import GetJobsCount from './graphql/queries/get_jobs_count.query.graphql';
|
||||
import JobsTable from './components/jobs_table.vue';
|
||||
|
|
@ -31,6 +32,7 @@ export default {
|
|||
GlLoadingIcon,
|
||||
JobsSkeletonLoader,
|
||||
},
|
||||
mixins: [glFeatureFlagsMixin()],
|
||||
inject: {
|
||||
fullPath: {
|
||||
default: '',
|
||||
|
|
@ -83,6 +85,7 @@ export default {
|
|||
filterSearchTriggered: false,
|
||||
jobsCount: null,
|
||||
count: 0,
|
||||
requestData: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -127,11 +130,18 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
updateHistoryAndFetchCount(status = null) {
|
||||
this.$apollo.queries.jobsCount.refetch({ statuses: status });
|
||||
resetRequestData() {
|
||||
if (this.glFeatures.populateAndUseBuildNamesTable) {
|
||||
this.requestData = { statuses: null, name: null };
|
||||
} else {
|
||||
this.requestData = { statuses: null };
|
||||
}
|
||||
},
|
||||
updateHistoryAndFetchCount() {
|
||||
this.$apollo.queries.jobsCount.refetch(this.requestData);
|
||||
|
||||
updateHistory({
|
||||
url: setUrlParams({ statuses: status }, window.location.href, true),
|
||||
url: setUrlParams(this.requestData, window.location.href, true),
|
||||
});
|
||||
},
|
||||
fetchJobsByStatus(scope) {
|
||||
|
|
@ -141,6 +151,8 @@ export default {
|
|||
|
||||
this.scope = scope;
|
||||
|
||||
this.resetRequestData();
|
||||
|
||||
if (!this.scope) this.updateHistoryAndFetchCount();
|
||||
|
||||
this.$apollo.queries.jobs.refetch({ statuses: scope });
|
||||
|
|
@ -149,33 +161,31 @@ export default {
|
|||
this.infiniteScrollingTriggered = false;
|
||||
this.filterSearchTriggered = true;
|
||||
|
||||
// all filters have been cleared reset query param
|
||||
// and refetch jobs/count with defaults
|
||||
if (!filters.length) {
|
||||
this.updateHistoryAndFetchCount();
|
||||
this.$apollo.queries.jobs.refetch({ statuses: null });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Eventually there will be more tokens available
|
||||
// this code is written to scale for those tokens
|
||||
filters.forEach((filter) => {
|
||||
// Raw text input in filtered search does not have a type
|
||||
// when a user enters raw text we alert them that it is
|
||||
// not supported and we do not make an additional API call
|
||||
if (!filter.type) {
|
||||
createAlert({
|
||||
message: RAW_TEXT_WARNING,
|
||||
variant: 'warning',
|
||||
});
|
||||
if (this.glFeatures.populateAndUseBuildNamesTable) {
|
||||
this.requestData.name = filter;
|
||||
} else {
|
||||
createAlert({
|
||||
message: RAW_TEXT_WARNING,
|
||||
variant: 'warning',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (filter.type === 'status') {
|
||||
this.updateHistoryAndFetchCount(filter.value.data);
|
||||
this.$apollo.queries.jobs.refetch({ statuses: filter.value.data });
|
||||
this.requestData.statuses = filter.value.data;
|
||||
}
|
||||
});
|
||||
|
||||
// all filters have been cleared reset query params
|
||||
// and refetch jobs/count with defaults
|
||||
if (!filters.length) {
|
||||
this.resetRequestData();
|
||||
}
|
||||
|
||||
this.$apollo.queries.jobs.refetch(this.requestData);
|
||||
this.updateHistoryAndFetchCount();
|
||||
},
|
||||
fetchMoreJobs() {
|
||||
if (!this.loading) {
|
||||
|
|
|
|||
|
|
@ -280,7 +280,11 @@ export default {
|
|||
>
|
||||
</div>
|
||||
</div>
|
||||
<node-view-content ref="nodeViewContent" as="code" class="gl-relative gl-z-1" />
|
||||
<node-view-content
|
||||
ref="nodeViewContent"
|
||||
as="code"
|
||||
class="gl-relative gl-z-1 !gl-break-words"
|
||||
/>
|
||||
</node-view-wrapper>
|
||||
</editor-state-observer>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ export default {
|
|||
'Environments|Are you sure you want to delete %{podName}? This action cannot be undone.',
|
||||
),
|
||||
buttonPrimary: s__('Environments|Delete pod'),
|
||||
buttonCancel: s__('Environments|Cancel'),
|
||||
buttonCancel: __('Cancel'),
|
||||
success: s__('Environments|Pod deleted successfully'),
|
||||
error: __('Error: '),
|
||||
},
|
||||
|
|
@ -84,7 +84,12 @@ export default {
|
|||
};
|
||||
</script>
|
||||
<template>
|
||||
<gl-modal v-model="visible" :modal-id="$options.DELETE_POD_MODAL_ID" @hide="hideModal">
|
||||
<gl-modal
|
||||
v-model="visible"
|
||||
:modal-id="$options.DELETE_POD_MODAL_ID"
|
||||
:aria-label="$options.i18n.buttonPrimary"
|
||||
@hide="hideModal"
|
||||
>
|
||||
<template #modal-title>
|
||||
<gl-sprintf :message="$options.i18n.title">
|
||||
<template #podName>
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ export default {
|
|||
fluxApiError: '',
|
||||
selectedItem: {},
|
||||
showDetailsDrawer: false,
|
||||
focusedElement: null,
|
||||
podToDelete: {},
|
||||
};
|
||||
},
|
||||
|
|
@ -173,6 +174,7 @@ export default {
|
|||
openDetailsDrawer(item) {
|
||||
this.selectedItem = item;
|
||||
this.showDetailsDrawer = true;
|
||||
this.focusedElement = document.activeElement;
|
||||
this.$nextTick(() => {
|
||||
this.$refs.drawer?.$el?.querySelector('button')?.focus();
|
||||
});
|
||||
|
|
@ -181,7 +183,7 @@ export default {
|
|||
this.showDetailsDrawer = false;
|
||||
this.selectedItem = {};
|
||||
this.$nextTick(() => {
|
||||
this.$refs.status_bar?.$refs?.flux_status_badge?.$el?.focus();
|
||||
this.focusedElement?.focus();
|
||||
});
|
||||
},
|
||||
onDeletePod(pod) {
|
||||
|
|
@ -261,7 +263,7 @@ export default {
|
|||
</h2>
|
||||
</template>
|
||||
<template #default>
|
||||
<workload-details v-if="hasSelectedItem" :item="selectedItem" />
|
||||
<workload-details v-if="hasSelectedItem" :item="selectedItem" @delete-pod="onDeletePod" />
|
||||
</template>
|
||||
</gl-drawer>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -48,8 +48,9 @@ export default {
|
|||
actions: [
|
||||
{
|
||||
name: 'delete-pod',
|
||||
text: s__('KubernetesDashboard|Delete Pod'),
|
||||
text: s__('KubernetesDashboard|Delete pod'),
|
||||
icon: 'remove',
|
||||
variant: 'danger',
|
||||
class: '!gl-text-red-500',
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -227,7 +227,6 @@ export default {
|
|||
<template v-else>
|
||||
<gl-badge
|
||||
:id="fluxBadgeId"
|
||||
ref="flux_status_badge"
|
||||
:icon="syncStatusBadge.icon"
|
||||
:variant="syncStatusBadge.variant"
|
||||
data-testid="sync-badge"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlBadge, GlTruncate } from '@gitlab/ui';
|
||||
import { GlBadge, GlTruncate, GlButton, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { stringify } from 'yaml';
|
||||
import { s__ } from '~/locale';
|
||||
import PodLogsButton from '~/environments/environment_details/components/kubernetes/pod_logs_button.vue';
|
||||
|
|
@ -10,9 +10,13 @@ export default {
|
|||
components: {
|
||||
GlBadge,
|
||||
GlTruncate,
|
||||
GlButton,
|
||||
WorkloadDetailsItem,
|
||||
PodLogsButton,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
|
|
@ -44,6 +48,9 @@ export default {
|
|||
hasContainers() {
|
||||
return Boolean(this.item.containers);
|
||||
},
|
||||
hasActions() {
|
||||
return Boolean(this.item.actions?.length);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getLabelBadgeText([key, value]) {
|
||||
|
|
@ -65,6 +72,7 @@ export default {
|
|||
},
|
||||
i18n: {
|
||||
name: s__('KubernetesDashboard|Name'),
|
||||
actions: s__('KubernetesDashboard|Actions'),
|
||||
kind: s__('KubernetesDashboard|Kind'),
|
||||
labels: s__('KubernetesDashboard|Labels'),
|
||||
status: s__('KubernetesDashboard|Status'),
|
||||
|
|
@ -82,6 +90,20 @@ export default {
|
|||
<workload-details-item :label="$options.i18n.name">
|
||||
<span class="gl-break-anywhere"> {{ item.name }}</span>
|
||||
</workload-details-item>
|
||||
<workload-details-item v-if="hasActions" :label="$options.i18n.actions">
|
||||
<span v-for="action of item.actions" :key="action.name">
|
||||
<gl-button
|
||||
v-gl-tooltip
|
||||
:title="action.text"
|
||||
:aria-label="action.text"
|
||||
:variant="action.variant"
|
||||
:icon="action.icon"
|
||||
category="secondary"
|
||||
class="gl-mr-3"
|
||||
@click="$emit(action.name, item)"
|
||||
/>
|
||||
</span>
|
||||
</workload-details-item>
|
||||
<workload-details-item :label="$options.i18n.kind">
|
||||
{{ item.kind }}
|
||||
</workload-details-item>
|
||||
|
|
|
|||
|
|
@ -425,12 +425,19 @@ export function relativePathToAbsolute(path, basePath) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Checks if the provided URL is a safe URL (absolute http(s) or root-relative URL)
|
||||
* Checks if the provided URL is a valid URL. Valid URLs are
|
||||
* - absolute URLs (`http(s)://...`)
|
||||
* - root-relative URLs (`/path/...`)
|
||||
* - parsable by the `URL` constructor
|
||||
* - has http or https protocol
|
||||
*
|
||||
* Relative URLs (`../path`), queries (`?...`), and hashes (`#...`) are not
|
||||
* considered valid.
|
||||
*
|
||||
* @param {String} url that will be checked
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function isSafeURL(url) {
|
||||
export function isValidURL(url) {
|
||||
if (!isAbsoluteOrRootRelative(url)) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -450,7 +457,7 @@ export function isSafeURL(url) {
|
|||
* @returns {String}
|
||||
*/
|
||||
export function sanitizeUrl(url) {
|
||||
if (!isSafeURL(url)) {
|
||||
if (!isValidURL(url)) {
|
||||
return 'about:blank';
|
||||
}
|
||||
return url;
|
||||
|
|
@ -721,7 +728,7 @@ export const removeLastSlashInUrlPath = (url) =>
|
|||
* Navigates to a URL.
|
||||
*
|
||||
* If destination is a querystring, it will be automatically transformed into a fully qualified URL.
|
||||
* If the URL is not a safe URL (see isSafeURL implementation), this function will log an exception into Sentry.
|
||||
* If the URL is not valid (see isValidURL implementation), this function will log an exception into Sentry.
|
||||
* If the URL is external it calls window.open so it has no referrer header or reference to its opener.
|
||||
*
|
||||
* @param {*} destination - url to navigate to. This can be a fully qualified URL or a querystring.
|
||||
|
|
@ -736,7 +743,7 @@ export function visitUrl(destination, openWindow = false) {
|
|||
url = currentUrl.toString();
|
||||
}
|
||||
|
||||
if (!isSafeURL(url)) {
|
||||
if (!isValidURL(url)) {
|
||||
throw new RangeError(`Only http and https protocols are allowed: ${url}`);
|
||||
}
|
||||
|
||||
|
|
@ -757,7 +764,7 @@ export function visitUrl(destination, openWindow = false) {
|
|||
* Navigates to a URL and display alerts.
|
||||
*
|
||||
* If destination is a querystring, it will be automatically transformed into a fully qualified URL.
|
||||
* If the URL is not a safe URL (see isSafeURL implementation), this function will log an exception into Sentry.
|
||||
* If the URL is not valid (see isValidURL implementation), this function will log an exception into Sentry.
|
||||
*
|
||||
* @param {*} destination - url to navigate to. This can be a fully qualified URL or a querystring.
|
||||
* @param {{id: String, title?: String, message: String, variant: String, dismissible?: Boolean, persistOnPages?: String[]}[]} alerts - Alerts to display
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
<script>
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapState } from 'vuex';
|
||||
import { GlBadge, GlTab, GlTabs } from '@gitlab/ui';
|
||||
import { createAlert } from '~/alert';
|
||||
import { s__ } from '~/locale';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
|
||||
import importSourceUsersQuery from '../graphql/queries/import_source_users.query.graphql';
|
||||
import PlaceholdersTable from './placeholders_table.vue';
|
||||
|
|
@ -21,6 +23,8 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
selectedTabIndex: 0,
|
||||
unassignedCount: null,
|
||||
reassignedCount: null,
|
||||
cursor: {
|
||||
before: null,
|
||||
after: null,
|
||||
|
|
@ -50,10 +54,7 @@ export default {
|
|||
},
|
||||
|
||||
computed: {
|
||||
tabCount() {
|
||||
// WIP: https://gitlab.com/groups/gitlab-org/-/epics/12378
|
||||
return 0;
|
||||
},
|
||||
...mapState('placeholder', ['pagination']),
|
||||
isLoading() {
|
||||
return Boolean(this.$apollo.queries.sourceUsers.loading);
|
||||
},
|
||||
|
|
@ -65,7 +66,23 @@ export default {
|
|||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.unassignedCount = this.pagination.awaitingReassignmentItems;
|
||||
this.reassignedCount = this.pagination.reassignedItems;
|
||||
},
|
||||
|
||||
methods: {
|
||||
onConfirm(item) {
|
||||
this.$toast.show(
|
||||
sprintf(s__('UserMapping|Placeholder %{name} (@%{username}) kept as placeholder.'), {
|
||||
name: item.placeholderUser.name,
|
||||
username: item.placeholderUser.username,
|
||||
}),
|
||||
);
|
||||
this.reassignedCount += 1;
|
||||
this.unassignedCount -= 1;
|
||||
},
|
||||
|
||||
onPrevPage() {
|
||||
this.cursor = {
|
||||
before: this.sourceUsers.pageInfo.startCursor,
|
||||
|
|
@ -88,7 +105,7 @@ export default {
|
|||
<gl-tab>
|
||||
<template #title>
|
||||
<span>{{ s__('UserMapping|Awaiting reassignment') }}</span>
|
||||
<gl-badge class="gl-tab-counter-badge">{{ tabCount }}</gl-badge>
|
||||
<gl-badge class="gl-tab-counter-badge">{{ unassignedCount || 0 }}</gl-badge>
|
||||
</template>
|
||||
|
||||
<placeholders-table
|
||||
|
|
@ -96,6 +113,7 @@ export default {
|
|||
:items="nodes"
|
||||
:page-info="pageInfo"
|
||||
:is-loading="isLoading"
|
||||
@confirm="onConfirm"
|
||||
@prev="onPrevPage"
|
||||
@next="onNextPage"
|
||||
/>
|
||||
|
|
@ -104,11 +122,11 @@ export default {
|
|||
<gl-tab>
|
||||
<template #title>
|
||||
<span>{{ s__('UserMapping|Reassigned') }}</span>
|
||||
<gl-badge class="gl-tab-counter-badge">{{ tabCount }}</gl-badge>
|
||||
<gl-badge class="gl-tab-counter-badge">{{ reassignedCount || 0 }}</gl-badge>
|
||||
</template>
|
||||
|
||||
<placeholders-table
|
||||
key="assigned"
|
||||
key="reassigned"
|
||||
reassigned
|
||||
:items="nodes"
|
||||
:page-info="pageInfo"
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
PLACEHOLDER_STATUS_AWAITING_APPROVAL,
|
||||
PLACEHOLDER_STATUS_REASSIGNING,
|
||||
} from '~/import_entities/import_groups/constants';
|
||||
import importSourceUsersQuery from '../graphql/queries/import_source_users.query.graphql';
|
||||
import importSourceUserReassignMutation from '../graphql/mutations/reassign.mutation.graphql';
|
||||
import importSourceUserKeepAsPlaceholderMutation from '../graphql/mutations/keep_as_placeholder.mutation.graphql';
|
||||
import importSourceUseResendNotificationMutation from '../graphql/mutations/resend_notification.mutation.graphql';
|
||||
|
|
@ -241,21 +242,26 @@ export default {
|
|||
onConfirm() {
|
||||
this.isValidated = true;
|
||||
if (!this.userSelectInvalid) {
|
||||
const hasSelectedUser = Boolean(this.selectedUser.id);
|
||||
this.isConfirmLoading = true;
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: this.selectedUser.id
|
||||
mutation: hasSelectedUser
|
||||
? importSourceUserReassignMutation
|
||||
: importSourceUserKeepAsPlaceholderMutation,
|
||||
variables: {
|
||||
id: this.sourceUser.id,
|
||||
...(this.selectedUser.id ? { userId: this.selectedUser.id } : {}),
|
||||
...(hasSelectedUser ? { userId: this.selectedUser.id } : {}),
|
||||
},
|
||||
// importSourceUsersQuery used in app.vue
|
||||
refetchQueries: [hasSelectedUser ? {} : importSourceUsersQuery],
|
||||
})
|
||||
.then(({ data }) => {
|
||||
const { errors } = getFirstPropertyValue(data);
|
||||
if (errors?.length) {
|
||||
createAlert({ message: errors.join() });
|
||||
} else if (!hasSelectedUser) {
|
||||
this.$emit('confirm');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
|
|
|
|||
|
|
@ -98,6 +98,10 @@ export default {
|
|||
|
||||
return {};
|
||||
},
|
||||
|
||||
onConfirm(item) {
|
||||
this.$emit('confirm', item);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -142,7 +146,7 @@ export default {
|
|||
:label="reassginedUser(item).name"
|
||||
:sub-label="`@${reassginedUser(item).username}`"
|
||||
/>
|
||||
<placeholder-actions v-else :source-user="item" />
|
||||
<placeholder-actions v-else :source-user="item" @confirm="onConfirm(item)" />
|
||||
</template>
|
||||
</gl-table>
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {
|
|||
convertToSentenceCase,
|
||||
splitCamelCase,
|
||||
} from '~/lib/utils/text_utility';
|
||||
import { isSafeURL } from '~/lib/utils/url_utility';
|
||||
import { isValidURL } from '~/lib/utils/url_utility';
|
||||
import { s__ } from '~/locale';
|
||||
import { PAGE_CONFIG } from '~/vue_shared/alert_details/constants';
|
||||
|
||||
|
|
@ -97,7 +97,7 @@ export default {
|
|||
return allowedFields.includes(fieldName);
|
||||
},
|
||||
isValidLink(value) {
|
||||
return typeof value === 'string' && isSafeURL(value);
|
||||
return typeof value === 'string' && isValidURL(value);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { GlButton, GlForm, GlFormGroup, GlFormInput, GlModal } from '@gitlab/ui'
|
|||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import { __, s__, sprintf } from '~/locale';
|
||||
import { isSafeURL } from '~/lib/utils/url_utility';
|
||||
import { isValidURL } from '~/lib/utils/url_utility';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
|
@ -63,7 +63,7 @@ export default {
|
|||
: this.$options.i18n.uploadTitle;
|
||||
},
|
||||
isUrlValid() {
|
||||
return this.modalUrl === '' || isSafeURL(this.modalUrl);
|
||||
return this.modalUrl === '' || isValidURL(this.modalUrl);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
|
|
|||
|
|
@ -205,7 +205,7 @@ export default {
|
|||
data-testid="user-popover"
|
||||
>
|
||||
<template v-if="userCannotMerge" #title>
|
||||
<div class="gl-pb-3 gl-display-flex gl-align-items-center" data-testid="cannot-merge">
|
||||
<div class="gl-flex gl-items-center gl-pb-3" data-testid="cannot-merge">
|
||||
<gl-icon name="warning-solid" class="gl-mr-2 gl-text-orange-400" />
|
||||
<span class="gl-font-normal">{{ __('Cannot merge') }}</span>
|
||||
</div>
|
||||
|
|
@ -232,7 +232,7 @@ export default {
|
|||
<template v-else>
|
||||
<gl-button
|
||||
v-if="shouldRenderToggleFollowButton"
|
||||
class="gl-mt-3 gl-align-self-start"
|
||||
class="gl-mt-3 gl-self-start"
|
||||
:variant="toggleFollowButtonVariant"
|
||||
:loading="toggleFollowLoading"
|
||||
size="small"
|
||||
|
|
@ -245,7 +245,7 @@ export default {
|
|||
<template #meta>
|
||||
<span
|
||||
v-if="hasPronouns"
|
||||
class="gl-text-gray-500 gl-font-sm gl-font-normal gl-p-1"
|
||||
class="gl-p-1 gl-text-sm gl-font-normal gl-text-gray-500"
|
||||
data-testid="user-popover-pronouns"
|
||||
>({{ user.pronouns }})</span
|
||||
>
|
||||
|
|
@ -267,25 +267,25 @@ export default {
|
|||
<template v-else>
|
||||
<template v-if="!isBlocked">
|
||||
<div class="gl-text-gray-500">
|
||||
<div v-if="user.email" class="gl-display-flex gl-mb-2">
|
||||
<div v-if="user.email" class="gl-mb-2 gl-flex">
|
||||
<gl-icon name="mail" class="gl-flex-shrink-0" />
|
||||
<span ref="email" class="gl-ml-2">{{ user.email }}</span>
|
||||
</div>
|
||||
<div v-if="user.bio" class="gl-display-flex gl-mb-2">
|
||||
<div v-if="user.bio" class="gl-mb-2 gl-flex">
|
||||
<gl-icon name="profile" class="gl-flex-shrink-0" />
|
||||
<span ref="bio" class="gl-ml-2">{{ user.bio }}</span>
|
||||
</div>
|
||||
<div v-if="user.workInformation" class="gl-display-flex gl-mb-2">
|
||||
<div v-if="user.workInformation" class="gl-mb-2 gl-flex">
|
||||
<gl-icon name="work" class="gl-flex-shrink-0" />
|
||||
<span ref="workInformation" class="gl-ml-2">{{ user.workInformation }}</span>
|
||||
</div>
|
||||
<div v-if="user.location" class="gl-display-flex gl-mb-2">
|
||||
<div v-if="user.location" class="gl-mb-2 gl-flex">
|
||||
<gl-icon name="location" class="gl-flex-shrink-0" />
|
||||
<span class="gl-ml-2">{{ user.location }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="user.localTime && !user.bot"
|
||||
class="gl-display-flex gl-mb-2"
|
||||
class="gl-mb-2 gl-flex"
|
||||
data-testid="user-popover-local-time"
|
||||
>
|
||||
<gl-icon name="clock" class="gl-flex-shrink-0" />
|
||||
|
|
|
|||
|
|
@ -311,7 +311,7 @@ export default {
|
|||
<template>
|
||||
<gl-dropdown ref="dropdown" :text="text" @toggle="$emit('toggle')" @shown="focusSearch">
|
||||
<template #header>
|
||||
<p class="gl-font-bold gl-text-center gl-mt-2 gl-mb-4">{{ headerText }}</p>
|
||||
<p class="gl-mb-4 gl-mt-2 gl-text-center gl-font-bold">{{ headerText }}</p>
|
||||
<gl-dropdown-divider />
|
||||
<gl-search-box-by-type
|
||||
ref="search"
|
||||
|
|
@ -325,7 +325,7 @@ export default {
|
|||
v-if="isLoading"
|
||||
data-testid="loading-participants"
|
||||
size="md"
|
||||
class="gl-absolute gl-left-0 gl-top-0 gl-right-0"
|
||||
class="gl-absolute gl-left-0 gl-right-0 gl-top-0"
|
||||
/>
|
||||
<template v-else>
|
||||
<template v-if="shouldShowParticipants">
|
||||
|
|
@ -364,7 +364,7 @@ export default {
|
|||
<sidebar-participant
|
||||
:user="currentUser"
|
||||
:issuable-type="issuableType"
|
||||
class="gl-pl-6!"
|
||||
class="!gl-pl-6"
|
||||
/>
|
||||
</gl-dropdown-item>
|
||||
</template>
|
||||
|
|
@ -376,7 +376,7 @@ export default {
|
|||
<sidebar-participant
|
||||
:user="issuableAuthor"
|
||||
:issuable-type="issuableType"
|
||||
class="gl-pl-6!"
|
||||
class="!gl-pl-6"
|
||||
/>
|
||||
</gl-dropdown-item>
|
||||
<gl-dropdown-item
|
||||
|
|
@ -391,10 +391,10 @@ export default {
|
|||
<sidebar-participant
|
||||
:user="unselectedUser"
|
||||
:issuable-type="issuableType"
|
||||
class="gl-pl-6!"
|
||||
class="!gl-pl-6"
|
||||
/>
|
||||
</gl-dropdown-item>
|
||||
<gl-dropdown-item v-if="noUsersFound" data-testid="empty-results" class="gl-pl-6!">
|
||||
<gl-dropdown-item v-if="noUsersFound" data-testid="empty-results" class="!gl-pl-6">
|
||||
{{ __('No matching results') }}
|
||||
</gl-dropdown-item>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -217,7 +217,7 @@ export default {
|
|||
@keydown.esc.stop="cancelEditing"
|
||||
/>
|
||||
</comment-field-layout>
|
||||
<div class="note-form-actions">
|
||||
<div class="note-form-actions" data-testid="work-item-comment-form-actions">
|
||||
<gl-form-checkbox
|
||||
v-if="isNewDiscussion"
|
||||
v-model="isNoteInternal"
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
|||
import { isNumeric } from '~/lib/utils/number_utils';
|
||||
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
|
||||
import SafeHtml from '~/vue_shared/directives/safe_html';
|
||||
import { isSafeURL } from '~/lib/utils/url_utility';
|
||||
import { isValidURL } from '~/lib/utils/url_utility';
|
||||
|
||||
import { highlighter } from 'ee_else_ce/gfm_auto_complete';
|
||||
|
||||
|
|
@ -114,7 +114,7 @@ export default {
|
|||
return this.isSearchingByReference ? this.workItemsByReference : this.workspaceWorkItems;
|
||||
},
|
||||
isSearchingByReference() {
|
||||
return isReference(this.searchTerm) || isSafeURL(this.searchTerm);
|
||||
return isReference(this.searchTerm) || isValidURL(this.searchTerm);
|
||||
},
|
||||
workItemsToAdd: {
|
||||
get() {
|
||||
|
|
|
|||
|
|
@ -211,6 +211,7 @@ export default {
|
|||
|
||||
<gl-modal
|
||||
modal-id="time-tracking-report"
|
||||
data-testid="time-tracking-report-modal"
|
||||
hide-footer
|
||||
size="lg"
|
||||
:title="__('Time tracking report')"
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ hr {
|
|||
code {
|
||||
padding: 2px 4px;
|
||||
color: $code-color;
|
||||
background-color: $gray-50;
|
||||
background-color: var(--gl-background-color-strong);
|
||||
border-radius: $gl-border-radius-base;
|
||||
|
||||
.code > &,
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ class Projects::JobsController < Projects::ApplicationController
|
|||
before_action :verify_proxy_request!, only: :proxy_websocket_authorize
|
||||
before_action :reject_if_build_artifacts_size_refreshing!, only: [:erase]
|
||||
before_action :push_ai_build_failure_cause, only: [:show]
|
||||
before_action :push_filter_by_name, only: [:index]
|
||||
layout 'project'
|
||||
|
||||
feature_category :continuous_integration
|
||||
|
|
@ -281,6 +282,10 @@ class Projects::JobsController < Projects::ApplicationController
|
|||
def push_ai_build_failure_cause
|
||||
push_frontend_feature_flag(:ai_build_failure_cause, @project)
|
||||
end
|
||||
|
||||
def push_filter_by_name
|
||||
push_frontend_feature_flag(:populate_and_use_build_names_table, @project)
|
||||
end
|
||||
end
|
||||
|
||||
Projects::JobsController.prepend_mod_with('Projects::JobsController')
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ class Project < ApplicationRecord
|
|||
BoardLimitExceeded = Class.new(StandardError)
|
||||
ExportLimitExceeded = Class.new(StandardError)
|
||||
|
||||
EPOCH_CACHE_EXPIRATION = 30.days
|
||||
STATISTICS_ATTRIBUTE = 'repositories_count'
|
||||
UNKNOWN_IMPORT_URL = 'http://unknown.git'
|
||||
# Hashed Storage versions handle rolling out new storage to project and dependents models:
|
||||
|
|
@ -3371,8 +3372,36 @@ class Project < ApplicationRecord
|
|||
false
|
||||
end
|
||||
|
||||
def lfs_file_locks_changed_epoch
|
||||
get_epoch_from(lfs_file_locks_changed_epoch_cache_key)
|
||||
end
|
||||
|
||||
def refresh_lfs_file_locks_changed_epoch
|
||||
refresh_epoch_cache(lfs_file_locks_changed_epoch_cache_key)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def with_redis(&block)
|
||||
Gitlab::Redis::Cache.with(&block)
|
||||
end
|
||||
|
||||
def lfs_file_locks_changed_epoch_cache_key
|
||||
"project:#{id}:lfs_file_locks_changed_epoch"
|
||||
end
|
||||
|
||||
def get_epoch_from(cache_key)
|
||||
with_redis { |redis| redis.get(cache_key) }&.to_i || refresh_epoch_cache(cache_key)
|
||||
end
|
||||
|
||||
def refresh_epoch_cache(cache_key)
|
||||
# %s = seconds since the Unix Epoch
|
||||
# %L = milliseconds of the second
|
||||
Time.current.strftime('%s%L').to_i.tap do |epoch|
|
||||
with_redis { |redis| redis.set(cache_key, epoch, ex: EPOCH_CACHE_EXPIRATION) }
|
||||
end
|
||||
end
|
||||
|
||||
# overridden in EE
|
||||
def project_group_links_with_preload
|
||||
project_group_links
|
||||
|
|
|
|||
|
|
@ -25,8 +25,9 @@ module Lfs
|
|||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
def create_lock!
|
||||
lock = project.lfs_file_locks.create!(user: current_user,
|
||||
path: params[:path])
|
||||
lock = project.lfs_file_locks.create!(user: current_user, path: params[:path])
|
||||
|
||||
project.refresh_lfs_file_locks_changed_epoch
|
||||
|
||||
success(http_status: 201, lock: lock)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ module Lfs
|
|||
if lock.can_be_unlocked_by?(current_user, forced)
|
||||
lock.destroy!
|
||||
|
||||
project.refresh_lfs_file_locks_changed_epoch
|
||||
|
||||
success(lock: lock, http_status: :ok)
|
||||
elsif forced
|
||||
error(_('You must have maintainer access to force delete a lock'), 403)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
- if can?(current_user, :"set_#{issuable.to_ability_name}_metadata", issuable)
|
||||
.row.gl-pt-4
|
||||
%div{ class: (has_due_date ? "col-lg-6" : "col-12") }
|
||||
.form-group.row.merge-request-assignee
|
||||
.form-group.row{ data: { testid: 'merge-request-assignee' } }
|
||||
= render "shared/issuable/form/metadata_issuable_assignee", issuable: issuable, form: form, has_due_date: has_due_date
|
||||
|
||||
- if issuable.allows_reviewers?
|
||||
|
|
|
|||
|
|
@ -486,7 +486,7 @@ References:
|
|||
|
||||
##### Vue
|
||||
|
||||
- [isSafeURL](https://gitlab.com/gitlab-org/gitlab/-/blob/v12.7.5-ee/app/assets/javascripts/lib/utils/url_utility.js#L190-207)
|
||||
- [isValidURL](https://gitlab.com/gitlab-org/gitlab/-/blob/v17.3.0-ee/app/assets/javascripts/lib/utils/url_utility.js#L427-451)
|
||||
- [GlSprintf](https://gitlab-org.gitlab.io/gitlab-ui/?path=/docs/utilities-sprintf--sentence-with-link)
|
||||
|
||||
#### Content Security Policy
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
# GitLab Advisory Database
|
||||
|
||||
The [GitLab Advisory Database](https://gitlab.com/gitlab-org/security-products/gemnasium-db) serves as a repository for security advisories related to software dependencies.
|
||||
The [GitLab Advisory Database](https://gitlab.com/gitlab-org/security-products/gemnasium-db) serves as a repository for security advisories related to software dependencies. It is updated on an hourly basis with the latest security advisories.
|
||||
|
||||
The database is an essential component of both [Dependency Scanning](../dependency_scanning/index.md) and [Container Scanning](../container_scanning/index.md).
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 82 KiB |
|
|
@ -53,9 +53,9 @@ This project provides you with:
|
|||
To create a GitLab agent for Kubernetes:
|
||||
|
||||
1. On the left sidebar, select **Operate > Kubernetes clusters**.
|
||||
1. Select **Connect a cluster (agent)**.
|
||||
1. From the **Select an agent** dropdown list, select `civo-agent` and select **Register an agent**.
|
||||
1. GitLab generates a registration token for the agent. Securely store this secret token, as you will need it later.
|
||||
1. Select **Connect a cluster**.
|
||||
1. From the **Select an agent** dropdown list, select `civo-agent` and select **Register**.
|
||||
1. GitLab generates an agent access token for the agent. Securely store this secret token, as you will need it later.
|
||||
1. GitLab provides an address for the agent server (KAS), which you will also need later.
|
||||
|
||||
## Configure your project
|
||||
|
|
@ -66,7 +66,7 @@ Use CI/CD environment variables to configure your project.
|
|||
|
||||
1. On the left sidebar, select **Settings > CI/CD**.
|
||||
1. Expand **Variables**.
|
||||
1. Set the variable `BASE64_CIVO_TOKEN` to the token from your Civo account.
|
||||
1. Set the variable `CIVO_TOKEN` to the token from your Civo account.
|
||||
1. Set the variable `TF_VAR_agent_token` to the agent token you received in the previous task.
|
||||
1. Set the variable `TF_VAR_kas_address` to the agent server address in the previous task.
|
||||
|
||||
|
|
@ -92,14 +92,17 @@ Refer to the [Civo Terraform provider](https://registry.terraform.io/providers/c
|
|||
After configuring your project, manually trigger the provisioning of your cluster. In GitLab:
|
||||
|
||||
1. On the left sidebar, go to **Build > Pipelines**.
|
||||
1. Next to **Play** (**{play}**), select the dropdown list icon (**{chevron-lg-down}**).
|
||||
1. Select **Deploy** to manually trigger the deployment job.
|
||||
1. On the left sidebar, select **Build > Pipelines**.
|
||||
1. Select **Run pipeline**, and then select the newly created pipeline from the list.
|
||||
1. Next to the **deploy** job, select **Manual action** (**{status_manual}**).
|
||||
|
||||
When the pipeline finishes successfully, you can see your new cluster:
|
||||
|
||||
- In Civo dashboard: on your Kubernetes tab.
|
||||
- In GitLab: from your project's sidebar, select **Operate > Kubernetes clusters**.
|
||||
|
||||
If you didn't set the `TF_VAR_civo_region` variable, the cluster will be created in the 'lon1' region.
|
||||
|
||||
## Use your cluster
|
||||
|
||||
After you provision the cluster, it is connected to GitLab and is ready for deployments. To check the connection:
|
||||
|
|
@ -111,28 +114,12 @@ For more information about the capabilities of the connection, see [the GitLab a
|
|||
|
||||
## Remove the cluster
|
||||
|
||||
A cleanup job is not included in your pipeline by default. To remove all created resources, you
|
||||
must modify your GitLab CI/CD template before running the cleanup job.
|
||||
A cleanup job is included in your pipeline by default.
|
||||
|
||||
To remove all resources:
|
||||
To remove all created resources:
|
||||
|
||||
1. Add the following to your `.gitlab-ci.yml` file:
|
||||
|
||||
```yaml
|
||||
stages:
|
||||
- init
|
||||
- validate
|
||||
- build
|
||||
- deploy
|
||||
- cleanup
|
||||
|
||||
destroy:
|
||||
extends: .destroy
|
||||
needs: []
|
||||
```
|
||||
|
||||
1. On the left sidebar, select **Build > Pipelines** and select the most recent pipeline.
|
||||
1. For the `destroy` job, select **Play** (**{play}**).
|
||||
1. On the left sidebar, select **Build > Pipelines**, and then select the most recent pipeline.
|
||||
1. Next to the **destroy-environment** job, select **Manual action** (**{status_manual}**).
|
||||
|
||||
## Civo support
|
||||
|
||||
|
|
|
|||
|
|
@ -37,41 +37,6 @@ Provide feedback on this feature in [issue 443236](https://gitlab.com/gitlab-org
|
|||
|
||||
**Data usage**: The diff of changes between the source branch's head and the target branch is sent to the large language model.
|
||||
|
||||
## Generate a description from a template
|
||||
|
||||
DETAILS:
|
||||
**Tier:** For a limited time, Ultimate. In the future, [GitLab Duo Enterprise](../../../subscriptions/subscription-add-ons.md).
|
||||
**Offering:** GitLab.com
|
||||
**Status:** Beta
|
||||
|
||||
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/10591) in GitLab 16.3 as an [experiment](../../../policy/experiment-beta-support.md#experiment).
|
||||
> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/429882) to beta in GitLab 16.10.
|
||||
|
||||
Many projects include [templates](../description_templates.md#create-a-merge-request-template)
|
||||
that you populate when you create a merge request. These templates help populate the description
|
||||
of the merge request. They can help the team conform to standards, and help reviewers
|
||||
and others understand the purpose and changes proposed in the merge request.
|
||||
|
||||
When you create a merge request, GitLab Duo Merge request template population
|
||||
can generate a description for your merge request, based on the contents of the template.
|
||||
GitLab Duo fills in the template and replaces the contents of the description.
|
||||
|
||||
To use GitLab Duo to generate a merge request description:
|
||||
|
||||
1. [Create a new merge request](creating_merge_requests.md) and go to the **Description** field.
|
||||
1. Select **GitLab Duo** (**{tanuki-ai}**).
|
||||
1. Select **Fill in merge request template**.
|
||||
|
||||
The updated description is applied. You can edit or revise the description before you finish creating your merge request.
|
||||
|
||||
Provide feedback on this experimental feature in [issue 416537](https://gitlab.com/gitlab-org/gitlab/-/issues/416537).
|
||||
|
||||
**Data usage**: When you use this feature, the following data is sent to the large language model referenced above:
|
||||
|
||||
- Title of the merge request
|
||||
- Contents of the description
|
||||
- Diff of changes between the source branch's head and the target branch
|
||||
|
||||
## Summarize a code review
|
||||
|
||||
DETAILS:
|
||||
|
|
|
|||
|
|
@ -207,6 +207,12 @@ check in directly to a protected branch:
|
|||
1. From the **Allowed to push and merge** list, select **No one**.
|
||||
1. Select **Protect**.
|
||||
|
||||
Alternatively, you can [create](repository/branches/index.md#create-a-branch-rule) or [edit](repository/branches/index.md#edit-a-branch-rule) a branch rule. Then:
|
||||
|
||||
1. Select **Edit** in the **Allowed to merge** section.
|
||||
1. Select **Developers and Maintainers**.
|
||||
1. Select **Save changes**.
|
||||
|
||||
## Allow everyone to push directly to a protected branch
|
||||
|
||||
You can allow everyone with write access to push to the protected branch.
|
||||
|
|
@ -219,6 +225,12 @@ You can allow everyone with write access to push to the protected branch.
|
|||
1. From the **Allowed to push and merge** list, select **Developers + Maintainers**.
|
||||
1. Select **Protect**.
|
||||
|
||||
Alternatively, you can [create](repository/branches/index.md#create-a-branch-rule) or [edit](repository/branches/index.md#edit-a-branch-rule) a branch rule. Then:
|
||||
|
||||
1. Select **Edit** in the **Allowed to push and merge** section.
|
||||
1. Select **Developers and Maintainers**.
|
||||
1. Select **Save changes**.
|
||||
|
||||
## Allow deploy keys to push to a protected branch
|
||||
|
||||
> - More restrictions on deploy keys [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/425926) in GitLab 16.5 [with a flag](../../administration/feature_flags.md) named `check_membership_in_protected_ref_access`. Disabled by default.
|
||||
|
|
@ -273,6 +285,10 @@ To enable force pushes on branches that are already protected:
|
|||
1. Select **Add protected branch**.
|
||||
1. In the list of protected branches, next to the branch, turn on the **Allowed to force push** toggle.
|
||||
|
||||
Alternatively, you can [create](repository/branches/index.md#create-a-branch-rule) or [edit](repository/branches/index.md#edit-a-branch-rule) a branch rule. Then:
|
||||
|
||||
1. In the list of protected branches, next to the branch, turn on the **Allowed to force push** toggle.
|
||||
|
||||
Members who can push to this branch can now also force push.
|
||||
|
||||
### When a branch matches multiple rules
|
||||
|
|
@ -328,6 +344,10 @@ To enable Code Owner's approval on branches that are already protected:
|
|||
1. Select **Add protected branch**.
|
||||
1. In the list of protected branches, next to the branch, turn on the **Code owner approval** toggle.
|
||||
|
||||
Alternatively, you can [create](repository/branches/index.md#create-a-branch-rule) or [edit](repository/branches/index.md#edit-a-branch-rule) a branch rule.
|
||||
Then, in the list of protected branches, next to the branch,
|
||||
turn on the **Code owner approval** toggle.
|
||||
|
||||
When enabled, all merge requests for these branches require approval
|
||||
by a Code Owner per matched rule before they can be merged.
|
||||
Additionally, direct pushes to the protected branch are denied if a rule is matched.
|
||||
|
|
|
|||
|
|
@ -101,14 +101,14 @@ To create a branch from an issue:
|
|||
GitLab provides multiple methods to protect individual branches. These methods
|
||||
ensure your branches receive oversight and quality checks from their creation to their deletion:
|
||||
|
||||
- The [default branch](default.md) in your project receives extra protection.
|
||||
- Configure [protected branches](../../protected_branches.md)
|
||||
to restrict who can commit to a branch, merge other branches into it, or merge
|
||||
the branch itself into another branch.
|
||||
- Configure [approval rules](../../merge_requests/approvals/rules.md) to set review
|
||||
requirements, including [security-related approvals](../../merge_requests/approvals/rules.md#security-approvals), before a branch can merge.
|
||||
- Apply enhanced security and protection to your project's [default branch](default.md).
|
||||
- Configure [protected branches](../../protected_branches.md) to:
|
||||
- Limit who can push and merge to a branch.
|
||||
- Manage if users can force push to the branch.
|
||||
- Manage if changes to files listed in the `CODEOWNERS` file can be pushed directly to the branch.
|
||||
- Configure [approval rules](../../merge_requests/approvals/rules.md#approvals-for-protected-branches) to manage review requirements and implement [security-related approvals](../../merge_requests/approvals/rules.md#security-approvals).
|
||||
- Integrate with third-party [status checks](../../merge_requests/status_checks.md)
|
||||
to ensure your branch contents meet your standards of quality.
|
||||
to ensure the contents of your branch meets your defined quality standards.
|
||||
|
||||
You can manage your branches:
|
||||
|
||||
|
|
@ -133,20 +133,13 @@ On this page, you can:
|
|||
- [Compare branches](#compare-branches).
|
||||
- Delete merged branches.
|
||||
|
||||
### View branches with configured protections
|
||||
### View branch rules
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/88279) in GitLab 15.1 with a flag named `branch_rules`. Disabled by default.
|
||||
> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/363170) in GitLab 15.10.
|
||||
> - [Enabled on self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/363170) in GitLab 15.11.
|
||||
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123368) in GitLab 16.1. Feature flag `branch_rules` removed.
|
||||
|
||||
Branches in your repository can be [protected](../../protected_branches.md) in multiple ways. You can:
|
||||
|
||||
- Limit who can push to the branch.
|
||||
- Limit who can merge the branch.
|
||||
- Require approval of all changes.
|
||||
- Require external tests to pass.
|
||||
|
||||
The **Branch rules overview** page shows all branches with any configured protections,
|
||||
and their protection methods:
|
||||
|
||||
|
|
@ -168,7 +161,7 @@ To view the **Branch rules overview** list:
|
|||
1. Identify the branch you want more information about.
|
||||
1. Select **View details** to see information about its:
|
||||
- [Branch protections](../../protected_branches.md).
|
||||
- [Approval rules](../../merge_requests/approvals/rules.md).
|
||||
- [Approval rules](../../merge_requests/approvals/rules.md#approvals-for-protected-branches).
|
||||
- [Status checks](../../merge_requests/status_checks.md).
|
||||
|
||||
#### Create a branch rule
|
||||
|
|
@ -223,7 +216,7 @@ To edit a branch rule:
|
|||
1. Expand **Branch rules**.
|
||||
1. Next to a rule you want to edit, select **View details**.
|
||||
1. In the upper-right corner, select **Edit**.
|
||||
1. In the dialog, from the **Create branch rule** dropdown list, select a branch name or create a wildcard by typing `*`.
|
||||
1. Edit the information as needed.
|
||||
1. Select **Update**.
|
||||
|
||||
#### Delete a branch rule
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
# Only one dashboard should be defined per file
|
||||
# More info: https://docs.gitlab.com/ee/operations/metrics/dashboards/yaml.html
|
||||
dashboard: 'Area Panel Example'
|
||||
|
||||
# For more information about the required properties of panel_groups
|
||||
# please visit: https://docs.gitlab.com/ee/operations/metrics/dashboards/yaml.html#panel-group-panel_groups-properties
|
||||
panel_groups:
|
||||
- group: 'Server Statistics'
|
||||
panels:
|
||||
- title: Average amount of time spent by the CPU
|
||||
type: area-chart
|
||||
metrics:
|
||||
- query_range: 'rate(node_cpu_seconds_total[15m])'
|
||||
unit: 'Seconds'
|
||||
label: "Time in Seconds"
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Only one dashboard should be defined per file
|
||||
# More info: https://docs.gitlab.com/ee/operations/metrics/dashboards/yaml.html
|
||||
dashboard: 'Single Stat'
|
||||
|
||||
# This is where all of the variables that can be manipulated via the UI
|
||||
# are initialized
|
||||
# Check out: https://docs.gitlab.com/ee/operations/metrics/dashboards/templating_variables.html#templating-variables-for-metrics-dashboards-core
|
||||
templating:
|
||||
variables:
|
||||
job: 'prometheus'
|
||||
|
||||
# For more information about the required properties of panel_groups
|
||||
# please visit: https://docs.gitlab.com/ee/operations/metrics/dashboards/yaml.html#panel-group-panel_groups-properties
|
||||
panel_groups:
|
||||
- group: 'Memory'
|
||||
panels:
|
||||
- title: Prometheus
|
||||
type: single-stat
|
||||
metrics:
|
||||
# Queries that make use of variables need to have double curly brackets {}
|
||||
# set to the variables, per the example below
|
||||
- query: 'max(go_memstats_alloc_bytes{job="{{job}}"}) / 1024 /1024'
|
||||
unit: '%'
|
||||
label: "Max"
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
# Only one dashboard should be defined per file
|
||||
# More info: https://docs.gitlab.com/ee/operations/metrics/dashboards/yaml.html
|
||||
dashboard: 'Gauge Panel Example'
|
||||
|
||||
# For more information about the required properties of panel_groups
|
||||
# please visit: https://docs.gitlab.com/ee/operations/metrics/dashboards/yaml.html#panel-group-panel_groups-properties
|
||||
panel_groups:
|
||||
- group: 'Server Statistics'
|
||||
panels:
|
||||
- title: "Memory usage"
|
||||
# More information about gauge panel types can be found here:
|
||||
# https://docs.gitlab.com/ee/operations/metrics/dashboards/panel_types.html#gauge
|
||||
type: "gauge-chart"
|
||||
min_value: 0
|
||||
max_value: 1024
|
||||
split: 10
|
||||
thresholds:
|
||||
mode: "percentage"
|
||||
values: [60, 90]
|
||||
format: "megabytes"
|
||||
metrics:
|
||||
- query: '(node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) / 1024 / 1024'
|
||||
unit: 'MB'
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# Development guide for Metrics Dashboard templates
|
||||
|
||||
Please follow [the development guideline](../../../../doc/development/operations/metrics/templates.md)
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
# Only one dashboard should be defined per file
|
||||
# More info: https://docs.gitlab.com/ee/operations/metrics/dashboards/yaml.html
|
||||
dashboard: 'Area Panel Example'
|
||||
|
||||
# For more information about the required properties of panel_groups
|
||||
# please visit: https://docs.gitlab.com/ee/operations/metrics/dashboards/yaml.html#panel-group-panel_groups-properties
|
||||
panel_groups:
|
||||
- group: 'Server Statistics'
|
||||
panels:
|
||||
- title: "Core Usage (Pod Average)"
|
||||
type: area-chart
|
||||
metrics:
|
||||
- query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container!="POD",pod=~"^{{ci_environment_slug}}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="{{kube_namespace}}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container!="POD",pod=~"^{{ci_environment_slug}}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="{{kube_namespace}}"}[15m])) by (pod)) OR avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^{{ci_environment_slug}}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="{{kube_namespace}}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^{{ci_environment_slug}}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="{{kube_namespace}}"}[15m])) by (pod_name))'
|
||||
unit: 'cores'
|
||||
label: "Pod Average (in seconds)"
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
# Only one dashboard should be defined per file
|
||||
# More info: https://docs.gitlab.com/ee/operations/metrics/dashboards/yaml.html
|
||||
dashboard: 'Gauge K8s Panel Example'
|
||||
|
||||
# For more information about the required properties of panel_groups
|
||||
# please visit: https://docs.gitlab.com/ee/operations/metrics/dashboards/yaml.html#panel-group-panel_groups-properties
|
||||
panel_groups:
|
||||
- group: 'Server Statistics'
|
||||
panels:
|
||||
- title: "Memory usage"
|
||||
# More information about gauge panel types can be found here:
|
||||
# https://docs.gitlab.com/ee/operations/metrics/dashboards/panel_types.html#gauge
|
||||
type: "gauge-chart"
|
||||
min_value: 0
|
||||
max_value: 1024
|
||||
split: 10
|
||||
thresholds:
|
||||
mode: "percentage"
|
||||
values: [60, 90]
|
||||
format: "megabytes"
|
||||
metrics:
|
||||
- query: 'avg(sum(container_memory_usage_bytes{container!="POD",pod=~"^{{ci_environment_slug}}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="{{kube_namespace}}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container!="POD",pod=~"^{{ci_environment_slug}}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="{{kube_namespace}}"}) without (job)) /1024/1024 OR avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^{{ci_environment_slug}}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="{{kube_namespace}}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^{{ci_environment_slug}}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="{{kube_namespace}}"}) without (job)) /1024/1024'
|
||||
unit: 'MB'
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
# Only one dashboard should be defined per file
|
||||
# More info: https://docs.gitlab.com/ee/operations/metrics/dashboards/yaml.html
|
||||
dashboard: 'Single Stat Panel Example'
|
||||
|
||||
# For more information about the required properties of panel_groups
|
||||
# please visit: https://docs.gitlab.com/ee/operations/metrics/dashboards/yaml.html#panel-group-panel_groups-properties
|
||||
panel_groups:
|
||||
- group: 'Server Statistics'
|
||||
panels:
|
||||
- title: "Memory usage"
|
||||
# More information about heatmap panel types can be found here:
|
||||
# https://docs.gitlab.com/ee/operations/metrics/dashboards/panel_types.html#single-stat
|
||||
type: "single-stat"
|
||||
metrics:
|
||||
- query: 'avg(sum(container_memory_usage_bytes{container!="POD",pod=~"^{{ci_environment_slug}}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="{{kube_namespace}}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container!="POD",pod=~"^{{ci_environment_slug}}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="{{kube_namespace}}"}) without (job)) /1024/1024 OR avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^{{ci_environment_slug}}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="{{kube_namespace}}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^{{ci_environment_slug}}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="{{kube_namespace}}"}) without (job)) /1024/1024'
|
||||
unit: 'MB'
|
||||
label: "Used memory"
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
# Only one dashboard should be defined per file
|
||||
# More info: https://docs.gitlab.com/ee/operations/metrics/dashboards/yaml.html
|
||||
dashboard: 'Heatmap Panel Example'
|
||||
|
||||
# For more information about the required properties of panel_groups
|
||||
# please visit: https://docs.gitlab.com/ee/operations/metrics/dashboards/yaml.html#panel-group-panel_groups-properties
|
||||
panel_groups:
|
||||
- group: 'Server Statistics'
|
||||
panels:
|
||||
- title: "Memory usage"
|
||||
# More information about heatmap panel types can be found here:
|
||||
# https://docs.gitlab.com/ee/operations/metrics/dashboards/panel_types.html#single-stat
|
||||
type: "single-stat"
|
||||
metrics:
|
||||
- query: '(node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) / 1024 / 1024'
|
||||
unit: 'MB'
|
||||
label: "Used memory"
|
||||
|
|
@ -20483,9 +20483,6 @@ msgstr ""
|
|||
msgid "Environments|Auto stops %{autoStopAt}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|Cancel"
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|Clean up"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -29972,6 +29969,9 @@ msgstr ""
|
|||
msgid "Jobs|Raw text search is not currently supported for the jobs filtered search feature. Please use the available search tokens."
|
||||
msgstr ""
|
||||
|
||||
msgid "Jobs|Search or filter jobs..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Jobs|Stage"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -30323,6 +30323,9 @@ msgstr ""
|
|||
msgid "Kubernetes deployment not found"
|
||||
msgstr ""
|
||||
|
||||
msgid "KubernetesDashboard|Actions"
|
||||
msgstr ""
|
||||
|
||||
msgid "KubernetesDashboard|Age"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -30356,7 +30359,7 @@ msgstr ""
|
|||
msgid "KubernetesDashboard|Dashboard"
|
||||
msgstr ""
|
||||
|
||||
msgid "KubernetesDashboard|Delete Pod"
|
||||
msgid "KubernetesDashboard|Delete pod"
|
||||
msgstr ""
|
||||
|
||||
msgid "KubernetesDashboard|Deployment"
|
||||
|
|
@ -57809,6 +57812,9 @@ msgstr ""
|
|||
msgid "UserMapping|Pending approval"
|
||||
msgstr ""
|
||||
|
||||
msgid "UserMapping|Placeholder %{name} (@%{username}) kept as placeholder."
|
||||
msgstr ""
|
||||
|
||||
msgid "UserMapping|Placeholder user"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ RSpec.describe 'Broadcast Messages', feature_category: :notifications do
|
|||
|
||||
expect_to_be_on_explore_projects_page
|
||||
|
||||
find('body.page-initialised .js-dismiss-current-broadcast-notification').click
|
||||
find(".js-dismiss-current-broadcast-notification[data-id='#{broadcast_message.id}']").click
|
||||
|
||||
expect_message_dismissed
|
||||
end
|
||||
|
|
@ -41,7 +41,7 @@ RSpec.describe 'Broadcast Messages', feature_category: :notifications do
|
|||
|
||||
expect_to_be_on_explore_projects_page
|
||||
|
||||
find('body.page-initialised .js-dismiss-current-broadcast-notification').click
|
||||
find(".js-dismiss-current-broadcast-notification[data-id='#{broadcast_message.id}']").click
|
||||
|
||||
expect_message_dismissed
|
||||
|
||||
|
|
@ -57,7 +57,7 @@ RSpec.describe 'Broadcast Messages', feature_category: :notifications do
|
|||
|
||||
expect_to_be_on_explore_projects_page
|
||||
|
||||
find('body.page-initialised .js-dismiss-current-broadcast-notification').click
|
||||
find(".js-dismiss-current-broadcast-notification[data-id='#{broadcast_message.id}']").click
|
||||
|
||||
expect_message_dismissed
|
||||
|
||||
|
|
@ -79,7 +79,7 @@ RSpec.describe 'Broadcast Messages', feature_category: :notifications do
|
|||
it 'is not dismissible' do
|
||||
visit path
|
||||
|
||||
expect(page).not_to have_selector('.js-dismiss-current-broadcast-notification')
|
||||
expect(page).not_to have_selector(".js-dismiss-current-broadcast-notification[data-id=#{broadcast_message.id}]")
|
||||
end
|
||||
|
||||
it 'does not replace placeholders' do
|
||||
|
|
@ -127,7 +127,7 @@ RSpec.describe 'Broadcast Messages', feature_category: :notifications do
|
|||
|
||||
visit path
|
||||
|
||||
expect_broadcast_message(text)
|
||||
expect_broadcast_message(message.id, text)
|
||||
|
||||
# seed the other cache
|
||||
original_strategy_value = Gitlab::Cache::JsonCache::STRATEGY_KEY_COMPONENTS
|
||||
|
|
@ -135,7 +135,7 @@ RSpec.describe 'Broadcast Messages', feature_category: :notifications do
|
|||
|
||||
page.refresh
|
||||
|
||||
expect_broadcast_message(text)
|
||||
expect_broadcast_message(message.id, text)
|
||||
|
||||
# delete on original cache
|
||||
stub_const('Gitlab::Cache::JsonCaches::JsonKeyed::STRATEGY_KEY_COMPONENTS', original_strategy_value)
|
||||
|
|
@ -153,27 +153,27 @@ RSpec.describe 'Broadcast Messages', feature_category: :notifications do
|
|||
|
||||
visit path
|
||||
|
||||
expect_no_broadcast_message
|
||||
expect_no_broadcast_message(message.id)
|
||||
|
||||
# other revision of GitLab does gets cache destroyed
|
||||
stub_const('Gitlab::Cache::JsonCaches::JsonKeyed::STRATEGY_KEY_COMPONENTS', new_strategy_value)
|
||||
|
||||
page.refresh
|
||||
|
||||
expect_no_broadcast_message
|
||||
expect_no_broadcast_message(message.id)
|
||||
end
|
||||
end
|
||||
|
||||
def expect_broadcast_message(text)
|
||||
within_testid('banner-broadcast-message') do
|
||||
def expect_broadcast_message(id, text)
|
||||
within(".js-broadcast-notification-#{id}") do
|
||||
expect(page).to have_content text
|
||||
end
|
||||
end
|
||||
|
||||
def expect_no_broadcast_message
|
||||
def expect_no_broadcast_message(id)
|
||||
expect_to_be_on_explore_projects_page
|
||||
|
||||
expect(page).not_to have_selector('[data-testid="banner-broadcast-message"]')
|
||||
expect(page).not_to have_selector(".js-broadcast-notification-#{id}")
|
||||
end
|
||||
|
||||
def expect_to_be_on_explore_projects_page
|
||||
|
|
|
|||
|
|
@ -353,7 +353,7 @@ RSpec.describe 'GFM autocomplete', :js, feature_category: :team_planning do
|
|||
end
|
||||
end
|
||||
|
||||
context 'issues' do
|
||||
context 'issues', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/471790' do
|
||||
let(:object) { issue }
|
||||
let(:expected_body) { object.to_reference }
|
||||
|
||||
|
|
|
|||
|
|
@ -50,8 +50,10 @@ RSpec.describe 'Issues > Real-time sidebar', :js, :with_license, feature_categor
|
|||
|
||||
wait_for_all_requests
|
||||
|
||||
click_button label.name
|
||||
click_button 'Close'
|
||||
page.within(labels_widget) do
|
||||
click_button label.name
|
||||
click_button 'Close'
|
||||
end
|
||||
|
||||
wait_for_requests
|
||||
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ RSpec.describe 'Work item children', :js, feature_category: :team_planning do
|
|||
end
|
||||
|
||||
it 'adds a new child task', :aggregate_failures do
|
||||
allow(Gitlab::QueryLimiting::Transaction).to receive(:threshold).and_return(104)
|
||||
allow(Gitlab::QueryLimiting::Transaction).to receive(:threshold).and_return(108)
|
||||
|
||||
within_testid('work-item-links') do
|
||||
click_button 'Add'
|
||||
|
|
@ -79,7 +79,7 @@ RSpec.describe 'Work item children', :js, feature_category: :team_planning do
|
|||
end
|
||||
|
||||
it 'removes a child task and undoing', :aggregate_failures do
|
||||
allow(Gitlab::QueryLimiting::Transaction).to receive(:threshold).and_return(104)
|
||||
allow(Gitlab::QueryLimiting::Transaction).to receive(:threshold).and_return(108)
|
||||
within_testid('work-item-links') do
|
||||
click_button 'Add'
|
||||
click_button 'New task'
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ describe('Jobs filtered search', () => {
|
|||
...props,
|
||||
},
|
||||
provide: {
|
||||
glFeatures: { adminJobsFilterRunnerType: true },
|
||||
glFeatures: { adminJobsFilterRunnerType: true, populateAndUseBuildNamesTable: true },
|
||||
...provideOptions,
|
||||
},
|
||||
});
|
||||
|
|
@ -40,6 +40,19 @@ describe('Jobs filtered search', () => {
|
|||
expect(findFilteredSearch().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('displays filtered search placeholder', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findFilteredSearch().props('placeholder')).toBe('Search or filter jobs...');
|
||||
});
|
||||
|
||||
it('displays filtered search text label', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findFilteredSearch().props('searchTextOptionLabel')).toBe('Search for this text');
|
||||
expect(findFilteredSearch().props('termsAsTokens')).toBe(true);
|
||||
});
|
||||
|
||||
it('displays status token', () => {
|
||||
createComponent();
|
||||
|
||||
|
|
@ -82,7 +95,11 @@ describe('Jobs filtered search', () => {
|
|||
const tokenRunnerTypesValue = 'INSTANCE_VALUE';
|
||||
|
||||
createComponent({
|
||||
queryString: { statuses: tokenStatusesValue, runnerTypes: tokenRunnerTypesValue },
|
||||
queryString: {
|
||||
statuses: tokenStatusesValue,
|
||||
runnerTypes: tokenRunnerTypesValue,
|
||||
name: 'rspec',
|
||||
},
|
||||
});
|
||||
|
||||
expect(findFilteredSearch().props('value')).toEqual([
|
||||
|
|
@ -91,6 +108,12 @@ describe('Jobs filtered search', () => {
|
|||
type: TOKEN_TYPE_JOBS_RUNNER_TYPE,
|
||||
value: { data: tokenRunnerTypesValue, operator: '=' },
|
||||
},
|
||||
{
|
||||
type: 'filtered-search-term',
|
||||
value: {
|
||||
data: 'rspec',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
@ -120,4 +143,37 @@ describe('Jobs filtered search', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when feature flag `populateAndUseBuildNamesTable` is disabled', () => {
|
||||
const provideOptions = { glFeatures: { populateAndUseBuildNamesTable: false } };
|
||||
|
||||
describe('with query string passed', () => {
|
||||
it('filtered search returns only data shape for search token `status`', () => {
|
||||
const tokenStatusesValue = 'SUCCESS';
|
||||
const tokenRunnerTypesValue = 'INSTANCE_VALUE';
|
||||
|
||||
createComponent(
|
||||
{
|
||||
queryString: {
|
||||
statuses: tokenStatusesValue,
|
||||
runnerTypes: tokenRunnerTypesValue,
|
||||
name: 'rspec',
|
||||
},
|
||||
},
|
||||
provideOptions,
|
||||
);
|
||||
|
||||
expect(findFilteredSearch().props('value')).toEqual([
|
||||
{ type: TOKEN_TYPE_STATUS, value: { data: tokenStatusesValue, operator: '=' } },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('displays legacy filtered search attributes', () => {
|
||||
createComponent({}, provideOptions);
|
||||
|
||||
expect(findFilteredSearch().props('placeholder')).toBe('Filter jobs');
|
||||
expect(findFilteredSearch().props('termsAsTokens')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -12,6 +12,7 @@ describe('Filtered search utils', () => {
|
|||
${{ wrong: 'SUCCESS' }} | ${null}
|
||||
${{ statuses: 'wrong' }} | ${null}
|
||||
${{ wrong: 'wrong' }} | ${null}
|
||||
${{ name: 'rspec' }} | ${{ name: 'rspec' }}
|
||||
`(
|
||||
'when provided $queryStringObject, the expected result is $expected',
|
||||
({ queryStringObject, expected }) => {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ Vue.use(VueApollo);
|
|||
|
||||
jest.mock('~/alert');
|
||||
|
||||
const mockJobName = 'rspec-job';
|
||||
|
||||
describe('Job table app', () => {
|
||||
let wrapper;
|
||||
|
||||
|
|
@ -60,10 +62,14 @@ describe('Job table app', () => {
|
|||
handler = successHandler,
|
||||
countHandler = countSuccessHandler,
|
||||
mountFn = shallowMount,
|
||||
flagState = false,
|
||||
} = {}) => {
|
||||
wrapper = mountFn(JobsTableApp, {
|
||||
provide: {
|
||||
fullPath: projectPath,
|
||||
glFeatures: {
|
||||
populateAndUseBuildNamesTable: flagState,
|
||||
},
|
||||
},
|
||||
apolloProvider: createMockApolloProvider(handler, countHandler),
|
||||
});
|
||||
|
|
@ -246,6 +252,22 @@ describe('Job table app', () => {
|
|||
},
|
||||
);
|
||||
|
||||
it('filters jobs by status', async () => {
|
||||
createComponent();
|
||||
|
||||
await findFilteredSearch().vm.$emit('filterJobsBySearch', [mockFailedSearchToken]);
|
||||
|
||||
expect(successHandler).toHaveBeenCalledWith({
|
||||
first: 30,
|
||||
fullPath: 'gitlab-org/gitlab',
|
||||
statuses: 'FAILED',
|
||||
});
|
||||
expect(countSuccessHandler).toHaveBeenCalledWith({
|
||||
fullPath: 'gitlab-org/gitlab',
|
||||
statuses: 'FAILED',
|
||||
});
|
||||
});
|
||||
|
||||
it('refetches jobs query when filtering', async () => {
|
||||
createComponent();
|
||||
|
||||
|
|
@ -334,5 +356,87 @@ describe('Job table app', () => {
|
|||
statuses: null,
|
||||
});
|
||||
});
|
||||
|
||||
describe('with feature flag populateAndUseBuildNamesTable enabled', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ flagState: true });
|
||||
});
|
||||
|
||||
it('filters jobs by name', async () => {
|
||||
await findFilteredSearch().vm.$emit('filterJobsBySearch', [mockJobName]);
|
||||
|
||||
expect(successHandler).toHaveBeenCalledWith({
|
||||
first: 30,
|
||||
fullPath: 'gitlab-org/gitlab',
|
||||
name: mockJobName,
|
||||
});
|
||||
expect(countSuccessHandler).toHaveBeenCalledWith({
|
||||
fullPath: 'gitlab-org/gitlab',
|
||||
name: mockJobName,
|
||||
});
|
||||
});
|
||||
|
||||
it('updates URL query string when filtering jobs by name', async () => {
|
||||
jest.spyOn(urlUtils, 'updateHistory');
|
||||
|
||||
await findFilteredSearch().vm.$emit('filterJobsBySearch', [mockJobName]);
|
||||
|
||||
expect(urlUtils.updateHistory).toHaveBeenCalledWith({
|
||||
url: `${TEST_HOST}/?name=${mockJobName}`,
|
||||
});
|
||||
});
|
||||
|
||||
it('updates URL query string when filtering jobs by name and status', async () => {
|
||||
jest.spyOn(urlUtils, 'updateHistory');
|
||||
|
||||
await findFilteredSearch().vm.$emit('filterJobsBySearch', [
|
||||
mockFailedSearchToken,
|
||||
mockJobName,
|
||||
]);
|
||||
|
||||
expect(urlUtils.updateHistory).toHaveBeenCalledWith({
|
||||
url: `${TEST_HOST}/?statuses=FAILED&name=${mockJobName}`,
|
||||
});
|
||||
});
|
||||
|
||||
it('resets query param after clearing tokens', () => {
|
||||
jest.spyOn(urlUtils, 'updateHistory');
|
||||
|
||||
findFilteredSearch().vm.$emit('filterJobsBySearch', [mockFailedSearchToken, mockJobName]);
|
||||
|
||||
expect(successHandler).toHaveBeenCalledWith({
|
||||
first: 30,
|
||||
fullPath: 'gitlab-org/gitlab',
|
||||
statuses: 'FAILED',
|
||||
name: mockJobName,
|
||||
});
|
||||
expect(countSuccessHandler).toHaveBeenCalledWith({
|
||||
fullPath: 'gitlab-org/gitlab',
|
||||
statuses: 'FAILED',
|
||||
name: mockJobName,
|
||||
});
|
||||
expect(urlUtils.updateHistory).toHaveBeenCalledWith({
|
||||
url: `${TEST_HOST}/?statuses=FAILED&name=${mockJobName}`,
|
||||
});
|
||||
|
||||
findFilteredSearch().vm.$emit('filterJobsBySearch', []);
|
||||
|
||||
expect(urlUtils.updateHistory).toHaveBeenCalledWith({
|
||||
url: `${TEST_HOST}/`,
|
||||
});
|
||||
|
||||
expect(successHandler).toHaveBeenCalledWith({
|
||||
first: 30,
|
||||
fullPath: 'gitlab-org/gitlab',
|
||||
statuses: null,
|
||||
name: null,
|
||||
});
|
||||
expect(countSuccessHandler).toHaveBeenCalledWith({
|
||||
fullPath: 'gitlab-org/gitlab',
|
||||
statuses: null,
|
||||
name: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -377,6 +377,16 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_ov
|
|||
|
||||
expect(findDeletePodModal().props('pod')).toEqual(podToDelete);
|
||||
});
|
||||
|
||||
it('provides correct pod when emitted from the details drawer', async () => {
|
||||
const podToDelete = mockPodsTableItems[1];
|
||||
findKubernetesTabs().vm.$emit('show-resource-details', mockPodsTableItems[1]);
|
||||
await nextTick();
|
||||
findWorkloadDetails().vm.$emit('delete-pod', podToDelete);
|
||||
await nextTick();
|
||||
|
||||
expect(findDeletePodModal().props('pod')).toEqual(podToDelete);
|
||||
});
|
||||
});
|
||||
|
||||
describe('on child component error', () => {
|
||||
|
|
|
|||
|
|
@ -106,8 +106,9 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_po
|
|||
const actions = [
|
||||
{
|
||||
name: 'delete-pod',
|
||||
text: 'Delete Pod',
|
||||
text: 'Delete pod',
|
||||
icon: 'remove',
|
||||
variant: 'danger',
|
||||
class: '!gl-text-red-500',
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -374,9 +374,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
|
|||
});
|
||||
|
||||
describe('deleteKubernetesPod', () => {
|
||||
const mockPodsDeleteFn = jest.fn().mockImplementation(() => {
|
||||
return Promise.resolve({ errors: [] });
|
||||
});
|
||||
const mockPodsDeleteFn = jest.fn().mockResolvedValue({ errors: [] });
|
||||
const podToDelete = 'my-pod';
|
||||
|
||||
it('should request delete pod API from the cluster_client library', async () => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { GlBadge, GlTruncate } from '@gitlab/ui';
|
||||
import { GlBadge, GlTruncate, GlButton } from '@gitlab/ui';
|
||||
import WorkloadDetails from '~/kubernetes_dashboard/components/workload_details.vue';
|
||||
import WorkloadDetailsItem from '~/kubernetes_dashboard/components/workload_details_item.vue';
|
||||
import { WORKLOAD_STATUS_BADGE_VARIANTS } from '~/kubernetes_dashboard/constants';
|
||||
|
|
@ -25,6 +25,8 @@ const findAllBadges = () => wrapper.findAllComponents(GlBadge);
|
|||
const findBadge = (at) => findAllBadges().at(at);
|
||||
const findAllPodLogsButtons = () => wrapper.findAllComponents(PodLogsButton);
|
||||
const findPodLogsButton = (at) => findAllPodLogsButtons().at(at);
|
||||
const findAllButtons = () => wrapper.findAllComponents(GlButton);
|
||||
const findButton = (at) => findAllButtons().at(at);
|
||||
|
||||
describe('Workload details component', () => {
|
||||
describe('when minimal fields are provided', () => {
|
||||
|
|
@ -74,6 +76,48 @@ describe('Workload details component', () => {
|
|||
expect(findWorkloadDetailsItem(index).props('collapsible')).toBe(true);
|
||||
});
|
||||
|
||||
describe('when actions are provided', () => {
|
||||
const actions = [
|
||||
{
|
||||
name: 'delete-pod',
|
||||
text: 'Delete pod',
|
||||
icon: 'remove',
|
||||
variant: 'danger',
|
||||
},
|
||||
];
|
||||
const mockTableItemsWithActions = {
|
||||
...mockPodsTableItems[0],
|
||||
actions,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
createWrapper(mockTableItemsWithActions);
|
||||
});
|
||||
|
||||
it('renders a non-collapsible list item for containers', () => {
|
||||
expect(findWorkloadDetailsItem(1).props('label')).toBe('Actions');
|
||||
expect(findWorkloadDetailsItem(1).props('collapsible')).toBe(false);
|
||||
});
|
||||
|
||||
it('renders a button for each action', () => {
|
||||
expect(findAllButtons()).toHaveLength(1);
|
||||
});
|
||||
|
||||
it.each(actions)('renders a button with the correct props', (action) => {
|
||||
const currentIndex = actions.indexOf(action);
|
||||
|
||||
expect(findButton(currentIndex).props()).toMatchObject({
|
||||
variant: action.variant,
|
||||
icon: action.icon,
|
||||
});
|
||||
|
||||
expect(findButton(currentIndex).attributes()).toMatchObject({
|
||||
title: action.text,
|
||||
'aria-label': action.text,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when containers are provided', () => {
|
||||
const mockTableItemsWithContainers = {
|
||||
...mockPodsTableItems[0],
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ describe('Workload table component', () => {
|
|||
});
|
||||
|
||||
it('renders correct props for each dropdown', () => {
|
||||
expect(findAllActionsDropdowns().at(0).attributes('title')).toEqual('Actions');
|
||||
expect(findAllActionsDropdowns().at(0).attributes('title')).toBe('Actions');
|
||||
expect(findAllActionsDropdowns().at(0).props('items')).toMatchObject([
|
||||
{
|
||||
text: 'Delete Pod',
|
||||
|
|
|
|||
|
|
@ -38,8 +38,8 @@ const encodedJavaScriptUrls = [
|
|||
'\\u006A\\u0061\\u0076\\u0061\\u0073\\u0063\\u0072\\u0069\\u0070\\u0074\\u003A\\u0061\\u006C\\u0065\\u0072\\u0074\\u0028\\u0027\\u0058\\u0053\\u0053\\u0027\\u0029',
|
||||
];
|
||||
|
||||
export const safeUrls = [...absoluteUrls, ...rootRelativeUrls];
|
||||
export const unsafeUrls = [
|
||||
export const validURLs = [...absoluteUrls, ...rootRelativeUrls];
|
||||
export const invalidURLs = [
|
||||
...relativeUrls,
|
||||
...urlsWithoutHost,
|
||||
...nonHttpUrls,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import setWindowLocation from 'helpers/set_window_location_helper';
|
|||
import { TEST_HOST } from 'helpers/test_constants';
|
||||
import * as urlUtils from '~/lib/utils/url_utility';
|
||||
import { setGlobalAlerts } from '~/lib/utils/global_alerts';
|
||||
import { safeUrls, unsafeUrls } from './mock_data';
|
||||
import { validURLs, invalidURLs } from './mock_data';
|
||||
|
||||
jest.mock('~/lib/utils/global_alerts', () => ({
|
||||
getGlobalAlerts: jest.fn().mockImplementation(() => [
|
||||
|
|
@ -781,24 +781,24 @@ describe('URL utility', () => {
|
|||
);
|
||||
});
|
||||
|
||||
describe('isSafeUrl', () => {
|
||||
describe('isValidURL', () => {
|
||||
describe('with URL constructor support', () => {
|
||||
it.each(safeUrls)('returns true for %s', (url) => {
|
||||
expect(urlUtils.isSafeURL(url)).toBe(true);
|
||||
it.each(validURLs)('returns true for %s', (url) => {
|
||||
expect(urlUtils.isValidURL(url)).toBe(true);
|
||||
});
|
||||
|
||||
it.each(unsafeUrls)('returns false for %s', (url) => {
|
||||
expect(urlUtils.isSafeURL(url)).toBe(false);
|
||||
it.each(invalidURLs)('returns false for %s', (url) => {
|
||||
expect(urlUtils.isValidURL(url)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitizeUrl', () => {
|
||||
it.each(safeUrls)('returns the url for %s', (url) => {
|
||||
it.each(validURLs)('returns the url for %s', (url) => {
|
||||
expect(urlUtils.sanitizeUrl(url)).toBe(url);
|
||||
});
|
||||
|
||||
it.each(unsafeUrls)('returns `about:blank` for %s', (url) => {
|
||||
it.each(invalidURLs)('returns `about:blank` for %s', (url) => {
|
||||
expect(urlUtils.sanitizeUrl(url)).toBe('about:blank');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import Vue from 'vue';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import Vuex from 'vuex';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { GlTabs } from '@gitlab/ui';
|
||||
import { GlTab, GlTabs } from '@gitlab/ui';
|
||||
import { createAlert } from '~/alert';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
|
|
@ -9,13 +11,16 @@ import waitForPromises from 'helpers/wait_for_promises';
|
|||
import PlaceholdersTabApp from '~/members/placeholders/components/app.vue';
|
||||
import PlaceholdersTable from '~/members/placeholders/components/placeholders_table.vue';
|
||||
import importSourceUsersQuery from '~/members/placeholders/graphql/queries/import_source_users.query.graphql';
|
||||
import { mockSourceUsersQueryResponse } from '../mock_data';
|
||||
import { MEMBERS_TAB_TYPES } from '~/members/constants';
|
||||
import { mockSourceUsersQueryResponse, mockSourceUsers, pagination } from '../mock_data';
|
||||
|
||||
Vue.use(Vuex);
|
||||
Vue.use(VueApollo);
|
||||
jest.mock('~/alert');
|
||||
|
||||
describe('PlaceholdersTabApp', () => {
|
||||
let wrapper;
|
||||
let store;
|
||||
let mockApollo;
|
||||
|
||||
const mockGroup = {
|
||||
|
|
@ -23,19 +28,37 @@ describe('PlaceholdersTabApp', () => {
|
|||
name: 'Imported group',
|
||||
};
|
||||
const sourceUsersQueryHandler = jest.fn().mockResolvedValue(mockSourceUsersQueryResponse());
|
||||
const $toast = {
|
||||
show: jest.fn(),
|
||||
};
|
||||
|
||||
const createComponent = ({ queryHandler = sourceUsersQueryHandler } = {}) => {
|
||||
store = new Vuex.Store({
|
||||
modules: {
|
||||
[MEMBERS_TAB_TYPES.placeholder]: {
|
||||
namespaced: true,
|
||||
state: {
|
||||
pagination,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
mockApollo = createMockApollo([[importSourceUsersQuery, queryHandler]]);
|
||||
|
||||
wrapper = shallowMount(PlaceholdersTabApp, {
|
||||
apolloProvider: mockApollo,
|
||||
store,
|
||||
provide: {
|
||||
group: mockGroup,
|
||||
},
|
||||
mocks: { $toast },
|
||||
stubs: { GlTab },
|
||||
});
|
||||
};
|
||||
|
||||
const findTabs = () => wrapper.findComponent(GlTabs);
|
||||
const findTabAt = (index) => wrapper.findAllComponents(GlTab).at(index);
|
||||
const findPlaceholdersTable = () => wrapper.findComponent(PlaceholdersTable);
|
||||
|
||||
it('renders tabs', () => {
|
||||
|
|
@ -44,6 +67,41 @@ describe('PlaceholdersTabApp', () => {
|
|||
expect(findTabs().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders tab titles with counts', async () => {
|
||||
createComponent();
|
||||
await nextTick();
|
||||
|
||||
expect(findTabAt(0).text()).toBe(
|
||||
`Awaiting reassignment ${pagination.awaitingReassignmentItems}`,
|
||||
);
|
||||
expect(findTabAt(1).text()).toBe(`Reassigned ${pagination.reassignedItems}`);
|
||||
});
|
||||
|
||||
describe('on table "confirm" event', () => {
|
||||
const mockSourceUser = mockSourceUsers[1];
|
||||
|
||||
beforeEach(async () => {
|
||||
createComponent();
|
||||
await nextTick();
|
||||
|
||||
findPlaceholdersTable().vm.$emit('confirm', mockSourceUser);
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
it('updates tab counts', () => {
|
||||
expect(findTabAt(0).text()).toBe(
|
||||
`Awaiting reassignment ${pagination.awaitingReassignmentItems - 1}`,
|
||||
);
|
||||
expect(findTabAt(1).text()).toBe(`Reassigned ${pagination.reassignedItems + 1}`);
|
||||
});
|
||||
|
||||
it('shows toast', () => {
|
||||
expect($toast.show).toHaveBeenCalledWith(
|
||||
'Placeholder Placeholder 2 (@placeholder_2) kept as placeholder.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when sourceUsers query is loading', () => {
|
||||
it('renders placeholders table as loading', () => {
|
||||
createComponent();
|
||||
|
|
@ -86,12 +144,12 @@ describe('PlaceholdersTabApp', () => {
|
|||
});
|
||||
|
||||
it('renders placeholders table', () => {
|
||||
const mockSourceUsers = mockSourceUsersQueryResponse().data.namespace.importSourceUsers;
|
||||
const sourceUsers = mockSourceUsersQueryResponse().data.namespace.importSourceUsers;
|
||||
|
||||
expect(findPlaceholdersTable().props()).toMatchObject({
|
||||
isLoading: false,
|
||||
items: mockSourceUsers.nodes,
|
||||
pageInfo: mockSourceUsers.pageInfo,
|
||||
items: sourceUsers.nodes,
|
||||
pageInfo: sourceUsers.pageInfo,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import waitForPromises from 'helpers/wait_for_promises';
|
|||
|
||||
import PlaceholderActions from '~/members/placeholders/components/placeholder_actions.vue';
|
||||
import searchUsersQuery from '~/graphql_shared/queries/users_search_all_paginated.query.graphql';
|
||||
import importSourceUsersQuery from '~/members/placeholders/graphql/queries/import_source_users.query.graphql';
|
||||
import importSourceUserReassignMutation from '~/members/placeholders/graphql/mutations/reassign.mutation.graphql';
|
||||
import importSourceUserKeepAsPlaceholderMutation from '~/members/placeholders/graphql/mutations/keep_as_placeholder.mutation.graphql';
|
||||
import importSourceUserResendNotificationMutation from '~/members/placeholders/graphql/mutations/resend_notification.mutation.graphql';
|
||||
|
|
@ -15,6 +16,7 @@ import importSourceUserCancelReassignmentMutation from '~/members/placeholders/g
|
|||
|
||||
import {
|
||||
mockSourceUsers,
|
||||
mockSourceUsersQueryResponse,
|
||||
mockReassignMutationResponse,
|
||||
mockKeepAsPlaceholderMutationResponse,
|
||||
mockResendNotificationMutationResponse,
|
||||
|
|
@ -36,6 +38,7 @@ describe('PlaceholderActions', () => {
|
|||
sourceUser: mockSourceUsers[0],
|
||||
};
|
||||
const usersQueryHandler = jest.fn().mockResolvedValue(mockUsersQueryResponse);
|
||||
const sourceUsersQueryHandler = jest.fn().mockResolvedValue(mockSourceUsersQueryResponse());
|
||||
const reassignMutationHandler = jest.fn().mockResolvedValue(mockReassignMutationResponse);
|
||||
const keepAsPlaceholderMutationHandler = jest
|
||||
.fn()
|
||||
|
|
@ -53,11 +56,22 @@ describe('PlaceholderActions', () => {
|
|||
const createComponent = ({ seachUsersQueryHandler = usersQueryHandler, props = {} } = {}) => {
|
||||
mockApollo = createMockApollo([
|
||||
[searchUsersQuery, seachUsersQueryHandler],
|
||||
[importSourceUsersQuery, sourceUsersQueryHandler],
|
||||
[importSourceUserReassignMutation, reassignMutationHandler],
|
||||
[importSourceUserKeepAsPlaceholderMutation, keepAsPlaceholderMutationHandler],
|
||||
[importSourceUserResendNotificationMutation, resendNotificationMutationHandler],
|
||||
[importSourceUserCancelReassignmentMutation, cancelReassignmentMutationHandler],
|
||||
]);
|
||||
// refetchQueries will only refetch active queries, so simply registering a query handler is not enough.
|
||||
// We need to call `subscribe()` to make the query observable and avoid "Unknown query" errors.
|
||||
// This simulates what the actual code in VueApollo is doing when adding a smart query.
|
||||
// Docs: https://www.apollographql.com/docs/react/api/core/ApolloClient/#watchquery
|
||||
mockApollo.clients.defaultClient
|
||||
.watchQuery({
|
||||
query: importSourceUsersQuery,
|
||||
variables: { fullPath: 'test' },
|
||||
})
|
||||
.subscribe();
|
||||
|
||||
wrapper = shallowMountExtended(PlaceholderActions, {
|
||||
apolloProvider: mockApollo,
|
||||
|
|
@ -158,6 +172,15 @@ describe('PlaceholderActions', () => {
|
|||
id: mockSourceUsers[0].id,
|
||||
});
|
||||
});
|
||||
|
||||
it('refetches sourceUsersQuery', () => {
|
||||
expect(sourceUsersQueryHandler).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('emits "confirm" event', async () => {
|
||||
await waitForPromises();
|
||||
expect(wrapper.emitted('confirm')[0]).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -194,6 +217,15 @@ describe('PlaceholderActions', () => {
|
|||
userId: mockUser1.id,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not refetch sourceUsersQuery', () => {
|
||||
expect(sourceUsersQueryHandler).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not emit "confirm" event', async () => {
|
||||
await waitForPromises();
|
||||
expect(wrapper.emitted('confirm')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -206,4 +206,18 @@ describe('PlaceholdersTable', () => {
|
|||
expect(wrapper.emitted('next')[0]).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('actions events', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ mountFn: mount });
|
||||
});
|
||||
|
||||
it('emits "confirm" event with item', () => {
|
||||
const actions = findTableRows().at(2).findComponent(PlaceholderActions);
|
||||
|
||||
actions.vm.$emit('confirm');
|
||||
|
||||
expect(wrapper.emitted('confirm')[0]).toEqual([mockSourceUsers[2]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -186,3 +186,9 @@ export const mockUsersWithPaginationQueryResponse = {
|
|||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const pagination = {
|
||||
awaitingReassignmentItems: 5,
|
||||
reassignedItems: 2,
|
||||
totalItems: 7,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ import UsersSelect from '~/users_select';
|
|||
const getUserSearchHTML = memoize((fixture) => {
|
||||
const parser = new DOMParser();
|
||||
|
||||
const el = parser.parseFromString(fixture, 'text/html').querySelector('.merge-request-assignee');
|
||||
const el = parser
|
||||
.parseFromString(fixture, 'text/html')
|
||||
.querySelector('[data-testid=merge-request-assignee]');
|
||||
|
||||
return el.outerHTML;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9559,4 +9559,49 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
|
|||
|
||||
it { expect(project.merge_trains_enabled?).to eq(false) }
|
||||
end
|
||||
|
||||
describe '#lfs_file_locks_changed_epoch', :clean_gitlab_redis_cache do
|
||||
let(:project) { build(:project, id: 1) }
|
||||
let(:epoch) { Time.current.strftime('%s%L').to_i }
|
||||
|
||||
it 'returns a cached epoch value in milliseconds', :aggregate_failures, :freeze_time do
|
||||
cold_cache_control = RedisCommands::Recorder.new do
|
||||
expect(project.lfs_file_locks_changed_epoch).to eq epoch
|
||||
end
|
||||
|
||||
expect(cold_cache_control.by_command('get').count).to eq 1
|
||||
expect(cold_cache_control.by_command('set').count).to eq 1
|
||||
|
||||
warm_cache_control = RedisCommands::Recorder.new do
|
||||
expect(project.lfs_file_locks_changed_epoch).to eq epoch
|
||||
end
|
||||
|
||||
expect(warm_cache_control.by_command('get').count).to eq 1
|
||||
expect(warm_cache_control.by_command('set').count).to eq 0
|
||||
end
|
||||
end
|
||||
|
||||
describe '#refresh_lfs_file_locks_changed_epoch' do
|
||||
let(:project) { build(:project, id: 1) }
|
||||
let(:original_time) { Time.current }
|
||||
let(:refresh_time) { original_time + 1.second }
|
||||
let(:original_epoch) { original_time.strftime('%s%L').to_i }
|
||||
let(:refreshed_epoch) { original_epoch + 1.second.in_milliseconds }
|
||||
|
||||
it 'refreshes the cache and returns the new epoch value', :aggregate_failures, :freeze_time do
|
||||
expect(project.lfs_file_locks_changed_epoch).to eq(original_epoch)
|
||||
|
||||
travel_to(refresh_time)
|
||||
|
||||
expect(project.lfs_file_locks_changed_epoch).to eq(original_epoch)
|
||||
|
||||
control = RedisCommands::Recorder.new do
|
||||
expect(project.refresh_lfs_file_locks_changed_epoch).to eq(refreshed_epoch)
|
||||
end
|
||||
expect(control.by_command('get').count).to eq 0
|
||||
expect(control.by_command('set').count).to eq 1
|
||||
|
||||
expect(project.lfs_file_locks_changed_epoch).to eq(refreshed_epoch)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,19 +6,21 @@ RSpec.describe Lfs::LockFileService, feature_category: :source_code_management d
|
|||
let(:project) { create(:project) }
|
||||
let(:current_user) { create(:user) }
|
||||
|
||||
subject { described_class.new(project, current_user, params) }
|
||||
|
||||
describe '#execute' do
|
||||
subject(:execute) { described_class.new(project, current_user, params).execute }
|
||||
|
||||
let(:params) { { path: 'README.md' } }
|
||||
|
||||
context 'when not authorized' do
|
||||
it "doesn't succeed" do
|
||||
result = subject.execute
|
||||
result = execute
|
||||
|
||||
expect(result[:status]).to eq(:error)
|
||||
expect(result[:http_status]).to eq(403)
|
||||
expect(result[:message]).to eq('You have no permissions')
|
||||
end
|
||||
|
||||
it_behaves_like 'does not refresh project.lfs_file_locks_changed_epoch'
|
||||
end
|
||||
|
||||
context 'when authorized' do
|
||||
|
|
@ -30,36 +32,40 @@ RSpec.describe Lfs::LockFileService, feature_category: :source_code_management d
|
|||
let!(:lock) { create(:lfs_file_lock, project: project) }
|
||||
|
||||
it "doesn't succeed" do
|
||||
expect(subject.execute[:status]).to eq(:error)
|
||||
expect(execute[:status]).to eq(:error)
|
||||
end
|
||||
|
||||
it "doesn't create the Lock" do
|
||||
expect do
|
||||
subject.execute
|
||||
end.not_to change { LfsFileLock.count }
|
||||
expect { execute }.not_to change { LfsFileLock.count }
|
||||
end
|
||||
|
||||
it_behaves_like 'does not refresh project.lfs_file_locks_changed_epoch'
|
||||
end
|
||||
|
||||
context 'without an existent lock' do
|
||||
it "succeeds" do
|
||||
expect(subject.execute[:status]).to eq(:success)
|
||||
expect(execute[:status]).to eq(:success)
|
||||
end
|
||||
|
||||
it "creates the Lock" do
|
||||
expect do
|
||||
subject.execute
|
||||
end.to change { LfsFileLock.count }.by(1)
|
||||
expect { execute }.to change { LfsFileLock.count }.by(1)
|
||||
end
|
||||
|
||||
it_behaves_like 'refreshes project.lfs_file_locks_changed_epoch value'
|
||||
end
|
||||
|
||||
context 'when an error is raised' do
|
||||
it "doesn't succeed" do
|
||||
before do
|
||||
allow_next_instance_of(described_class) do |instance|
|
||||
allow(instance).to receive(:create_lock!).and_raise(StandardError)
|
||||
end
|
||||
|
||||
expect(subject.execute[:status]).to eq(:error)
|
||||
end
|
||||
|
||||
it "doesn't succeed" do
|
||||
expect(execute[:status]).to eq(:error)
|
||||
end
|
||||
|
||||
it_behaves_like 'does not refresh project.lfs_file_locks_changed_epoch'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,17 +9,19 @@ RSpec.describe Lfs::UnlockFileService, feature_category: :source_code_management
|
|||
let!(:lock) { create(:lfs_file_lock, user: lock_author, project: project) }
|
||||
let(:params) { {} }
|
||||
|
||||
subject { described_class.new(project, current_user, params) }
|
||||
|
||||
describe '#execute' do
|
||||
subject(:execute) { described_class.new(project, current_user, params).execute }
|
||||
|
||||
context 'when not authorized' do
|
||||
it "doesn't succeed" do
|
||||
result = subject.execute
|
||||
result = execute
|
||||
|
||||
expect(result[:status]).to eq(:error)
|
||||
expect(result[:http_status]).to eq(403)
|
||||
expect(result[:message]).to eq(_('You have no permissions'))
|
||||
end
|
||||
|
||||
it_behaves_like 'does not refresh project.lfs_file_locks_changed_epoch'
|
||||
end
|
||||
|
||||
context 'when authorized' do
|
||||
|
|
@ -31,11 +33,13 @@ RSpec.describe Lfs::UnlockFileService, feature_category: :source_code_management
|
|||
let(:params) { { id: 123 } }
|
||||
|
||||
it "doesn't succeed" do
|
||||
result = subject.execute
|
||||
result = execute
|
||||
|
||||
expect(result[:status]).to eq(:error)
|
||||
expect(result[:http_status]).to eq(404)
|
||||
end
|
||||
|
||||
it_behaves_like 'does not refresh project.lfs_file_locks_changed_epoch'
|
||||
end
|
||||
|
||||
context 'when unlocked by the author' do
|
||||
|
|
@ -43,11 +47,13 @@ RSpec.describe Lfs::UnlockFileService, feature_category: :source_code_management
|
|||
let(:params) { { id: lock.id } }
|
||||
|
||||
it "succeeds" do
|
||||
result = subject.execute
|
||||
result = execute
|
||||
|
||||
expect(result[:status]).to eq(:success)
|
||||
expect(result[:lock]).to be_present
|
||||
end
|
||||
|
||||
it_behaves_like 'refreshes project.lfs_file_locks_changed_epoch value'
|
||||
end
|
||||
|
||||
context 'when unlocked by a different user' do
|
||||
|
|
@ -55,12 +61,14 @@ RSpec.describe Lfs::UnlockFileService, feature_category: :source_code_management
|
|||
let(:params) { { id: lock.id } }
|
||||
|
||||
it "doesn't succeed" do
|
||||
result = subject.execute
|
||||
result = execute
|
||||
|
||||
expect(result[:status]).to eq(:error)
|
||||
expect(result[:message]).to match(/'README.md' is locked by @#{lock_author.username}/)
|
||||
expect(result[:http_status]).to eq(403)
|
||||
end
|
||||
|
||||
it_behaves_like 'does not refresh project.lfs_file_locks_changed_epoch'
|
||||
end
|
||||
|
||||
context 'when forced' do
|
||||
|
|
@ -80,12 +88,14 @@ RSpec.describe Lfs::UnlockFileService, feature_category: :source_code_management
|
|||
end
|
||||
|
||||
it "doesn't succeed" do
|
||||
result = subject.execute
|
||||
result = execute
|
||||
|
||||
expect(result[:status]).to eq(:error)
|
||||
expect(result[:message]).to eq(_('You must have maintainer access to force delete a lock'))
|
||||
expect(result[:http_status]).to eq(403)
|
||||
end
|
||||
|
||||
it_behaves_like 'does not refresh project.lfs_file_locks_changed_epoch'
|
||||
end
|
||||
|
||||
context 'by a maintainer user' do
|
||||
|
|
@ -96,11 +106,13 @@ RSpec.describe Lfs::UnlockFileService, feature_category: :source_code_management
|
|||
end
|
||||
|
||||
it "succeeds" do
|
||||
result = subject.execute
|
||||
result = execute
|
||||
|
||||
expect(result[:status]).to eq(:success)
|
||||
expect(result[:lock]).to be_present
|
||||
end
|
||||
|
||||
it_behaves_like 'refreshes project.lfs_file_locks_changed_epoch value'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -27,9 +27,12 @@ end
|
|||
|
||||
RSpec.shared_examples 'work items toggle status button' do
|
||||
it 'successfully shows and changes the status of the work item' do
|
||||
click_button 'Close', match: :first
|
||||
within_testid 'work-item-comment-form-actions' do
|
||||
# Depending of the context, the button's text could be `Close issue`, `Close key result`, `Close objective`, etc.
|
||||
click_button 'Close', match: :first
|
||||
|
||||
expect(page).to have_button 'Reopen'
|
||||
expect(page).to have_button 'Reopen'
|
||||
end
|
||||
expect(work_item.reload.state).to eq('closed')
|
||||
end
|
||||
end
|
||||
|
|
@ -624,7 +627,9 @@ RSpec.shared_examples 'work items time tracking' do
|
|||
|
||||
expect(page).to be_axe_clean.within('[role="dialog"]')
|
||||
|
||||
click_button 'Close'
|
||||
within_testid 'set-time-estimate-modal' do
|
||||
click_button 'Close'
|
||||
end
|
||||
click_button 'time spent'
|
||||
|
||||
expect(page).to be_axe_clean.within('[role="dialog"]')
|
||||
|
|
@ -632,15 +637,19 @@ RSpec.shared_examples 'work items time tracking' do
|
|||
|
||||
it 'adds and removes an estimate', :aggregate_failures do
|
||||
click_button 'estimate'
|
||||
fill_in 'Estimate', with: '5d'
|
||||
click_button 'Save'
|
||||
within_testid 'set-time-estimate-modal' do
|
||||
fill_in 'Estimate', with: '5d'
|
||||
click_button 'Save'
|
||||
end
|
||||
|
||||
expect(page).to have_text 'Estimate 5d'
|
||||
expect(page).to have_button '5d'
|
||||
expect(page).not_to have_button 'estimate'
|
||||
|
||||
click_button '5d'
|
||||
click_button 'Remove'
|
||||
within_testid 'set-time-estimate-modal' do
|
||||
click_button 'Remove'
|
||||
end
|
||||
|
||||
expect(page).not_to have_text 'Estimate 5d'
|
||||
expect(page).not_to have_button '5d'
|
||||
|
|
@ -648,31 +657,39 @@ RSpec.shared_examples 'work items time tracking' do
|
|||
end
|
||||
|
||||
it 'adds and deletes time entries and view report', :aggregate_failures do
|
||||
click_button 'time entry'
|
||||
fill_in 'Time spent', with: '1d'
|
||||
fill_in 'Summary', with: 'First summary'
|
||||
click_button 'Save'
|
||||
click_button 'Add time entry'
|
||||
|
||||
within_testid 'create-timelog-modal' do
|
||||
fill_in 'Time spent', with: '1d'
|
||||
fill_in 'Summary', with: 'First summary'
|
||||
click_button 'Save'
|
||||
end
|
||||
|
||||
click_button 'Add time entry'
|
||||
fill_in 'Time spent', with: '2d'
|
||||
fill_in 'Summary', with: 'Second summary'
|
||||
click_button 'Save'
|
||||
|
||||
within_testid 'create-timelog-modal' do
|
||||
fill_in 'Time spent', with: '2d'
|
||||
fill_in 'Summary', with: 'Second summary'
|
||||
click_button 'Save'
|
||||
end
|
||||
|
||||
expect(page).to have_text 'Spent 3d'
|
||||
expect(page).to have_button '3d'
|
||||
|
||||
click_button '3d'
|
||||
|
||||
expect(page).to have_css 'h2', text: 'Time tracking report'
|
||||
expect(page).to have_text "1d #{user.name} First summary"
|
||||
expect(page).to have_text "2d #{user.name} Second summary"
|
||||
within_testid 'time-tracking-report-modal' do
|
||||
expect(page).to have_css 'h2', text: 'Time tracking report'
|
||||
expect(page).to have_text "1d #{user.name} First summary"
|
||||
expect(page).to have_text "2d #{user.name} Second summary"
|
||||
|
||||
click_button 'Delete time spent', match: :first
|
||||
click_button 'Delete time spent', match: :first
|
||||
|
||||
expect(page).to have_text "1d #{user.name} First summary"
|
||||
expect(page).not_to have_text "2d #{user.name} Second summary"
|
||||
expect(page).to have_text "1d #{user.name} First summary"
|
||||
expect(page).not_to have_text "2d #{user.name} Second summary"
|
||||
|
||||
click_button 'Close'
|
||||
click_button 'Close'
|
||||
end
|
||||
|
||||
expect(page).to have_text 'Spent 1d'
|
||||
expect(page).to have_button '1d'
|
||||
|
|
|
|||
|
|
@ -60,3 +60,27 @@ RSpec.shared_examples 'checks parent group feature flag' do
|
|||
it { is_expected.to be_truthy }
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'refreshes project.lfs_file_locks_changed_epoch value' do
|
||||
it 'updates the lfs_file_locks_changed_epoch value', :clean_gitlab_redis_cache do
|
||||
travel_to(1.hour.ago) { project.refresh_lfs_file_locks_changed_epoch }
|
||||
|
||||
original_epoch = project.lfs_file_locks_changed_epoch
|
||||
|
||||
subject
|
||||
|
||||
expect(project.lfs_file_locks_changed_epoch).to be > original_epoch
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'does not refresh project.lfs_file_locks_changed_epoch' do
|
||||
it 'does not update the lfs_file_locks_changed_epoch value', :clean_gitlab_redis_cache do
|
||||
travel_to(1.hour.ago) { project.refresh_lfs_file_locks_changed_epoch }
|
||||
|
||||
original_epoch = project.lfs_file_locks_changed_epoch
|
||||
|
||||
subject
|
||||
|
||||
expect(project.lfs_file_locks_changed_epoch).to eq original_epoch
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue