Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-12-03 21:09:35 +00:00
parent c2a6cc8675
commit e701659ba3
127 changed files with 2260 additions and 1452 deletions

View File

@ -32,6 +32,75 @@ import {
// feature rollout plan - https://gitlab.com/gitlab-org/gitlab/-/issues/262707#note_442529171
import mockedCustomMapping from './mocks/parsedMapping.json';
export const i18n = {
integrationFormSteps: {
step1: {
label: s__('AlertSettings|1. Select integration type'),
enterprise: s__(
'AlertSettings|In free versions of GitLab, only one integration for each type can be added. %{linkStart}Upgrade your subscription%{linkEnd} to add additional integrations.',
),
},
step2: {
label: s__('AlertSettings|2. Name integration'),
placeholder: s__('AlertSettings|Enter integration name'),
prometheus: s__('AlertSettings|Prometheus'),
},
step3: {
label: s__('AlertSettings|3. Set up webhook'),
help: s__(
"AlertSettings|Utilize the URL and authorization key below to authorize an external service to send alerts to GitLab. Review your external service's documentation to learn where to add these details, and the %{linkStart}GitLab documentation%{linkEnd} to learn more about configuring your endpoint.",
),
prometheusHelp: s__(
'AlertSettings|Utilize the URL and authorization key below to authorize Prometheus to send alerts to GitLab. Review the Prometheus documentation to learn where to add these details, and the %{linkStart}GitLab documentation%{linkEnd} to learn more about configuring your endpoint.',
),
info: s__('AlertSettings|Authorization key'),
reset: s__('AlertSettings|Reset Key'),
},
step4: {
label: s__('AlertSettings|4. Sample alert payload (optional)'),
help: s__(
'AlertSettings|Provide an example payload from the monitoring tool you intend to integrate with. This payload can be used to create a custom mapping (optional), or to test the integration (also optional).',
),
prometheusHelp: s__(
'AlertSettings|Provide an example payload from the monitoring tool you intend to integrate with. This payload can be used to test the integration (optional).',
),
placeholder: s__('AlertSettings|{ "events": [{ "application": "Name of application" }] }'),
resetHeader: s__('AlertSettings|Reset the mapping'),
resetBody: s__(
"AlertSettings|If you edit the payload, the stored mapping will be reset, and you'll need to re-map the fields.",
),
resetOk: s__('AlertSettings|Proceed with editing'),
editPayload: s__('AlertSettings|Edit payload'),
submitPayload: s__('AlertSettings|Submit payload'),
payloadParsedSucessMsg: s__(
'AlertSettings|Sample payload has been parsed. You can now map the fields.',
),
},
step5: {
label: s__('AlertSettings|5. Map fields (optional)'),
intro: s__(
"AlertSettings|If you've provided a sample alert payload, you can create a custom mapping for your endpoint. The default GitLab alert keys are listed below. Please define which payload key should map to the specified GitLab key.",
),
},
prometheusFormUrl: {
label: s__('AlertSettings|Prometheus API base URL'),
help: s__('AlertSettings|URL cannot be blank and must start with http or https'),
},
restKeyInfo: {
label: s__(
'AlertSettings|Resetting the authorization key for this project will require updating the authorization key in every alert source it is enabled in.',
),
},
// TODO: Will be removed in 13.7 as part of: https://gitlab.com/gitlab-org/gitlab/-/issues/273657
opsgenie: {
label: s__('AlertSettings|2. Add link to your Opsgenie alert list'),
info: s__(
'AlertSettings|Utilizing this option will link the GitLab Alerts navigation item to your existing Opsgenie instance. By selecting this option, you cannot receive alerts from any other source in GitLab; it will effectively be turning Alerts within GitLab off as a feature.',
),
},
},
};
export default {
placeholders: {
prometheus: targetPrometheusUrlPlaceholder,
@ -39,73 +108,7 @@ export default {
},
JSON_VALIDATE_DELAY,
typeSet,
i18n: {
integrationFormSteps: {
step1: {
label: s__('AlertSettings|1. Select integration type'),
enterprise: s__(
'AlertSettings|In free versions of GitLab, only one integration for each type can be added. %{linkStart}Upgrade your subscription%{linkEnd} to add additional integrations.',
),
},
step2: {
label: s__('AlertSettings|2. Name integration'),
placeholder: s__('AlertSettings|Enter integration name'),
},
step3: {
label: s__('AlertSettings|3. Set up webhook'),
help: s__(
"AlertSettings|Utilize the URL and authorization key below to authorize an external service to send alerts to GitLab. Review your external service's documentation to learn where to add these details, and the %{linkStart}GitLab documentation%{linkEnd} to learn more about configuring your endpoint.",
),
prometheusHelp: s__(
'AlertSettings|Utilize the URL and authorization key below to authorize Prometheus to send alerts to GitLab. Review the Prometheus documentation to learn where to add these details, and the %{linkStart}GitLab documentation%{linkEnd} to learn more about configuring your endpoint.',
),
info: s__('AlertSettings|Authorization key'),
reset: s__('AlertSettings|Reset Key'),
},
step4: {
label: s__('AlertSettings|4. Sample alert payload (optional)'),
help: s__(
'AlertSettings|Provide an example payload from the monitoring tool you intend to integrate with. This payload can be used to create a custom mapping (optional), or to test the integration (also optional).',
),
prometheusHelp: s__(
'AlertSettings|Provide an example payload from the monitoring tool you intend to integrate with. This payload can be used to test the integration (optional).',
),
placeholder: s__('AlertSettings|{ "events": [{ "application": "Name of application" }] }'),
resetHeader: s__('AlertSettings|Reset the mapping'),
resetBody: s__(
"AlertSettings|If you edit the payload, the stored mapping will be reset, and you'll need to re-map the fields.",
),
resetOk: s__('AlertSettings|Proceed with editing'),
editPayload: s__('AlertSettings|Edit payload'),
submitPayload: s__('AlertSettings|Submit payload'),
payloadParsedSucessMsg: s__(
'AlertSettings|Sample payload has been parsed. You can now map the fields.',
),
},
step5: {
label: s__('AlertSettings|5. Map fields (optional)'),
intro: s__(
"AlertSettings|If you've provided a sample alert payload, you can create a custom mapping for your endpoint. The default GitLab alert keys are listed below. Please define which payload key should map to the specified GitLab key.",
),
},
prometheusFormUrl: {
label: s__('AlertSettings|Prometheus API base URL'),
help: s__('AlertSettings|URL cannot be blank and must start with http or https'),
},
restKeyInfo: {
label: s__(
'AlertSettings|Resetting the authorization key for this project will require updating the authorization key in every alert source it is enabled in.',
),
},
// TODO: Will be removed in 13.7 as part of: https://gitlab.com/gitlab-org/gitlab/-/issues/273657
opsgenie: {
label: s__('AlertSettings|2. Add link to your Opsgenie alert list'),
info: s__(
'AlertSettings|Utilizing this option will link the GitLab Alerts navigation item to your existing Opsgenie instance. By selecting this option, you cannot receive alerts from any other source in GitLab; it will effectively be turning Alerts within GitLab off as a feature.',
),
},
},
},
i18n,
components: {
ClipboardButton,
GlButton,
@ -265,6 +268,9 @@ export default {
this.integrationTestPayload.json === ''
);
},
isSelectDisabled() {
return this.currentIntegration !== null || !this.canAddIntegration;
},
},
watch: {
currentIntegration(val) {
@ -421,7 +427,8 @@ export default {
>
<gl-form-select
v-model="selectedIntegration"
:disabled="currentIntegration !== null || !canAddIntegration"
:disabled="isSelectDisabled"
:class="{ 'gl-bg-gray-100!': isSelectDisabled }"
:options="options"
@change="integrationTypeSelect"
/>
@ -472,8 +479,13 @@ export default {
>
<gl-form-input
v-model="integrationForm.name"
:disabled="isPrometheus"
type="text"
:placeholder="$options.i18n.integrationFormSteps.step2.placeholder"
:placeholder="
isPrometheus
? $options.i18n.integrationFormSteps.step2.prometheus
: $options.i18n.integrationFormSteps.step2.placeholder
"
/>
</gl-form-group>
<gl-form-group

View File

@ -4,7 +4,7 @@ import { mapState, mapActions } from 'vuex';
import axios from '~/lib/utils/axios_utils';
import { sprintf, s__ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { LEGACY_FLAG, NEW_FLAG_ALERT } from '../constants';
import { LEGACY_FLAG } from '../constants';
import FeatureFlagForm from './form.vue';
export default {
@ -36,7 +36,6 @@ export default {
legacyReadOnlyFlagAlert: s__(
'FeatureFlags|GitLab is moving to a new way of managing feature flags. This feature flag is read-only, and it will be removed in 14.0. Please create a new feature flag.',
),
newFlagAlert: NEW_FLAG_ALERT,
},
computed: {
...mapState([
@ -58,7 +57,7 @@ export default {
: sprintf(s__('Edit %{name}'), { name: this.name });
},
deprecated() {
return this.hasNewVersionFlags && this.version === LEGACY_FLAG;
return this.version === LEGACY_FLAG;
},
deprecatedAndEditable() {
return this.deprecated && !this.hasLegacyReadOnlyFlags;
@ -66,18 +65,12 @@ export default {
deprecatedAndReadOnly() {
return this.deprecated && this.hasLegacyReadOnlyFlags;
},
hasNewVersionFlags() {
return this.glFeatures.featureFlagsNewVersion;
},
hasLegacyReadOnlyFlags() {
return (
this.glFeatures.featureFlagsLegacyReadOnly &&
!this.glFeatures.featureFlagsLegacyReadOnlyOverride
);
},
shouldShowNewFlagAlert() {
return !this.hasNewVersionFlags && this.userShouldSeeNewFlagAlert;
},
},
created() {
return this.fetchFeatureFlag();
@ -95,14 +88,6 @@ export default {
</script>
<template>
<div>
<gl-alert
v-if="shouldShowNewFlagAlert"
variant="warning"
class="gl-my-5"
@dismiss="dismissNewVersionFlagAlert"
>
{{ $options.translations.newFlagAlert }}
</gl-alert>
<gl-loading-icon v-if="isLoading" size="xl" class="gl-mt-7" />
<template v-else-if="!isLoading && !hasError">

View File

@ -38,9 +38,6 @@ export default {
permissions() {
return this.glFeatures.featureFlagPermissions;
},
isNewVersionFlagsEnabled() {
return this.glFeatures.featureFlagsNewVersion;
},
isLegacyReadOnlyFlagsEnabled() {
return (
this.glFeatures.featureFlagsLegacyReadOnly &&
@ -68,7 +65,7 @@ export default {
},
methods: {
isLegacyFlag(flag) {
return !this.isNewVersionFlagsEnabled || flag.version !== NEW_VERSION_FLAG;
return flag.version !== NEW_VERSION_FLAG;
},
statusToggleDisabled(flag) {
return this.isLegacyReadOnlyFlagsEnabled && flag.version === LEGACY_FLAG;

View File

@ -137,14 +137,13 @@ export default {
return this.glFeatures.featureFlagPermissions;
},
supportsStrategies() {
return this.glFeatures.featureFlagsNewVersion && this.version === NEW_VERSION_FLAG;
return this.version === NEW_VERSION_FLAG;
},
showRelatedIssues() {
return this.featureFlagIssuesEndpoint.length > 0;
},
readOnly() {
return (
this.glFeatures.featureFlagsNewVersion &&
this.glFeatures.featureFlagsLegacyReadOnly &&
!this.glFeatures.featureFlagsLegacyReadOnlyOverride &&
this.version === LEGACY_FLAG

View File

@ -1,21 +1,14 @@
<script>
import { mapState, mapActions } from 'vuex';
import { GlAlert } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import FeatureFlagForm from './form.vue';
import {
LEGACY_FLAG,
NEW_VERSION_FLAG,
NEW_FLAG_ALERT,
ROLLOUT_STRATEGY_ALL_USERS,
} from '../constants';
import { NEW_VERSION_FLAG, ROLLOUT_STRATEGY_ALL_USERS } from '../constants';
import { createNewEnvironmentScope } from '../store/helpers';
import featureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
components: {
GlAlert,
FeatureFlagForm,
},
mixins: [featureFlagsMixin()],
@ -33,9 +26,6 @@ export default {
userShouldSeeNewFlagAlert: this.showUserCallout,
};
},
translations: {
newFlagAlert: NEW_FLAG_ALERT,
},
computed: {
...mapState(['error', 'path']),
scopes() {
@ -50,13 +40,7 @@ export default {
];
},
version() {
return this.hasNewVersionFlags ? NEW_VERSION_FLAG : LEGACY_FLAG;
},
hasNewVersionFlags() {
return this.glFeatures.featureFlagsNewVersion;
},
shouldShowNewFlagAlert() {
return !this.hasNewVersionFlags && this.userShouldSeeNewFlagAlert;
return NEW_VERSION_FLAG;
},
strategies() {
return [{ name: ROLLOUT_STRATEGY_ALL_USERS, parameters: {}, scopes: [] }];
@ -75,14 +59,6 @@ export default {
</script>
<template>
<div>
<gl-alert
v-if="shouldShowNewFlagAlert"
variant="warning"
class="gl-my-5"
@dismiss="dismissNewVersionFlagAlert"
>
{{ $options.translations.newFlagAlert }}
</gl-alert>
<h3 class="page-title">{{ s__('FeatureFlags|New feature flag') }}</h3>
<div v-if="error.length" class="alert alert-danger">

View File

@ -21,10 +21,6 @@ export const fetchUserIdParams = property(['parameters', 'userIds']);
export const NEW_VERSION_FLAG = 'new_version_flag';
export const LEGACY_FLAG = 'legacy_flag';
export const NEW_FLAG_ALERT = s__(
'FeatureFlags|Feature Flags will look different in the next milestone. No action is needed, but you may notice the functionality was changed to improve the workflow.',
);
export const FEATURE_FLAG_SCOPE = 'featureFlags';
export const USER_LIST_SCOPE = 'userLists';

View File

@ -1,11 +1,11 @@
<script>
import { GlPagination } from '@gitlab/ui';
import { GlKeysetPagination } from '@gitlab/ui';
import ImageListRow from './image_list_row.vue';
export default {
name: 'ImageList',
components: {
GlPagination,
GlKeysetPagination,
ImageListRow,
},
props: {
@ -13,19 +13,14 @@ export default {
type: Array,
required: true,
},
pagination: {
pageInfo: {
type: Object,
required: true,
},
},
computed: {
currentPage: {
get() {
return this.pagination.page;
},
set(page) {
this.$emit('pageChange', page);
},
showPagination() {
return this.pageInfo.hasPreviousPage || this.pageInfo.hasNextPage;
},
},
};
@ -40,13 +35,15 @@ export default {
:first="index === 0"
@delete="$emit('delete', $event)"
/>
<gl-pagination
v-model="currentPage"
:per-page="pagination.perPage"
:total-items="pagination.total"
align="center"
class="w-100 gl-mt-3"
/>
<div class="gl-display-flex gl-justify-content-center">
<gl-keyset-pagination
v-if="showPagination"
:has-next-page="pageInfo.hasNextPage"
:has-previous-page="pageInfo.hasPreviousPage"
class="gl-mt-3"
@prev="$emit('prev-page')"
@next="$emit('next-page')"
/>
</div>
</div>
</template>

View File

@ -1,6 +1,8 @@
<script>
import { GlTooltipDirective, GlIcon, GlSprintf } from '@gitlab/ui';
import { n__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import DeleteButton from '../delete_button.vue';
@ -11,6 +13,8 @@ import {
REMOVE_REPOSITORY_LABEL,
ROW_SCHEDULED_FOR_DELETION,
CLEANUP_TIMED_OUT_ERROR_MESSAGE,
IMAGE_DELETE_SCHEDULED_STATUS,
IMAGE_FAILED_DELETED_STATUS,
} from '../../constants/index';
export default {
@ -38,19 +42,29 @@ export default {
},
computed: {
disabledDelete() {
return !this.item.destroy_path || this.item.deleting;
return !this.item.canDelete || this.deleting;
},
id() {
return getIdFromGraphQLId(this.item.id);
},
deleting() {
return this.item.status === IMAGE_DELETE_SCHEDULED_STATUS;
},
failedDelete() {
return this.item.status === IMAGE_FAILED_DELETED_STATUS;
},
tagsCountText() {
return n__(
'ContainerRegistry|%{count} Tag',
'ContainerRegistry|%{count} Tags',
this.item.tags_count,
this.item.tagsCount,
);
},
warningIconText() {
if (this.item.failedDelete) {
if (this.failedDelete) {
return ASYNC_DELETE_IMAGE_ERROR_MESSAGE;
} else if (this.item.cleanup_policy_started_at) {
}
if (this.item.expirationPolicyStartedAt) {
return CLEANUP_TIMED_OUT_ERROR_MESSAGE;
}
return null;
@ -63,23 +77,23 @@ export default {
<list-item
v-gl-tooltip="{
placement: 'left',
disabled: !item.deleting,
disabled: !deleting,
title: $options.i18n.ROW_SCHEDULED_FOR_DELETION,
}"
v-bind="$attrs"
:disabled="item.deleting"
:disabled="deleting"
>
<template #left-primary>
<router-link
class="gl-text-body gl-font-weight-bold"
data-testid="details-link"
:to="{ name: 'details', params: { id: item.id } }"
:to="{ name: 'details', params: { id } }"
>
{{ item.path }}
</router-link>
<clipboard-button
v-if="item.location"
:disabled="item.deleting"
:disabled="deleting"
:text="item.location"
:title="item.location"
category="tertiary"
@ -97,7 +111,7 @@ export default {
<gl-icon name="tag" class="gl-mr-2" />
<gl-sprintf :message="tagsCountText">
<template #count>
{{ item.tags_count }}
{{ item.tagsCount }}
</template>
</gl-sprintf>
</span>
@ -106,7 +120,7 @@ export default {
<delete-button
:title="$options.i18n.REMOVE_REPOSITORY_LABEL"
:disabled="disabledDelete"
:tooltip-disabled="Boolean(item.destroy_path)"
:tooltip-disabled="item.canDelete"
:tooltip-title="$options.i18n.LIST_DELETE_BUTTON_DISABLED"
@delete="$emit('delete', item)"
/>

View File

@ -44,5 +44,6 @@ export const EMPTY_RESULT_MESSAGE = s__(
// Parameters
export const IMAGE_DELETE_SCHEDULED_STATUS = 'delete_scheduled';
export const IMAGE_FAILED_DELETED_STATUS = 'delete_failed';
export const IMAGE_DELETE_SCHEDULED_STATUS = 'DELETE_SCHEDULED';
export const IMAGE_FAILED_DELETED_STATUS = 'DELETE_FAILED';
export const GRAPHQL_PAGE_SIZE = 10;

View File

@ -0,0 +1,11 @@
fragment ContainerRepositoryFields on ContainerRepository {
id
name
path
status
location
canDelete
createdAt
tagsCount
expirationPolicyStartedAt
}

View File

@ -0,0 +1,14 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo);
export const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(
{},
{
assumeImmutableResults: true,
},
),
});

View File

@ -0,0 +1,9 @@
mutation destroyContainerRepository($id: ContainerRepositoryID!) {
destroyContainerRepository(input: { id: $id }) {
containerRepository {
id
status
}
errors
}
}

View File

@ -0,0 +1,23 @@
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
#import "../fragments/container_repository.fragment.graphql"
query getProjectContainerRepositories(
$fullPath: ID!
$name: String
$first: Int
$last: Int
$after: String
$before: String
) {
group(fullPath: $fullPath) {
containerRepositoriesCount
containerRepositories(name: $name, after: $after, before: $before, first: $first, last: $last) {
nodes {
...ContainerRepositoryFields
}
pageInfo {
...PageInfo
}
}
}
}

View File

@ -0,0 +1,23 @@
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
#import "../fragments/container_repository.fragment.graphql"
query getProjectContainerRepositories(
$fullPath: ID!
$name: String
$first: Int
$last: Int
$after: String
$before: String
) {
project(fullPath: $fullPath) {
containerRepositoriesCount
containerRepositories(name: $name, after: $after, before: $before, first: $first, last: $last) {
nodes {
...ContainerRepositoryFields
}
pageInfo {
...PageInfo
}
}
}
}

View File

@ -5,6 +5,7 @@ import RegistryExplorer from './pages/index.vue';
import RegistryBreadcrumb from './components/registry_breadcrumb.vue';
import { createStore } from './stores';
import createRouter from './router';
import { apolloProvider } from './graphql/index';
Vue.use(Translate);
Vue.use(GlToast);
@ -27,6 +28,7 @@ export default () => {
el,
store,
router,
apolloProvider,
components: {
RegistryExplorer,
},

View File

@ -1,5 +1,5 @@
<script>
import { mapState, mapActions } from 'vuex';
import { mapState } from 'vuex';
import {
GlEmptyState,
GlTooltipDirective,
@ -11,6 +11,7 @@ import {
GlSearchBoxByClick,
} from '@gitlab/ui';
import Tracking from '~/tracking';
import createFlash from '~/flash';
import ProjectEmptyState from '../components/list_page/project_empty_state.vue';
import GroupEmptyState from '../components/list_page/group_empty_state.vue';
@ -18,6 +19,10 @@ import RegistryHeader from '../components/list_page/registry_header.vue';
import ImageList from '../components/list_page/image_list.vue';
import CliCommands from '../components/list_page/cli_commands.vue';
import getProjectContainerRepositories from '../graphql/queries/get_project_container_repositories.graphql';
import getGroupContainerRepositories from '../graphql/queries/get_group_container_repositories.graphql';
import deleteContainerRepository from '../graphql/mutations/delete_container_repository.graphql';
import {
DELETE_IMAGE_SUCCESS_MESSAGE,
DELETE_IMAGE_ERROR_MESSAGE,
@ -29,6 +34,8 @@ import {
IMAGE_REPOSITORY_LIST_LABEL,
EMPTY_RESULT_TITLE,
EMPTY_RESULT_MESSAGE,
GRAPHQL_PAGE_SIZE,
FETCH_IMAGES_LIST_ERROR_MESSAGE,
} from '../constants/index';
export default {
@ -66,21 +73,63 @@ export default {
EMPTY_RESULT_TITLE,
EMPTY_RESULT_MESSAGE,
},
apollo: {
images: {
query() {
return this.graphQlQuery;
},
variables() {
return this.queryVariables;
},
update(data) {
return data[this.graphqlResource]?.containerRepositories.nodes;
},
result({ data }) {
this.pageInfo = data[this.graphqlResource]?.containerRepositories?.pageInfo;
this.containerRepositoriesCount = data[this.graphqlResource]?.containerRepositoriesCount;
},
error() {
createFlash({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE });
},
},
},
data() {
return {
images: [],
pageInfo: {},
containerRepositoriesCount: 0,
itemToDelete: {},
deleteAlertType: null,
search: null,
isEmpty: false,
searchValue: null,
name: null,
mutationLoading: false,
};
},
computed: {
...mapState(['config', 'isLoading', 'images', 'pagination']),
...mapState(['config']),
graphqlResource() {
return this.config.isGroupPage ? 'group' : 'project';
},
graphQlQuery() {
return this.config.isGroupPage
? getGroupContainerRepositories
: getProjectContainerRepositories;
},
queryVariables() {
return {
name: this.name,
fullPath: this.config.isGroupPage ? this.config.groupPath : this.config.projectPath,
first: GRAPHQL_PAGE_SIZE,
};
},
tracking() {
return {
label: 'registry_repository_delete',
};
},
isLoading() {
return this.$apollo.queries.images.loading || this.mutationLoading;
},
showCommands() {
return Boolean(!this.isLoading && !this.config?.isGroupPage && this.images?.length);
},
@ -93,19 +142,7 @@ export default {
: DELETE_IMAGE_ERROR_MESSAGE;
},
},
mounted() {
this.loadImageList(this.$route.name);
},
methods: {
...mapActions(['requestImagesList', 'requestDeleteImage']),
loadImageList(fromName) {
if (!fromName || !this.images?.length) {
return this.requestImagesList().then(() => {
this.isEmpty = this.images.length === 0;
});
}
return Promise.resolve();
},
deleteImage(item) {
this.track('click_button');
this.itemToDelete = item;
@ -113,18 +150,59 @@ export default {
},
handleDeleteImage() {
this.track('confirm_delete');
return this.requestDeleteImage(this.itemToDelete)
.then(() => {
this.deleteAlertType = 'success';
this.mutationLoading = true;
return this.$apollo
.mutate({
mutation: deleteContainerRepository,
variables: {
id: this.itemToDelete.id,
},
})
.then(({ data }) => {
if (data?.destroyContainerRepository?.errors[0]) {
this.deleteAlertType = 'danger';
} else {
this.deleteAlertType = 'success';
}
})
.catch(() => {
this.deleteAlertType = 'danger';
})
.finally(() => {
this.mutationLoading = false;
});
},
dismissDeleteAlert() {
this.deleteAlertType = null;
this.itemToDelete = {};
},
fetchNextPage() {
if (this.pageInfo?.hasNextPage) {
this.$apollo.queries.images.fetchMore({
variables: {
after: this.pageInfo?.endCursor,
first: GRAPHQL_PAGE_SIZE,
},
updateQuery(previousResult, { fetchMoreResult }) {
return fetchMoreResult;
},
});
}
},
fetchPreviousPage() {
if (this.pageInfo?.hasPreviousPage) {
this.$apollo.queries.images.fetchMore({
variables: {
first: null,
before: this.pageInfo?.startCursor,
last: GRAPHQL_PAGE_SIZE,
},
updateQuery(previousResult, { fetchMoreResult }) {
return fetchMoreResult;
},
});
}
},
},
};
</script>
@ -134,7 +212,7 @@ export default {
<gl-alert
v-if="showDeleteAlert"
:variant="deleteAlertType"
class="mt-2"
class="gl-mt-5"
dismissible
@dismiss="dismissDeleteAlert"
>
@ -165,7 +243,7 @@ export default {
<template v-else>
<registry-header
:images-count="pagination.total"
:images-count="containerRepositoriesCount"
:expiration-policy="config.expirationPolicy"
:help-page-path="config.helpPagePath"
:expiration-policy-help-page-path="config.expirationPolicyHelpPagePath"
@ -176,7 +254,7 @@ export default {
</template>
</registry-header>
<div v-if="isLoading" class="mt-2">
<div v-if="isLoading" class="gl-mt-5">
<gl-skeleton-loader
v-for="index in $options.loader.repeat"
:key="index"
@ -190,16 +268,17 @@ export default {
</gl-skeleton-loader>
</div>
<template v-else>
<template v-if="!isEmpty">
<template v-if="images.length > 0 || name">
<div class="gl-display-flex gl-p-1 gl-mt-3" data-testid="listHeader">
<div class="gl-flex-fill-1">
<h5>{{ $options.i18n.IMAGE_REPOSITORY_LIST_LABEL }}</h5>
</div>
<div>
<gl-search-box-by-click
v-model="search"
v-model="searchValue"
:placeholder="$options.i18n.SEARCH_PLACEHOLDER_TEXT"
@submit="requestImagesList({ name: $event })"
@clear="name = null"
@submit="name = $event"
/>
</div>
</div>
@ -207,9 +286,10 @@ export default {
<image-list
v-if="images.length"
:images="images"
:pagination="pagination"
@pageChange="requestImagesList({ pagination: { page: $event }, name: search })"
:page-info="pageInfo"
@delete="deleteImage"
@prev-page="fetchPreviousPage"
@next-page="fetchNextPage"
/>
<gl-empty-state

View File

@ -1,124 +0,0 @@
<script>
import {
GlDropdown,
GlDropdownItem,
GlSearchBoxByType,
GlLoadingIcon,
GlIcon,
GlSkeletonLoader,
GlTooltipDirective,
} from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import { isEmpty } from 'lodash';
import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
import { ANY_GROUP, GROUP_QUERY_PARAM, PROJECT_QUERY_PARAM } from '../constants';
export default {
name: 'GroupFilter',
components: {
GlDropdown,
GlDropdownItem,
GlSearchBoxByType,
GlLoadingIcon,
GlIcon,
GlSkeletonLoader,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
initialGroup: {
type: Object,
required: false,
default: () => ({}),
},
},
data() {
return {
groupSearch: '',
};
},
computed: {
...mapState(['groups', 'fetchingGroups']),
selectedGroup: {
get() {
return isEmpty(this.initialGroup) ? ANY_GROUP : this.initialGroup;
},
set(group) {
visitUrl(setUrlParams({ [GROUP_QUERY_PARAM]: group.id, [PROJECT_QUERY_PARAM]: null }));
},
},
},
methods: {
...mapActions(['fetchGroups']),
isGroupSelected(group) {
return group.id === this.selectedGroup.id;
},
handleGroupChange(group) {
this.selectedGroup = group;
},
},
ANY_GROUP,
};
</script>
<template>
<gl-dropdown
ref="groupFilter"
class="gl-w-full"
menu-class="gl-w-full!"
toggle-class="gl-text-truncate gl-reset-line-height!"
:header-text="__('Filter results by group')"
@show="fetchGroups(groupSearch)"
>
<template #button-content>
<span class="dropdown-toggle-text gl-flex-grow-1 gl-text-truncate">
{{ selectedGroup.name }}
</span>
<gl-loading-icon v-if="fetchingGroups" inline class="mr-2" />
<gl-icon
v-if="!isGroupSelected($options.ANY_GROUP)"
v-gl-tooltip
name="clear"
:title="__('Clear')"
class="gl-text-gray-200! gl-hover-text-blue-800!"
@click.stop="handleGroupChange($options.ANY_GROUP)"
/>
<gl-icon name="chevron-down" />
</template>
<div class="gl-sticky gl-top-0 gl-z-index-1 gl-bg-white">
<gl-search-box-by-type
v-model="groupSearch"
class="m-2"
:debounce="500"
@input="fetchGroups"
/>
<gl-dropdown-item
class="gl-border-b-solid gl-border-b-gray-100 gl-border-b-1 gl-pb-2! gl-mb-2"
:is-check-item="true"
:is-checked="isGroupSelected($options.ANY_GROUP)"
@click="handleGroupChange($options.ANY_GROUP)"
>
{{ $options.ANY_GROUP.name }}
</gl-dropdown-item>
</div>
<div v-if="!fetchingGroups">
<gl-dropdown-item
v-for="group in groups"
:key="group.id"
:is-check-item="true"
:is-checked="isGroupSelected(group)"
@click="handleGroupChange(group)"
>
{{ group.full_name }}
</gl-dropdown-item>
</div>
<div v-if="fetchingGroups" class="mx-3 mt-2">
<gl-skeleton-loader :height="100">
<rect y="0" width="90%" height="20" rx="4" />
<rect y="40" width="70%" height="20" rx="4" />
<rect y="80" width="80%" height="20" rx="4" />
</gl-skeleton-loader>
</div>
</gl-dropdown>
</template>

View File

@ -1,10 +0,0 @@
import { __ } from '~/locale';
export const ANY_GROUP = Object.freeze({
id: null,
name: __('Any'),
});
export const GROUP_QUERY_PARAM = 'group_id';
export const PROJECT_QUERY_PARAM = 'project_id';

View File

@ -1,28 +0,0 @@
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import GroupFilter from './components/group_filter.vue';
Vue.use(Translate);
export default store => {
let initialGroup;
const el = document.getElementById('js-search-group-dropdown');
const { initialGroupData } = el.dataset;
initialGroup = JSON.parse(initialGroupData);
initialGroup = convertObjectPropsToCamelCase(initialGroup, { deep: true });
return new Vue({
el,
store,
render(createElement) {
return createElement(GroupFilter, {
props: {
initialGroup,
},
});
},
});
};

View File

@ -1,7 +1,7 @@
import { queryToObject } from '~/lib/utils/url_utility';
import createStore from './store';
import { initTopbar } from './topbar';
import { initSidebar } from './sidebar';
import initGroupFilter from './group_filter';
export const initSearchApp = () => {
// Similar to url_utility.decodeUrlParameter
@ -9,6 +9,6 @@ export const initSearchApp = () => {
const sanitizedSearch = window.location.search.replace(/\+/g, '%20');
const store = createStore({ query: queryToObject(sanitizedSearch) });
initTopbar(store);
initSidebar(store);
initGroupFilter(store);
};

View File

@ -0,0 +1,49 @@
<script>
import { mapState, mapActions } from 'vuex';
import { isEmpty } from 'lodash';
import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
import SearchableDropdown from './searchable_dropdown.vue';
import { ANY_OPTION, GROUP_DATA, PROJECT_DATA } from '../constants';
export default {
name: 'GroupFilter',
components: {
SearchableDropdown,
},
props: {
initialData: {
type: Object,
required: false,
default: () => ({}),
},
},
computed: {
...mapState(['groups', 'fetchingGroups']),
selectedGroup() {
return isEmpty(this.initialData) ? ANY_OPTION : this.initialData;
},
},
methods: {
...mapActions(['fetchGroups']),
handleGroupChange(group) {
visitUrl(
setUrlParams({ [GROUP_DATA.queryParam]: group.id, [PROJECT_DATA.queryParam]: null }),
);
},
},
GROUP_DATA,
};
</script>
<template>
<searchable-dropdown
:header-text="$options.GROUP_DATA.headerText"
:selected-display-value="$options.GROUP_DATA.selectedDisplayValue"
:items-display-value="$options.GROUP_DATA.itemsDisplayValue"
:loading="fetchingGroups"
:selected-item="selectedGroup"
:items="groups"
@search="fetchGroups"
@change="handleGroupChange"
/>
</template>

View File

@ -0,0 +1,144 @@
<script>
import {
GlDropdown,
GlDropdownItem,
GlSearchBoxByType,
GlLoadingIcon,
GlIcon,
GlButton,
GlSkeletonLoader,
GlTooltipDirective,
} from '@gitlab/ui';
import { ANY_OPTION } from '../constants';
export default {
name: 'SearchableDropdown',
components: {
GlDropdown,
GlDropdownItem,
GlSearchBoxByType,
GlLoadingIcon,
GlIcon,
GlButton,
GlSkeletonLoader,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
headerText: {
type: String,
required: false,
default: "__('Filter')",
},
selectedDisplayValue: {
type: String,
required: false,
default: 'name',
},
itemsDisplayValue: {
type: String,
required: false,
default: 'name',
},
loading: {
type: Boolean,
required: false,
default: false,
},
selectedItem: {
type: Object,
required: true,
},
items: {
type: Array,
required: false,
default: () => [],
},
},
data() {
return {
searchText: '',
};
},
methods: {
isSelected(selected) {
return selected.id === this.selectedItem.id;
},
openDropdown() {
this.$emit('search', this.searchText);
},
resetDropdown() {
this.$emit('change', ANY_OPTION);
},
},
ANY_OPTION,
};
</script>
<template>
<gl-dropdown
class="gl-w-full"
menu-class="gl-w-full!"
toggle-class="gl-text-truncate gl-reset-line-height!"
:header-text="headerText"
@show="$emit('search', searchText)"
@shown="$refs.searchBox.focusInput()"
>
<template #button-content>
<span class="dropdown-toggle-text gl-flex-grow-1 gl-text-truncate">
{{ selectedItem[selectedDisplayValue] }}
</span>
<gl-loading-icon v-if="loading" inline class="gl-mr-3" />
<gl-button
v-if="!isSelected($options.ANY_OPTION)"
v-gl-tooltip
name="clear"
category="tertiary"
:title="__('Clear')"
class="gl-p-0! gl-mr-2"
@keydown.enter.stop="resetDropdown"
@click.stop="resetDropdown"
>
<gl-icon name="clear" class="gl-text-gray-200! gl-hover-text-blue-800!" />
</gl-button>
<gl-icon name="chevron-down" />
</template>
<div class="gl-sticky gl-top-0 gl-z-index-1 gl-bg-white">
<gl-search-box-by-type
ref="searchBox"
v-model="searchText"
class="gl-m-3"
:debounce="500"
@input="$emit('search', searchText)"
/>
<gl-dropdown-item
class="gl-border-b-solid gl-border-b-gray-100 gl-border-b-1 gl-pb-2! gl-mb-2"
:is-check-item="true"
:is-checked="isSelected($options.ANY_OPTION)"
@click="resetDropdown"
>
{{ $options.ANY_OPTION.name }}
</gl-dropdown-item>
</div>
<div v-if="!loading">
<gl-dropdown-item
v-for="item in items"
:key="item.id"
:is-check-item="true"
:is-checked="isSelected(item)"
@click="$emit('change', item)"
>
{{ item[itemsDisplayValue] }}
</gl-dropdown-item>
</div>
<div v-if="loading" class="gl-mx-4 gl-mt-3">
<gl-skeleton-loader :height="100">
<rect y="0" width="90%" height="20" rx="4" />
<rect y="40" width="70%" height="20" rx="4" />
<rect y="80" width="80%" height="20" rx="4" />
</gl-skeleton-loader>
</div>
</gl-dropdown>
</template>

View File

@ -0,0 +1,21 @@
import { __ } from '~/locale';
export const ANY_OPTION = Object.freeze({
id: null,
name: __('Any'),
name_with_namespace: __('Any'),
});
export const GROUP_DATA = {
headerText: __('Filter results by group'),
queryParam: 'group_id',
selectedDisplayValue: 'name',
itemsDisplayValue: 'full_name',
};
export const PROJECT_DATA = {
headerText: __('Filter results by project'),
queryParam: 'project_id',
selectedDisplayValue: 'name_with_namespace',
itemsDisplayValue: 'name_with_namespace',
};

View File

@ -0,0 +1,39 @@
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import GroupFilter from './components/group_filter.vue';
Vue.use(Translate);
const mountSearchableDropdown = (store, { id, component }) => {
const el = document.getElementById(id);
if (!el) {
return false;
}
let { initialData } = el.dataset;
initialData = JSON.parse(initialData);
return new Vue({
el,
store,
render(createElement) {
return createElement(component, {
props: {
initialData,
},
});
},
});
};
const searchableDropdowns = [
{
id: 'js-search-group-dropdown',
component: GroupFilter,
},
];
export const initTopbar = store =>
searchableDropdowns.map(dropdown => mountSearchableDropdown(store, dropdown));

View File

@ -14,7 +14,6 @@ class Projects::FeatureFlagsController < Projects::ApplicationController
before_action do
push_frontend_feature_flag(:feature_flag_permissions)
push_frontend_feature_flag(:feature_flags_new_version, project, default_enabled: true)
push_frontend_feature_flag(:feature_flags_legacy_read_only, project, default_enabled: true)
push_frontend_feature_flag(:feature_flags_legacy_read_only_override, project)
end
@ -101,15 +100,7 @@ class Projects::FeatureFlagsController < Projects::ApplicationController
protected
def feature_flag
@feature_flag ||= @noteable = if new_version_feature_flags_enabled?
project.operations_feature_flags.find_by_iid!(params[:iid])
else
project.operations_feature_flags.legacy_flag.find_by_iid!(params[:iid])
end
end
def new_version_feature_flags_enabled?
::Feature.enabled?(:feature_flags_new_version, project, default_enabled: true)
@feature_flag ||= @noteable = project.operations_feature_flags.find_by_iid!(params[:iid])
end
def ensure_legacy_flags_writable!

View File

@ -24,11 +24,7 @@ class FeatureFlagsFinder
private
def feature_flags
if Feature.enabled?(:feature_flags_new_version, project, default_enabled: true)
project.operations_feature_flags
else
project.operations_feature_flags.legacy_flag
end
project.operations_feature_flags
end
def by_scope(items)

View File

@ -38,7 +38,8 @@ module SystemNoteHelper
'status' => 'status',
'alert_issue_added' => 'issues',
'new_alert_added' => 'warning',
'severity' => 'information-o'
'severity' => 'information-o',
'cloned' => 'documents'
}.freeze
def system_note_icon_name(note)

View File

@ -32,6 +32,7 @@ class Environment < ApplicationRecord
has_one :last_visible_deployment, -> { visible.distinct_on_environment }, inverse_of: :environment, class_name: 'Deployment'
has_one :last_visible_deployable, through: :last_visible_deployment, source: 'deployable', source_type: 'CommitStatus'
has_one :last_visible_pipeline, through: :last_visible_deployable, source: 'pipeline'
has_one :upcoming_deployment, -> { running.order('deployments.id DESC') }, class_name: 'Deployment'
has_one :latest_opened_most_severe_alert, -> { order_severity_with_open_prometheus_alert }, class_name: 'AlertManagement::Alert', inverse_of: :environment
before_validation :nullify_external_url

View File

@ -5,8 +5,8 @@ class Experiment < ApplicationRecord
validates :name, presence: true, uniqueness: true, length: { maximum: 255 }
def self.add_user(name, group_type, user)
find_or_create_by!(name: name).record_user_and_group(user, group_type)
def self.add_user(name, group_type, user, context = {})
find_or_create_by!(name: name).record_user_and_group(user, group_type, context)
end
def self.record_conversion_event(name, user)
@ -14,8 +14,8 @@ class Experiment < ApplicationRecord
end
# Create or update the recorded experiment_user row for the user in this experiment.
def record_user_and_group(user, group_type)
experiment_users.find_or_initialize_by(user: user).update!(group_type: group_type)
def record_user_and_group(user, group_type, context = {})
experiment_users.find_or_initialize_by(user: user).update!(group_type: group_type, context: context)
end
def record_conversion_event_for_user(user)

View File

@ -308,6 +308,7 @@ class Issue < ApplicationRecord
!moved? && persisted? &&
user.can?(:admin_issue, self.project)
end
alias_method :can_clone?, :can_move?
def to_branch_name
if self.confidential?

View File

@ -14,12 +14,13 @@ class SystemNoteMetadata < ApplicationRecord
moved merge
label milestone
relate unrelate
cloned
].freeze
ICON_TYPES = %w[
commit description merge confidential visible label assignee cross_reference
designs_added designs_modified designs_removed designs_discussion_added
title time_tracking branch milestone discussion task moved
title time_tracking branch milestone discussion task moved cloned
opened closed merged duplicate locked unlocked outdated reviewer
tag due_date pinned_embed cherry_pick health_status approved unapproved
status alert_issue_added relate unrelate new_alert_added severity

View File

@ -3,6 +3,9 @@
class EnvironmentEntity < Grape::Entity
include RequestAwareEntity
UNNECESSARY_ENTRIES_FOR_UPCOMING_DEPLOYMENT =
%i[manual_actions scheduled_actions playable_build cluster].freeze
expose :id
expose :global_id do |environment|
@ -17,6 +20,11 @@ class EnvironmentEntity < Grape::Entity
expose :last_deployment, using: DeploymentEntity
expose :stop_action_available?, as: :has_stop_action
expose :upcoming_deployment, expose_nil: false do |environment, ops|
DeploymentEntity.represent(environment.upcoming_deployment,
ops.merge(except: UNNECESSARY_ENTRIES_FOR_UPCOMING_DEPLOYMENT))
end
expose :metrics_path, if: -> (*) { environment.has_metrics? } do |environment|
metrics_project_environment_path(environment.project, environment)
end

View File

@ -5,7 +5,6 @@ module FeatureFlags
def execute
return error('Access Denied', 403) unless can_create?
return error('Version is invalid', :bad_request) unless valid_version?
return error('New version feature flags are not enabled for this project', :bad_request) unless flag_version_enabled?
ActiveRecord::Base.transaction do
feature_flag = project.operations_feature_flags.new(params)
@ -40,13 +39,5 @@ module FeatureFlags
def valid_version?
!params.key?(:version) || Operations::FeatureFlag.versions.key?(params[:version])
end
def flag_version_enabled?
params[:version] != 'new_version_flag' || new_version_feature_flags_enabled?
end
def new_version_feature_flags_enabled?
::Feature.enabled?(:feature_flags_new_version, project, default_enabled: true)
end
end
end

View File

@ -0,0 +1,81 @@
# frozen_string_literal: true
module Issues
class CloneService < Issuable::Clone::BaseService
CloneError = Class.new(StandardError)
def execute(issue, target_project)
@target_project = target_project
unless issue.can_clone?(current_user, @target_project)
raise CloneError, s_('CloneIssue|Cannot clone issue due to insufficient permissions!')
end
if target_project.pending_delete?
raise CloneError, s_('CloneIssue|Cannot clone issue to target project as it is pending deletion.')
end
super(issue, target_project)
queue_copy_designs
new_entity
end
private
attr_reader :target_project
def update_new_entity
# we don't call `super` because we want to be able to decide whether or not to copy all comments over.
update_new_entity_description
update_new_entity_attributes
copy_award_emoji
end
def update_old_entity
# no-op
# The base_service closes the old issue, we don't want that, so we override here so nothing happens.
end
def create_new_entity
new_params = {
id: nil,
iid: nil,
project: target_project,
author: original_entity.author,
assignee_ids: original_entity.assignee_ids
}
new_params = original_entity.serializable_hash.symbolize_keys.merge(new_params)
# Skip creation of system notes for existing attributes of the issue. The system notes of the old
# issue are copied over so we don't want to end up with duplicate notes.
CreateService.new(@target_project, @current_user, new_params).execute(skip_system_notes: true)
end
def queue_copy_designs
return unless original_entity.designs.present?
response = DesignManagement::CopyDesignCollection::QueueService.new(
current_user,
original_entity,
new_entity
).execute
log_error(response.message) if response.error?
end
def add_note_from
SystemNoteService.noteable_cloned(new_entity, target_project,
original_entity, current_user,
direction: :from)
end
def add_note_to
SystemNoteService.noteable_cloned(original_entity, old_project,
new_entity, current_user,
direction: :to)
end
end
end

View File

@ -9,7 +9,7 @@ module Issues
handle_move_between_ids(issue)
filter_spam_check_params
change_issue_duplicate(issue)
move_issue_to_new_project(issue) || update_task_event(issue) || update(issue)
move_issue_to_new_project(issue) || clone_issue(issue) || update_task_event(issue) || update(issue)
end
def update(issue)
@ -127,6 +127,17 @@ module Issues
private
def clone_issue(issue)
target_project = params.delete(:target_clone_project)
return unless target_project &&
issue.can_clone?(current_user, target_project)
# we've pre-empted this from running in #execute, so let's go ahead and update the Issue now.
update(issue)
Issues::CloneService.new(project, current_user).execute(issue, target_project)
end
def create_merge_request_from_quick_action
create_merge_request_params = params.delete(:create_merge_request)
return unless create_merge_request_params

View File

@ -226,6 +226,10 @@ module SystemNoteService
::SystemNotes::IssuablesService.new(noteable: noteable, project: project, author: author).noteable_moved(noteable_ref, direction)
end
def noteable_cloned(noteable, project, noteable_ref, author, direction:)
::SystemNotes::IssuablesService.new(noteable: noteable, project: project, author: author).noteable_cloned(noteable_ref, direction)
end
def mark_duplicate_issue(noteable, project, author, canonical_issue)
::SystemNotes::IssuablesService.new(noteable: noteable, project: project, author: author).mark_duplicate_issue(canonical_issue)
end

View File

@ -242,6 +242,27 @@ module SystemNotes
create_note(NoteSummary.new(noteable, project, author, body, action: 'moved'))
end
# Called when noteable has been cloned
#
# noteable_ref - Referenced noteable
# direction - symbol, :to or :from
#
# Example Note text:
#
# "cloned to some_namespace/project_new#11"
#
# Returns the created Note object
def noteable_cloned(noteable_ref, direction)
unless [:to, :from].include?(direction)
raise ArgumentError, "Invalid direction `#{direction}`"
end
cross_reference = noteable_ref.to_reference(project)
body = "cloned #{direction} #{cross_reference}"
create_note(NoteSummary.new(noteable, project, author, body, action: 'cloned'))
end
# Called when the confidentiality changes
#
# Example Note text:

View File

@ -16,4 +16,5 @@
"cleanup_policies_help_page_path" => help_page_path('user/packages/container_registry/index', anchor: 'how-the-cleanup-policy-works'),
"is_admin": current_user&.admin.to_s,
is_group_page: "true",
"group_path": @group.full_path,
character_error: @character_error.to_s } }

View File

@ -6,6 +6,5 @@
- else
%link{ { rel: 'preload', href: stylesheet_url('application'), as: 'style' }, ActionController::Base.asset_host ? { crossorigin: 'anonymous' } : {} }
%link{ { rel: 'preload', href: stylesheet_url("highlight/themes/#{user_color_scheme}"), as: 'style' }, ActionController::Base.asset_host ? { crossorigin: 'anonymous' } : {} }
%link{ { rel: 'preload', href: asset_url("fontawesome-webfont.woff2?v=4.7.0"), as: 'font', type: 'font/woff2' }, ActionController::Base.asset_host ? { crossorigin: 'anonymous' } : {} }
- if Gitlab::CurrentSettings.snowplow_enabled? && Gitlab::CurrentSettings.snowplow_collector_hostname
%link{ rel: 'preconnect', href: Gitlab::CurrentSettings.snowplow_collector_hostname, crossorigin: '' }

View File

@ -17,6 +17,6 @@
"garbage_collection_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'container-registry-garbage-collection'),
"run_cleanup_policies_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'run-the-cleanup-policy-now'),
"cleanup_policies_help_page_path" => help_page_path('user/packages/container_registry/index', anchor: 'how-the-cleanup-policy-works'),
"project_path": @project.full_path,
"is_admin": current_user&.admin.to_s,
character_error: @character_error.to_s } }

View File

@ -5,7 +5,7 @@
.dropdown.form-group.mb-lg-0.mx-lg-1.gl-p-0{ data: { testid: "group-filter" } }
%label.d-block{ for: "dashboard_search_group" }
= _("Group")
%input#js-search-group-dropdown.dropdown-menu-toggle{ value: "Loading...", data: { "initial-group-data": @group.to_json } }
%input#js-search-group-dropdown.dropdown-menu-toggle{ value: "Loading...", data: { "initial-data": @group.to_json } }
.dropdown.form-group.mb-lg-0.mx-lg-1{ data: { testid: "project-filter" } }
%label.d-block{ for: "dashboard_search_project" }
= _("Project")

View File

@ -0,0 +1,5 @@
---
title: Add context to the experiment user records
merge_request: 48896
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Add primary key to elasticsearch_indexed_projects
merge_request: 48919
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Remove avg_cycle_analytics from usage ping
merge_request: 47812
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Refactor container registry list page to grapqhl
merge_request: 48602
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Prometheus integration name should not have a modifiable input field
merge_request: 48437
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Implement a /clone quick-action to quickly clone an Issue
merge_request: 48394
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Expose upcoming deployment in environment.json
merge_request: 48449
author:
type: added

View File

@ -1,8 +0,0 @@
---
name: feature_flags_new_version
introduced_by_url:
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/258831
milestone: '13.7'
type: development
group: group::progressive delivery
default_enabled: true

View File

@ -1,8 +0,0 @@
---
name: security_on_demand_scans_http_header_validation
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/42812
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/276403
milestone: '13.6'
type: development
group: group::dynamic analysis
default_enabled: false

View File

@ -0,0 +1,31 @@
# frozen_string_literal: true
class AddPrimaryKeyToElasticSearchIndexedProjects < ActiveRecord::Migration[6.0]
DOWNTIME = false
UNIQUE_INDEX_NAME = 'index_elasticsearch_indexed_projects_on_project_id'
PRIMARY_KEY_NAME = 'elasticsearch_indexed_projects_pkey'
def up
execute(<<~SQL)
DELETE FROM elasticsearch_indexed_projects
WHERE project_id IS NULL
SQL
execute(<<~SQL)
ALTER TABLE elasticsearch_indexed_projects
ALTER COLUMN project_id SET NOT NULL,
ADD CONSTRAINT #{PRIMARY_KEY_NAME} PRIMARY KEY USING INDEX #{UNIQUE_INDEX_NAME}
SQL
end
def down
add_index :elasticsearch_indexed_projects, :project_id, unique: true, name: UNIQUE_INDEX_NAME # rubocop:disable Migration/AddIndex
execute(<<~SQL)
ALTER TABLE elasticsearch_indexed_projects
DROP CONSTRAINT #{PRIMARY_KEY_NAME},
ALTER COLUMN project_id DROP NOT NULL
SQL
end
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
class AddOtherContextToExperimentUser < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
with_lock_retries do
add_column :experiment_users, :context, :jsonb, default: {}, null: false
end
end
def down
with_lock_retries do
remove_column :experiment_users, :context
end
end
end

View File

@ -0,0 +1 @@
d9ad12dce02d6823536f3206e9c90a0da82c08089c3ce252e8ef28a59589e747

View File

@ -0,0 +1 @@
f4ec800e68cbe092775b428d3ff85a4a84be0d55d70e59d23de390847ea3c2b7

View File

@ -11935,7 +11935,7 @@ CREATE TABLE elasticsearch_indexed_namespaces (
CREATE TABLE elasticsearch_indexed_projects (
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
project_id integer
project_id integer NOT NULL
);
CREATE TABLE emails (
@ -12124,7 +12124,8 @@ CREATE TABLE experiment_users (
group_type smallint DEFAULT 0 NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
converted_at timestamp with time zone
converted_at timestamp with time zone,
context jsonb DEFAULT '{}'::jsonb NOT NULL
);
CREATE SEQUENCE experiment_users_id_seq
@ -19255,6 +19256,9 @@ ALTER TABLE ONLY draft_notes
ALTER TABLE ONLY elastic_reindexing_tasks
ADD CONSTRAINT elastic_reindexing_tasks_pkey PRIMARY KEY (id);
ALTER TABLE ONLY elasticsearch_indexed_projects
ADD CONSTRAINT elasticsearch_indexed_projects_pkey PRIMARY KEY (project_id);
ALTER TABLE ONLY emails
ADD CONSTRAINT emails_pkey PRIMARY KEY (id);
@ -21056,8 +21060,6 @@ CREATE INDEX index_elasticsearch_indexed_namespaces_on_created_at ON elasticsear
CREATE UNIQUE INDEX index_elasticsearch_indexed_namespaces_on_namespace_id ON elasticsearch_indexed_namespaces USING btree (namespace_id);
CREATE UNIQUE INDEX index_elasticsearch_indexed_projects_on_project_id ON elasticsearch_indexed_projects USING btree (project_id);
CREATE UNIQUE INDEX index_emails_on_confirmation_token ON emails USING btree (confirmation_token);
CREATE UNIQUE INDEX index_emails_on_email ON emails USING btree (email);

View File

@ -99,6 +99,7 @@ exceptions:
- SEO
- SHA
- SLA
- SMS
- SMTP
- SQL
- SSD

View File

@ -484,6 +484,7 @@ sudo
swimlane
swimlanes
syslog
tanuki
tcpdump
Thanos
Tiller

View File

@ -144,7 +144,7 @@ package (highly recommended), follow the steps below:
Before beginning, you should already have a working GitLab instance. [Learn how
to install GitLab](https://about.gitlab.com/install/).
Provision a PostgreSQL server (PostgreSQL 11 or newer).
Provision a PostgreSQL server (PostgreSQL 11 or newer).
Prepare all your new nodes by [installing
GitLab](https://about.gitlab.com/install/).

View File

@ -185,7 +185,7 @@ Feature.enable(:ci_enable_live_trace)
```
NOTE: **Note:**
The transition period is handled gracefully. Upcoming logs are
The transition period is handled gracefully. Upcoming logs are
generated with the incremental architecture, and on-going logs stay with the
legacy architecture, which means that on-going logs aren't forcibly
re-generated with the incremental architecture.

View File

@ -55,7 +55,7 @@ guides you through the process.
NOTE: **Note:**
After the Packages feature is enabled, the repositories are available
for all new projects by default. To enable it for existing projects, users
for all new projects by default. To enable it for existing projects, users
explicitly do so in the project's settings.
To enable the Packages feature:

View File

@ -94,7 +94,7 @@ Upload a logo to your GitLab instance.
To upload an avatar from your file system, use the `--form` argument. This causes
cURL to post data using the header `Content-Type: multipart/form-data`. The
`file=` parameter must point to an image file on your file system and be
`file=` parameter must point to an image file on your file system and be
preceded by `@`.
```plaintext

View File

@ -10,10 +10,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/212318) to [GitLab Starter](https://about.gitlab.com/pricing/) in 13.4.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/212318) to [GitLab Core](https://about.gitlab.com/pricing/) in 13.5.
NOTE: **Note:**
This API is behind a [feature flag](../operations/feature_flags.md#enable-or-disable-feature-flag-strategies).
If this flag is not enabled in your environment, you can use the [legacy feature flags API](feature_flags_legacy.md).
API for accessing resources of [GitLab Feature Flags](../operations/feature_flags.md).
Users with Developer or higher [permissions](../user/permissions.md) can access Feature Flag API.

View File

@ -533,7 +533,9 @@ tenses, words, and phrases:
content is accessible to more readers.
- Don't write in the first person singular.
(Tested in [`FirstPerson.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/FirstPerson.yml).)
<!-- vale gitlab.FirstPerson = NO -->
- Instead of _I_ or _me_, use _we_, _you_, _us_, or _one_.
<!-- vale gitlab.FirstPerson = YES -->
- When possible, stay user focused by writing in the second person (_you_ or
the imperative).
- Don't overuse "that". In many cases, you can remove "that" from a sentence
@ -795,6 +797,8 @@ Items nested in lists should always align with the first character of the list
item. In unordered lists (using `-`), this means two spaces for each level of
indentation:
<!-- vale off -->
````markdown
- Unordered list item 1
@ -816,8 +820,12 @@ indentation:
![an image that will nest inside list item 4](image.png)
````
<!-- vale on -->
For ordered lists, use three spaces for each level of indentation:
<!-- vale off -->
````markdown
1. Ordered list item 1
@ -839,6 +847,8 @@ For ordered lists, use three spaces for each level of indentation:
![an image that will nest inside list item 4](image.png)
````
<!-- vale on -->
You can nest full lists inside other lists using the same rules as above. If you
want to mix types, that's also possible, if you don't mix items at the same
level:
@ -904,7 +914,7 @@ Valid for Markdown content only, not for front matter entries:
- Standard quotes: double quotes (`"`). Example: "This is wrapped in double
quotes".
- Quote inside a quote: double quotes (`"`) wrap single quotes (`'`). Example:
"I am 'quoting' something in a quote".
"This sentence 'quotes' something in a quote".
For other punctuation rules, refer to the
[GitLab UX guide](https://design.gitlab.com/content/punctuation/).
@ -1367,6 +1377,8 @@ hidden on the documentation site, but is displayed by `/help`.
- For regular fenced code blocks, always use a highlighting class corresponding to
the language for better readability. Examples:
<!-- vale off -->
````markdown
```ruby
Ruby code
@ -1385,6 +1397,8 @@ hidden on the documentation site, but is displayed by `/help`.
```
````
<!-- vale on -->
Syntax highlighting is required for fenced code blocks added to the GitLab
documentation. Refer to the following table for the most common language classes,
or check the [complete list](https://github.com/rouge-ruby/rouge/wiki/List-of-supported-languages-and-lexers)
@ -1771,8 +1785,7 @@ for use in GitLab X.X, and is planned for [removal](link-to-issue) in GitLab X.X
```
After the feature or product is officially deprecated and removed, remove
its information from the GitLab documentation based on
the GitLab version where it's actually removed.
its information from the GitLab documentation.
### Versions in the past or future
@ -1926,6 +1939,8 @@ Configuration settings include:
When you document a list of steps, it may entail editing the configuration file
and reconfiguring or restarting GitLab. In that case, use these styles:
<!-- vale off -->
````markdown
**For Omnibus installations**
@ -1953,6 +1968,8 @@ and reconfiguring or restarting GitLab. In that case, use these styles:
GitLab for the changes to take effect.
````
<!-- vale on -->
In this case:
- Before each step list the installation method is declared in bold.

View File

@ -119,21 +119,21 @@ browser's developer console while on any page within GitLab.
```
Note that `waitForCSSLoaded()` methods supports receiving the action in different ways:
- With a callback:
```javascript
waitForCSSLoaded(action)
```
- With `then()`:
```javascript
waitForCSSLoaded().then(action);
```
- With `await` followed by `action`:
```javascript
await waitForCSSLoaded;
action();

View File

@ -131,6 +131,8 @@ Once [recursive includes](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/568
become available, you can share job templates like this
[analyzer](https://gitlab.com/gitlab-org/security-products/ci-templates/raw/master/includes-dev/analyzer.yml).
Go GitLab linter plugins are maintained in the [`gitlab-org/language-tools/go/linters`](https://gitlab.com/gitlab-org/language-tools/go/linters/) namespace.
## Dependencies
Dependencies should be kept to the minimum. The introduction of a new

View File

@ -243,8 +243,8 @@ end
The iteration uses the primary key index (on the `id` column) which makes it safe from statement
timeouts. The filter (`sign_in_count: 0`) is applied on the `relation` where the `id` is already constrained (range). The number of rows are limited.
Slow iteration generally takes more time to finish. The iteration count is higher and
one iteration could yield fewer records than the batch size. Iterations may even yield
Slow iteration generally takes more time to finish. The iteration count is higher and
one iteration could yield fewer records than the batch size. Iterations may even yield
0 records. This is not an optimal solution; however, in some cases (especially when
dealing with large tables) this is the only viable option.
@ -346,7 +346,7 @@ Here, we expect that the `relation` query reads the `BATCH_SIZE` of user records
filters down the results according to the provided queries. The planner might decide that
using a bitmap index lookup with the index on the `confidential` column is a better way to
execute the query. This can cause unexpectedly high amount of rows to be read and the query
could time out.
could time out.
Problem: we know for sure that the relation is returning maximum `BATCH_SIZE` of records, however the planner does not know this.

View File

@ -462,8 +462,9 @@ class MyMigration < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
INDEX_NAME = 'index_name'
def up
remove_concurrent_index :table_name, :column_name, name: :index_name
remove_concurrent_index :table_name, :column_name, name: INDEX_NAME
end
end
```

View File

@ -118,7 +118,7 @@ sequenceDiagram
1. `GitLab::UsageData.to_json` [cascades down](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/usage_data.rb#L22) to ~400+ other counter method calls.
1. The response of all methods calls are [merged together](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/usage_data.rb#L14) into a single JSON payload in `GitLab::UsageData.to_json`.
1. The JSON payload is then [posted to the Versions application]( https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/services/submit_usage_ping_service.rb#L20)
If a firewall exception is needed, the required URL depends on several things. If
If a firewall exception is needed, the required URL depends on several things. If
the hostname is `version.gitlab.com`, the protocol is `TCP`, and the port number is `443`,
the required URL is <https://version.gitlab.com/>.
@ -477,11 +477,11 @@ Next, get the unique events for the current week.
We have the following recommendations for [Adding new events](#adding-new-events):
- Event aggregation: weekly.
- Key expiry time:
- Key expiry time:
- Daily: 29 days.
- Weekly: 42 days.
- When adding new metrics, use a [feature flag](../../operations/feature_flags.md) to control the impact.
- For feature flags triggered by another service, set `default_enabled: false`,
- For feature flags triggered by another service, set `default_enabled: false`,
- Events can be triggered using the `UsageData` API, which helps when there are > 10 events per change
##### Enable/Disable Redis HLL tracking
@ -869,44 +869,6 @@ The following is example content of the Usage Ping payload.
"version": "9.6.15",
"pg_system_id": 6842684531675334351
},
"avg_cycle_analytics": {
"issue": {
"average": 999,
"sd": 999,
"missing": 999
},
"plan": {
"average": null,
"sd": 999,
"missing": 999
},
"code": {
"average": null,
"sd": 999,
"missing": 999
},
"test": {
"average": null,
"sd": 999,
"missing": 999
},
"review": {
"average": null,
"sd": 999,
"missing": 999
},
"staging": {
"average": null,
"sd": 999,
"missing": 999
},
"production": {
"average": null,
"sd": 999,
"missing": 999
},
"total": 999
},
"analytics_unique_visits": {
"g_analytics_contribution": 999,
...

View File

@ -27,7 +27,7 @@ After adding a new queue, run `bin/rake
gitlab:sidekiq:all_queues_yml:generate` to regenerate
`app/workers/all_queues.yml` or `ee/app/workers/all_queues.yml` so that
it can be picked up by
[`sidekiq-cluster`](../administration/operations/extra_sidekiq_processes.md).
[`sidekiq-cluster`](../administration/operations/extra_sidekiq_processes.md).
Additionally, run
`bin/rake gitlab:sidekiq:sidekiq_queues_yml:generate` to regenerate
`config/sidekiq_queues.yml`.

View File

@ -106,7 +106,7 @@ Remember that the performance of each test depends on the environment.
### Timout error due to async components
If your component is fetching some other components asynchroneously based on some conditions, it might happen so that your Jest suite for this component will become flaky timing out from time to time.
If your component is fetching some other components asynchroneously based on some conditions, it might happen so that your Jest suite for this component will become flaky timing out from time to time.
```javascript
// ide.vue

View File

@ -227,7 +227,7 @@ The storage requirements for Redis are minimal, about 25kB per user.
Sidekiq processes the background jobs with a multithreaded process.
This process starts with the entire Rails stack (200MB+) but it can grow over time due to memory leaks.
On a very active server (10,000 billable users) the Sidekiq process can use 1GB+ of memory.
## Prometheus and its exporters
As of Omnibus GitLab 9.0, [Prometheus](https://prometheus.io) and its related

View File

@ -196,14 +196,14 @@ WebHook Error => execution expired
```
If those are present, the request is exceeding the
[webhook timeout](../user/project/integrations/webhooks.md#receiving-duplicate-or-multiple-webhook-requests-triggered-by-one-event),
[webhook timeout](../user/project/integrations/webhooks.md#webhook-fails-or-multiple-webhook-requests-are-triggered),
which is set to 10 seconds by default.
To fix this the `gitlab_rails['webhook_timeout']` value must be increased
in the `gitlab.rb` config file, followed by the [`gitlab-ctl reconfigure` command](../administration/restart_gitlab.md).
If you don't find the errors above, but do find *duplicate* entries like below (in `/var/log/gitlab/gitlab-rail`), this
could also indicate that [webhook requests are timing out](../user/project/integrations/webhooks.md#receiving-duplicate-or-multiple-webhook-requests-triggered-by-one-event):
could also indicate that [webhook requests are timing out](../user/project/integrations/webhooks.md#webhook-fails-or-multiple-webhook-requests-are-triggered):
```plaintext
2019-10-25_04:22:41.25630 2019-10-25T04:22:41.256Z 1584 TID-ovowh4tek WebHookWorker JID-941fb7f40b69dff3d833c99b INFO: start

View File

@ -64,7 +64,7 @@ Feature.enable(:sourcegraph, Project.find_by_full_path('my_group/my_project'))
If you are new to Sourcegraph, head over to the [Sourcegraph installation documentation](https://docs.sourcegraph.com/admin) and get your instance up and running.
If you are using an HTTPS connection to GitLab, you will need to [configure HTTPS](https://docs.sourcegraph.com/admin/http_https_configuration) for your Sourcegraph instance.
If you are using an HTTPS connection to GitLab, you will need to [configure HTTPS](https://docs.sourcegraph.com/admin/http_https_configuration) for your Sourcegraph instance.
### Connect your Sourcegraph instance to your GitLab instance

View File

@ -77,7 +77,6 @@ is 200. On GitLab.com, the maximum number is determined by [GitLab.com tier](htt
> - It became [enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/214684) in GitLab 13.2.
> - It's recommended for production use.
> - It's enabled on GitLab.com.
> - For GitLab self-managed instances, a GitLab administrator can choose to [disable it](#enable-or-disable-feature-flag-strategies). **(CORE ONLY)**
You can apply a feature flag strategy across multiple environments, without defining
the strategy multiple times.
@ -222,25 +221,6 @@ To remove users from a user list:
1. Click on the **{pencil}** (edit) button next to the list you want to change.
1. Click on the **{remove}** (remove) button next to the ID you want to remove.
### Enable or disable feature flag strategies
This feature is under development, but is ready for production use. It's
deployed behind a feature flag that is **enabled by default**.
[GitLab administrators with access to the GitLab Rails console](../administration/feature_flags.md)
can disable it for your instance.
To disable it:
```ruby
Feature.disable(:feature_flags_new_version)
```
To enable it:
```ruby
Feature.enable(:feature_flags_new_version)
```
## Rollout strategy (legacy)
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/8240) in GitLab 12.2.

View File

@ -266,12 +266,12 @@ You can exclude specific directories from the backup by adding the environment v
- `lfs` (LFS objects)
- `registry` (Container Registry images)
- `pages` (Pages content)
- `repositories` (Git repositories data)
- `repositories` (Git repositories data)
All wikis will be backed up as part of the `repositories` group. Non-existent wikis will be skipped during a backup.
NOTE: **Note:**
When [backing up and restoring Helm Charts](https://docs.gitlab.com/charts/architecture/backup-restore.html), there is an additional option `packages`, which refers to any packages managed by the GitLab [package registry](../user/packages/package_registry/index.md).
When [backing up and restoring Helm Charts](https://docs.gitlab.com/charts/architecture/backup-restore.html), there is an additional option `packages`, which refers to any packages managed by the GitLab [package registry](../user/packages/package_registry/index.md).
For more information see [command line arguments](https://docs.gitlab.com/charts/architecture/backup-restore.html#command-line-arguments).
All wikis are backed up as part of the `repositories` group. Non-existent

View File

@ -130,13 +130,13 @@ always take the latest Secret Detection artifact available.
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/4639) in GitLab 13.6.
Upon detection of a secret, GitLab supports post processing hooks. These can be used to take actions like notifying the cloud service who issued the secret. The cloud provider can confirm the credentials and take remediation actions like revoking or reissuing a new secret and notifying the creator of the secret. Post-processing workflows vary by supported cloud providers.
Upon detection of a secret, GitLab supports post processing hooks. These can be used to take actions like notifying the cloud service who issued the secret. The cloud provider can confirm the credentials and take remediation actions like revoking or reissuing a new secret and notifying the creator of the secret. Post-processing workflows vary by supported cloud providers.
GitLab currently supports post-processing for following service providers:
- Amazon Web Services (AWS)
Third party cloud and SaaS providers can [express integration interest by filling out this form](https://forms.gle/wWpvrtLRK21Q2WJL9). Learn more about the [technical details of post-processing secrets](https://gitlab.com/groups/gitlab-org/-/epics/4639).
Third party cloud and SaaS providers can [express integration interest by filling out this form](https://forms.gle/wWpvrtLRK21Q2WJL9). Learn more about the [technical details of post-processing secrets](https://gitlab.com/groups/gitlab-org/-/epics/4639).
### Customizing settings
@ -286,14 +286,14 @@ For information on this, see the [general Application Security troubleshooting s
### Error: `Couldn't run the gitleaks command: exit status 2`
This error is usually caused by the `GIT_DEPTH` value of 50 that is set for all [projects by default](../../../ci/pipelines/settings.md#git-shallow-clone).
This error is usually caused by the `GIT_DEPTH` value of 50 that is set for all [projects by default](../../../ci/pipelines/settings.md#git-shallow-clone).
For example, if a pipeline is triggered from a Merge Request containing 60 commits while the `GIT_DEPTH` is set to 50, the Secret Detection job will fail as the clone will not have been deep enough to contain all of the relevant commits.
For example, if a pipeline is triggered from a Merge Request containing 60 commits while the `GIT_DEPTH` is set to 50, the Secret Detection job will fail as the clone will not have been deep enough to contain all of the relevant commits.
You can confirm this to be the cause of the error by implementing a [logging level](../../application_security/secret_detection/index.md#logging-level) of `debug`. Once implemented, the logs should look similar to the following example, wherein an "object not found" error can be seen:
```plaintext
ERRO[2020-11-18T18:05:52Z] object not found
ERRO[2020-11-18T18:05:52Z] object not found
[ERRO] [secrets] [2020-11-18T18:05:52Z] ▶ Couldn't run the gitleaks command: exit status 2
[ERRO] [secrets] [2020-11-18T18:05:52Z] ▶ Gitleaks analysis failed: exit status 2
```

View File

@ -241,7 +241,7 @@ Users can unlink SAML for a group from their profile page. This can be helpful i
- Your SAML NameID has changed and so GitLab can no longer find your user.
CAUTION: **Warning:**
Unlinking an account removes all roles assigned to that user within the group.
Unlinking an account removes all roles assigned to that user within the group.
If a user relinks their account, roles need to be reassigned.
For example, to unlink the `MyOrg` account, the following **Disconnect** button is available under **Profile > Accounts**:
@ -274,14 +274,14 @@ To link the SAML `Freelancers` group in the attribute statement example above:
1. Enter `Freelancers` in the `SAML Group Name` field.
1. Choose the desired `Access Level`.
1. **Save** the group link.
1. Repeat to add additional group links if desired.
1. **Save** the group link.
1. Repeat to add additional group links if desired.
![SAML Group Links](img/saml_group_links_v13_6.png)
If a user is a member of multiple SAML groups mapped to the same GitLab group,
If a user is a member of multiple SAML groups mapped to the same GitLab group,
the user gets the highest access level from the groups. For example, if one group
is linked as `Guest` and another `Maintainer`, a user in both groups gets `Maintainer`
is linked as `Guest` and another `Maintainer`, a user in both groups gets `Maintainer`
access.
## Glossary

View File

@ -292,7 +292,7 @@ Prerequisites:
- [Authentication](#authenticate-to-the-package-registry) with the
Package Registry must be configured.
1. In the project where you want to install the package as a dependency, open
1. In the project where you want to install the package as a dependency, open
`conanfile.txt`. Or, in the root of your project, create a file called
`conanfile.txt`.

View File

@ -32,7 +32,7 @@ You can also use the [API](../../api/packages.md) to administer the Package Regi
## Accepting contributions
The below table lists formats that are not supported, but are accepting Community contributions for. Consider contributing to GitLab. This [development documentation](../../development/packages.md)
The below table lists formats that are not supported, but are accepting Community contributions for. Consider contributing to GitLab. This [development documentation](../../development/packages.md)
guides you through the process.
| Format | Status |

View File

@ -23,7 +23,7 @@ access to your Jira projects. This is covered in the process below.
1. The next step is to create a new user (e.g., `gitlab`) who has write access
to projects in Jira. Enter the user's name and a _valid_ e-mail address
since Jira sends a verification e-mail to set up the password.
Jira creates the username automatically by using the e-mail
prefix. You can change it later, if needed. Our integration does not support SSO (such as SAML). You
need to create an HTTP basic authentication password. You can do this by visiting the user

View File

@ -11,8 +11,8 @@ a new issue is created. You can configure webhooks to listen for specific events
like pushes, issues or merge requests. GitLab sends a POST request with data
to the webhook URL.
In most cases, you need to set up your own [webhook receiver](#example-webhook-receiver)
to receive information from GitLab, and send it to another app, according to your needs.
You usually need to set up your own [webhook receiver](#example-webhook-receiver)
to receive information from GitLab and send it to another app, according to your requirements.
We already have a [built-in receiver](slack.md)
for sending [Slack](https://api.slack.com/incoming-webhooks) notifications _per project_.
@ -33,7 +33,7 @@ and **per project and per group** for **GitLab Enterprise Edition**.
Navigate to the webhooks page at your project's **Settings > Webhooks**.
NOTE: **Note:**
NOTE:
On GitLab.com, the [maximum number of webhooks and their size](../../../user/gitlab_com/index.md#webhooks) per project, and per group, is limited.
## Version history
@ -54,7 +54,7 @@ Starting from GitLab 11.2:
`![](/uploads/...)`) have their target URL changed to an absolute URL. See
[image URL rewriting](#image-url-rewriting) for more details.
## Use-cases
## Possible uses for webhooks
- You can set up a webhook in GitLab to send a notification to
[Slack](https://api.slack.com/incoming-webhooks) every time a job fails.
@ -65,12 +65,12 @@ Starting from GitLab 11.2:
## Webhook endpoint tips
If you are writing your own endpoint (web server) to receive
GitLab webhooks, keep in mind the following things:
GitLab webhooks, keep in mind the following:
- Your endpoint should send its HTTP response as fast as possible. If
you wait too long (by default, a timeout of 10 seconds), GitLab may decide
the hook failed and retry it. You can configure this timeout with
`gitlab_rails['webhook_timeout']`.
- Your endpoint should send its HTTP response as fast as possible. If the response takes longer than
the configured timeout, GitLab decides the hook failed and retries it. For information on
customizing this timeout, see
[Webhook fails or multiple webhook requests are triggered](#webhook-fails-or-multiple-webhook-requests-are-triggered).
- Your endpoint should ALWAYS return a valid HTTP response. If you do
not do this then GitLab thinks the hook failed and retries it.
Most HTTP libraries take care of this for you automatically but if
@ -86,7 +86,7 @@ that the request is legitimate.
## SSL verification
By default, the SSL certificate of the webhook endpoint is verified based on
an internal list of Certificate Authorities, which means the certificate cannot
an internal list of Certificate Authorities. This means the certificate cannot
be self-signed.
You can turn this off in the webhook settings in your GitLab projects.
@ -109,7 +109,7 @@ Below are described the supported events.
Triggered when you push to the repository except when pushing tags.
NOTE: **Note:**
NOTE:
When more than 20 commits are pushed at once, the `commits` webhook
attribute only contains the first 20 for performance reasons. Loading
detailed commit data is expensive. Note that despite only 20 commits being
@ -204,7 +204,7 @@ X-Gitlab-Event: Push Hook
Triggered when you create (or delete) tags to the repository.
NOTE: **Note:**
NOTE:
If a single push includes changes for more than three (by default, depending on
[`push_event_hooks_limit` setting](../../../api/settings.md#list-of-settings-that-can-be-accessed-via-api-calls))
tags, this hook is not executed.
@ -409,12 +409,12 @@ X-Gitlab-Event: Issue Hook
}
```
> **Note**: `assignee` and `assignee_id` keys are deprecated and now show the first assignee only.
NOTE: `assignee` and `assignee_id` keys are deprecated and now show the first assignee only.
### Comment events
Triggered when a new comment is made on commits, merge requests, issues, and code snippets.
The note data is stored in `object_attributes` (e.g. `note`, `noteable_type`). The
The note data is stored in `object_attributes` (for example, `note` or `noteable_type`). The
payload also includes information about the target of the comment. For example,
a comment on an issue includes the specific issue information under the `issue` key.
Valid target types:
@ -734,7 +734,7 @@ X-Gitlab-Event: Note Hook
}
```
> **Note**: `assignee_id` field is deprecated and now shows the first assignee only.
NOTE: `assignee_id` field is deprecated and now shows the first assignee only.
#### Comment on code snippet
@ -1531,29 +1531,36 @@ You can find records for last 2 days in "Recent Deliveries" section on the edit
![Recent deliveries](img/webhook_logs.png)
In this section you can see HTTP status code (green for 200-299 codes, red for the others, `internal error` for failed deliveries ), triggered event, a time when the event was called, elapsed time of the request.
In this section you can see:
- HTTP status code (green for `200-299` codes, red for the others, `internal error` for failed deliveries).
- Triggered event.
- A time when the event was called.
- Elapsed time of the request.
If you need more information about execution, you can click `View details` link.
On this page, you can see data that GitLab sends (request headers and body) and data that it received (response headers and body).
From this page, you can repeat delivery with the same data by clicking `Resend Request` button.
NOTE: **Note:**
NOTE:
If URL or secret token of the webhook were updated, data is delivered to the new address.
### Receiving duplicate or multiple webhook requests triggered by one event
### Webhook fails or multiple webhook requests are triggered
When GitLab sends a webhook, it expects a response in 10 seconds (set default value). If it does not receive one, it retries the webhook.
If the endpoint doesn't send its HTTP response within those 10 seconds, GitLab may decide the hook failed and retry it.
When GitLab sends a webhook, it expects a response in 10 seconds by default. If it does not receive
one, it retries the webhook. If the endpoint doesn't send its HTTP response within those 10 seconds,
GitLab may decide the hook failed and retry it.
If you are receiving multiple requests, you can try increasing the default value to wait for the HTTP response after sending the webhook
by uncommenting or adding the following setting to your `/etc/gitlab/gitlab.rb`:
If your webhooks are failing or you are receiving multiple requests, you can try changing the
default value. You can do this by uncommenting or adding the following setting to your
`/etc/gitlab/gitlab.rb` file:
```ruby
gitlab_rails['webhook_timeout'] = 10
```
### Troubleshooting: "Unable to get local issuer certificate"
### Unable to get local issuer certificate
When SSL verification is enabled, this error indicates that GitLab isn't able to verify the SSL certificate of the webhook endpoint.
Typically, this is because the root certificate isn't issued by a trusted certification authority as
@ -1584,7 +1591,7 @@ end
server.start
```
Pick an unused port (e.g. 8000) and start the script: `ruby print_http_body.rb
Pick an unused port (for example, `8000`) and start the script: `ruby print_http_body.rb
8000`. Then add your server as a webhook receiver in GitLab as
`http://my.host:8000/`.
@ -1597,5 +1604,6 @@ example.com - - [14/May/2014:07:45:26 EDT] "POST / HTTP/1.1" 200 0
- -> /
```
NOTE: **Note:**
You may need to [allow requests to the local network](../../../security/webhooks.md) for this receiver to be added.
NOTE:
You may need to [allow requests to the local network](../../../security/webhooks.md) for this
receiver to be added.

View File

@ -34,6 +34,7 @@ The following quick actions are applicable to descriptions, discussions and thre
| `/award :emoji:` | ✓ | ✓ | ✓ | Toggle emoji award. |
| `/child_epic <epic>` | | | ✓ | Add child epic to `<epic>`. The `<epic>` value should be in the format of `&epic`, `group&epic`, or a URL to an epic ([introduced in GitLab 12.0](https://gitlab.com/gitlab-org/gitlab/-/issues/7330)). **(ULTIMATE)** |
| `/clear_weight` | ✓ | | | Clear weight. **(STARTER)** |
| `/clone <path/to/project>` | ✓ | | | Clone the issue to given project, or the current one if no arguments are given ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/9421) in GitLab 13.7). Copies as much data as possible as long as the target project contains equivalent labels, milestones, etc. Does not copy comments or system notes. |
| `/close` | ✓ | ✓ | ✓ | Close. |
| `/confidential` | ✓ | | | Make confidential. |
| `/copy_metadata <!merge_request>` | ✓ | ✓ | | Copy labels and milestone from another merge request in the project. |

View File

@ -79,7 +79,7 @@ To create a new release through the GitLab UI:
[release notes](#release-notes-description), or [assets links](#links).
1. Click **Create release**.
### Create release from GitLab CI
### Create release from GitLab CI
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/19298) in GitLab 12.7.

View File

@ -16,6 +16,9 @@ Project access tokens are supported for self-managed instances on Core and above
> - [Became available on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/235765) in 13.5.
> - It's recommended for production use.
CAUTION: **Warning:**
This feature might not be available to you. Check the **version history** note above for details.
Project access tokens are scoped to a project and can be used to authenticate with the [GitLab API](../../../api/README.md#personalproject-access-tokens). You can also use project access tokens with Git to authenticate over HTTP.
Project access tokens expire on the date you define, at midnight UTC.
@ -75,3 +78,33 @@ the following table.
| `write_registry` | Allows write-access (push) to [container registry](../../packages/container_registry/index.md). |
| `read_repository` | Allows read-only access (pull) to the repository. |
| `write_repository` | Allows read-write access (pull, push) to the repository. |
### Enable or disable project access tokens
Project access tokens are deployed behind a feature flag that is **enabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
can disable it for your instance, globally or by project.
To disable it globally:
```ruby
Feature.disable(:resource_access_token)
```
To disable it for a specific project:
```ruby
Feature.disable(:resource_access_token, project)
```
To enable it globally:
```ruby
Feature.enable(:resource_access_token)
```
To enable it for a specific project:
```ruby
Feature.enable(:resource_access_token, project)
```

View File

@ -6,11 +6,11 @@ module API
expose :name
expose :description
expose :active
expose :version, if: :feature_flags_new_version_enabled
expose :version
expose :created_at
expose :updated_at
expose :scopes, using: FeatureFlag::LegacyScope
expose :strategies, using: FeatureFlag::Strategy, if: :feature_flags_new_version_enabled
expose :strategies, using: FeatureFlag::Strategy
end
end
end

View File

@ -62,8 +62,6 @@ module API
attrs = declared_params(include_missing: false)
ensure_post_version_2_flags_enabled! if attrs[:version] == 'new_version_flag'
rename_key(attrs, :scopes, :scopes_attributes)
rename_key(attrs, :strategies, :strategies_attributes)
update_value(attrs, :strategies_attributes) do |strategies|
@ -143,7 +141,7 @@ module API
end
desc 'Update a feature flag' do
detail 'This feature will be introduced in GitLab 13.1 if feature_flags_new_version feature flag is removed'
detail 'This feature was introduced in GitLab 13.2'
success ::API::Entities::FeatureFlag
end
params do
@ -163,7 +161,6 @@ module API
end
end
put do
not_found! unless feature_flags_new_version_enabled?
authorize_update_feature_flag!
render_api_error!('PUT operations are not supported for legacy feature flags', :unprocessable_entity) if feature_flag.legacy_flag?
@ -228,32 +225,17 @@ module API
def present_entity(result)
present result,
with: ::API::Entities::FeatureFlag,
feature_flags_new_version_enabled: feature_flags_new_version_enabled?
end
def ensure_post_version_2_flags_enabled!
unless feature_flags_new_version_enabled?
render_api_error!('Version 2 flags are not enabled for this project', :unprocessable_entity)
end
with: ::API::Entities::FeatureFlag
end
def feature_flag
@feature_flag ||= if feature_flags_new_version_enabled?
user_project.operations_feature_flags.find_by_name!(params[:feature_flag_name])
else
user_project.operations_feature_flags.legacy_flag.find_by_name!(params[:feature_flag_name])
end
@feature_flag ||= user_project.operations_feature_flags.find_by_name!(params[:feature_flag_name])
end
def new_version_flag_present?
user_project.operations_feature_flags.new_version_flag.find_by_name(params[:name]).present?
end
def feature_flags_new_version_enabled?
Feature.enabled?(:feature_flags_new_version, user_project, default_enabled: true)
end
def rename_key(hash, old_key, new_key)
hash[new_key] = hash.delete(old_key) if hash.key?(old_key)
hash

View File

@ -1,91 +0,0 @@
# frozen_string_literal: true
module Gitlab
module CycleAnalytics
class UsageData
include Gitlab::Utils::StrongMemoize
PROJECTS_LIMIT = 10
attr_reader :options
def initialize
@options = { from: 7.days.ago }
end
def projects
strong_memoize(:projects) do
projects = Project.where.not(last_activity_at: nil).order(last_activity_at: :desc).limit(10) +
Project.where.not(last_repository_updated_at: nil).order(last_repository_updated_at: :desc).limit(10)
projects = projects.uniq.sort_by do |project|
[project.last_activity_at, project.last_repository_updated_at].min
end
if projects.size < 10
projects.concat(Project.where(last_activity_at: nil, last_repository_updated_at: nil).limit(10))
end
projects.uniq.first(10)
end
end
def to_json(*)
total = 0
values =
medians_per_stage.each_with_object({}) do |(stage_name, medians), hsh|
calculations = stage_values(medians)
total += calculations.values.compact.sum
hsh[stage_name] = calculations
end
values[:total] = total
{ avg_cycle_analytics: values }
end
private
def medians_per_stage
projects.each_with_object({}) do |project, hsh|
::CycleAnalytics::ProjectLevel.new(project, options: options).all_medians_by_stage.each do |stage_name, median|
hsh[stage_name] ||= []
hsh[stage_name] << median
end
end
end
def stage_values(medians)
medians = medians.map(&:presence).compact
average = calc_average(medians)
{
average: average,
sd: standard_deviation(medians, average),
missing: projects.length - medians.length
}
end
def calc_average(values)
return if values.empty?
(values.sum / values.length).to_i
end
def standard_deviation(values, average)
Math.sqrt(sample_variance(values, average)).to_i
end
def sample_variance(values, average)
return 0 if values.length <= 1
sum = values.inject(0) do |acc, val|
acc + (val - average)**2
end
sum / (values.length - 1)
end
end
end
end

View File

@ -61,11 +61,11 @@ module Gitlab
end
end
def record_experiment_user(experiment_key)
def record_experiment_user(experiment_key, context = {})
return if dnt_enabled?
return unless Experimentation.active?(experiment_key) && current_user
::Experiment.add_user(experiment_key, tracking_group(experiment_key, nil, subject: current_user), current_user)
::Experiment.add_user(experiment_key, tracking_group(experiment_key, nil, subject: current_user), current_user, context)
end
def record_experiment_conversion_event(experiment_key)

View File

@ -102,6 +102,30 @@ module Gitlab
@execution_message[:duplicate] = message
end
desc _('Clone this issue')
explanation do |project = quick_action_target.project.full_path|
_("Clones this issue, without comments, to %{project}.") % { project: project }
end
params 'path/to/project'
types Issue
condition do
quick_action_target.persisted? &&
current_user.can?(:"admin_#{quick_action_target.to_ability_name}", project)
end
command :clone do |target_project_path = nil|
target_project = target_project_path.present? ? Project.find_by_full_path(target_project_path) : quick_action_target.project
if target_project.present?
@updates[:target_clone_project] = target_project
message = _("Cloned this issue to %{path_to_project}.") % { path_to_project: target_project_path || quick_action_target.project.full_path }
else
message = _("Failed to clone this issue because target project doesn't exist.")
end
@execution_message[:clone] = message
end
desc _('Move this issue to another project.')
explanation do |path_to_project|
_("Moves this issue to %{path_to_project}.") % { path_to_project: path_to_project }

View File

@ -47,7 +47,6 @@ module Gitlab
.merge(system_usage_data_weekly)
.merge(features_usage_data)
.merge(components_usage_data)
.merge(cycle_analytics_usage_data)
.merge(object_store_usage_data)
.merge(topology_usage_data)
.merge(usage_activity_by_stage)
@ -250,12 +249,6 @@ module Gitlab
}
end
def cycle_analytics_usage_data
Gitlab::CycleAnalytics::UsageData.new.to_json
rescue ActiveRecord::StatementInvalid
{ avg_cycle_analytics: {} }
end
# rubocop:disable CodeReuse/ActiveRecord
def grafana_embed_usage_data
count(Issue.joins('JOIN grafana_integrations USING (project_id)')

View File

@ -2619,6 +2619,9 @@ msgstr ""
msgid "AlertSettings|Proceed with editing"
msgstr ""
msgid "AlertSettings|Prometheus"
msgstr ""
msgid "AlertSettings|Prometheus API base URL"
msgstr ""
@ -5662,6 +5665,9 @@ msgstr ""
msgid "Clone repository"
msgstr ""
msgid "Clone this issue"
msgstr ""
msgid "Clone with %{http_label}"
msgstr ""
@ -5674,6 +5680,18 @@ msgstr ""
msgid "Clone with SSH"
msgstr ""
msgid "CloneIssue|Cannot clone issue due to insufficient permissions!"
msgstr ""
msgid "CloneIssue|Cannot clone issue to target project as it is pending deletion."
msgstr ""
msgid "Cloned this issue to %{path_to_project}."
msgstr ""
msgid "Clones this issue, without comments, to %{project}."
msgstr ""
msgid "Close"
msgstr ""
@ -11421,6 +11439,9 @@ msgstr ""
msgid "Failed to check related branches."
msgstr ""
msgid "Failed to clone this issue because target project doesn't exist."
msgstr ""
msgid "Failed to create Merge Request. Please try again."
msgstr ""
@ -11756,9 +11777,6 @@ msgstr ""
msgid "FeatureFlags|Feature Flags"
msgstr ""
msgid "FeatureFlags|Feature Flags will look different in the next milestone. No action is needed, but you may notice the functionality was changed to improve the workflow."
msgstr ""
msgid "FeatureFlags|Feature flag %{name} will be removed. Are you sure?"
msgstr ""

View File

@ -42,7 +42,7 @@
"@babel/plugin-syntax-import-meta": "^7.10.1",
"@babel/preset-env": "^7.10.1",
"@gitlab/at.js": "1.5.5",
"@gitlab/svgs": "1.175.0",
"@gitlab/svgs": "1.176.0",
"@gitlab/ui": "24.4.0",
"@gitlab/visual-review-tools": "1.6.1",
"@rails/actioncable": "^6.0.3-3",

View File

@ -217,15 +217,6 @@ RSpec.describe Projects::FeatureFlagsController do
expect(json_response['feature_flags'].count).to eq(3)
end
it 'returns only version 1 flags when new version flags are disabled' do
stub_feature_flags(feature_flags_new_version: false)
subject
expected = [feature_flag_active.name, feature_flag_inactive.name].sort
expect(json_response['feature_flags'].map { |f| f['name'] }.sort).to eq(expected)
end
end
end
@ -283,24 +274,6 @@ RSpec.describe Projects::FeatureFlagsController do
expect(json_response['name']).to eq(other_feature_flag.name)
end
it 'routes based on iid when new version flags are disabled' do
stub_feature_flags(feature_flags_new_version: false)
other_project = create(:project)
other_project.add_developer(user)
other_feature_flag = create(:operations_feature_flag, project: other_project,
name: 'other_flag')
params = {
namespace_id: other_project.namespace,
project_id: other_project,
iid: other_feature_flag.iid
}
get(:show, params: params, format: :json)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['name']).to eq(other_feature_flag.name)
end
context 'when feature flag is not found' do
let!(:feature_flag) { }
@ -386,14 +359,6 @@ RSpec.describe Projects::FeatureFlagsController do
expect(json_response['version']).to eq('new_version_flag')
end
it 'returns a 404 when new version flags are disabled' do
stub_feature_flags(feature_flags_new_version: false)
subject
expect(response).to have_gitlab_http_status(:not_found)
end
it 'returns strategies ordered by id' do
first_strategy = create(:operations_strategy, feature_flag: new_version_feature_flag)
second_strategy = create(:operations_strategy, feature_flag: new_version_feature_flag)
@ -791,54 +756,6 @@ RSpec.describe Projects::FeatureFlagsController do
expect(Operations::FeatureFlag.count).to eq(0)
end
end
context 'when version 2 flags are disabled' do
context 'and attempting to create a version 2 flag' do
let(:params) do
{
namespace_id: project.namespace,
project_id: project,
operations_feature_flag: {
name: 'my_feature_flag',
active: true,
version: 'new_version_flag'
}
}
end
it 'returns a 400' do
stub_feature_flags(feature_flags_new_version: false)
subject
expect(response).to have_gitlab_http_status(:bad_request)
expect(Operations::FeatureFlag.count).to eq(0)
end
end
context 'and attempting to create a version 1 flag' do
let(:params) do
{
namespace_id: project.namespace,
project_id: project,
operations_feature_flag: {
name: 'my_feature_flag',
active: true
}
}
end
it 'creates the flag' do
stub_feature_flags(feature_flags_new_version: false)
subject
expect(response).to have_gitlab_http_status(:ok)
expect(Operations::FeatureFlag.count).to eq(1)
expect(json_response['version']).to eq('legacy_flag')
end
end
end
end
describe 'DELETE destroy.json' do
@ -913,15 +830,6 @@ RSpec.describe Projects::FeatureFlagsController do
it 'deletes the flag' do
expect { subject }.to change { Operations::FeatureFlag.count }.by(-1)
end
context 'when new version flags are disabled' do
it 'returns a 404' do
stub_feature_flags(feature_flags_new_version: false)
expect { subject }.not_to change { Operations::FeatureFlag.count }
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end
@ -1576,15 +1484,6 @@ RSpec.describe Projects::FeatureFlagsController do
expect(json_response['strategies'].first['scopes']).to eq([])
end
it 'does not update the flag if version 2 flags are disabled' do
stub_feature_flags(feature_flags_new_version: false)
put_request(new_version_flag, { name: 'some-other-name' })
expect(response).to have_gitlab_http_status(:not_found)
expect(new_version_flag.reload.name).to eq('new-feature')
end
it 'updates the flag when legacy feature flags are set to be read only' do
stub_feature_flags(feature_flags_legacy_read_only: true)

View File

@ -184,6 +184,7 @@ RSpec.describe 'Database schema' do
"ApplicationSetting" => %w[repository_storages_weighted],
"AlertManagement::Alert" => %w[payload],
"Ci::BuildMetadata" => %w[config_options config_variables],
"ExperimentUser" => %w[context],
"Geo::Event" => %w[payload],
"GeoNodeStatus" => %w[status],
"Operations::FeatureFlagScope" => %w[strategies],

View File

@ -51,13 +51,6 @@ RSpec.describe 'Container Registry', :js do
expect(page).to have_content 'my/image'
end
it 'image repository delete is disabled' do
visit_container_registry
delete_btn = find('[title="Remove repository"]')
expect(delete_btn).to be_disabled
end
it 'navigates to repo details' do
visit_container_registry_details('my/image')

View File

@ -43,5 +43,6 @@ RSpec.describe 'Issues > User uses quick actions', :js do
it_behaves_like 'create_merge_request quick action'
it_behaves_like 'move quick action'
it_behaves_like 'zoom quick actions'
it_behaves_like 'clone quick action'
end
end

View File

@ -94,7 +94,8 @@ RSpec.describe 'Container Registry', :js do
end
it('pagination navigate to the second page') do
visit_second_page
visit_details_second_page
expect(page).to have_content '20'
end
end
@ -116,22 +117,23 @@ RSpec.describe 'Container Registry', :js do
context 'when there are more than 10 images' do
before do
create_list(:container_repository, 12, project: project)
project.container_repositories << container_repository
create_list(:container_repository, 12, project: project)
visit_container_registry
end
it 'shows pagination' do
expect(page).to have_css '.gl-pagination'
expect(page).to have_css '.gl-keyset-pagination'
end
it 'pagination goes to second page' do
visit_second_page
visit_list_next_page
expect(page).to have_content 'my/image'
end
it 'pagination is preserved after navigating back from details' do
visit_second_page
visit_list_next_page
click_link 'my/image'
breadcrumb = find '.breadcrumbs'
breadcrumb.click_link 'Container Registry'
@ -148,7 +150,12 @@ RSpec.describe 'Container Registry', :js do
click_link name
end
def visit_second_page
def visit_list_next_page
pagination = find '.gl-keyset-pagination'
pagination.click_button 'Next'
end
def visit_details_second_page
pagination = find '.gl-pagination'
pagination.click_link '2'
end

View File

@ -67,118 +67,6 @@ RSpec.describe 'User creates feature flag', :js do
end
end
context 'with new version flags disabled' do
before do
stub_feature_flags(feature_flags_new_version: false)
end
context 'when creates without changing scopes' do
before do
visit(new_project_feature_flag_path(project))
set_feature_flag_info('ci_live_trace', 'For live trace')
click_button 'Create feature flag'
expect(page).to have_current_path(project_feature_flags_path(project))
end
it 'shows the created feature flag' do
within_feature_flag_row(1) do
expect(page.find('.feature-flag-name')).to have_content('ci_live_trace')
expect_status_toggle_button_to_be_checked
within_feature_flag_scopes do
expect(page.find('[data-qa-selector="feature-flag-scope-info-badge"]:nth-child(1)')).to have_content('*')
end
end
end
end
context 'when creates with disabling the default scope' do
before do
visit(new_project_feature_flag_path(project))
set_feature_flag_info('ci_live_trace', 'For live trace')
within_scope_row(1) do
within_status { find('.project-feature-toggle').click }
end
click_button 'Create feature flag'
end
it 'shows the created feature flag' do
within_feature_flag_row(1) do
expect(page.find('.feature-flag-name')).to have_content('ci_live_trace')
expect_status_toggle_button_to_be_checked
within_feature_flag_scopes do
expect(page.find('[data-qa-selector="feature-flag-scope-muted-badge"]:nth-child(1)')).to have_content('*')
end
end
end
end
context 'when creates with an additional scope' do
before do
visit(new_project_feature_flag_path(project))
set_feature_flag_info('mr_train', '')
within_scope_row(2) do
within_environment_spec do
find('.js-env-search > input').set("review/*")
find('.js-create-button').click
end
end
within_scope_row(2) do
within_status { find('.project-feature-toggle').click }
end
click_button 'Create feature flag'
end
it 'shows the created feature flag' do
within_feature_flag_row(1) do
expect(page.find('.feature-flag-name')).to have_content('mr_train')
expect_status_toggle_button_to_be_checked
within_feature_flag_scopes do
expect(page.find('[data-qa-selector="feature-flag-scope-info-badge"]:nth-child(1)')).to have_content('*')
expect(page.find('[data-qa-selector="feature-flag-scope-info-badge"]:nth-child(2)')).to have_content('review/*')
end
end
end
end
context 'when searches an environment name for scope creation' do
let!(:environment) { create(:environment, name: 'production', project: project) }
before do
visit(new_project_feature_flag_path(project))
set_feature_flag_info('mr_train', '')
within_scope_row(2) do
within_environment_spec do
find('.js-env-search > input').set('prod')
click_button 'production'
end
end
click_button 'Create feature flag'
end
it 'shows the created feature flag' do
within_feature_flag_row(1) do
expect(page.find('.feature-flag-name')).to have_content('mr_train')
expect_status_toggle_button_to_be_checked
within_feature_flag_scopes do
expect(page.find('[data-qa-selector="feature-flag-scope-info-badge"]:nth-child(1)')).to have_content('*')
expect(page.find('[data-qa-selector="feature-flag-scope-muted-badge"]:nth-child(2)')).to have_content('production')
end
end
end
end
end
private
def set_feature_flag_info(name, description)

View File

@ -80,15 +80,5 @@ RSpec.describe FeatureFlagsFinder do
is_expected.to eq([feature_flag_1, feature_flag_2, feature_flag_3])
end
end
context 'when new version flags are disabled' do
let!(:feature_flag_3) { create(:operations_feature_flag, :new_version_flag, name: 'flag-c', project: project) }
it 'returns only legacy flags' do
stub_feature_flags(feature_flags_new_version: false)
is_expected.to eq([feature_flag_1, feature_flag_2])
end
end
end
end

View File

@ -42,8 +42,8 @@ describe('AlertsServiceForm', () => {
mockAxios = new MockAdapter(axios);
setFixtures(`
<div>
<span class="js-service-active-status fa fa-circle" data-value="true"></span>
<span class="js-service-active-status fa fa-power-off" data-value="false"></span>
<span class="js-service-active-status" data-value="true"><svg class="s16 cgreen" data-testid="check-icon"><use xlink:href="icons.svg#check" /></svg></span>
<span class="js-service-active-status" data-value="false"><svg class="s16 clgray" data-testid="power-icon"><use xlink:href="icons.svg#power" /></svg></span>
</div>`);
});

Some files were not shown because too many files have changed in this diff Show More