Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
3214c36592
commit
5aaced570d
|
|
@ -3020,7 +3020,6 @@ Style/InlineDisableAnnotation:
|
|||
- 'spec/support/sidekiq_middleware.rb'
|
||||
- 'spec/support_specs/ability_check_spec.rb'
|
||||
- 'spec/support_specs/capybara_slow_finder_spec.rb'
|
||||
- 'spec/support_specs/capybara_wait_for_all_requests_spec.rb'
|
||||
- 'spec/support_specs/database/multiple_databases_helpers_spec.rb'
|
||||
- 'spec/support_specs/helpers/stub_feature_flags_spec.rb'
|
||||
- 'spec/support_specs/matchers/event_store_spec.rb'
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export const i18n = {
|
|||
'Branches|This bulk action is %{strongStart}permanent and cannot be undone or recovered%{strongEnd}.',
|
||||
),
|
||||
confirmationMessage: s__(
|
||||
'Branches|Plese type the following to confirm: %{codeStart}delete%{codeEnd}.',
|
||||
'Branches|Please type the following to confirm: %{codeStart}delete%{codeEnd}.',
|
||||
),
|
||||
cancelButtonText: __('Cancel'),
|
||||
actionsToggleText: __('More actions'),
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
<script>
|
||||
import { GlSearchBoxByClick, GlSorting } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import {
|
||||
SORT_ASC,
|
||||
SORT_DESC,
|
||||
SORT_OPTION_CREATED,
|
||||
SORT_OPTION_RELEASED,
|
||||
SORT_OPTION_STAR_COUNT,
|
||||
SORT_OPTION_POPULARITY,
|
||||
} from '../../constants';
|
||||
|
||||
export default {
|
||||
|
|
@ -14,6 +16,7 @@ export default {
|
|||
GlSearchBoxByClick,
|
||||
GlSorting,
|
||||
},
|
||||
mixins: [glFeatureFlagMixin()],
|
||||
props: {
|
||||
initialSearchTerm: {
|
||||
default: '',
|
||||
|
|
@ -29,6 +32,13 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
sortOptions() {
|
||||
const options = [...this.$options.defaultSortOptions];
|
||||
if (this.glFeatures?.ciCatalogPopularity) {
|
||||
options.push({ value: SORT_OPTION_POPULARITY, text: __('Popularity') });
|
||||
}
|
||||
return options;
|
||||
},
|
||||
currentSortDirection() {
|
||||
return this.isAscending ? SORT_ASC : SORT_DESC;
|
||||
},
|
||||
|
|
@ -36,9 +46,7 @@ export default {
|
|||
return `${this.currentSortOption}_${this.currentSortDirection}`;
|
||||
},
|
||||
currentSortText() {
|
||||
const currentSort = this.$options.sortOptions.find(
|
||||
(sort) => sort.value === this.currentSortOption,
|
||||
);
|
||||
const currentSort = this.sortOptions.find((sort) => sort.value === this.currentSortOption);
|
||||
return currentSort.text;
|
||||
},
|
||||
},
|
||||
|
|
@ -61,7 +69,7 @@ export default {
|
|||
this.currentSortOption = sortingItem;
|
||||
},
|
||||
},
|
||||
sortOptions: [
|
||||
defaultSortOptions: [
|
||||
{ value: SORT_OPTION_RELEASED, text: __('Released at') },
|
||||
{ value: SORT_OPTION_CREATED, text: __('Created at') },
|
||||
{ value: SORT_OPTION_STAR_COUNT, text: __('Star count') },
|
||||
|
|
@ -79,7 +87,7 @@ export default {
|
|||
<gl-sorting
|
||||
:is-ascending="isAscending"
|
||||
:text="currentSortText"
|
||||
:sort-options="$options.sortOptions"
|
||||
:sort-options="sortOptions"
|
||||
:sort-by="currentSortOption"
|
||||
data-testid="catalog-sorting-option-button"
|
||||
@sortByChange="setSelectedSortOption"
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import {
|
|||
GlAvatar,
|
||||
GlBadge,
|
||||
GlButton,
|
||||
GlIcon,
|
||||
GlLink,
|
||||
GlSprintf,
|
||||
GlTooltipDirective,
|
||||
|
|
@ -14,6 +15,7 @@ import { formatDate, getTimeago } from '~/lib/utils/datetime_utility';
|
|||
import { toNounSeriesText } from '~/lib/utils/grammar';
|
||||
import { cleanLeadingSeparator } from '~/lib/utils/url_utility';
|
||||
import Markdown from '~/vue_shared/components/markdown/non_gfm_markdown.vue';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import { CI_RESOURCE_DETAILS_PAGE_NAME } from '../../router/constants';
|
||||
import { VERIFICATION_LEVEL_UNVERIFIED } from '../../constants';
|
||||
import CiVerificationBadge from '../shared/ci_verification_badge.vue';
|
||||
|
|
@ -29,6 +31,7 @@ export default {
|
|||
GlAvatar,
|
||||
GlBadge,
|
||||
GlButton,
|
||||
GlIcon,
|
||||
GlLink,
|
||||
GlSprintf,
|
||||
GlTruncate,
|
||||
|
|
@ -37,6 +40,7 @@ export default {
|
|||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
mixins: [glFeatureFlagMixin()],
|
||||
props: {
|
||||
resource: {
|
||||
type: Object,
|
||||
|
|
@ -86,6 +90,9 @@ export default {
|
|||
hasReleasedVersion() {
|
||||
return Boolean(this.latestVersion?.createdAt);
|
||||
},
|
||||
isPopularityFeatureEnabled() {
|
||||
return Boolean(this.glFeatures?.ciCatalogPopularity);
|
||||
},
|
||||
isVerified() {
|
||||
return this.resource?.verificationLevel !== VERIFICATION_LEVEL_UNVERIFIED;
|
||||
},
|
||||
|
|
@ -107,6 +114,14 @@ export default {
|
|||
starCountText() {
|
||||
return n__('Star', 'Stars', this.starCount);
|
||||
},
|
||||
usageCount() {
|
||||
return this.resource?.last30DayUsageCount || 0;
|
||||
},
|
||||
usageText() {
|
||||
return s__(
|
||||
'CiCatalog|The number of projects that used a component from this project in a pipeline, by using "include:component", in the last 30 days.',
|
||||
);
|
||||
},
|
||||
webPath() {
|
||||
return cleanLeadingSeparator(this.resource?.webPath);
|
||||
},
|
||||
|
|
@ -170,18 +185,31 @@ export default {
|
|||
<gl-badge size="sm" class="gl-h-5 gl-align-self-center" variant="info">{{
|
||||
name
|
||||
}}</gl-badge>
|
||||
<gl-button
|
||||
v-gl-tooltip.top
|
||||
data-testid="stats-favorites"
|
||||
class="!gl-text-inherit"
|
||||
icon="star-o"
|
||||
:title="starCountText"
|
||||
:href="starsHref"
|
||||
size="small"
|
||||
variant="link"
|
||||
>
|
||||
{{ starCount }}
|
||||
</gl-button>
|
||||
<div class="gl-display-flex gl-align-items-center gl-ml-3">
|
||||
<div
|
||||
v-if="isPopularityFeatureEnabled"
|
||||
v-gl-tooltip.top
|
||||
class="gl-display-flex gl-align-items-center gl-mr-3"
|
||||
:title="usageText"
|
||||
>
|
||||
<gl-icon name="chart" :size="16" />
|
||||
<span class="gl-ml-2" data-testid="stats-usage">
|
||||
{{ usageCount }}
|
||||
</span>
|
||||
</div>
|
||||
<gl-button
|
||||
v-gl-tooltip.top
|
||||
data-testid="stats-favorites"
|
||||
class="!gl-text-inherit"
|
||||
icon="star-o"
|
||||
:title="starCountText"
|
||||
:href="starsHref"
|
||||
size="small"
|
||||
variant="link"
|
||||
>
|
||||
{{ starCount }}
|
||||
</gl-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export const SCOPE = {
|
|||
};
|
||||
|
||||
export const SORT_OPTION_CREATED = 'CREATED';
|
||||
export const SORT_OPTION_POPULARITY = 'USAGE_COUNT';
|
||||
export const SORT_OPTION_RELEASED = 'LATEST_RELEASED_AT';
|
||||
export const SORT_OPTION_STAR_COUNT = 'STAR_COUNT';
|
||||
export const SORT_ASC = 'ASC';
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ fragment CatalogResourceFields on CiCatalogResource {
|
|||
icon
|
||||
name
|
||||
starCount
|
||||
last30DayUsageCount
|
||||
starrersPath
|
||||
verificationLevel
|
||||
versions(first: 1) {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import deletePipelineScheduleMutation from '../graphql/mutations/delete_pipeline
|
|||
import playPipelineScheduleMutation from '../graphql/mutations/play_pipeline_schedule.mutation.graphql';
|
||||
import takeOwnershipMutation from '../graphql/mutations/take_ownership.mutation.graphql';
|
||||
import getPipelineSchedulesQuery from '../graphql/queries/get_pipeline_schedules.query.graphql';
|
||||
import { ALL_SCOPE, SCHEDULES_PER_PAGE, DEFAULT_SORT_VALUE } from '../constants';
|
||||
import { ALL_SCOPE, SCHEDULES_PER_PAGE } from '../constants';
|
||||
import PipelineSchedulesTable from './table/pipeline_schedules_table.vue';
|
||||
import TakeOwnershipModal from './take_ownership_modal.vue';
|
||||
import DeletePipelineScheduleModal from './delete_pipeline_schedule_modal.vue';
|
||||
|
|
@ -90,7 +90,6 @@ export default {
|
|||
// we need to ensure we send null to the API when
|
||||
// the scope is 'ALL'
|
||||
status: this.scope === ALL_SCOPE ? null : this.scope,
|
||||
sortValue: this.sortValue,
|
||||
first: this.pagination.first,
|
||||
last: this.pagination.last,
|
||||
prevPageCursor: this.pagination.prevPageCursor,
|
||||
|
|
@ -129,9 +128,6 @@ export default {
|
|||
playSuccess: false,
|
||||
errorMessage: '',
|
||||
scheduleId: null,
|
||||
sortValue: DEFAULT_SORT_VALUE,
|
||||
sortBy: 'ID',
|
||||
sortDesc: true,
|
||||
showDeleteModal: false,
|
||||
showTakeOwnershipModal: false,
|
||||
count: 0,
|
||||
|
|
@ -336,13 +332,6 @@ export default {
|
|||
};
|
||||
}
|
||||
},
|
||||
onUpdateSorting(sortValue, sortBy, sortDesc) {
|
||||
this.sortValue = sortValue;
|
||||
this.sortBy = sortBy;
|
||||
this.sortDesc = sortDesc;
|
||||
|
||||
this.resetPagination();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -408,12 +397,9 @@ export default {
|
|||
<pipeline-schedules-table
|
||||
:schedules="schedules.list"
|
||||
:current-user="schedules.currentUser"
|
||||
:sort-by="sortBy"
|
||||
:sort-desc="sortDesc"
|
||||
@showTakeOwnershipModal="setTakeOwnershipModal"
|
||||
@showDeleteModal="setDeleteModal"
|
||||
@playPipelineSchedule="playPipelineSchedule"
|
||||
@update-sorting="onUpdateSorting"
|
||||
/>
|
||||
|
||||
<gl-pagination
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<script>
|
||||
import { GlTable } from '@gitlab/ui';
|
||||
import { s__ } from '~/locale';
|
||||
import { TH_DESCRIPTION_TEST_ID, TH_TARGET_TEST_ID, TH_NEXT_TEST_ID } from '../../constants';
|
||||
import PipelineScheduleActions from './cells/pipeline_schedule_actions.vue';
|
||||
import PipelineScheduleLastPipeline from './cells/pipeline_schedule_last_pipeline.vue';
|
||||
import PipelineScheduleNextRun from './cells/pipeline_schedule_next_run.vue';
|
||||
|
|
@ -15,21 +14,15 @@ export default {
|
|||
fields: [
|
||||
{
|
||||
key: 'description',
|
||||
actualSortKey: 'DESCRIPTION',
|
||||
label: s__('PipelineSchedules|Description'),
|
||||
thClass: 'gl-border-t-none!',
|
||||
columnClass: 'gl-w-8/20',
|
||||
sortable: true,
|
||||
thAttr: TH_DESCRIPTION_TEST_ID,
|
||||
},
|
||||
{
|
||||
key: 'target',
|
||||
actualSortKey: 'REF',
|
||||
sortable: true,
|
||||
label: s__('PipelineSchedules|Target'),
|
||||
thClass: 'gl-border-t-none!',
|
||||
columnClass: 'gl-w-2/20',
|
||||
thAttr: TH_TARGET_TEST_ID,
|
||||
},
|
||||
{
|
||||
key: 'pipeline',
|
||||
|
|
@ -39,12 +32,9 @@ export default {
|
|||
},
|
||||
{
|
||||
key: 'next',
|
||||
actualSortKey: 'NEXT_RUN_AT',
|
||||
label: s__('PipelineSchedules|Next Run'),
|
||||
thClass: 'gl-border-t-none!',
|
||||
columnClass: 'gl-w-3/20',
|
||||
sortable: true,
|
||||
thAttr: TH_NEXT_TEST_ID,
|
||||
},
|
||||
{
|
||||
key: 'owner',
|
||||
|
|
@ -76,24 +66,6 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
sortBy: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
sortDesc: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
fetchSortedData({ sortBy, sortDesc }) {
|
||||
const field = this.$options.fields.find(({ key }) => key === sortBy);
|
||||
const sortingDirection = sortDesc ? 'DESC' : 'ASC';
|
||||
|
||||
if (!field?.actualSortKey) return;
|
||||
|
||||
this.$emit('update-sorting', `${field.actualSortKey}_${sortingDirection}`, sortBy, sortDesc);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -104,11 +76,8 @@ export default {
|
|||
:items="schedules"
|
||||
:tbody-tr-attr="{ 'data-testid': 'pipeline-schedule-table-row' }"
|
||||
:empty-text="$options.i18n.emptyText"
|
||||
:sort-by="sortBy"
|
||||
:sort-desc="sortDesc"
|
||||
show-empty
|
||||
stacked="md"
|
||||
@sort-changed="fetchSortedData"
|
||||
>
|
||||
<template #table-colgroup="{ fields }">
|
||||
<col v-for="field in fields" :key="field.key" :class="field.columnClass" />
|
||||
|
|
|
|||
|
|
@ -2,8 +2,3 @@ export const VARIABLE_TYPE = 'ENV_VAR';
|
|||
export const FILE_TYPE = 'FILE';
|
||||
export const ALL_SCOPE = 'ALL';
|
||||
export const SCHEDULES_PER_PAGE = 50;
|
||||
export const DEFAULT_SORT_VALUE = 'ID_DESC';
|
||||
|
||||
export const TH_DESCRIPTION_TEST_ID = { 'data-testid': 'pipeline-schedules-description-sort' };
|
||||
export const TH_TARGET_TEST_ID = { 'data-testid': 'pipeline-schedules-target-sort' };
|
||||
export const TH_NEXT_TEST_ID = { 'data-testid': 'pipeline-schedules-next-sort' };
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ query getPipelineSchedulesQuery(
|
|||
$projectPath: ID!
|
||||
$status: PipelineScheduleStatus
|
||||
$ids: [ID!] = null
|
||||
$sortValue: PipelineScheduleSort
|
||||
$first: Int
|
||||
$last: Int
|
||||
$prevPageCursor: String = ""
|
||||
|
|
@ -22,7 +21,6 @@ query getPipelineSchedulesQuery(
|
|||
pipelineSchedules(
|
||||
status: $status
|
||||
ids: $ids
|
||||
sort: $sortValue
|
||||
first: $first
|
||||
last: $last
|
||||
after: $nextPageCursor
|
||||
|
|
|
|||
|
|
@ -22,6 +22,11 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
containerName: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
namespace: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
|
@ -49,6 +54,7 @@ export default {
|
|||
configuration: this.k8sAccessConfiguration,
|
||||
namespace: this.namespace,
|
||||
podName: this.podName,
|
||||
containerName: this.containerName,
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
|
|
@ -98,9 +104,17 @@ export default {
|
|||
isLoading() {
|
||||
return this.$apollo.queries.k8sLogs.loading || this.$apollo.queries.environment.loading;
|
||||
},
|
||||
emptyStateTitle() {
|
||||
return this.containerName
|
||||
? this.$options.i18n.emptyStateTitleForContainer
|
||||
: this.$options.i18n.emptyStateTitleForPod;
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
emptyStateTitle: s__('KubernetesLogs|No logs available for pod %{podName}'),
|
||||
emptyStateTitleForPod: s__('KubernetesLogs|No logs available for pod %{podName}'),
|
||||
emptyStateTitleForContainer: s__(
|
||||
'KubernetesLogs|No logs available for container %{containerName} of pod %{podName}',
|
||||
),
|
||||
},
|
||||
EmptyStateSvg,
|
||||
};
|
||||
|
|
@ -117,8 +131,9 @@ export default {
|
|||
<gl-empty-state v-else :svg-path="$options.EmptyStateSvg">
|
||||
<template #title>
|
||||
<h3>
|
||||
<gl-sprintf :message="$options.i18n.emptyStateTitle"
|
||||
><template #podName>{{ podName }}</template>
|
||||
<gl-sprintf :message="emptyStateTitle"
|
||||
><template #podName>{{ podName }}</template
|
||||
><template #containerName>{{ containerName }}</template>
|
||||
</gl-sprintf>
|
||||
</h3>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,15 @@
|
|||
#import "~/kubernetes_dashboard/graphql/queries/workload_item.fragment.graphql"
|
||||
|
||||
query getK8sLogs($configuration: LocalConfiguration, $namespace: String, $podName: String) {
|
||||
k8sLogs(configuration: $configuration, namespace: $namespace, podName: $podName) @client
|
||||
query getK8sLogs(
|
||||
$configuration: LocalConfiguration
|
||||
$namespace: String
|
||||
$podName: String
|
||||
$containerName: String
|
||||
) {
|
||||
k8sLogs(
|
||||
configuration: $configuration
|
||||
namespace: $namespace
|
||||
podName: $podName
|
||||
containerName: $containerName
|
||||
) @client
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,16 +47,19 @@ class LogsCacheWrapper {
|
|||
}
|
||||
}
|
||||
|
||||
export const k8sLogs = (_, { configuration, namespace, podName }, { client }) => {
|
||||
export const k8sLogs = (_, { configuration, namespace, podName, containerName }, { client }) => {
|
||||
const config = new Configuration(configuration);
|
||||
const watchApi = new WatchApi(config);
|
||||
const watchPath = buildWatchPath({ resource: podName, namespace });
|
||||
|
||||
const variables = { configuration, namespace, podName };
|
||||
const variables = { configuration, namespace, podName, containerName };
|
||||
const cacheWrapper = new LogsCacheWrapper(client, variables);
|
||||
|
||||
const watchQuery = { follow: true };
|
||||
if (containerName) watchQuery.container = containerName;
|
||||
|
||||
watchApi
|
||||
.subscribeToStream(watchPath, { follow: true })
|
||||
.subscribeToStream(watchPath, watchQuery)
|
||||
.then((watcher) => {
|
||||
watcher.on(EVENT_PLAIN_TEXT, (data) => {
|
||||
const logsData = cacheWrapper.readLogsData();
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ export const initPage = async () => {
|
|||
},
|
||||
component: () => import('./environment_details/components/kubernetes/kubernetes_logs.vue'),
|
||||
props: (route) => ({
|
||||
containerName: route.query.container,
|
||||
podName: route.params.podName,
|
||||
namespace: route.params.namespace,
|
||||
environmentName: dataSet.name,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
<script>
|
||||
import { GlAlert, GlButton, GlSprintf, GlLink } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
|
||||
export default {
|
||||
name: 'WebIdeError',
|
||||
components: {
|
||||
GlAlert,
|
||||
GlButton,
|
||||
GlSprintf,
|
||||
GlLink,
|
||||
},
|
||||
props: {
|
||||
signOutPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
reload: () => window.location.reload(),
|
||||
},
|
||||
i18n: {
|
||||
title: __('Failed to load the Web IDE'),
|
||||
message: __(
|
||||
'For more information, see the developer console. Try to reload the page or sign out and in again. If the issue persists, %{reportIssueStart}report a problem%{reportIssueEnd}.',
|
||||
),
|
||||
primaryButtonText: __('Reload'),
|
||||
secondaryButtonText: __('Sign out'),
|
||||
},
|
||||
REPORT_ISSUE_URL: helpPagePath('user/project/web_ide/index', { anchor: '#report-a-problem' }),
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="gl-max-w-80 m-auto gl-pt-6">
|
||||
<gl-alert variant="danger" :dismissible="false" :title="$options.i18n.title">
|
||||
<gl-sprintf :message="$options.i18n.message">
|
||||
<template #reportIssue="{ content }">
|
||||
<gl-link :href="$options.REPORT_ISSUE_URL" target="_blank">{{ content }}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
<template #actions>
|
||||
<gl-button variant="confirm" category="primary" @click="reload">{{
|
||||
$options.i18n.primaryButtonText
|
||||
}}</gl-button>
|
||||
<gl-button category="secondary" class="gl-ml-3" data-method="post" :href="signOutPath">
|
||||
{{ $options.i18n.secondaryButtonText }}
|
||||
</gl-button>
|
||||
</template>
|
||||
</gl-alert>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -13,6 +13,7 @@ import {
|
|||
handleTracking,
|
||||
} from './lib/gitlab_web_ide';
|
||||
import { GITLAB_WEB_IDE_FEEDBACK_ISSUE } from './constants';
|
||||
import { renderWebIdeError } from './render_web_ide_error';
|
||||
|
||||
const buildRemoteIdeURL = (ideRemotePath, remoteHost, remotePathArg) => {
|
||||
const remotePath = cleanLeadingSeparator(remotePathArg);
|
||||
|
|
@ -55,6 +56,7 @@ export const initGitlabWebIDE = async (el) => {
|
|||
editorFont: editorFontJSON,
|
||||
codeSuggestionsEnabled,
|
||||
extensionsGallerySettings: extensionsGallerySettingsJSON,
|
||||
signOutPath,
|
||||
} = el.dataset;
|
||||
|
||||
const rootEl = setupRootElement(el);
|
||||
|
|
@ -75,54 +77,58 @@ export const initGitlabWebIDE = async (el) => {
|
|||
'X-Requested-With': 'XMLHttpRequest',
|
||||
};
|
||||
|
||||
// See ClientOnlyConfig https://gitlab.com/gitlab-org/gitlab-web-ide/-/blob/main/packages/web-ide-types/src/config.ts#L17
|
||||
start(rootEl, {
|
||||
...getBaseConfig(),
|
||||
nonce,
|
||||
httpHeaders,
|
||||
auth: oauthConfig,
|
||||
projectPath,
|
||||
ref,
|
||||
filePath,
|
||||
mrId,
|
||||
mrTargetProject: getMRTargetProject(),
|
||||
forkInfo,
|
||||
username: gon.current_username,
|
||||
links: {
|
||||
feedbackIssue: GITLAB_WEB_IDE_FEEDBACK_ISSUE,
|
||||
userPreferences: el.dataset.userPreferencesPath,
|
||||
signIn: el.dataset.signInPath,
|
||||
},
|
||||
featureFlags: {
|
||||
settingsSync: true,
|
||||
crossOriginExtensionHost: getCrossOriginExtensionHostFlagValue(extensionsGallerySettings),
|
||||
},
|
||||
editorFont,
|
||||
extensionsGallerySettings,
|
||||
codeSuggestionsEnabled,
|
||||
handleTracking,
|
||||
// See https://gitlab.com/gitlab-org/gitlab-web-ide/-/blob/main/packages/web-ide-types/src/config.ts#L86
|
||||
telemetryEnabled: Tracking.enabled(),
|
||||
async handleStartRemote({ remoteHost, remotePath, connectionToken }) {
|
||||
const confirmed = await confirmAction(
|
||||
__('Are you sure you want to leave the Web IDE? All unsaved changes will be lost.'),
|
||||
{
|
||||
primaryBtnText: __('Start remote connection'),
|
||||
cancelBtnText: __('Continue editing'),
|
||||
},
|
||||
);
|
||||
try {
|
||||
// See ClientOnlyConfig https://gitlab.com/gitlab-org/gitlab-web-ide/-/blob/main/packages/web-ide-types/src/config.ts#L17
|
||||
await start(rootEl, {
|
||||
...getBaseConfig(),
|
||||
nonce,
|
||||
httpHeaders,
|
||||
auth: oauthConfig,
|
||||
projectPath,
|
||||
ref,
|
||||
filePath,
|
||||
mrId,
|
||||
mrTargetProject: getMRTargetProject(),
|
||||
forkInfo,
|
||||
username: gon.current_username,
|
||||
links: {
|
||||
feedbackIssue: GITLAB_WEB_IDE_FEEDBACK_ISSUE,
|
||||
userPreferences: el.dataset.userPreferencesPath,
|
||||
signIn: el.dataset.signInPath,
|
||||
},
|
||||
featureFlags: {
|
||||
settingsSync: true,
|
||||
crossOriginExtensionHost: getCrossOriginExtensionHostFlagValue(extensionsGallerySettings),
|
||||
},
|
||||
editorFont,
|
||||
extensionsGallerySettings,
|
||||
codeSuggestionsEnabled,
|
||||
handleTracking,
|
||||
// See https://gitlab.com/gitlab-org/gitlab-web-ide/-/blob/main/packages/web-ide-types/src/config.ts#L86
|
||||
telemetryEnabled: Tracking.enabled(),
|
||||
async handleStartRemote({ remoteHost, remotePath, connectionToken }) {
|
||||
const confirmed = await confirmAction(
|
||||
__('Are you sure you want to leave the Web IDE? All unsaved changes will be lost.'),
|
||||
{
|
||||
primaryBtnText: __('Start remote connection'),
|
||||
cancelBtnText: __('Continue editing'),
|
||||
},
|
||||
);
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
createAndSubmitForm({
|
||||
url: buildRemoteIdeURL(ideRemotePath, remoteHost, remotePath),
|
||||
data: {
|
||||
connection_token: connectionToken,
|
||||
return_url: window.location.href,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
createAndSubmitForm({
|
||||
url: buildRemoteIdeURL(ideRemotePath, remoteHost, remotePath),
|
||||
data: {
|
||||
connection_token: connectionToken,
|
||||
return_url: window.location.href,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
renderWebIdeError({ error, signOutPath });
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
import Vue from 'vue';
|
||||
import { logError } from '~/lib/logger';
|
||||
import WebIdeError from '~/ide/components/web_ide_error.vue';
|
||||
import * as Sentry from '~/sentry/sentry_browser_wrapper';
|
||||
|
||||
export function renderWebIdeError({ error, signOutPath }) {
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
logError('Failed to load Web IDE', error);
|
||||
Sentry.captureException(error);
|
||||
|
||||
const alertContainer = document.querySelector('.flash-container');
|
||||
if (!alertContainer) return null;
|
||||
|
||||
const el = document.createElement('div');
|
||||
alertContainer.appendChild(el);
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
render(createElement) {
|
||||
return createElement(WebIdeError, { props: { signOutPath } });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -107,7 +107,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="gl-border-t">
|
||||
<refs-list
|
||||
v-if="hasBranches"
|
||||
:has-containing-refs="hasContainingBranches"
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ref-list gl-p-5 gl-border-b-solid gl-border-b-1">
|
||||
<div class="well-segment">
|
||||
<gl-icon :name="refIcon" :size="14" class="gl-ml-2 gl-mr-3" />
|
||||
<span data-testid="title" class="gl-mr-2">{{ namespace }}</span>
|
||||
<gl-badge
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ export default {
|
|||
href: chunk,
|
||||
class: 'gl-reset-color! gl-text-decoration-underline',
|
||||
rel: 'nofollow noopener noreferrer', // eslint-disable-line @gitlab/require-i18n-strings
|
||||
target: '_blank',
|
||||
},
|
||||
},
|
||||
chunk,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,6 @@
|
|||
* an opening or closing square bracket
|
||||
* [[\]]
|
||||
*/
|
||||
export const linkRegex = /(https?:\/\/[^"<>()\\^`{|}\s]+[^"<>()\\^`{|}\s.,:;!?])(?<![[\]])/g;
|
||||
export const linkRegex = /(https?:\/\/[^"'<>()\\^`{|}\s]+[^"'<>()\\^`{|}\s.,:;!?])(?<![[\]])/g;
|
||||
|
||||
export default { linkRegex };
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ module Explore
|
|||
feature_category :pipeline_composition
|
||||
before_action :check_resource_access, only: :show
|
||||
track_internal_event :index, name: 'unique_users_visiting_ci_catalog', conditions: :current_user
|
||||
before_action do
|
||||
push_frontend_feature_flag(:ci_catalog_popularity)
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ module Groups
|
|||
feature_category :package_registry
|
||||
urgency :low
|
||||
|
||||
before_action :set_feature_flag_packages_protected_packages, only: :show
|
||||
|
||||
# The show action renders index to allow frontend routing to work on page refresh
|
||||
def show
|
||||
render :index
|
||||
|
|
@ -17,5 +19,9 @@ module Groups
|
|||
def verify_packages_enabled!
|
||||
render_404 unless group.packages_feature_enabled?
|
||||
end
|
||||
|
||||
def set_feature_flag_packages_protected_packages
|
||||
push_frontend_feature_flag(:packages_protected_packages, group)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ module IdeHelper
|
|||
'use-new-web-ide' => use_new_web_ide?.to_s,
|
||||
'new-web-ide-help-page-path' => help_page_path('user/project/web_ide/index', anchor: 'vscode-reimplementation'),
|
||||
'sign-in-path' => new_session_path(current_user),
|
||||
'sign-out-path' => destroy_user_session_path,
|
||||
'user-preferences-path' => profile_preferences_path
|
||||
}.merge(use_new_web_ide? ? new_ide_data(project: project) : legacy_ide_data(project: project))
|
||||
|
||||
|
|
|
|||
|
|
@ -583,23 +583,15 @@ class Commit
|
|||
end
|
||||
|
||||
def branches_containing(limit: 0, exclude_tipped: false)
|
||||
# WARNING: This argument can be confusing, if there is a limit.
|
||||
# for example set the limit to 5 and in the 5 out a total of 25 refs there is 2 tipped refs,
|
||||
# then the method will only 3 refs, even though there is more.
|
||||
excluded = exclude_tipped ? tipping_branches : []
|
||||
|
||||
refs = repository.branch_names_contains(id, limit: limit) || []
|
||||
refs - excluded
|
||||
repository.branch_names_contains(id, limit: limit, exclude_refs: excluded) || []
|
||||
end
|
||||
|
||||
def tags_containing(limit: 0, exclude_tipped: false)
|
||||
# WARNING: This argument can be confusing, if there is a limit.
|
||||
# for example set the limit to 5 and in the 5 out a total of 25 refs there is 2 tipped refs,
|
||||
# then the method will only 3 refs, even though there is more.
|
||||
excluded = exclude_tipped ? tipping_tags : []
|
||||
|
||||
refs = repository.tag_names_contains(id, limit: limit) || []
|
||||
refs - excluded
|
||||
repository.tag_names_contains(id, limit: limit, exclude_refs: excluded) || []
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ module HasUserType
|
|||
scope :without_ghosts, -> { where(user_type: USER_TYPES.keys - ['ghost']) }
|
||||
scope :without_project_bot, -> { where(user_type: USER_TYPES.keys - ['project_bot']) }
|
||||
scope :human_or_service_user, -> { where(user_type: %i[human service_user]) }
|
||||
scope :resource_access_token_bot, -> { where(user_type: 'project_bot') }
|
||||
|
||||
validates :user_type, presence: true
|
||||
end
|
||||
|
|
|
|||
|
|
@ -787,12 +787,16 @@ class Repository
|
|||
Commit.order_by(collection: commits, order_by: order_by, sort: sort)
|
||||
end
|
||||
|
||||
def branch_names_contains(sha, limit: 0)
|
||||
raw_repository.branch_names_contains_sha(sha, limit: limit)
|
||||
def branch_names_contains(sha, limit: 0, exclude_refs: [])
|
||||
refs = raw_repository.branch_names_contains_sha(sha, limit: adjust_containing_limit(limit: limit, exclude_refs: exclude_refs))
|
||||
|
||||
adjust_containing_refs(limit: limit, refs: refs - exclude_refs)
|
||||
end
|
||||
|
||||
def tag_names_contains(sha, limit: 0)
|
||||
raw_repository.tag_names_contains_sha(sha, limit: limit)
|
||||
def tag_names_contains(sha, limit: 0, exclude_refs: [])
|
||||
refs = raw_repository.tag_names_contains_sha(sha, limit: adjust_containing_limit(limit: limit, exclude_refs: exclude_refs))
|
||||
|
||||
adjust_containing_refs(limit: limit, refs: refs - exclude_refs)
|
||||
end
|
||||
|
||||
def local_branches
|
||||
|
|
@ -1306,6 +1310,22 @@ class Repository
|
|||
|
||||
private
|
||||
|
||||
# Increase the limit by number of excluded refs
|
||||
# to prevent a situation when we return less refs than requested
|
||||
def adjust_containing_limit(limit:, exclude_refs:)
|
||||
return limit if limit == 0
|
||||
|
||||
limit + exclude_refs.size
|
||||
end
|
||||
|
||||
# Limit number of returned refs
|
||||
# in case the result has more refs than requested
|
||||
def adjust_containing_refs(limit:, refs:)
|
||||
return refs if limit == 0
|
||||
|
||||
refs.take(limit)
|
||||
end
|
||||
|
||||
def ancestor_cache_key(ancestor_id, descendant_id)
|
||||
"ancestor:#{ancestor_id}:#{descendant_id}"
|
||||
end
|
||||
|
|
|
|||
|
|
@ -607,13 +607,6 @@ class User < MainClusterwide::ApplicationRecord
|
|||
scope :with_emails, -> { preload(:emails) }
|
||||
scope :with_dashboard, ->(dashboard) { where(dashboard: dashboard) }
|
||||
scope :with_public_profile, -> { where(private_profile: false) }
|
||||
scope :with_expiring_and_not_notified_personal_access_tokens, ->(at) do
|
||||
where('EXISTS (?)', ::PersonalAccessToken
|
||||
.where('personal_access_tokens.user_id = users.id')
|
||||
.without_impersonation
|
||||
.expiring_and_not_notified(at).select(1)
|
||||
)
|
||||
end
|
||||
scope :with_personal_access_tokens_expired_today, -> do
|
||||
where('EXISTS (?)', ::PersonalAccessToken
|
||||
.select(1)
|
||||
|
|
|
|||
|
|
@ -18,4 +18,4 @@
|
|||
|
||||
- data = ide_data(project: @project, fork_info: @fork_info, params: params)
|
||||
|
||||
= render partial: 'shared/ide_root', locals: { data: data, loading_text: _('Loading the GitLab IDE') }
|
||||
= render partial: 'shared/ide_root', locals: { data: data, loading_text: _('Loading the Web IDE') }
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ module PersonalAccessTokens
|
|||
|
||||
def perform(*args)
|
||||
process_user_tokens
|
||||
process_project_access_tokens
|
||||
process_project_bot_tokens
|
||||
end
|
||||
|
||||
private
|
||||
|
|
@ -59,12 +59,14 @@ module PersonalAccessTokens
|
|||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
end
|
||||
|
||||
def process_project_access_tokens
|
||||
def process_project_bot_tokens
|
||||
# rubocop: disable CodeReuse/ActiveRecord -- We need to specify batch size to avoid timing out of worker
|
||||
notifications_delivered = 0
|
||||
project_bot_ids_without_resource = []
|
||||
project_bot_ids_with_failed_delivery = []
|
||||
loop do
|
||||
tokens = PersonalAccessToken
|
||||
.without_impersonation
|
||||
.where.not(user_id: project_bot_ids_without_resource | project_bot_ids_with_failed_delivery)
|
||||
.expiring_and_not_notified_without_impersonation
|
||||
.project_access_token
|
||||
.select(:id, :user_id)
|
||||
|
|
@ -75,37 +77,68 @@ module PersonalAccessTokens
|
|||
|
||||
bot_users = User.id_in(tokens.pluck(:user_id).uniq).with_personal_access_tokens_and_resources
|
||||
|
||||
bot_users.each do |user|
|
||||
with_context(user: user) do
|
||||
expiring_user_token = user.personal_access_tokens.first # bot user should not have more than 1 token
|
||||
bot_users.each do |project_bot|
|
||||
if project_bot.resource_bot_resource.nil?
|
||||
project_bot_ids_without_resource << project_bot.id
|
||||
|
||||
execute_web_hooks(user, expiring_user_token)
|
||||
deliver_bot_notifications(user, expiring_user_token.name)
|
||||
next
|
||||
end
|
||||
|
||||
begin
|
||||
with_context(user: project_bot) do
|
||||
# project bot does not have more than 1 token
|
||||
expiring_user_token = project_bot.personal_access_tokens.first
|
||||
|
||||
execute_web_hooks(project_bot, expiring_user_token)
|
||||
deliver_bot_notifications(project_bot, expiring_user_token.name)
|
||||
end
|
||||
rescue StandardError => e
|
||||
project_bot_ids_with_failed_delivery << project_bot.id
|
||||
|
||||
log_exception(e, project_bot)
|
||||
end
|
||||
end
|
||||
|
||||
tokens.update_all(expire_notification_delivered: true)
|
||||
notifications_delivered += tokens.count
|
||||
tokens_with_delivered_notifications =
|
||||
tokens
|
||||
.where.not(user_id: project_bot_ids_without_resource | project_bot_ids_with_failed_delivery)
|
||||
tokens_with_delivered_notifications.update_all(expire_notification_delivered: true)
|
||||
|
||||
notifications_delivered += tokens_with_delivered_notifications.count
|
||||
end
|
||||
log_extra_metadata_on_done(:total_notification_delivered_for_bot_personal_access_tokens, notifications_delivered)
|
||||
|
||||
log_extra_metadata_on_done(
|
||||
:total_notification_delivered_for_resource_access_tokens, notifications_delivered)
|
||||
log_extra_metadata_on_done(
|
||||
:total_resource_bot_without_membership, project_bot_ids_without_resource.count)
|
||||
log_extra_metadata_on_done(
|
||||
:total_failed_notifications_for_resource_bots, project_bot_ids_with_failed_delivery.count)
|
||||
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
end
|
||||
|
||||
def deliver_bot_notifications(bot_user, token_name)
|
||||
notification_service.bot_resource_access_token_about_to_expire(bot_user, token_name)
|
||||
|
||||
Gitlab::AppLogger.info(
|
||||
message: "Notifying Bot User resource owners about expiring tokens",
|
||||
class: self.class,
|
||||
user_id: bot_user.id
|
||||
)
|
||||
end
|
||||
|
||||
def deliver_user_notifications(user, token_names)
|
||||
notification_service.access_token_about_to_expire(user, token_names)
|
||||
log_info("Notifying User about expiring tokens", user)
|
||||
end
|
||||
|
||||
def log_info(message_text, user)
|
||||
Gitlab::AppLogger.info(
|
||||
message: "Notifying User about expiring tokens",
|
||||
message: message_text,
|
||||
class: self.class,
|
||||
user_id: user.id
|
||||
)
|
||||
end
|
||||
|
||||
def log_exception(ex, user)
|
||||
Gitlab::AppLogger.error(
|
||||
message: 'Failed to send notification about expiring resource access tokens',
|
||||
'exception.message': ex.message,
|
||||
'exception.class': ex.class.name,
|
||||
class: self.class,
|
||||
user_id: user.id
|
||||
)
|
||||
|
|
@ -114,6 +147,7 @@ module PersonalAccessTokens
|
|||
def execute_web_hooks(bot_user, token)
|
||||
resource = bot_user.resource_bot_resource
|
||||
|
||||
return unless resource
|
||||
return if resource.is_a?(Project) && !resource.has_active_hooks?(:resource_access_token_hooks)
|
||||
|
||||
hook_data = Gitlab::DataBuilder::ResourceAccessToken.build(token, :expiring, resource)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
name: ci_job_artifacts_use_primary_to_authenticate
|
||||
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/466138
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/155684
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/466145
|
||||
milestone: '17.1'
|
||||
group: group::pipeline execution
|
||||
type: gitlab_com_derisk
|
||||
default_enabled: false
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: ci_catalog_popularity
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/issues/434333
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/464593
|
||||
milestone: '17.1'
|
||||
type: wip
|
||||
group: group::pipeline authoring
|
||||
default_enabled: false
|
||||
|
|
@ -1051,6 +1051,21 @@ This file is located at:
|
|||
- `/var/log/gitlab/gitlab-rails/epic_work_item_sync.log` on Linux package installations.
|
||||
- `/home/git/gitlab/log/epic_work_item_sync.log` on self-compiled installations.
|
||||
|
||||
## `secret_detection.log`
|
||||
|
||||
DETAILS:
|
||||
**Tier:** Ultimate
|
||||
**Offering:** GitLab Dedicated
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/137812) in GitLab 16.7.
|
||||
|
||||
The `secret_detection.log` file logs information related to [Secret Push Protection](../../user/application_security/secret_detection/secret_push_protection/index.md) feature.
|
||||
|
||||
This file is located at:
|
||||
|
||||
- `/var/log/gitlab/gitlab-rails/secret_detection.log` on Linux package installations.
|
||||
- `/home/git/gitlab/log/secret_detection.log` on self-compiled installations.
|
||||
|
||||
## Registry logs
|
||||
|
||||
For Linux package installations, container registry logs are in `/var/log/gitlab/registry/current`.
|
||||
|
|
|
|||
|
|
@ -250,7 +250,7 @@ When reviewing code changes, you can hide inline comments:
|
|||
1. Below the title, select **Changes**.
|
||||
1. Scroll to the file that contains the comments you want to hide.
|
||||
1. Scroll to the line the comment is attached to, and select **Collapse** (**{collapse}**):
|
||||

|
||||

|
||||
|
||||
To expand inline comments and show them again:
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 9.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.9 KiB |
|
|
@ -280,3 +280,15 @@ As a workaround:
|
|||
or modify the `"editor.fontFamily"` setting.
|
||||
|
||||
For more information, see [VS Code issue 80170](https://github.com/microsoft/vscode/issues/80170).
|
||||
|
||||
### Report a problem
|
||||
|
||||
To report a problem, [create a new issue](https://gitlab.com/gitlab-org/gitlab-web-ide/-/issues/new)
|
||||
with the following information:
|
||||
|
||||
- The error message
|
||||
- The full error details
|
||||
- How often the problem occurs
|
||||
- Steps to reproduce the problem
|
||||
|
||||
If you're on a paid tier, you can also [contact Support](https://about.gitlab.com/support/#contact-support) for help.
|
||||
|
|
|
|||
|
|
@ -100,8 +100,13 @@ module API
|
|||
job
|
||||
end
|
||||
|
||||
def authenticate_job_via_dependent_job!
|
||||
authenticate!
|
||||
def authenticate_job_via_dependent_job!(use_primary_to_authenticate: false)
|
||||
if use_primary_to_authenticate
|
||||
::Gitlab::Database::LoadBalancing::Session.current.use_primary { authenticate! }
|
||||
else
|
||||
authenticate!
|
||||
end
|
||||
|
||||
forbidden! unless current_job
|
||||
forbidden! unless can?(current_user, :read_build, current_job)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -385,7 +385,8 @@ module API
|
|||
end
|
||||
route_setting :authentication, job_token_allowed: true
|
||||
get '/:id/artifacts', feature_category: :build_artifacts do
|
||||
authenticate_job_via_dependent_job!
|
||||
use_primary = Feature.enabled?(:ci_job_artifacts_use_primary_to_authenticate, current_job&.project)
|
||||
authenticate_job_via_dependent_job!(use_primary_to_authenticate: use_primary)
|
||||
|
||||
audit_download(current_job, current_job.artifacts_file&.filename) if current_job.artifacts_file
|
||||
present_artifacts_file!(current_job.artifacts_file, supports_direct_download: params[:direct_download])
|
||||
|
|
|
|||
|
|
@ -9368,7 +9368,7 @@ msgstr ""
|
|||
msgid "Branches|Please type the following to confirm:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Branches|Plese type the following to confirm: %{codeStart}delete%{codeEnd}."
|
||||
msgid "Branches|Please type the following to confirm: %{codeStart}delete%{codeEnd}."
|
||||
msgstr ""
|
||||
|
||||
msgid "Branches|See all branch-related settings together with branch rules"
|
||||
|
|
@ -11129,6 +11129,9 @@ msgstr ""
|
|||
msgid "CiCatalog|Set component project as a CI/CD Catalog project. %{linkStart}What is the CI/CD Catalog?%{linkEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "CiCatalog|The number of projects that used a component from this project in a pipeline, by using \"include:component\", in the last 30 days."
|
||||
msgstr ""
|
||||
|
||||
msgid "CiCatalog|The project and any released versions will be removed from the CI/CD Catalog. If you re-enable this toggle, the project's existing releases are not re-added to the catalog. You must %{linkStart}create a new release%{linkEnd}."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -21838,6 +21841,9 @@ msgstr ""
|
|||
msgid "Failed to load stacktrace."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to load the Web IDE"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to make repository read-only: %{reason}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -22547,6 +22553,9 @@ msgstr ""
|
|||
msgid "For more information, see the File Hooks documentation."
|
||||
msgstr ""
|
||||
|
||||
msgid "For more information, see the developer console. Try to reload the page or sign out and in again. If the issue persists, %{reportIssueStart}report a problem%{reportIssueEnd}."
|
||||
msgstr ""
|
||||
|
||||
msgid "For the GitLab Team to keep your subscription data up to date, this is a reminder to report your license usage on a monthly basis, or at the cadence set in your agreement with GitLab. This allows us to simplify the billing process for overages and renewals. To report your usage data, export your license usage file and email it to %{renewal_service_email}. If you need an updated license, GitLab will send the license to the email address registered in the %{customers_dot}, and you can upload this license to your instance."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -30161,6 +30170,9 @@ msgstr ""
|
|||
msgid "KubernetesDashboard|You can select an agent from a project's environment page."
|
||||
msgstr ""
|
||||
|
||||
msgid "KubernetesLogs|No logs available for container %{containerName} of pod %{podName}"
|
||||
msgstr ""
|
||||
|
||||
msgid "KubernetesLogs|No logs available for pod %{podName}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -31063,7 +31075,7 @@ msgstr ""
|
|||
msgid "Loading snippet"
|
||||
msgstr ""
|
||||
|
||||
msgid "Loading the GitLab IDE"
|
||||
msgid "Loading the Web IDE"
|
||||
msgstr ""
|
||||
|
||||
msgid "Loading..."
|
||||
|
|
@ -43417,6 +43429,9 @@ msgstr ""
|
|||
msgid "Release|You can edit the content later by editing the release. %{linkStart}How do I edit a release?%{linkEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Reload"
|
||||
msgstr ""
|
||||
|
||||
msgid "Reload page"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -45218,6 +45233,9 @@ msgstr ""
|
|||
msgid "Runners|Most recent failures"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Most used group runners"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Most used instance runners"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -45685,6 +45703,9 @@ msgstr ""
|
|||
msgid "Runners|Token expiry"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Top projects consuming group runners"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Top projects consuming runners"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ gem 'faker', '~> 3.4', '>= 3.4.1'
|
|||
gem 'knapsack', '~> 4.0'
|
||||
gem 'parallel_tests', '~> 4.7', '>= 4.7.1'
|
||||
gem 'rotp', '~> 6.3.0'
|
||||
gem 'parallel', '~> 1.24'
|
||||
gem 'parallel', '~> 1.25', '>= 1.25.1'
|
||||
gem 'rainbow', '~> 3.1.1'
|
||||
gem 'rspec-parameterized', '~> 1.0.2'
|
||||
gem 'octokit', '~> 8.1.0'
|
||||
|
|
|
|||
|
|
@ -237,7 +237,7 @@ GEM
|
|||
faraday (>= 1, < 3)
|
||||
sawyer (~> 0.9)
|
||||
os (1.1.4)
|
||||
parallel (1.24.0)
|
||||
parallel (1.25.1)
|
||||
parallel_tests (4.7.1)
|
||||
parallel
|
||||
parser (3.3.0.5)
|
||||
|
|
@ -399,7 +399,7 @@ DEPENDENCIES
|
|||
knapsack (~> 4.0)
|
||||
nokogiri (~> 1.16, >= 1.16.5)
|
||||
octokit (~> 8.1.0)
|
||||
parallel (~> 1.24)
|
||||
parallel (~> 1.25, >= 1.25.1)
|
||||
parallel_tests (~> 4.7, >= 4.7.1)
|
||||
pry-byebug (~> 3.10.1)
|
||||
rainbow (~> 3.1.1)
|
||||
|
|
|
|||
|
|
@ -37,56 +37,17 @@ module QA
|
|||
context "when tls is disabled" do
|
||||
where do
|
||||
{
|
||||
'using docker:20.10.23 and a personal access token' => {
|
||||
docker_client_version: 'docker:20.10.23',
|
||||
authentication_token_type: :personal_access_token,
|
||||
token_name: 'Personal Access Token',
|
||||
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/412807'
|
||||
},
|
||||
'using docker:20.10.23 and a project deploy token' => {
|
||||
docker_client_version: 'docker:20.10.23',
|
||||
authentication_token_type: :project_deploy_token,
|
||||
token_name: 'Deploy Token',
|
||||
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/412808'
|
||||
},
|
||||
'using docker:20.10.23 and a ci job token' => {
|
||||
docker_client_version: 'docker:20.10.23',
|
||||
authentication_token_type: :ci_job_token,
|
||||
token_name: 'Job Token',
|
||||
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/412809'
|
||||
},
|
||||
'using docker:23.0.6 and a personal access token' => {
|
||||
docker_client_version: 'docker:23.0.6',
|
||||
authentication_token_type: :personal_access_token,
|
||||
token_name: 'Personal Access Token',
|
||||
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/412810'
|
||||
},
|
||||
'using docker:23.0.6 and a project deploy token' => {
|
||||
docker_client_version: 'docker:23.0.6',
|
||||
authentication_token_type: :project_deploy_token,
|
||||
token_name: 'Deploy Token',
|
||||
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/412813'
|
||||
},
|
||||
'using docker:23.0.6 and a ci job token' => {
|
||||
docker_client_version: 'docker:23.0.6',
|
||||
authentication_token_type: :ci_job_token,
|
||||
token_name: 'Job Token',
|
||||
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/412814'
|
||||
},
|
||||
'using docker:24.0.1 and a personal access token' => {
|
||||
docker_client_version: 'docker:24.0.1',
|
||||
authentication_token_type: :personal_access_token,
|
||||
token_name: 'Personal Access Token',
|
||||
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/412817'
|
||||
},
|
||||
'using docker:24.0.1 and a project deploy token' => {
|
||||
docker_client_version: 'docker:24.0.1',
|
||||
authentication_token_type: :project_deploy_token,
|
||||
token_name: 'Deploy Token',
|
||||
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/412818'
|
||||
},
|
||||
'using docker:24.0.1 and a ci job token' => {
|
||||
docker_client_version: 'docker:24.0.1',
|
||||
authentication_token_type: :ci_job_token,
|
||||
token_name: 'Job Token',
|
||||
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/412819'
|
||||
|
|
@ -125,10 +86,10 @@ module QA
|
|||
file_path: '.gitlab-ci.yml',
|
||||
content: <<~YAML
|
||||
build:
|
||||
image: "#{docker_client_version}"
|
||||
image: "docker:24.0.1"
|
||||
stage: build
|
||||
services:
|
||||
- name: "#{docker_client_version}-dind"
|
||||
- name: "docker:24.0.1-dind"
|
||||
command: ["--insecure-registry=gitlab.test:5050"]
|
||||
variables:
|
||||
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
|
||||
|
|
@ -178,10 +139,10 @@ module QA
|
|||
file_path: '.gitlab-ci.yml',
|
||||
content: <<~YAML
|
||||
build:
|
||||
image: docker:23.0.6
|
||||
image: "docker:24.0.1"
|
||||
stage: build
|
||||
services:
|
||||
- name: docker:23.0.6-dind
|
||||
- name: "docker:24.0.1-dind"
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
|
|
|
|||
|
|
@ -12,8 +12,6 @@ RSpec.describe 'Issues > Real-time sidebar', :js, :with_license, feature_categor
|
|||
let(:labels_value) { find_by_testid('value-wrapper') }
|
||||
|
||||
it 'updates the assignee in real-time' do
|
||||
Capybara::Session.new(:other_session)
|
||||
|
||||
using_session :other_session do
|
||||
visit project_issue_path(project, issue)
|
||||
expect(page.find('.assignee')).to have_content 'None'
|
||||
|
|
@ -34,8 +32,6 @@ RSpec.describe 'Issues > Real-time sidebar', :js, :with_license, feature_categor
|
|||
end
|
||||
|
||||
it 'updates the label in real-time' do
|
||||
Capybara::Session.new(:other_session)
|
||||
|
||||
using_session :other_session do
|
||||
visit project_issue_path(project, issue)
|
||||
wait_for_requests
|
||||
|
|
|
|||
|
|
@ -15,10 +15,6 @@ RSpec.describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state, fe
|
|||
|
||||
it 'user sees their active sessions' do
|
||||
travel_to(Time.zone.parse('2018-03-12 09:06')) do
|
||||
Capybara::Session.new(:session1)
|
||||
Capybara::Session.new(:session2)
|
||||
Capybara::Session.new(:session3)
|
||||
|
||||
# note: headers can only be set on the non-js (aka. rack-test) driver
|
||||
using_session :session1 do
|
||||
Capybara.page.driver.header(
|
||||
|
|
@ -83,8 +79,6 @@ RSpec.describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state, fe
|
|||
end
|
||||
|
||||
it 'admin sees if the session is with admin mode', :enable_admin_mode do
|
||||
Capybara::Session.new(:admin_session)
|
||||
|
||||
using_session :admin_session do
|
||||
gitlab_sign_in(admin)
|
||||
visit user_settings_active_sessions_path
|
||||
|
|
@ -93,8 +87,6 @@ RSpec.describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state, fe
|
|||
end
|
||||
|
||||
it 'does not display admin mode text in case its not' do
|
||||
Capybara::Session.new(:admin_session)
|
||||
|
||||
using_session :admin_session do
|
||||
gitlab_sign_in(admin)
|
||||
visit user_settings_active_sessions_path
|
||||
|
|
@ -103,9 +95,6 @@ RSpec.describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state, fe
|
|||
end
|
||||
|
||||
it 'user can revoke a session', :js do
|
||||
Capybara::Session.new(:session1)
|
||||
Capybara::Session.new(:session2)
|
||||
|
||||
# set an additional session in another browser
|
||||
using_session :session2 do
|
||||
gitlab_sign_in(user)
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ exports[`Delete merged branches component Delete merged branches confirmation mo
|
|||
.
|
||||
</p>
|
||||
<p>
|
||||
Plese type the following to confirm:
|
||||
Please type the following to confirm:
|
||||
<code>
|
||||
delete
|
||||
</code>
|
||||
|
|
|
|||
|
|
@ -16,20 +16,22 @@ describe('CatalogSearch', () => {
|
|||
const findSorting = () => wrapper.findComponent(GlSorting);
|
||||
const findAllSortingItems = () => findSorting().props('sortOptions');
|
||||
|
||||
const createComponent = () => {
|
||||
wrapper = shallowMountExtended(CatalogSearch, {});
|
||||
const createComponent = ({ withCatalogPopularity = false } = {}) => {
|
||||
wrapper = shallowMountExtended(CatalogSearch, {
|
||||
provide: { glFeatures: { ciCatalogPopularity: withCatalogPopularity } },
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
describe('default UI', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('renders the search bar', () => {
|
||||
expect(findSearchBar().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('sets sorting options', () => {
|
||||
it('adds sorting options', () => {
|
||||
const sortOptionsProp = findAllSortingItems();
|
||||
expect(sortOptionsProp).toHaveLength(3);
|
||||
expect(sortOptionsProp[0].text).toBe('Released at');
|
||||
|
|
@ -40,7 +42,23 @@ describe('CatalogSearch', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('with `ci_catalog_popularity` ff turned on', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ withCatalogPopularity: true });
|
||||
});
|
||||
|
||||
it('adds the popularity option', () => {
|
||||
const sortOptionsProp = findAllSortingItems();
|
||||
expect(sortOptionsProp).toHaveLength(4);
|
||||
expect(sortOptionsProp[3].text).toBe('Popularity');
|
||||
});
|
||||
});
|
||||
|
||||
describe('search', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('passes down the search value to the search component', async () => {
|
||||
const newSearchTerm = 'cat';
|
||||
|
||||
|
|
@ -85,6 +103,10 @@ describe('CatalogSearch', () => {
|
|||
});
|
||||
|
||||
describe('sort', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
describe('when changing sort order', () => {
|
||||
it('changes the `isAscending` prop to the sorting component', async () => {
|
||||
expect(findSorting().props().isAscending).toBe(false);
|
||||
|
|
|
|||
|
|
@ -29,13 +29,18 @@ describe('CiResourcesListItem', () => {
|
|||
resource,
|
||||
};
|
||||
|
||||
const createComponent = ({ props = {} } = {}) => {
|
||||
const createComponent = ({ props = {}, withCatalogPopularity = false } = {}) => {
|
||||
wrapper = shallowMountExtended(CiResourcesListItem, {
|
||||
router,
|
||||
propsData: {
|
||||
...defaultProps,
|
||||
...props,
|
||||
},
|
||||
provide: {
|
||||
glFeatures: {
|
||||
ciCatalogPopularity: withCatalogPopularity,
|
||||
},
|
||||
},
|
||||
stubs: {
|
||||
GlSprintf,
|
||||
GlTruncate,
|
||||
|
|
@ -51,6 +56,7 @@ describe('CiResourcesListItem', () => {
|
|||
const findVerificationBadge = () => wrapper.findComponent(CiVerificationBadge);
|
||||
const findTimeAgoMessage = () => wrapper.findComponent(GlSprintf);
|
||||
const findFavorites = () => wrapper.findByTestId('stats-favorites');
|
||||
const findUsage = () => wrapper.findByTestId('stats-usage');
|
||||
const findMarkdown = () => wrapper.findComponent(Markdown);
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
@ -286,8 +292,29 @@ describe('CiResourcesListItem', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('with FF `ci_catalog_popularity` turned on', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ withCatalogPopularity: true });
|
||||
});
|
||||
|
||||
it('renders the statistics', () => {
|
||||
expect(findUsage().exists()).toBe(true);
|
||||
expect(findUsage().text()).toBe('4');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with FF `ci_catalog_popularity` disabled', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('renders the statistics', () => {
|
||||
expect(findUsage().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there are no statistics', () => {
|
||||
it('render favorites as 0', () => {
|
||||
it('render favorites and usage as 0', () => {
|
||||
createComponent({
|
||||
props: {
|
||||
resource: {
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ export const catalogResponseBody = {
|
|||
name: 'Project-42 Name',
|
||||
description: 'A simple component',
|
||||
starCount: 0,
|
||||
last30DayUsageCount: 0,
|
||||
starrersPath: '/frontend-fixtures/project-42/-/starrers',
|
||||
verificationLevel: 'UNVERIFIED',
|
||||
versions: {
|
||||
|
|
@ -111,6 +112,7 @@ export const catalogResponseBody = {
|
|||
name: 'Project-41 Name',
|
||||
description: 'A simple component',
|
||||
starCount: 0,
|
||||
last30DayUsageCount: 0,
|
||||
starrersPath: '/frontend-fixtures/project-41/-/starrers',
|
||||
verificationLevel: 'UNVERIFIED',
|
||||
versions: {
|
||||
|
|
@ -127,6 +129,7 @@ export const catalogResponseBody = {
|
|||
name: 'Project-40 Name',
|
||||
description: 'A simple component',
|
||||
starCount: 0,
|
||||
last30DayUsageCount: 0,
|
||||
starrersPath: '/frontend-fixtures/project-40/-/starrers',
|
||||
verificationLevel: 'UNVERIFIED',
|
||||
versions: {
|
||||
|
|
@ -143,6 +146,7 @@ export const catalogResponseBody = {
|
|||
name: 'Project-39 Name',
|
||||
description: 'A simple component',
|
||||
starCount: 0,
|
||||
last30DayUsageCount: 0,
|
||||
starrersPath: '/frontend-fixtures/project-39/-/starrers',
|
||||
verificationLevel: 'UNVERIFIED',
|
||||
versions: {
|
||||
|
|
@ -159,6 +163,7 @@ export const catalogResponseBody = {
|
|||
name: 'Project-38 Name',
|
||||
description: 'A simple component',
|
||||
starCount: 0,
|
||||
last30DayUsageCount: 0,
|
||||
starrersPath: '/frontend-fixtures/project-38/-/starrers',
|
||||
verificationLevel: 'UNVERIFIED',
|
||||
versions: {
|
||||
|
|
@ -175,6 +180,7 @@ export const catalogResponseBody = {
|
|||
name: 'Project-37 Name',
|
||||
description: 'A simple component',
|
||||
starCount: 0,
|
||||
last30DayUsageCount: 0,
|
||||
starrersPath: '/frontend-fixtures/project-37/-/starrers',
|
||||
verificationLevel: 'UNVERIFIED',
|
||||
versions: {
|
||||
|
|
@ -191,6 +197,7 @@ export const catalogResponseBody = {
|
|||
name: 'Project-36 Name',
|
||||
description: 'A simple component',
|
||||
starCount: 0,
|
||||
last30DayUsageCount: 0,
|
||||
starrersPath: '/frontend-fixtures/project-36/-/starrers',
|
||||
verificationLevel: 'UNVERIFIED',
|
||||
versions: {
|
||||
|
|
@ -207,6 +214,7 @@ export const catalogResponseBody = {
|
|||
name: 'Project-35 Name',
|
||||
description: 'A simple component',
|
||||
starCount: 0,
|
||||
last30DayUsageCount: 0,
|
||||
starrersPath: '/frontend-fixtures/project-35/-/starrers',
|
||||
verificationLevel: 'UNVERIFIED',
|
||||
versions: {
|
||||
|
|
@ -223,6 +231,7 @@ export const catalogResponseBody = {
|
|||
name: 'Project-34 Name',
|
||||
description: 'A simple component',
|
||||
starCount: 0,
|
||||
last30DayUsageCount: 0,
|
||||
starrersPath: '/frontend-fixtures/project-34/-/starrers',
|
||||
verificationLevel: 'UNVERIFIED',
|
||||
versions: {
|
||||
|
|
@ -239,6 +248,7 @@ export const catalogResponseBody = {
|
|||
name: 'Project-33 Name',
|
||||
description: 'A simple component',
|
||||
starCount: 0,
|
||||
last30DayUsageCount: 0,
|
||||
starrersPath: '/frontend-fixtures/project-33/-/starrers',
|
||||
verificationLevel: 'UNVERIFIED',
|
||||
versions: {
|
||||
|
|
@ -255,6 +265,7 @@ export const catalogResponseBody = {
|
|||
name: 'Project-32 Name',
|
||||
description: 'A simple component',
|
||||
starCount: 0,
|
||||
last30DayUsageCount: 0,
|
||||
starrersPath: '/frontend-fixtures/project-32/-/starrers',
|
||||
verificationLevel: 'UNVERIFIED',
|
||||
versions: {
|
||||
|
|
@ -271,6 +282,7 @@ export const catalogResponseBody = {
|
|||
name: 'Project-31 Name',
|
||||
description: 'A simple component',
|
||||
starCount: 0,
|
||||
last30DayUsageCount: 0,
|
||||
starrersPath: '/frontend-fixtures/project-31/-/starrers',
|
||||
verificationLevel: 'UNVERIFIED',
|
||||
versions: {
|
||||
|
|
@ -287,6 +299,7 @@ export const catalogResponseBody = {
|
|||
name: 'Project-30 Name',
|
||||
description: 'A simple component',
|
||||
starCount: 0,
|
||||
last30DayUsageCount: 0,
|
||||
starrersPath: '/frontend-fixtures/project-30/-/starrers',
|
||||
verificationLevel: 'UNVERIFIED',
|
||||
versions: {
|
||||
|
|
@ -303,6 +316,7 @@ export const catalogResponseBody = {
|
|||
name: 'Project-29 Name',
|
||||
description: 'A simple component',
|
||||
starCount: 0,
|
||||
last30DayUsageCount: 0,
|
||||
starrersPath: '/frontend-fixtures/project-29/-/starrers',
|
||||
verificationLevel: 'UNVERIFIED',
|
||||
versions: {
|
||||
|
|
@ -319,6 +333,7 @@ export const catalogResponseBody = {
|
|||
name: 'Project-28 Name',
|
||||
description: 'A simple component',
|
||||
starCount: 0,
|
||||
last30DayUsageCount: 0,
|
||||
starrersPath: '/frontend-fixtures/project-28/-/starrers',
|
||||
verificationLevel: 'UNVERIFIED',
|
||||
versions: {
|
||||
|
|
@ -335,6 +350,7 @@ export const catalogResponseBody = {
|
|||
name: 'Project-27 Name',
|
||||
description: 'A simple component',
|
||||
starCount: 0,
|
||||
last30DayUsageCount: 0,
|
||||
starrersPath: '/frontend-fixtures/project-27/-/starrers',
|
||||
verificationLevel: 'UNVERIFIED',
|
||||
versions: {
|
||||
|
|
@ -351,6 +367,7 @@ export const catalogResponseBody = {
|
|||
name: 'Project-26 Name',
|
||||
description: 'A simple component',
|
||||
starCount: 0,
|
||||
last30DayUsageCount: 0,
|
||||
starrersPath: '/frontend-fixtures/project-26/-/starrers',
|
||||
verificationLevel: 'UNVERIFIED',
|
||||
versions: {
|
||||
|
|
@ -367,6 +384,7 @@ export const catalogResponseBody = {
|
|||
name: 'Project-25 Name',
|
||||
description: 'A simple component',
|
||||
starCount: 0,
|
||||
last30DayUsageCount: 0,
|
||||
starrersPath: '/frontend-fixtures/project-25/-/starrers',
|
||||
verificationLevel: 'UNVERIFIED',
|
||||
versions: {
|
||||
|
|
@ -383,6 +401,7 @@ export const catalogResponseBody = {
|
|||
name: 'Project-24 Name',
|
||||
description: 'A simple component',
|
||||
starCount: 0,
|
||||
last30DayUsageCount: 0,
|
||||
starrersPath: '/frontend-fixtures/project-24/-/starrers',
|
||||
verificationLevel: 'UNVERIFIED',
|
||||
versions: {
|
||||
|
|
@ -399,6 +418,7 @@ export const catalogResponseBody = {
|
|||
name: 'Project-23 Name',
|
||||
description: 'A simple component',
|
||||
starCount: 0,
|
||||
last30DayUsageCount: 0,
|
||||
starrersPath: '/frontend-fixtures/project-23/-/starrers',
|
||||
verificationLevel: 'UNVERIFIED',
|
||||
versions: {
|
||||
|
|
@ -434,6 +454,7 @@ export const catalogSinglePageResponse = {
|
|||
name: 'Project-45 Name',
|
||||
description: 'A simple component',
|
||||
starCount: 0,
|
||||
last30DayUsageCount: 4,
|
||||
verificationLevel: 'UNVERIFIED',
|
||||
versions: {
|
||||
__typename: 'CiCatalogResourceVersionConnection',
|
||||
|
|
@ -505,6 +526,7 @@ export const catalogSharedDataMock = {
|
|||
description: 'This is the description of the repo',
|
||||
name: 'Ruby',
|
||||
starCount: 1,
|
||||
last30DayUsageCount: 4,
|
||||
starrersPath: '/path/to/project/-/starrers',
|
||||
verificationLevel: 'UNVERIFIED',
|
||||
versions: {
|
||||
|
|
@ -543,6 +565,7 @@ const generateResourcesNodes = (count = 20, startId = 0) => {
|
|||
icon: 'my-icon',
|
||||
name: `My component #${i}`,
|
||||
starCount: 10,
|
||||
last30DayUsageCount: 4,
|
||||
versions: {
|
||||
__typename: 'CiCatalogResourceVersionConnection',
|
||||
nodes: [
|
||||
|
|
|
|||
|
|
@ -365,7 +365,6 @@ describe('Pipeline schedules app', () => {
|
|||
last: null,
|
||||
nextPageCursor: '',
|
||||
prevPageCursor: '',
|
||||
sortValue: 'ID_DESC',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -427,7 +426,6 @@ describe('Pipeline schedules app', () => {
|
|||
last: null,
|
||||
nextPageCursor: '',
|
||||
prevPageCursor: '',
|
||||
sortValue: 'ID_DESC',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -441,7 +439,6 @@ describe('Pipeline schedules app', () => {
|
|||
last: null,
|
||||
prevPageCursor: '',
|
||||
nextPageCursor: pageInfo.endCursor,
|
||||
sortValue: 'ID_DESC',
|
||||
});
|
||||
expect(findPagination().props('value')).toEqual(2);
|
||||
});
|
||||
|
|
@ -459,50 +456,6 @@ describe('Pipeline schedules app', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when sorting changes', () => {
|
||||
const newSort = 'DESCRIPTION_ASC';
|
||||
|
||||
beforeEach(async () => {
|
||||
createComponent([[getPipelineSchedulesQuery, successHandler]]);
|
||||
|
||||
await waitForPromises();
|
||||
await findTable().vm.$emit('update-sorting', newSort, 'description', false);
|
||||
});
|
||||
|
||||
it('passes it to the graphql query', () => {
|
||||
expect(successHandler).toHaveBeenCalledTimes(2);
|
||||
expect(successHandler.mock.calls[1][0]).toEqual({
|
||||
projectPath: 'gitlab-org/gitlab',
|
||||
ids: null,
|
||||
first: SCHEDULES_PER_PAGE,
|
||||
last: null,
|
||||
nextPageCursor: '',
|
||||
prevPageCursor: '',
|
||||
sortValue: newSort,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when update-sorting event is emitted', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent([[getPipelineSchedulesQuery, successHandlerWithPagination]]);
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('resets the page count', async () => {
|
||||
expect(findPagination().props('value')).toEqual(1);
|
||||
|
||||
await setPage(2);
|
||||
|
||||
expect(findPagination().props('value')).toEqual(2);
|
||||
|
||||
await findTable().vm.$emit('update-sorting', 'DESCRIPTION_DESC', 'description', true);
|
||||
await waitForPromises();
|
||||
|
||||
expect(findPagination().props('value')).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
it.each`
|
||||
description | handler | buttonDisabled | alertExists
|
||||
${'limit reached'} | ${planLimitReachedHandler} | ${true} | ${true}
|
||||
|
|
|
|||
|
|
@ -1,89 +0,0 @@
|
|||
import { GlTable } from '@gitlab/ui';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import PipelineSchedulesTable from '~/ci/pipeline_schedules/components/table/pipeline_schedules_table.vue';
|
||||
import {
|
||||
TH_DESCRIPTION_TEST_ID,
|
||||
TH_TARGET_TEST_ID,
|
||||
TH_NEXT_TEST_ID,
|
||||
} from '~/ci/pipeline_schedules/constants';
|
||||
import { mockPipelineScheduleNodes, mockPipelineScheduleCurrentUser } from '../mock_data';
|
||||
|
||||
describe('Pipeline schedules table', () => {
|
||||
let wrapper;
|
||||
|
||||
const createComponent = () => {
|
||||
wrapper = mountExtended(PipelineSchedulesTable, {
|
||||
propsData: {
|
||||
schedules: mockPipelineScheduleNodes,
|
||||
currentUser: mockPipelineScheduleCurrentUser,
|
||||
sortBy: 'ID',
|
||||
sortDesc: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findTable = () => wrapper.findComponent(GlTable);
|
||||
|
||||
describe('sorting', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent();
|
||||
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it.each`
|
||||
sortValue | sortBy | sortDesc
|
||||
${'DESCRIPTION_ASC'} | ${'description'} | ${false}
|
||||
${'DESCRIPTION_DESC'} | ${'description'} | ${true}
|
||||
${'REF_ASC'} | ${'target'} | ${false}
|
||||
${'REF_DESC'} | ${'target'} | ${true}
|
||||
${'NEXT_RUN_AT_ASC'} | ${'next'} | ${false}
|
||||
${'NEXT_RUN_AT_DESC'} | ${'next'} | ${true}
|
||||
`(
|
||||
'emits sort data in expected format for sortValue $sortValue',
|
||||
({ sortValue, sortBy, sortDesc }) => {
|
||||
findTable().vm.$emit('sort-changed', { sortBy, sortDesc });
|
||||
|
||||
expect(wrapper.emitted('update-sorting')[0]).toEqual([sortValue, sortBy, sortDesc]);
|
||||
},
|
||||
);
|
||||
|
||||
it('emits no update-sorting event when called with unsortable column', () => {
|
||||
findTable().vm.$emit('sort-changed', { sortBy: 'actions', sortDesc: false });
|
||||
|
||||
expect(wrapper.emitted('update-sorting')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('emits no update-sorting event when called with unknown column', () => {
|
||||
findTable().vm.$emit('sort-changed', { sortBy: 'not-defined-never', sortDesc: false });
|
||||
|
||||
expect(wrapper.emitted('update-sorting')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('sorting the pipeline schedules table by column', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent();
|
||||
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it.each`
|
||||
description | selector
|
||||
${'description'} | ${TH_DESCRIPTION_TEST_ID}
|
||||
${'target'} | ${TH_TARGET_TEST_ID}
|
||||
${'next'} | ${TH_NEXT_TEST_ID}
|
||||
`('updates sort with new direction when sorting by $description', async ({ selector }) => {
|
||||
const [[attr, value]] = Object.entries(selector);
|
||||
const columnHeader = () => wrapper.find(`[${attr}="${value}"]`);
|
||||
expect(columnHeader().attributes('aria-sort')).toBe('none');
|
||||
columnHeader().trigger('click');
|
||||
await waitForPromises();
|
||||
expect(columnHeader().attributes('aria-sort')).toBe('ascending');
|
||||
columnHeader().trigger('click');
|
||||
await waitForPromises();
|
||||
expect(columnHeader().attributes('aria-sort')).toBe('descending');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -135,98 +135,128 @@ describe('kubernetes_logs', () => {
|
|||
});
|
||||
|
||||
describe('when environment data is ready', () => {
|
||||
describe('when logs data is empty', () => {
|
||||
beforeEach(async () => {
|
||||
k8sLogsQueryMock = jest.fn().mockResolvedValue({});
|
||||
mountComponent();
|
||||
await waitForPromises();
|
||||
describe('when no container is specified for the logs', () => {
|
||||
describe('when logs data is empty', () => {
|
||||
beforeEach(async () => {
|
||||
k8sLogsQueryMock = jest.fn().mockResolvedValue({});
|
||||
mountComponent();
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('should not render loading state', () => {
|
||||
expect(findLoadingIcon().exists()).toBe(false);
|
||||
});
|
||||
it('should not render error state', () => {
|
||||
expect(findAlert().exists()).toBe(false);
|
||||
});
|
||||
it('should not render logs viewer', () => {
|
||||
expect(findLogsViewer().exists()).toBe(false);
|
||||
});
|
||||
it('should render empty state with pod name', () => {
|
||||
expect(findEmptyState().text()).toBe('No logs available for pod test-pod');
|
||||
});
|
||||
});
|
||||
|
||||
it('should not render loading state', () => {
|
||||
expect(findLoadingIcon().exists()).toBe(false);
|
||||
describe('when logs data fetched successfully', () => {
|
||||
beforeEach(async () => {
|
||||
mountComponent();
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('should not render loading state', () => {
|
||||
expect(findLoadingIcon().exists()).toBe(false);
|
||||
});
|
||||
it('should not render error state', () => {
|
||||
expect(findAlert().exists()).toBe(false);
|
||||
});
|
||||
it('should not render empty state', () => {
|
||||
expect(findEmptyState().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('should query logs', () => {
|
||||
expect(k8sLogsQueryMock).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
{
|
||||
configuration,
|
||||
namespace: defaultProps.namespace,
|
||||
podName: defaultProps.podName,
|
||||
containerName: '',
|
||||
},
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
it('should render logs viewer component with correct parameters', () => {
|
||||
const expectedLogLines = [
|
||||
{
|
||||
content: [{ text: logsMockData[0].content }],
|
||||
lineNumber: 1,
|
||||
lineId: 'L1',
|
||||
},
|
||||
{
|
||||
content: [{ text: logsMockData[1].content }],
|
||||
lineNumber: 2,
|
||||
lineId: 'L2',
|
||||
},
|
||||
];
|
||||
expect(findLogsViewer().props()).toMatchObject({
|
||||
logLines: expectedLogLines,
|
||||
highlightedLine: 'L2',
|
||||
});
|
||||
});
|
||||
});
|
||||
it('should not render error state', () => {
|
||||
expect(findAlert().exists()).toBe(false);
|
||||
});
|
||||
it('should not render logs viewer', () => {
|
||||
expect(findLogsViewer().exists()).toBe(false);
|
||||
});
|
||||
it('should render empty state', () => {
|
||||
expect(findEmptyState().text()).toBe('No logs available for pod test-pod');
|
||||
|
||||
describe('when logs data fetch failed', () => {
|
||||
const errorMessage = 'Error while fetching logs';
|
||||
|
||||
beforeEach(async () => {
|
||||
k8sLogsQueryMock = jest.fn().mockResolvedValue({
|
||||
error: { message: errorMessage },
|
||||
});
|
||||
mountComponent();
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('should not render loading state', () => {
|
||||
expect(findLoadingIcon().exists()).toBe(false);
|
||||
});
|
||||
it('should render error state', () => {
|
||||
expect(findAlert().text()).toBe(errorMessage);
|
||||
});
|
||||
it('should render empty state', () => {
|
||||
expect(findEmptyState().exists()).toBe(true);
|
||||
});
|
||||
it('should not render logs viewer', () => {
|
||||
expect(findLogsViewer().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when logs data fetched successfully', () => {
|
||||
describe('when a container is specified for the logs', () => {
|
||||
beforeEach(async () => {
|
||||
mountComponent();
|
||||
k8sLogsQueryMock = jest.fn().mockResolvedValue({});
|
||||
mountComponent({ containerName: 'my-container' });
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('should not render loading state', () => {
|
||||
expect(findLoadingIcon().exists()).toBe(false);
|
||||
});
|
||||
it('should not render error state', () => {
|
||||
expect(findAlert().exists()).toBe(false);
|
||||
});
|
||||
it('should not render empty state', () => {
|
||||
expect(findEmptyState().exists()).toBe(false);
|
||||
it('should render empty state with pod and container name', () => {
|
||||
expect(findEmptyState().text()).toBe(
|
||||
'No logs available for container my-container of pod test-pod',
|
||||
);
|
||||
});
|
||||
|
||||
it('should query logs', () => {
|
||||
it('should query logs with the container name included', () => {
|
||||
expect(k8sLogsQueryMock).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
{
|
||||
configuration,
|
||||
namespace: defaultProps.namespace,
|
||||
podName: defaultProps.podName,
|
||||
containerName: 'my-container',
|
||||
},
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
it('should render logs viewer component with correct parameters', () => {
|
||||
const expectedLogLines = [
|
||||
{
|
||||
content: [{ text: logsMockData[0].content }],
|
||||
lineNumber: 1,
|
||||
lineId: 'L1',
|
||||
},
|
||||
{
|
||||
content: [{ text: logsMockData[1].content }],
|
||||
lineNumber: 2,
|
||||
lineId: 'L2',
|
||||
},
|
||||
];
|
||||
expect(findLogsViewer().props()).toMatchObject({
|
||||
logLines: expectedLogLines,
|
||||
highlightedLine: 'L2',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when logs data fetch failed', () => {
|
||||
const errorMessage = 'Error while fetching logs';
|
||||
|
||||
beforeEach(async () => {
|
||||
k8sLogsQueryMock = jest.fn().mockResolvedValue({
|
||||
error: { message: errorMessage },
|
||||
});
|
||||
mountComponent();
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('should not render loading state', () => {
|
||||
expect(findLoadingIcon().exists()).toBe(false);
|
||||
});
|
||||
it('should render error state', () => {
|
||||
expect(findAlert().text()).toBe(errorMessage);
|
||||
});
|
||||
it('should render empty state', () => {
|
||||
expect(findEmptyState().exists()).toBe(true);
|
||||
});
|
||||
it('should not render logs viewer', () => {
|
||||
expect(findLogsViewer().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -30,6 +30,27 @@ describe('k8sLogs', () => {
|
|||
watchStream = bootstrapWatcherMock();
|
||||
});
|
||||
|
||||
it('should request pods logs if no container is specified', async () => {
|
||||
await k8sLogs(null, { configuration, namespace, podName }, { client });
|
||||
|
||||
expect(
|
||||
watchStream.subscribeToStreamMock,
|
||||
).toHaveBeenCalledWith('/api/v1/namespaces/default/pods/test-pod/log', { follow: true });
|
||||
});
|
||||
|
||||
it('should request specific container logs if container is specified', async () => {
|
||||
const containerName = 'my-container';
|
||||
await k8sLogs(null, { configuration, namespace, podName, containerName }, { client });
|
||||
|
||||
expect(watchStream.subscribeToStreamMock).toHaveBeenCalledWith(
|
||||
'/api/v1/namespaces/default/pods/test-pod/log',
|
||||
{
|
||||
follow: true,
|
||||
container: containerName,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const errorMessage = 'event error message';
|
||||
const logContent = 'Plain text log data';
|
||||
it.each([
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
import { GlAlert, GlButton } from '@gitlab/ui';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import WebIdeError from '~/ide/components/web_ide_error.vue';
|
||||
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
|
||||
|
||||
const findButtons = (wrapper) => wrapper.findAllComponents(GlButton);
|
||||
|
||||
describe('WebIdeError', () => {
|
||||
const MOCK_SIGN_OUT_PATH = '/users/sign_out';
|
||||
|
||||
let wrapper;
|
||||
|
||||
useMockLocationHelper();
|
||||
function createWrapper() {
|
||||
wrapper = mount(WebIdeError, {
|
||||
propsData: {
|
||||
signOutPath: MOCK_SIGN_OUT_PATH,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
it('renders alert component', () => {
|
||||
createWrapper();
|
||||
const alert = wrapper.findComponent(GlAlert);
|
||||
|
||||
expect(alert.text()).toMatchInterpolatedText(
|
||||
'Failed to load the Web IDE For more information, see the developer console. Try to reload the page or sign out and in again. If the issue persists, report a problem. Reload Sign out',
|
||||
);
|
||||
});
|
||||
|
||||
it('renders reload page button', () => {
|
||||
createWrapper();
|
||||
const reloadButton = findButtons(wrapper).at(0);
|
||||
|
||||
expect(reloadButton.text()).toEqual('Reload');
|
||||
|
||||
reloadButton.vm.$emit('click');
|
||||
expect(window.location.reload).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders sign out button', () => {
|
||||
createWrapper();
|
||||
const signOutButton = findButtons(wrapper).at(1);
|
||||
|
||||
expect(signOutButton.text()).toEqual('Sign out');
|
||||
expect(signOutButton.attributes()).toMatchObject({
|
||||
'data-method': 'post',
|
||||
href: MOCK_SIGN_OUT_PATH,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -8,6 +8,7 @@ import Tracking from '~/tracking';
|
|||
import { TEST_HOST } from 'helpers/test_constants';
|
||||
import setWindowLocation from 'helpers/set_window_location_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { renderWebIdeError } from '~/ide/render_web_ide_error';
|
||||
import { getMockCallbackUrl } from './helpers';
|
||||
|
||||
jest.mock('@gitlab/web-ide');
|
||||
|
|
@ -18,6 +19,7 @@ jest.mock('~/lib/utils/csrf', () => ({
|
|||
headerKey: 'mock-csrf-header',
|
||||
}));
|
||||
jest.mock('~/tracking');
|
||||
jest.mock('~/ide/render_web_ide_error');
|
||||
|
||||
const ROOT_ELEMENT_ID = 'ide';
|
||||
const TEST_NONCE = 'test123nonce';
|
||||
|
|
@ -30,6 +32,7 @@ const TEST_FILE_PATH = 'foo/README.md';
|
|||
const TEST_MR_ID = '7';
|
||||
const TEST_MR_TARGET_PROJECT = 'gitlab-org/the-real-gitlab';
|
||||
const TEST_SIGN_IN_PATH = 'sign-in';
|
||||
const TEST_SIGN_OUT_PATH = 'sign-out';
|
||||
const TEST_FORK_INFO = { fork_path: '/forky' };
|
||||
const TEST_IDE_REMOTE_PATH = '/-/ide/remote/:remote_host/:remote_path';
|
||||
const TEST_START_REMOTE_PARAMS = {
|
||||
|
|
@ -82,6 +85,7 @@ describe('ide/init_gitlab_web_ide', () => {
|
|||
],
|
||||
});
|
||||
el.dataset.signInPath = TEST_SIGN_IN_PATH;
|
||||
el.dataset.signOutPath = TEST_SIGN_OUT_PATH;
|
||||
|
||||
document.body.append(el);
|
||||
};
|
||||
|
|
@ -272,6 +276,27 @@ describe('ide/init_gitlab_web_ide', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('on start error', () => {
|
||||
const mockError = new Error('error');
|
||||
|
||||
beforeEach(() => {
|
||||
jest.mocked(start).mockImplementationOnce(() => {
|
||||
throw mockError;
|
||||
});
|
||||
|
||||
createSubject();
|
||||
});
|
||||
|
||||
it('shows alert', () => {
|
||||
expect(start).toHaveBeenCalledTimes(1);
|
||||
expect(renderWebIdeError).toHaveBeenCalledTimes(1);
|
||||
expect(renderWebIdeError).toHaveBeenCalledWith({
|
||||
error: mockError,
|
||||
signOutPath: TEST_SIGN_OUT_PATH,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when extensionsGallerySettings is in dataset', () => {
|
||||
function setMockExtensionGallerySettingsDataset(
|
||||
mockSettings = TEST_EXTENSIONS_GALLERY_SETTINGS,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
import * as Sentry from '~/sentry/sentry_browser_wrapper';
|
||||
import { renderWebIdeError } from '~/ide/render_web_ide_error';
|
||||
import { logError } from '~/lib/logger';
|
||||
import { resetHTMLFixture } from 'helpers/fixtures';
|
||||
|
||||
jest.mock('~/sentry/sentry_browser_wrapper');
|
||||
jest.mock('~/lib/logger');
|
||||
|
||||
describe('render web IDE error', () => {
|
||||
const MOCK_ERROR = new Error('error');
|
||||
const MOCK_SIGNOUT_PATH = '/signout';
|
||||
|
||||
const setupFlashContainer = () => {
|
||||
const flashContainer = document.createElement('div');
|
||||
flashContainer.classList.add('flash-container');
|
||||
|
||||
document.body.appendChild(flashContainer);
|
||||
};
|
||||
|
||||
const findAlert = () => document.querySelector('.flash-container .gl-alert');
|
||||
|
||||
afterEach(() => {
|
||||
resetHTMLFixture();
|
||||
});
|
||||
|
||||
describe('with flash-container', () => {
|
||||
beforeEach(() => {
|
||||
setupFlashContainer();
|
||||
|
||||
renderWebIdeError({ error: MOCK_ERROR, signOutPath: MOCK_SIGNOUT_PATH });
|
||||
});
|
||||
|
||||
it('logs error to Sentry', () => {
|
||||
expect(Sentry.captureException).toHaveBeenCalledWith(MOCK_ERROR);
|
||||
});
|
||||
|
||||
it('logs error to console', () => {
|
||||
expect(logError).toHaveBeenCalledWith('Failed to load Web IDE', MOCK_ERROR);
|
||||
});
|
||||
|
||||
it('should render alert', () => {
|
||||
expect(findAlert()).toBeInstanceOf(HTMLElement);
|
||||
});
|
||||
});
|
||||
|
||||
describe('no .flash-container', () => {
|
||||
beforeEach(() => {
|
||||
renderWebIdeError({ error: MOCK_ERROR, signOutPath: MOCK_SIGNOUT_PATH });
|
||||
});
|
||||
|
||||
it('does not render alert', () => {
|
||||
expect(findAlert()).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -57,6 +57,18 @@ RSpec.describe Repository, feature_category: :source_code_management do
|
|||
it { is_expected.to match_array(["'test'"]) }
|
||||
end
|
||||
|
||||
context 'when exclude_refs is provided' do
|
||||
let(:opts) { { exclude_refs: ['master'] } }
|
||||
|
||||
it { is_expected.not_to include('master') }
|
||||
end
|
||||
|
||||
context 'with limit + exclude_refs options' do
|
||||
let(:opts) { { limit: 1, exclude_refs: ["'test'"] } }
|
||||
|
||||
it { is_expected.to match_array(["2-mb-file"]) }
|
||||
end
|
||||
|
||||
describe 'when storage is broken', :broken_storage do
|
||||
it 'raises a storage error' do
|
||||
expect_to_raise_storage_error do
|
||||
|
|
@ -79,6 +91,18 @@ RSpec.describe Repository, feature_category: :source_code_management do
|
|||
|
||||
it { is_expected.to match_array(['v1.1.0']) }
|
||||
end
|
||||
|
||||
context 'when exclude_refs is provided' do
|
||||
let(:opts) { { exclude_refs: ['v1.1.0'] } }
|
||||
|
||||
it { is_expected.not_to include('v1.1.0') }
|
||||
end
|
||||
|
||||
context 'with limit + exclude_refs options' do
|
||||
let(:opts) { { limit: 1, exclude_refs: ["v1.1.0"] } }
|
||||
|
||||
it { is_expected.to match_array(["v1.1.1"]) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#tags_sorted_by' do
|
||||
|
|
|
|||
|
|
@ -1377,37 +1377,6 @@ RSpec.describe User, feature_category: :user_profile do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.with_expiring_and_not_notified_personal_access_tokens' do
|
||||
let_it_be(:user1) { create(:user) }
|
||||
let_it_be(:user2) { create(:user) }
|
||||
let_it_be(:user3) { create(:user) }
|
||||
|
||||
let_it_be(:expired_token) { create(:personal_access_token, user: user1, expires_at: 2.days.ago) }
|
||||
let_it_be(:revoked_token) { create(:personal_access_token, user: user1, revoked: true) }
|
||||
let_it_be(:impersonation_token) { create(:personal_access_token, :impersonation, user: user1, expires_at: 2.days.from_now) }
|
||||
let_it_be(:valid_token_and_notified) { create(:personal_access_token, user: user2, expires_at: 2.days.from_now, expire_notification_delivered: true) }
|
||||
let_it_be(:valid_token1) { create(:personal_access_token, user: user2, expires_at: 2.days.from_now) }
|
||||
let_it_be(:valid_token2) { create(:personal_access_token, user: user2, expires_at: 2.days.from_now) }
|
||||
|
||||
let(:users) { described_class.with_expiring_and_not_notified_personal_access_tokens(from) }
|
||||
|
||||
context 'in one day' do
|
||||
let(:from) { 1.day.from_now }
|
||||
|
||||
it "doesn't include an user" do
|
||||
expect(users).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'in three days' do
|
||||
let(:from) { 3.days.from_now }
|
||||
|
||||
it 'only includes user2' do
|
||||
expect(users).to contain_exactly(user2)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.with_personal_access_tokens_expired_today' do
|
||||
let_it_be(:user1) { create(:user) }
|
||||
let_it_be(:expired_today) { create(:personal_access_token, user: user1, expires_at: Date.current) }
|
||||
|
|
|
|||
|
|
@ -896,6 +896,22 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
|
|||
describe 'GET /api/v4/jobs/:id/artifacts' do
|
||||
let(:token) { job.token }
|
||||
|
||||
def expect_use_primary
|
||||
lb_session = ::Gitlab::Database::LoadBalancing::Session.current
|
||||
|
||||
expect(lb_session).to receive(:use_primary).and_call_original
|
||||
|
||||
allow(::Gitlab::Database::LoadBalancing::Session).to receive(:current).and_return(lb_session)
|
||||
end
|
||||
|
||||
def expect_no_use_primary
|
||||
lb_session = ::Gitlab::Database::LoadBalancing::Session.current
|
||||
|
||||
expect(lb_session).not_to receive(:use_primary)
|
||||
|
||||
allow(::Gitlab::Database::LoadBalancing::Session).to receive(:current).and_return(lb_session)
|
||||
end
|
||||
|
||||
it_behaves_like 'API::CI::Runner application context metadata', 'GET /api/:version/jobs/:id/artifacts' do
|
||||
let(:send_request) { download_artifact }
|
||||
end
|
||||
|
|
@ -927,13 +943,32 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
|
|||
allow(Gitlab::ApplicationContext).to receive(:push).and_call_original
|
||||
end
|
||||
|
||||
it 'downloads artifacts' do
|
||||
expect(Gitlab::ApplicationContext).to receive(:push).with(artifact: an_instance_of(Ci::JobArtifact)).once.and_call_original
|
||||
context 'when ci_job_artifacts_use_primary_to_authenticate feature flag is on' do
|
||||
it 'downloads artifacts' do
|
||||
expect_use_primary
|
||||
expect(Gitlab::ApplicationContext).to receive(:push).with(artifact: an_instance_of(Ci::JobArtifact)).once.and_call_original
|
||||
|
||||
download_artifact
|
||||
download_artifact
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response.headers.to_h).to include download_headers
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response.headers.to_h).to include download_headers
|
||||
end
|
||||
end
|
||||
|
||||
context 'when ci_job_artifacts_use_primary_to_authenticate feature flag is off' do
|
||||
before do
|
||||
stub_feature_flags(ci_job_artifacts_use_primary_to_authenticate: false)
|
||||
end
|
||||
|
||||
it 'downloads artifacts' do
|
||||
expect_no_use_primary
|
||||
expect(Gitlab::ApplicationContext).to receive(:push).with(artifact: an_instance_of(Ci::JobArtifact)).once.and_call_original
|
||||
|
||||
download_artifact
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response.headers.to_h).to include download_headers
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Groups::PackagesController, feature_category: :package_registry do
|
||||
let_it_be(:group) { create(:group, :public) }
|
||||
let_it_be(:project) { create(:project, :public, group: group) }
|
||||
|
||||
describe 'GET #show' do
|
||||
let_it_be(:package) { create(:package, project: project) }
|
||||
|
||||
subject do
|
||||
get group_package_path(group_id: group.full_path, id: package.id)
|
||||
response
|
||||
end
|
||||
|
||||
it { is_expected.to have_gitlab_http_status(:ok) }
|
||||
|
||||
it { is_expected.to have_attributes(body: have_pushed_frontend_feature_flags(packagesProtectedPackages: true)) }
|
||||
|
||||
context 'when feature flag "packages_protected_packages" is disabled' do
|
||||
before do
|
||||
stub_feature_flags(packages_protected_packages: false)
|
||||
end
|
||||
|
||||
it { is_expected.to have_gitlab_http_status(:ok) }
|
||||
|
||||
it { is_expected.to have_attributes(body: have_pushed_frontend_feature_flags(packagesProtectedPackages: false)) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -5,7 +5,7 @@ require_relative 'helpers/wait_for_requests'
|
|||
|
||||
module Capybara
|
||||
class Session
|
||||
module WaitForAllRequestsAfterVisitPage
|
||||
module WaitForRequestsAfterVisitPage
|
||||
include CapybaraHelpers
|
||||
include WaitForRequests
|
||||
|
||||
|
|
@ -14,11 +14,11 @@ module Capybara
|
|||
|
||||
yield if block
|
||||
|
||||
wait_for_all_requests
|
||||
wait_for_requests
|
||||
end
|
||||
end
|
||||
|
||||
prepend WaitForAllRequestsAfterVisitPage
|
||||
prepend WaitForRequestsAfterVisitPage
|
||||
end
|
||||
|
||||
module Node
|
||||
|
|
@ -181,8 +181,6 @@ RSpec.shared_examples 'work items assignees' do
|
|||
end
|
||||
|
||||
it 'updates the assignee in real-time' do
|
||||
Capybara::Session.new(:other_session)
|
||||
|
||||
using_session :other_session do
|
||||
visit work_items_path
|
||||
expect(work_item.reload.assignees).not_to include(user)
|
||||
|
|
@ -240,8 +238,6 @@ RSpec.shared_examples 'work items labels' do
|
|||
end
|
||||
|
||||
it 'updates the assignee in real-time' do
|
||||
Capybara::Session.new(:other_session)
|
||||
|
||||
using_session :other_session do
|
||||
visit work_items_path
|
||||
expect(work_item.reload.labels).not_to include(label)
|
||||
|
|
|
|||
|
|
@ -2,22 +2,22 @@
|
|||
|
||||
require 'fast_spec_helper'
|
||||
require 'capybara'
|
||||
require 'support/capybara_wait_for_all_requests'
|
||||
require 'support/capybara_wait_for_requests'
|
||||
|
||||
RSpec.describe 'capybara_wait_for_all_requests', feature_category: :tooling do # rubocop:disable RSpec/FilePath
|
||||
context 'for Capybara::Session::WaitForAllRequestsAfterVisitPage' do
|
||||
RSpec.describe 'capybara_wait_for_requests', feature_category: :tooling do
|
||||
context 'for Capybara::Session::WaitForRequestsAfterVisitPage' do
|
||||
let(:page_visitor) do
|
||||
Class.new do
|
||||
def visit(visit_uri)
|
||||
visit_uri
|
||||
end
|
||||
|
||||
prepend Capybara::Session::WaitForAllRequestsAfterVisitPage
|
||||
prepend Capybara::Session::WaitForRequestsAfterVisitPage
|
||||
end.new
|
||||
end
|
||||
|
||||
it 'waits for all requests after a page visit' do
|
||||
expect(page_visitor).to receive(:wait_for_all_requests)
|
||||
it 'waits for requests after a page visit' do
|
||||
expect(page_visitor).to receive(:wait_for_requests)
|
||||
|
||||
page_visitor.visit('http://test.com')
|
||||
end
|
||||
|
|
@ -130,14 +130,87 @@ RSpec.describe PersonalAccessTokens::ExpiringWorker, type: :worker, feature_cate
|
|||
|
||||
context 'when a token is owned by a group bot' do
|
||||
let_it_be(:project_bot) { create(:user, :project_bot) }
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:expiring_token) { create(:personal_access_token, user: project_bot, expires_at: 5.days.from_now) }
|
||||
|
||||
before_all do
|
||||
group.add_developer(project_bot)
|
||||
context 'when the group of the resource bot exists' do
|
||||
let_it_be(:group) { create(:group) }
|
||||
|
||||
before_all do
|
||||
group.add_maintainer(project_bot)
|
||||
end
|
||||
|
||||
it_behaves_like 'sends notification about expiry of bot user tokens'
|
||||
|
||||
it 'updates expire notification delivered attribute of the token' do
|
||||
expect { worker.perform }.to change { expiring_token.reload.expire_notification_delivered }.from(false).to(true)
|
||||
end
|
||||
|
||||
context 'when exception is raised during processing' do
|
||||
context 'with a single resource access token' do
|
||||
before do
|
||||
allow_next_instance_of(NotificationService) do |service|
|
||||
allow(service).to(
|
||||
receive(:bot_resource_access_token_about_to_expire)
|
||||
.with(project_bot, expiring_token.name)
|
||||
.and_raise('boom!')
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
it 'logs error' do
|
||||
expect(Gitlab::AppLogger).to(
|
||||
receive(:error)
|
||||
.with({ message: 'Failed to send notification about expiring resource access tokens',
|
||||
class: described_class,
|
||||
"exception.class": "RuntimeError",
|
||||
"exception.message": "boom!",
|
||||
user_id: project_bot.id })
|
||||
)
|
||||
|
||||
worker.perform
|
||||
end
|
||||
|
||||
it 'does not update token with failed delivery' do
|
||||
expect { worker.perform }.not_to change { expiring_token.reload.expire_notification_delivered }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with multiple resource access tokens' do
|
||||
let_it_be(:another_project_bot) { create(:user, :project_bot) }
|
||||
let_it_be(:another_expiring_token) { create(:personal_access_token, user: another_project_bot, expires_at: 5.days.from_now) }
|
||||
|
||||
before_all do
|
||||
group.add_maintainer(another_project_bot)
|
||||
end
|
||||
|
||||
it 'continues sending email' do
|
||||
expect_next_instance_of(NotificationService) do |service|
|
||||
expect(service).to(
|
||||
receive(:bot_resource_access_token_about_to_expire)
|
||||
.with(project_bot, expiring_token.name)
|
||||
.and_raise('boom!')
|
||||
)
|
||||
expect(service).to(
|
||||
receive(:bot_resource_access_token_about_to_expire)
|
||||
.with(another_project_bot, another_expiring_token.name)
|
||||
.and_call_original
|
||||
)
|
||||
end
|
||||
|
||||
worker.perform
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'sends notification about expiry of bot user tokens'
|
||||
context 'when the group of the resource bot has been deleted' do
|
||||
it 'does not update token with no delivery' do
|
||||
expect(Group).to be_none
|
||||
expect(Project).to be_none
|
||||
|
||||
expect { worker.perform }.not_to change { expiring_token.reload.expire_notification_delivered }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue