Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
37f03adfda
commit
c80f596324
|
|
@ -453,7 +453,6 @@ Layout/ClassStructure:
|
|||
- 'qa/qa/support/parallel_pipeline_jobs.rb'
|
||||
- 'qa/qa/tools/ci/test_metrics.rb'
|
||||
- 'qa/qa/tools/migrate_influx_data_to_gcs.rb'
|
||||
- 'qa/qa/tools/reliable_report.rb'
|
||||
- 'rubocop/cop/migration/ensure_factory_for_table.rb'
|
||||
- 'rubocop/cop/rails/migration_timestamp.rb'
|
||||
- 'rubocop/cop_todo.rb'
|
||||
|
|
|
|||
|
|
@ -57,7 +57,6 @@ Layout/LineEndStringConcatenationIndentation:
|
|||
- 'ee/app/components/namespaces/free_user_cap/enforcement_at_limit_alert_component.rb'
|
||||
- 'ee/app/components/namespaces/free_user_cap/usage_quota_alert_component.rb'
|
||||
- 'ee/app/components/namespaces/free_user_cap/usage_quota_trial_alert_component.rb'
|
||||
- 'ee/app/components/namespaces/storage/limit_alert_component.rb'
|
||||
- 'ee/app/components/namespaces/storage/repository_limit_alert_component.rb'
|
||||
- 'ee/app/controllers/concerns/insights_actions.rb'
|
||||
- 'ee/app/controllers/ee/ldap/omniauth_callbacks_controller.rb'
|
||||
|
|
@ -422,7 +421,6 @@ Layout/LineEndStringConcatenationIndentation:
|
|||
- 'qa/qa/specs/features/ee/browser_ui/10_govern/export_vulnerability_report_spec.rb'
|
||||
- 'qa/qa/specs/features/ee/browser_ui/9_data_stores/elasticsearch/elasticsearch_reindexing_spec.rb'
|
||||
- 'qa/qa/support/system_logs/kibana.rb'
|
||||
- 'qa/qa/tools/reliable_report.rb'
|
||||
- 'qa/qa/tools/revoke_user_personal_access_tokens.rb'
|
||||
- 'qa/qa/tools/test_resources_handler.rb'
|
||||
- 'qa/spec/support/system_logs/kibana_spec.rb'
|
||||
|
|
|
|||
|
|
@ -234,7 +234,6 @@ Lint/UnusedBlockArgument:
|
|||
- 'qa/qa/specs/features/api/12_systems/gitaly/distributed_reads_spec.rb'
|
||||
- 'qa/qa/support/matchers/eventually_matcher.rb'
|
||||
- 'qa/qa/tools/generate_perf_testdata.rb'
|
||||
- 'qa/qa/tools/reliable_report.rb'
|
||||
- 'qa/tasks/vulnerabilities.rake'
|
||||
- 'scripts/docs_screenshots.rb'
|
||||
- 'scripts/packages/automated_cleanup.rb'
|
||||
|
|
|
|||
|
|
@ -141,9 +141,7 @@ Rails/Date:
|
|||
- 'qa/qa/specs/features/ee/browser_ui/12_systems/geo/wiki_ssh_push_to_secondary_spec.rb'
|
||||
- 'qa/qa/tools/delete_resource_base.rb'
|
||||
- 'qa/qa/tools/delete_test_users.rb'
|
||||
- 'qa/qa/tools/reliable_report.rb'
|
||||
- 'qa/qa/tools/revoke_user_personal_access_tokens.rb'
|
||||
- 'qa/spec/tools/reliable_report_spec.rb'
|
||||
- 'spec/controllers/admin/users_controller_spec.rb'
|
||||
- 'spec/controllers/groups/milestones_controller_spec.rb'
|
||||
- 'spec/controllers/sessions_controller_spec.rb'
|
||||
|
|
|
|||
|
|
@ -57,7 +57,6 @@ Rails/OutputSafety:
|
|||
- 'ee/app/components/namespaces/free_user_cap/non_owner_enforcement_alert_component.rb'
|
||||
- 'ee/app/components/namespaces/free_user_cap/usage_quota_alert_component.rb'
|
||||
- 'ee/app/components/namespaces/free_user_cap/usage_quota_trial_alert_component.rb'
|
||||
- 'ee/app/components/namespaces/storage/limit_alert_component.rb'
|
||||
- 'ee/app/components/namespaces/storage/repository_limit_alert_component.rb'
|
||||
- 'ee/app/controllers/ee/projects/issues_controller.rb'
|
||||
- 'ee/app/controllers/ee/repositories/lfs_api_controller.rb'
|
||||
|
|
|
|||
|
|
@ -229,7 +229,6 @@ RSpec/ExampleWithoutDescription:
|
|||
- 'ee/spec/workers/security/scan_execution_policies/rule_schedule_worker_spec.rb'
|
||||
- 'qa/qa/specs/features/browser_ui/9_data_stores/project/dashboard_images_spec.rb'
|
||||
- 'qa/spec/specs/helpers/feature_flag_spec.rb'
|
||||
- 'qa/spec/tools/reliable_report_spec.rb'
|
||||
- 'spec/bin/audit_event_type_spec.rb'
|
||||
- 'spec/bin/feature_flag_spec.rb'
|
||||
- 'spec/bin/saas_feature_spec.rb'
|
||||
|
|
|
|||
|
|
@ -83,7 +83,6 @@ RSpec/NoExpectationExample:
|
|||
- 'spec/features/projects/files/undo_template_spec.rb'
|
||||
- 'spec/features/user_settings/ssh_keys_spec.rb'
|
||||
- 'spec/features/users/login_spec.rb'
|
||||
- 'spec/features/work_items/linked_work_items_spec.rb'
|
||||
- 'spec/frontend/fixtures/issues.rb'
|
||||
- 'spec/frontend/fixtures/listbox.rb'
|
||||
- 'spec/frontend/fixtures/merge_requests.rb'
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
RSpec/ReceiveMessages:
|
||||
Exclude:
|
||||
- 'ee/spec/bin/custom_ability_spec.rb'
|
||||
- 'ee/spec/components/namespaces/storage/limit_alert_component_spec.rb'
|
||||
- 'ee/spec/components/namespaces/storage/repository_limit_alert_component_spec.rb'
|
||||
- 'ee/spec/controllers/concerns/analytics/cycle_analytics/value_stream_actions_spec.rb'
|
||||
- 'ee/spec/controllers/concerns/audit_events/audit_events_params_spec.rb'
|
||||
|
|
@ -167,7 +166,6 @@ RSpec/ReceiveMessages:
|
|||
- 'ee/spec/services/merge_requests/mergeability/check_path_locks_service_spec.rb'
|
||||
- 'ee/spec/services/resource_access_tokens/create_service_spec.rb'
|
||||
- 'ee/spec/services/search/elastic/cluster_reindexing_service_spec.rb'
|
||||
- 'ee/spec/services/search/index_repair_service_spec.rb'
|
||||
- 'ee/spec/services/search/project_service_spec.rb'
|
||||
- 'ee/spec/services/search/rake_task_executor_service_spec.rb'
|
||||
- 'ee/spec/services/search_service_spec.rb'
|
||||
|
|
|
|||
|
|
@ -102,7 +102,6 @@ RSpec/VerifiedDoubleReference:
|
|||
- 'qa/spec/tools/ci/code_paths_mapping_spec.rb'
|
||||
- 'qa/spec/tools/ci/test_metrics_spec.rb'
|
||||
- 'qa/spec/tools/knapsack_report_updater_spec.rb'
|
||||
- 'qa/spec/tools/reliable_report_spec.rb'
|
||||
- 'spec/benchmarks/banzai_benchmark.rb'
|
||||
- 'spec/commands/sidekiq_cluster/cli_spec.rb'
|
||||
- 'spec/components/pajamas/concerns/checkbox_radio_label_with_help_text_spec.rb'
|
||||
|
|
|
|||
|
|
@ -91,7 +91,6 @@ Style/HashEachMethods:
|
|||
- 'lib/tasks/gitlab/db.rake'
|
||||
- 'lib/tasks/gitlab/setup.rake'
|
||||
- 'qa/qa/specs/features/ee/browser_ui/10_govern/change_vulnerability_status_spec.rb'
|
||||
- 'qa/qa/tools/reliable_report.rb'
|
||||
- 'qa/spec/specs/runner_spec.rb'
|
||||
- 'scripts/generate_rspec_pipeline.rb'
|
||||
- 'spec/config/settings_spec.rb'
|
||||
|
|
|
|||
|
|
@ -2137,7 +2137,6 @@ Style/InlineDisableAnnotation:
|
|||
- 'qa/qa/support/page_error_checker.rb'
|
||||
- 'qa/qa/support/run.rb'
|
||||
- 'qa/qa/support/wait_for_requests.rb'
|
||||
- 'qa/qa/tools/reliable_report.rb'
|
||||
- 'qa/spec/ee/resource/mixins/group_base_spec.rb'
|
||||
- 'qa/spec/page/base_spec.rb'
|
||||
- 'qa/spec/resource/project_web_hook_spec.rb'
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { uniq } from 'lodash';
|
|||
import { transformAstToDisplayFields } from '../transformer/ast';
|
||||
import { parseFields } from './fields';
|
||||
|
||||
export const parseConfig = (frontmatter, defaults = {}) => {
|
||||
export const parseYAMLConfig = (frontmatter, defaults = {}) => {
|
||||
const config = jsYaml.safeLoad(frontmatter) || {};
|
||||
const parsedFields = transformAstToDisplayFields(
|
||||
parseFields(config.fields || defaults?.fields.join(',')),
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { parseConfig } from './config';
|
||||
import jsYaml from 'js-yaml';
|
||||
import { parseYAMLConfig } from './config';
|
||||
import { parseQuery } from './query';
|
||||
|
||||
const DEFAULT_DISPLAY_FIELDS = ['title'];
|
||||
|
||||
export const parseQueryText = (text) => {
|
||||
export const parseQueryTextWithFrontmatter = (text) => {
|
||||
const frontmatter = text.match(/---\n([\s\S]*?)\n---/);
|
||||
const remaining = text.replace(frontmatter ? frontmatter[0] : '', '');
|
||||
return {
|
||||
|
|
@ -12,9 +13,17 @@ export const parseQueryText = (text) => {
|
|||
};
|
||||
};
|
||||
|
||||
const isValidYAML = (text) => typeof jsYaml.safeLoad(text) === 'object';
|
||||
|
||||
export const parse = async (glqlQuery, target = 'graphql') => {
|
||||
const { frontmatter, query } = parseQueryText(glqlQuery);
|
||||
const config = parseConfig(frontmatter, { fields: DEFAULT_DISPLAY_FIELDS });
|
||||
let { frontmatter: config, query } = parseQueryTextWithFrontmatter(glqlQuery);
|
||||
if (!config && isValidYAML(glqlQuery)) {
|
||||
// if frontmatter isn't present, query is a part of the config
|
||||
({ query, ...config } = parseYAMLConfig(glqlQuery, { fields: DEFAULT_DISPLAY_FIELDS }));
|
||||
} else {
|
||||
config = parseYAMLConfig(config, { fields: DEFAULT_DISPLAY_FIELDS });
|
||||
}
|
||||
|
||||
const limit = parseInt(config.limit, 10) || undefined;
|
||||
|
||||
return { query: await parseQuery(query, { ...config, target, limit }), config };
|
||||
|
|
|
|||
|
|
@ -3,14 +3,12 @@ import * as Sentry from '~/sentry/sentry_browser_wrapper';
|
|||
import { makeLoadCandidatesErrorMessage, NO_CANDIDATES_LABEL } from '../translations';
|
||||
import getModelCandidatesQuery from '../graphql/queries/get_model_candidates.query.graphql';
|
||||
import { GRAPHQL_PAGE_SIZE } from '../constants';
|
||||
import SearchableList from './searchable_list.vue';
|
||||
import CandidateListRow from './candidate_list_row.vue';
|
||||
import SearchableTable from './searchable_table.vue';
|
||||
|
||||
export default {
|
||||
name: 'MlCandidateList',
|
||||
components: {
|
||||
CandidateListRow,
|
||||
SearchableList,
|
||||
SearchableTable,
|
||||
},
|
||||
props: {
|
||||
modelId: {
|
||||
|
|
@ -74,21 +72,16 @@ export default {
|
|||
};
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<searchable-list
|
||||
:page-info="pageInfo"
|
||||
:items="items"
|
||||
:error-message="errorMessage"
|
||||
:is-loading="isLoading"
|
||||
@fetch-page="fetchPage"
|
||||
>
|
||||
<template #empty-state>
|
||||
{{ $options.i18n.NO_CANDIDATES_LABEL }}
|
||||
</template>
|
||||
|
||||
<template #item="{ item }">
|
||||
<candidate-list-row :candidate="item" />
|
||||
</template>
|
||||
</searchable-list>
|
||||
</div>
|
||||
<searchable-table
|
||||
:candidates="items"
|
||||
:page-info="pageInfo"
|
||||
:error-message="errorMessage"
|
||||
:is-loading="isLoading"
|
||||
:can-write-model-registry="false"
|
||||
@fetch-page="fetchPage"
|
||||
>
|
||||
<template #empty-state>
|
||||
{{ $options.i18n.NO_CANDIDATES_LABEL }}
|
||||
</template>
|
||||
</searchable-table>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
<script>
|
||||
import { GlTableLite, GlLink, GlBadge } from '@gitlab/ui';
|
||||
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
import { s__ } from '~/locale';
|
||||
|
||||
export default {
|
||||
name: 'CandidatesTable',
|
||||
components: {
|
||||
GlTableLite,
|
||||
TimeAgoTooltip,
|
||||
GlLink,
|
||||
GlBadge,
|
||||
},
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
statusBadge(status) {
|
||||
return {
|
||||
success: 'success',
|
||||
failed: 'danger',
|
||||
pending: 'info',
|
||||
running: 'info',
|
||||
canceled: 'warning',
|
||||
}[status];
|
||||
},
|
||||
hasCiJob(item) {
|
||||
return item.ciJob;
|
||||
},
|
||||
},
|
||||
tableFields: [
|
||||
{ key: 'eid', label: s__('ModelRegistry|MLflow Run ID'), thClass: 'gl-w-2/8' },
|
||||
{ key: 'ciJob', label: s__('ModelRegistry|CI Job'), thClass: 'gl-w-1/8' },
|
||||
{ key: 'createdAt', label: s__('ModelRegistry|Created'), thClass: 'gl-w-1/8' },
|
||||
{ key: 'status', label: s__('ModelRegistry|Status'), thClass: 'gl-w-1/8' },
|
||||
],
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-table-lite class="fixed" :items="items" :fields="$options.tableFields" stacked="sm">
|
||||
<template #cell(eid)="{ item }">
|
||||
<gl-link :href="item._links.showPath">
|
||||
{{ item.eid }}
|
||||
</gl-link>
|
||||
</template>
|
||||
<template #cell(ciJob)="{ item }">
|
||||
<span v-if="hasCiJob(item)">{{ item.ciJob.name }}</span>
|
||||
</template>
|
||||
<template #cell(createdAt)="{ item: { createdAt } }">
|
||||
<time-ago-tooltip v-if="createdAt" :time="createdAt" />
|
||||
</template>
|
||||
<template #cell(status)="{ item }">
|
||||
<gl-badge :variant="statusBadge(item.status)">
|
||||
{{ item.status }}
|
||||
</gl-badge>
|
||||
</template>
|
||||
</gl-table-lite>
|
||||
</template>
|
||||
|
|
@ -7,6 +7,7 @@ import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_ba
|
|||
import LoadOrErrorOrShow from '~/ml/model_registry/components/load_or_error_or_show.vue';
|
||||
import ModelsTable from '~/ml/model_registry/components/models_table.vue';
|
||||
import ModelVersionsTable from '~/ml/model_registry/components/model_versions_table.vue';
|
||||
import CandidatesTable from '~/ml/model_registry/components/candidates_table.vue';
|
||||
|
||||
export default {
|
||||
name: 'SearchableTable',
|
||||
|
|
@ -16,6 +17,7 @@ export default {
|
|||
GlKeysetPagination,
|
||||
ModelsTable,
|
||||
ModelVersionsTable,
|
||||
CandidatesTable,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip,
|
||||
|
|
@ -31,6 +33,11 @@ export default {
|
|||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
candidates: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
pageInfo: {
|
||||
type: Object,
|
||||
required: true,
|
||||
|
|
@ -77,15 +84,6 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
isModelVersionsEmpty() {
|
||||
return this.modelVersions.length === 0;
|
||||
},
|
||||
isModelsEmpty() {
|
||||
return this.models.length === 0;
|
||||
},
|
||||
isListEmpty() {
|
||||
return this.isModelVersionsEmpty && this.isModelsEmpty;
|
||||
},
|
||||
parsedQuery() {
|
||||
const name = this.filters
|
||||
.map((f) => f.value.data)
|
||||
|
|
@ -161,19 +159,20 @@ export default {
|
|||
@filter:clear="filters = []"
|
||||
/>
|
||||
<load-or-error-or-show :is-loading="isLoading" :error-message="errorMessage">
|
||||
<slot v-if="isListEmpty" name="empty-state"></slot>
|
||||
<model-versions-table
|
||||
v-else-if="!isModelVersionsEmpty"
|
||||
v-if="modelVersions.length"
|
||||
:items="modelVersions"
|
||||
can-write-model-registry
|
||||
@model-versions-update="submitFilters"
|
||||
/>
|
||||
<models-table
|
||||
v-else-if="!isModelsEmpty"
|
||||
v-else-if="models.length"
|
||||
:items="models"
|
||||
can-write-model-registry
|
||||
@models-update="submitFilters"
|
||||
/>
|
||||
<candidates-table v-else-if="candidates.length" :items="candidates" />
|
||||
<slot v-else name="empty-state"></slot>
|
||||
<gl-keyset-pagination
|
||||
v-if="pageInfo.hasPreviousPage || pageInfo.hasNextPage"
|
||||
v-bind="pageInfo"
|
||||
|
|
|
|||
|
|
@ -11,7 +11,13 @@ query getModelCandidates(
|
|||
count
|
||||
nodes {
|
||||
id
|
||||
eid
|
||||
ciJob {
|
||||
id
|
||||
name
|
||||
}
|
||||
name
|
||||
status
|
||||
createdAt
|
||||
_links {
|
||||
showPath
|
||||
|
|
|
|||
|
|
@ -158,7 +158,7 @@ export default {
|
|||
@mouseleave="$emit('mouseleave')"
|
||||
>
|
||||
<ul
|
||||
class="gl-min-w-20 gl-max-w-34 gl-list-none gl-rounded-base gl-border-1 gl-border-solid gl-border-default gl-bg-white gl-p-2 gl-pb-1 gl-shadow-md"
|
||||
class="gl-min-w-20 gl-max-w-34 gl-list-none gl-rounded-base gl-border-1 gl-border-solid gl-border-default gl-bg-overlap gl-p-2 gl-pb-1 gl-shadow-md"
|
||||
@mouseenter="showSVG = false"
|
||||
>
|
||||
<nav-item
|
||||
|
|
|
|||
|
|
@ -145,8 +145,8 @@ export default {
|
|||
{{ item.title }}
|
||||
</span>
|
||||
|
||||
<span class="gl-text-right gl-text-gray-400">
|
||||
<gl-icon class="super-sidebar-mix-blend-mode" :name="collapseIcon" />
|
||||
<span class="gl-text-right">
|
||||
<gl-icon class="super-sidebar-mix-blend-mode" :name="collapseIcon" variant="subtle" />
|
||||
</span>
|
||||
</button>
|
||||
|
||||
|
|
|
|||
|
|
@ -353,51 +353,53 @@ export default {
|
|||
<div
|
||||
class="vue-filtered-search-bar-container gl-flex gl-min-w-0 gl-flex-col sm:gl-flex-row sm:gl-gap-3"
|
||||
>
|
||||
<gl-form-checkbox
|
||||
v-if="showCheckbox"
|
||||
class="gl-self-center"
|
||||
:checked="checkboxChecked"
|
||||
@change="$emit('checked-input', $event)"
|
||||
>
|
||||
<span class="gl-sr-only">{{ __('Select all') }}</span>
|
||||
</gl-form-checkbox>
|
||||
<gl-filtered-search
|
||||
ref="filteredSearchInput"
|
||||
v-model="filterValue"
|
||||
:placeholder="searchInputPlaceholder"
|
||||
:available-tokens="tokens"
|
||||
:history-items="filteredRecentSearches"
|
||||
:suggestions-list-class="suggestionsListClass"
|
||||
:search-button-attributes="searchButtonAttributes"
|
||||
:search-input-attributes="searchInputAttributes"
|
||||
:recent-searches-header="__('Recent searches')"
|
||||
:clear-button-title="__('Clear')"
|
||||
:close-button-title="__('Close')"
|
||||
:clear-recent-searches-text="__('Clear recent searches')"
|
||||
:no-recent-searches-text="__(`You don't have any recent searches`)"
|
||||
:search-text-option-label="searchTextOptionLabel"
|
||||
:show-friendly-text="showFriendlyText"
|
||||
:show-search-button="showSearchButton"
|
||||
:terms-as-tokens="termsAsTokens"
|
||||
class="flex-grow-1"
|
||||
@history-item-selected="handleHistoryItemSelected"
|
||||
@clear="onClear"
|
||||
@clear-history="handleClearHistory"
|
||||
@submit="handleFilterSubmit"
|
||||
@input="onInput"
|
||||
>
|
||||
<template #history-item="{ historyItem }">
|
||||
<template v-for="(token, index) in historyItem">
|
||||
<span v-if="typeof token === 'string'" :key="index" class="gl-px-1">"{{ token }}"</span>
|
||||
<span v-else :key="`${index}-${token.type}-${token.value.data}`" class="gl-px-1">
|
||||
<span v-if="tokenTitles[token.type]"
|
||||
>{{ tokenTitles[token.type] }} :{{ token.value.operator }}</span
|
||||
>
|
||||
<strong>{{ tokenSymbols[token.type] }}{{ historyTokenOptionTitle(token) }}</strong>
|
||||
</span>
|
||||
<div class="flex-grow-1 gl-flex gl-gap-3">
|
||||
<gl-form-checkbox
|
||||
v-if="showCheckbox"
|
||||
class="gl-min-h-0 gl-self-center"
|
||||
:checked="checkboxChecked"
|
||||
@change="$emit('checked-input', $event)"
|
||||
>
|
||||
<span class="gl-sr-only">{{ __('Select all') }}</span>
|
||||
</gl-form-checkbox>
|
||||
<gl-filtered-search
|
||||
ref="filteredSearchInput"
|
||||
v-model="filterValue"
|
||||
:placeholder="searchInputPlaceholder"
|
||||
:available-tokens="tokens"
|
||||
:history-items="filteredRecentSearches"
|
||||
:suggestions-list-class="suggestionsListClass"
|
||||
:search-button-attributes="searchButtonAttributes"
|
||||
:search-input-attributes="searchInputAttributes"
|
||||
:recent-searches-header="__('Recent searches')"
|
||||
:clear-button-title="__('Clear')"
|
||||
:close-button-title="__('Close')"
|
||||
:clear-recent-searches-text="__('Clear recent searches')"
|
||||
:no-recent-searches-text="__(`You don't have any recent searches`)"
|
||||
:search-text-option-label="searchTextOptionLabel"
|
||||
:show-friendly-text="showFriendlyText"
|
||||
:show-search-button="showSearchButton"
|
||||
:terms-as-tokens="termsAsTokens"
|
||||
class="flex-grow-1"
|
||||
@history-item-selected="handleHistoryItemSelected"
|
||||
@clear="onClear"
|
||||
@clear-history="handleClearHistory"
|
||||
@submit="handleFilterSubmit"
|
||||
@input="onInput"
|
||||
>
|
||||
<template #history-item="{ historyItem }">
|
||||
<template v-for="(token, index) in historyItem">
|
||||
<span v-if="typeof token === 'string'" :key="index" class="gl-px-1">"{{ token }}"</span>
|
||||
<span v-else :key="`${index}-${token.type}-${token.value.data}`" class="gl-px-1">
|
||||
<span v-if="tokenTitles[token.type]"
|
||||
>{{ tokenTitles[token.type] }} :{{ token.value.operator }}</span
|
||||
>
|
||||
<strong>{{ tokenSymbols[token.type] }}{{ historyTokenOptionTitle(token) }}</strong>
|
||||
</span>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</gl-filtered-search>
|
||||
</gl-filtered-search>
|
||||
</div>
|
||||
<gl-sorting
|
||||
v-if="selectedSortOption"
|
||||
:sort-options="transformedSortOptions"
|
||||
|
|
|
|||
|
|
@ -344,6 +344,7 @@ export default {
|
|||
>
|
||||
<gl-form-checkbox
|
||||
v-if="showCheckbox"
|
||||
class="gl-pr-3 gl-pt-2"
|
||||
:checked="checked"
|
||||
:data-id="issuableId"
|
||||
:data-iid="issuableIid"
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@
|
|||
border: 0;
|
||||
|
||||
background-color: var(--rd-expand-lines-button-background-color, $gray-100);
|
||||
color: var(--rd-expand-lines-button-color, $gray-700);
|
||||
color: var(--rd-expand-lines-button-color, var(--gl-text-color-subtle));
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ table {
|
|||
.loading {
|
||||
margin: 20px auto;
|
||||
height: 40px;
|
||||
color: $gray-700;
|
||||
@apply gl-text-subtle;
|
||||
font-size: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,8 @@ $crud-header-min-height: px-to-rem(49px);
|
|||
}
|
||||
|
||||
table.b-table-stacked-sm,
|
||||
table.b-table-stacked-md {
|
||||
table.b-table-stacked-md,
|
||||
table.b-table-stacked-lg {
|
||||
margin-bottom: 0;
|
||||
|
||||
tr:first-of-type th {
|
||||
|
|
@ -78,6 +79,12 @@ $crud-header-min-height: px-to-rem(49px);
|
|||
@include new-card-table-adjustments;
|
||||
}
|
||||
}
|
||||
|
||||
table.gl-table.b-table.b-table-stacked-lg {
|
||||
@include gl-media-breakpoint-down(lg) {
|
||||
@include new-card-table-adjustments;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.crud-body:has(.content-list) {
|
||||
|
|
|
|||
|
|
@ -629,7 +629,7 @@
|
|||
padding-bottom: 0;
|
||||
padding-left: $gl-spacing-scale-3;
|
||||
padding-right: $gl-spacing-scale-3;
|
||||
color: $gray-700;
|
||||
@apply gl-text-subtle;
|
||||
line-height: 30px;
|
||||
border: 1px solid var(--gl-control-border-color-default);
|
||||
border-radius: $gl-border-radius-base;
|
||||
|
|
@ -641,7 +641,7 @@
|
|||
|
||||
&:hover {
|
||||
~ .dropdown-input-clear {
|
||||
color: $gray-700;
|
||||
@apply gl-text-subtle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -833,15 +833,3 @@
|
|||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
// Overriding the following rule with the negative margin
|
||||
// https://gitlab.com/gitlab-org/gitlab/-/blob/146c43c931c3743a140529307aea616e4aa9ff21/app/assets/stylesheets/framework/sidebar.scss#L1-5
|
||||
.container-fluid {
|
||||
.issuable-list,
|
||||
.issues-filters,
|
||||
.epics-filters {
|
||||
margin: 0 (-$gl-padding);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
max-width: 900px;
|
||||
margin: 0 auto 10px;
|
||||
}
|
||||
|
||||
|
||||
.div-dropzone {
|
||||
textarea.note-textarea {
|
||||
resize: none !important;
|
||||
|
|
@ -39,7 +39,7 @@
|
|||
|
||||
.zen-control {
|
||||
padding: 0;
|
||||
color: $gray-700;
|
||||
@apply gl-text-subtle;
|
||||
background: none;
|
||||
border: 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@
|
|||
}
|
||||
|
||||
.badge.badge-pill {
|
||||
color: var(--ide-text-color, $gray-700);
|
||||
color: var(--ide-text-color, var(--gl-icon-color-subtle));
|
||||
background-color: var(--ide-background, $badge-bg);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@
|
|||
}
|
||||
|
||||
.milestone {
|
||||
color: var(--gray-700, $gray-700);
|
||||
@apply gl-text-subtle;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@
|
|||
.save-project-loader {
|
||||
margin-top: 50px;
|
||||
margin-bottom: 50px;
|
||||
color: var(--gray-700, $gray-700);
|
||||
@apply gl-text-subtle;
|
||||
}
|
||||
|
||||
.deploy-key {
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@
|
|||
|
||||
.settings-header {
|
||||
position: relative;
|
||||
padding: $gl-spacing-scale-5 110px 0 0;
|
||||
padding: $gl-spacing-scale-5 $gl-spacing-scale-7 0 0;
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
|
|
@ -94,7 +94,7 @@
|
|||
position: relative;
|
||||
max-height: 1px;
|
||||
overflow-y: hidden;
|
||||
padding-right: 110px;
|
||||
@apply gl-pr-7;
|
||||
animation: collapseMaxHeight 300ms ease-out;
|
||||
// Keep the section from expanding when we scroll over it
|
||||
pointer-events: none;
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
.save-group-loader {
|
||||
margin-top: $gl-spacing-scale-9;
|
||||
margin-bottom: $gl-spacing-scale-9;
|
||||
color: $gray-700;
|
||||
@apply gl-text-subtle;
|
||||
}
|
||||
|
||||
.card {
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@
|
|||
|
||||
position: relative;
|
||||
font-size: inherit;
|
||||
color: $gray-700;
|
||||
@apply gl-text-subtle;
|
||||
padding-top: $gl-spacing-scale-3;
|
||||
padding-bottom: $gl-spacing-scale-3;
|
||||
padding-left: $gl-spacing-scale-6;
|
||||
|
|
|
|||
|
|
@ -89,11 +89,9 @@ class PagesDeployment < ApplicationRecord
|
|||
end
|
||||
|
||||
def url
|
||||
base_url = ::Gitlab::Pages::UrlBuilder
|
||||
.new(project)
|
||||
::Gitlab::Pages::UrlBuilder
|
||||
.new(project, { path_prefix: path_prefix.to_s })
|
||||
.pages_url
|
||||
|
||||
File.join(base_url.to_s, path_prefix.to_s)
|
||||
end
|
||||
|
||||
def deactivate
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ module Projects
|
|||
validate :validate_protected_branch_not_wildcard
|
||||
validate :validate_protected_branch_belongs_to_project
|
||||
|
||||
def self.declarative_policy_class
|
||||
'Projects::BranchRulePolicy'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_protected_branch_not_wildcard
|
||||
|
|
|
|||
|
|
@ -2548,9 +2548,13 @@ class User < ApplicationRecord
|
|||
true
|
||||
end
|
||||
|
||||
# Deprecated method. We are currently transitioning to the use of composite_identity_enforced attribute
|
||||
def has_composite_identity?
|
||||
false
|
||||
# Since this is called in a number of places in both Sidekiq and Web,
|
||||
# be extra paranoid that this column exists before reading it. This check
|
||||
# can be removed in GitLab 17.8 or later.
|
||||
return false unless has_attribute?(:composite_identity_enforced)
|
||||
|
||||
composite_identity_enforced
|
||||
end
|
||||
|
||||
def composite_identity_enforced
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'resolv'
|
||||
|
||||
class VerifyPagesDomainService < BaseService
|
||||
class VerifyPagesDomainService
|
||||
# The maximum number of seconds to be spent on each DNS lookup
|
||||
RESOLVER_TIMEOUT_SECONDS = 15
|
||||
|
||||
|
|
@ -17,7 +17,9 @@ class VerifyPagesDomainService < BaseService
|
|||
end
|
||||
|
||||
def execute
|
||||
return error("No verification code set for #{domain.domain}") unless domain.verification_code.present?
|
||||
unless domain.verification_code.present?
|
||||
return ServiceResponse.error(message: "No verification code set for #{domain.domain}")
|
||||
end
|
||||
|
||||
if !verification_enabled? || dns_record_present?
|
||||
verify_domain!
|
||||
|
|
@ -48,7 +50,7 @@ class VerifyPagesDomainService < BaseService
|
|||
|
||||
after_successful_verification
|
||||
|
||||
success
|
||||
ServiceResponse.success
|
||||
end
|
||||
|
||||
def after_successful_verification
|
||||
|
|
@ -64,7 +66,7 @@ class VerifyPagesDomainService < BaseService
|
|||
|
||||
notify(:verification_failed) if was_verified
|
||||
|
||||
error("Couldn't verify #{domain.domain}")
|
||||
ServiceResponse.error(message: "Couldn't verify #{domain.domain}")
|
||||
end
|
||||
|
||||
def disable_domain!
|
||||
|
|
@ -74,7 +76,7 @@ class VerifyPagesDomainService < BaseService
|
|||
|
||||
notify(:disabled)
|
||||
|
||||
error("Couldn't verify #{domain.domain}. It is now disabled.")
|
||||
ServiceResponse.error(message: "Couldn't verify #{domain.domain}. It is now disabled.")
|
||||
end
|
||||
|
||||
# A domain is only expired until `disable!` has been called
|
||||
|
|
@ -118,7 +120,7 @@ class VerifyPagesDomainService < BaseService
|
|||
return unless verification_enabled?
|
||||
|
||||
Gitlab::AppLogger.info("Pages domain '#{domain.domain}' changed state to '#{type}'")
|
||||
notification_service.public_send("pages_domain_#{type}", domain) # rubocop:disable GitlabSecurity/PublicSend
|
||||
NotificationService.new.public_send("pages_domain_#{type}", domain) # rubocop:disable GitlabSecurity/PublicSend -- Technical debt
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
= s_("ProtectedBranch|There are currently no protected branches, to protect a branch start by creating a new one above.")
|
||||
- else
|
||||
.flash-container
|
||||
%table.table.b-table.gl-table.b-table-stacked-md
|
||||
%table.table.b-table.gl-table.b-table-stacked-lg
|
||||
%colgroup
|
||||
%col{ width: "30%" }
|
||||
%col{ width: "20%" }
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
= render Pajamas::ButtonComponent.new(href: update_milestone_path(milestone, { state_event: :close }), method: :put, button_options: { class: 'btn-close gl-hidden md:gl-inline-flex' }) do
|
||||
= _('Close milestone')
|
||||
- else
|
||||
= render Pajamas::ButtonComponent.new(href: update_milestone_path(milestone, { state_event: :activate }), method: :put, button_options: { class: 'gl-hidden md:gl-inline-block' }) do
|
||||
= render Pajamas::ButtonComponent.new(href: update_milestone_path(milestone, { state_event: :activate }), method: :put, button_options: { class: 'gl-hidden md:gl-inline-flex' }) do
|
||||
= _('Reopen milestone')
|
||||
|
||||
.js-vue-milestone-actions{ data: { id: milestone.id,
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
---
|
||||
name: admin_duo_page_configuration_settings
|
||||
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/493383
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/168702
|
||||
rollout_issue_url:
|
||||
milestone: '17.7'
|
||||
group: group::ai framework
|
||||
type: wip
|
||||
default_enabled: false
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
---
|
||||
name: group_duo_page_configuration_settings
|
||||
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/493383
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/168581
|
||||
rollout_issue_url:
|
||||
milestone: '17.7'
|
||||
group: group::ai framework
|
||||
type: wip
|
||||
default_enabled: false
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddAssignDuoSeatsToSamlGroupLinks < Gitlab::Database::Migration[2.2]
|
||||
disable_ddl_transaction!
|
||||
milestone '17.7'
|
||||
|
||||
def up
|
||||
add_column :saml_group_links, :assign_duo_seats, :boolean, null: false, default: false, if_not_exists: true
|
||||
end
|
||||
|
||||
def down
|
||||
remove_column :saml_group_links, :assign_duo_seats
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
f2da82c29d69c98179828263b4a12faccbbb41d816d2b939ad93c8db7782d158
|
||||
|
|
@ -19403,6 +19403,7 @@ CREATE TABLE saml_group_links (
|
|||
updated_at timestamp with time zone NOT NULL,
|
||||
saml_group_name text NOT NULL,
|
||||
member_role_id bigint,
|
||||
assign_duo_seats boolean DEFAULT false NOT NULL,
|
||||
CONSTRAINT check_1b3fc49d1e CHECK ((char_length(saml_group_name) <= 255))
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -4,34 +4,28 @@ group: Import and Integrate
|
|||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Project integration administration
|
||||
# Integration administration
|
||||
|
||||
DETAILS:
|
||||
**Tier:** Free, Premium, Ultimate
|
||||
**Offering:** Self-managed
|
||||
**Offering:** Self-managed, GitLab Dedicated
|
||||
|
||||
NOTE:
|
||||
This page contains administrator documentation for project integrations. For user documentation, see [Project integrations](../../user/project/integrations/index.md).
|
||||
This page contains administrator documentation for project and group integrations. For user documentation, see [Project integrations](../../user/project/integrations/index.md).
|
||||
|
||||
Project integrations can be configured and enabled by project administrators. As a GitLab instance
|
||||
administrator, you can set default configuration parameters for a given integration that all projects
|
||||
can inherit and use, enabling the integration for all projects that are not already using custom
|
||||
settings.
|
||||
Project and group administrators can configure and enable integrations.
|
||||
As an instance administrator, you can:
|
||||
|
||||
You can update these default settings at any time, changing the settings used for all projects that
|
||||
are set to use instance-level or group-level defaults. Updating the default settings also enables the integration
|
||||
for all projects that didn't have it already enabled.
|
||||
- Set default configuration parameters for an integration.
|
||||
- Configure an allowlist to control which integrations can be enabled on a GitLab instance.
|
||||
|
||||
Only the entire settings for an integration can be inherited. Per-field inheritance
|
||||
is proposed in [epic 2137](https://gitlab.com/groups/gitlab-org/-/epics/2137).
|
||||
|
||||
## Manage instance-level default settings for a project integration
|
||||
## Configure default settings for an integration
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must have administrator access to the instance.
|
||||
|
||||
To manage instance-level default settings for a project integration:
|
||||
To configure default settings for an integration:
|
||||
|
||||
1. On the left sidebar, at the bottom, select **Admin**.
|
||||
1. Select **Settings > Integrations**.
|
||||
|
|
@ -66,13 +60,13 @@ instead of the instance-level settings.
|
|||
Only the entire settings for an integration can be inherited. Per-field inheritance
|
||||
is proposed in [epic 2137](https://gitlab.com/groups/gitlab-org/-/epics/2137).
|
||||
|
||||
### Remove an instance-level default setting
|
||||
### Remove default settings for an integration
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must have administrator access to the instance.
|
||||
|
||||
To remove an instance-level default setting:
|
||||
To remove default settings for an integration:
|
||||
|
||||
1. On the left sidebar, at the bottom, select **Admin**.
|
||||
1. Select **Settings > Integrations**.
|
||||
|
|
@ -93,3 +87,49 @@ To view projects in your instance that [use custom settings](../../user/project/
|
|||
1. Select **Settings > Integrations**.
|
||||
1. Select an integration.
|
||||
1. Select the **Projects using custom settings** tab.
|
||||
|
||||
## Integration allowlist
|
||||
|
||||
DETAILS:
|
||||
**Tier:** Ultimate
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/500610) in GitLab 17.7.
|
||||
|
||||
By default, project and group administrators can enable integrations.
|
||||
However, instance administrators can configure an allowlist to control
|
||||
which integrations can be enabled on a GitLab instance.
|
||||
|
||||
Enabled integrations that are later blocked by the allowlist settings are disabled.
|
||||
If these integrations are allowed again, they are re-enabled with their existing configuration.
|
||||
|
||||
If you configure an empty allowlist, no integrations are allowed on the instance.
|
||||
After you configure an allowlist, new GitLab integrations are not on the allowlist by default.
|
||||
|
||||
### Allow some integrations
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must have administrator access to the instance.
|
||||
|
||||
To allow only integrations on the allowlist:
|
||||
|
||||
1. On the left sidebar, at the bottom, select **Admin**.
|
||||
1. Select **Settings > General**.
|
||||
1. Expand the **Integration settings** section.
|
||||
1. Select **Allow only integrations on this allowlist**.
|
||||
1. Select the checkbox for each integration you want to allow on the instance.
|
||||
1. Select **Save changes**.
|
||||
|
||||
### Allow all integrations
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must have administrator access to the instance.
|
||||
|
||||
To allow all integrations on a GitLab instance:
|
||||
|
||||
1. On the left sidebar, at the bottom, select **Admin**.
|
||||
1. Select **Settings > General**.
|
||||
1. Expand the **Integration settings** section.
|
||||
1. Select **Allow all integrations**.
|
||||
1. Select **Save changes**.
|
||||
|
|
|
|||
|
|
@ -19908,6 +19908,7 @@ Branch rules configured for a rule target.
|
|||
| <a id="branchruleisprotected"></a>`isProtected` | [`Boolean!`](#boolean) | Check if this branch rule protects access for the branch. |
|
||||
| <a id="branchrulematchingbranchescount"></a>`matchingBranchesCount` | [`Int!`](#int) | Number of existing branches that match this branch rule. |
|
||||
| <a id="branchrulename"></a>`name` | [`String!`](#string) | Name of the branch rule target. Includes wildcards. |
|
||||
| <a id="branchrulesquashoption"></a>`squashOption` **{warning-solid}** | [`SquashOption`](#squashoption) | **Introduced** in GitLab 17.7. **Status**: Experiment. The default behavior for squashing in merge requests. Returns null if `branch_rule_squash_settings` feature flag is disabled. |
|
||||
| <a id="branchruleupdatedat"></a>`updatedAt` | [`Time`](#time) | Timestamp of when the branch rule was last updated. |
|
||||
|
||||
### `BurnupChartDailyTotals`
|
||||
|
|
@ -34972,6 +34973,17 @@ Represents the Geo sync and verification state of a snippet repository.
|
|||
| <a id="snippetrepositoryregistryverificationstate"></a>`verificationState` | [`VerificationStateEnum`](#verificationstateenum) | Verification state of the SnippetRepositoryRegistry. |
|
||||
| <a id="snippetrepositoryregistryverifiedat"></a>`verifiedAt` | [`Time`](#time) | Timestamp of the most recent successful verification of the SnippetRepositoryRegistry. |
|
||||
|
||||
### `SquashOption`
|
||||
|
||||
Squash option overrides for a protected branch.
|
||||
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="squashoptionhelptext"></a>`helpText` | [`String!`](#string) | Help text for the squash option. |
|
||||
| <a id="squashoptionoption"></a>`option` | [`String!`](#string) | Human-readable description of the squash option. |
|
||||
|
||||
### `SshSignature`
|
||||
|
||||
SSH signature for a signed commit.
|
||||
|
|
|
|||
|
|
@ -775,16 +775,13 @@ GET /projects/:id/integrations/external-wiki
|
|||
|
||||
DETAILS:
|
||||
**Tier:** Premium, Ultimate
|
||||
**Offering:** Self-managed, GitLab Dedicated
|
||||
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/435706) in GitLab 16.9 [with a flag](../administration/feature_flags.md) named `git_guardian_integration`. Enabled by default. Disabled on GitLab.com.
|
||||
> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/438695#note_2226917025) in GitLab 17.7.
|
||||
> - `use_inherited_settings` parameter [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/467089) in GitLab 17.2 [with a flag](../administration/feature_flags.md) named `integration_api_inheritance`. Disabled by default.
|
||||
> - `use_inherited_settings` parameter [generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/467186) in GitLab 17.3. Feature flag `integration_api_inheritance` removed.
|
||||
|
||||
FLAG:
|
||||
On self-managed GitLab, by default this feature is available. To hide the feature, ask an administrator to [disable the feature flag](../administration/feature_flags.md) named `git_guardian_integration`.
|
||||
On GitLab.com, this feature is not available. On GitLab Dedicated, this feature is available.
|
||||
|
||||
[GitGuardian](https://www.gitguardian.com/) is a cybersecurity service that detects sensitive data such as API keys
|
||||
and passwords in source code repositories.
|
||||
It scans Git repositories, alerts on policy violations, and helps organizations
|
||||
|
|
|
|||
|
|
@ -480,7 +480,7 @@ DETAILS:
|
|||
**Tier:** Free, Premium, Ultimate
|
||||
**Offering:** Self-managed, GitLab Dedicated
|
||||
|
||||
See the [User tokens API](user_tokens.md#create-a-personal-access-token-with-limited-scopes-for-your-account) for
|
||||
See the [User tokens API](user_tokens.md#create-a-personal-access-token) for
|
||||
information on creating a personal access token for the currently authenticated user.
|
||||
|
||||
## Troubleshooting access tokens
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ Use this API to perform follower actions for user accounts. For more information
|
|||
|
||||
## Follow a user
|
||||
|
||||
Follow a user.
|
||||
Follow a given user account.
|
||||
|
||||
```plaintext
|
||||
POST /users/:id/follow
|
||||
|
|
@ -24,7 +24,7 @@ Supported attributes:
|
|||
|
||||
| Attribute | Type | Required | Description |
|
||||
|:----------|:--------|:---------|:------------|
|
||||
| `id` | integer | yes | ID of the user to follow |
|
||||
| `id` | integer | yes | ID of user account |
|
||||
|
||||
Example request:
|
||||
|
||||
|
|
@ -48,7 +48,7 @@ Example response:
|
|||
|
||||
## Unfollow a user
|
||||
|
||||
Unfollow a user.
|
||||
Unfollow a given user account.
|
||||
|
||||
```plaintext
|
||||
POST /users/:id/unfollow
|
||||
|
|
@ -58,7 +58,7 @@ Supported attributes:
|
|||
|
||||
| Attribute | Type | Required | Description |
|
||||
|:----------|:--------|:---------|:------------|
|
||||
| `id` | integer | yes | ID of the user to unfollow |
|
||||
| `id` | integer | yes | ID of user account |
|
||||
|
||||
Example request:
|
||||
|
||||
|
|
@ -66,9 +66,9 @@ Example request:
|
|||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/users/3/unfollow"
|
||||
```
|
||||
|
||||
## Get the followers of a user
|
||||
## List all accounts that follow a user
|
||||
|
||||
Get the followers of a user.
|
||||
Lists all users accounts that follow a given user.
|
||||
|
||||
```plaintext
|
||||
GET /users/:id/followers
|
||||
|
|
@ -78,7 +78,7 @@ Supported attributes:
|
|||
|
||||
| Attribute | Type | Required | Description |
|
||||
|:----------|:--------|:---------|:------------|
|
||||
| `id` | integer | yes | ID of the user |
|
||||
| `id` | integer | yes | ID of user account |
|
||||
|
||||
Example request:
|
||||
|
||||
|
|
@ -111,9 +111,9 @@ Example response:
|
|||
]
|
||||
```
|
||||
|
||||
## Get the users that a user is following
|
||||
## List all accounts followed by a user
|
||||
|
||||
Get the list of users being followed by a user.
|
||||
Lists all users accounts being followed by a given user.
|
||||
|
||||
```plaintext
|
||||
GET /users/:id/following
|
||||
|
|
@ -123,7 +123,7 @@ Supported attributes:
|
|||
|
||||
| Attribute | Type | Required | Description |
|
||||
|:----------|:--------|:---------|:------------|
|
||||
| `id` | integer | yes | ID of the user |
|
||||
| `id` | integer | yes | ID of user account |
|
||||
|
||||
Example request:
|
||||
|
||||
|
|
|
|||
|
|
@ -12,16 +12,17 @@ DETAILS:
|
|||
|
||||
Use this API to interact with personal access tokens and impersonation tokens. For more information, see [personal access tokens](../user/profile/personal_access_tokens.md) and [impersonation tokens](rest/authentication.md#impersonation-tokens).
|
||||
|
||||
## Create a personal access token
|
||||
## Create a personal access token for a user
|
||||
|
||||
> - The `expires_at` attribute default was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/120213) in GitLab 16.0.
|
||||
|
||||
Create a new personal access token. Token values are returned once so, make sure you save it because you can't access it
|
||||
again.
|
||||
Creates a personal access token for a given user.
|
||||
|
||||
Token values are included with the response, but cannot be retrieved later.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must be an administrator.
|
||||
- You must have administrator access to the instance.
|
||||
|
||||
```plaintext
|
||||
POST /users/:user_id/personal_access_tokens
|
||||
|
|
@ -31,11 +32,11 @@ Supported attributes:
|
|||
|
||||
| Attribute | Type | Required | Description |
|
||||
|:-------------|:--------|:---------|:------------|
|
||||
| `user_id` | integer | yes | ID of the user. |
|
||||
| `name` | string | yes | Name of the personal access token. |
|
||||
| `description`| string | no | Description of the personal access token. |
|
||||
| `expires_at` | date | no | Expiration date of the access token in ISO format (`YYYY-MM-DD`). If no date is set, the expiration is set to the [maximum allowable lifetime of an access token](../user/profile/personal_access_tokens.md#access-token-expiration). |
|
||||
| `scopes` | array | yes | Array of scopes of the personal access token. See [personal access token scopes](../user/profile/personal_access_tokens.md#personal-access-token-scopes) for possible values. |
|
||||
| `user_id` | integer | yes | ID of user account |
|
||||
| `name` | string | yes | Name of personal access token |
|
||||
| `description`| string | no | Description of personal access token |
|
||||
| `expires_at` | date | no | Expiration date of the access token in ISO format (`YYYY-MM-DD`). If undefined, the date is set to the [maximum allowable lifetime limit](../user/profile/personal_access_tokens.md#access-token-expiration). |
|
||||
| `scopes` | array | yes | Array of approved scopes. For a list of possible values, see [Personal access token scopes](../user/profile/personal_access_tokens.md#personal-access-token-scopes): |
|
||||
|
||||
Example request:
|
||||
|
||||
|
|
@ -63,23 +64,21 @@ Example response:
|
|||
}
|
||||
```
|
||||
|
||||
## Create a personal access token with limited scopes for your account
|
||||
## Create a personal access token
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131923) in GitLab 16.5.
|
||||
|
||||
Create a new personal access token for your account.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must be authenticated.
|
||||
|
||||
For security purposes, the token:
|
||||
Creates a personal access token for your account. For security purposes, the token:
|
||||
|
||||
- Is limited to the [`k8s_proxy` scope](../user/profile/personal_access_tokens.md#personal-access-token-scopes).
|
||||
This scope grants permission to perform Kubernetes API calls using the agent for Kubernetes.
|
||||
- By default, expires at the end of the day it was created on.
|
||||
|
||||
Token values are returned once, so make sure you save the token because you cannot access it again.
|
||||
Token values are included with the response, but cannot be retrieved later.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must be authenticated.
|
||||
|
||||
```plaintext
|
||||
POST /user/personal_access_tokens
|
||||
|
|
@ -89,10 +88,10 @@ Supported attributes:
|
|||
|
||||
| Attribute | Type | Required | Description |
|
||||
|:-------------|:-------|:---------|:------------|
|
||||
| `name` | string | yes | Name of the personal access token. |
|
||||
| `description`| string | no | Description of the personal access token. |
|
||||
| `scopes` | array | yes | Array of scopes of the personal access token. Possible values are `k8s_proxy`. |
|
||||
| `expires_at` | array | no | Expiration date of the access token in ISO format (`YYYY-MM-DD`). If no date is set, the expiration is at the end of the current day. The expiration is subject to the [maximum allowable lifetime of an access token](../user/profile/personal_access_tokens.md#access-token-expiration). |
|
||||
| `name` | string | yes | Name of personal access token |
|
||||
| `description`| string | no | Description of personal access token |
|
||||
| `scopes` | array | yes | Array of approved scopes. Only accepts `k8s_proxy`. |
|
||||
| `expires_at` | array | no | Expiration date of the access token in ISO format (`YYYY-MM-DD`). If undefined, token expires at the end of the current day. Subject to the [maximum allowable lifetime limits](../user/profile/personal_access_tokens.md#access-token-expiration). |
|
||||
|
||||
Example request:
|
||||
|
||||
|
|
@ -119,14 +118,15 @@ Example response:
|
|||
}
|
||||
```
|
||||
|
||||
## Get all impersonation tokens of a user
|
||||
## List all impersonation tokens for a user
|
||||
|
||||
Retrieve every impersonation token of a user. Use the [pagination parameters](rest/index.md#offset-based-pagination)
|
||||
`page` and `per_page` to restrict the list of impersonation tokens.
|
||||
Lists all impersonation tokens for a given user.
|
||||
|
||||
Use the `page` and `per_page` [pagination parameters](rest/index.md#offset-based-pagination) to filter the results.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must be an administrator.
|
||||
- You must have administrator access to the instance.
|
||||
|
||||
```plaintext
|
||||
GET /users/:user_id/impersonation_tokens
|
||||
|
|
@ -136,8 +136,8 @@ Supported attributes:
|
|||
|
||||
| Attribute | Type | Required | Description |
|
||||
|:----------|:--------|:---------|:------------|
|
||||
| `user_id` | integer | yes | ID of the user. |
|
||||
| `state` | string | no | Filter tokens based on state: `all`, `active`, or `inactive`. |
|
||||
| `user_id` | integer | yes | ID of user account |
|
||||
| `state` | string | no | Filter tokens based on state. Possible values: `all`, `active`, or `inactive`. |
|
||||
|
||||
Example request:
|
||||
|
||||
|
|
@ -182,13 +182,13 @@ Example response:
|
|||
]
|
||||
```
|
||||
|
||||
## Get an impersonation token of a user
|
||||
## Get an impersonation token for a user
|
||||
|
||||
Get a user's impersonation token.
|
||||
Gets an impersonation token for a given user.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must be an administrator.
|
||||
- You must have administrator access to the instance.
|
||||
|
||||
```plaintext
|
||||
GET /users/:user_id/impersonation_tokens/:impersonation_token_id
|
||||
|
|
@ -198,8 +198,8 @@ Supported attributes:
|
|||
|
||||
| Attribute | Type | Required | Description |
|
||||
|:-------------------------|:--------|:---------|:------------|
|
||||
| `user_id` | integer | yes | ID of the user. |
|
||||
| `impersonation_token_id` | integer | yes | ID of the impersonation token. |
|
||||
| `user_id` | integer | yes | ID of user account |
|
||||
| `impersonation_token_id` | integer | yes | ID of impersonation token |
|
||||
|
||||
Example request:
|
||||
|
||||
|
|
@ -228,14 +228,13 @@ Example response:
|
|||
|
||||
## Create an impersonation token
|
||||
|
||||
Create a new impersonation token. You can only create impersonation tokens to impersonate the user and perform
|
||||
both API calls and Git reads and writes. The user can't see these tokens in their profile settings page.
|
||||
Creates an impersonation token for a given user. These tokens are used to act on behalf of a user and can perform API calls as well as Git read and write actions. These tokens are not visible to the associated user on their profile settings page.
|
||||
|
||||
Token values are returned once. Make sure you save it because you can't access it again.
|
||||
Token values are included with the response, but cannot be retrieved later.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must be an administrator.
|
||||
- You must have administrator access to the instance.
|
||||
|
||||
```plaintext
|
||||
POST /users/:user_id/impersonation_tokens
|
||||
|
|
@ -245,11 +244,11 @@ Supported attributes:
|
|||
|
||||
| Attribute | Type | Required | Description |
|
||||
|:-------------|:--------|:---------|:------------|
|
||||
| `user_id` | integer | yes | ID of the user. |
|
||||
| `name` | string | yes | Name of the impersonation token. |
|
||||
| `description`| string | no | Description of the personal access token. |
|
||||
| `expires_at` | date | yes | Expiration date of the impersonation token in ISO format (`YYYY-MM-DD`). |
|
||||
| `scopes` | array | yes | Array of scopes of the impersonation token (`api`, `read_user`). |
|
||||
| `user_id` | integer | yes | ID of user account |
|
||||
| `name` | string | yes | Name of impersonation token |
|
||||
| `description`| string | no | Description of impersonation token |
|
||||
| `expires_at` | date | yes | Expiration date of the impersonation token in ISO format (`YYYY-MM-DD`) |
|
||||
| `scopes` | array | yes | Array of approved scopes. For a list of possible values, see [Personal access token scopes](../user/profile/personal_access_tokens.md#personal-access-token-scopes): |
|
||||
|
||||
Example request:
|
||||
|
||||
|
|
@ -280,11 +279,11 @@ Example response:
|
|||
|
||||
## Revoke an impersonation token
|
||||
|
||||
Revoke an impersonation token.
|
||||
Revokes an impersonation token for a given user.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must be an administrator.
|
||||
- You must have administrator access to the instance.
|
||||
|
||||
```plaintext
|
||||
DELETE /users/:user_id/impersonation_tokens/:impersonation_token_id
|
||||
|
|
@ -294,8 +293,8 @@ Supported attributes:
|
|||
|
||||
| Attribute | Type | Required | Description |
|
||||
|:-------------------------|:--------|:---------|:------------|
|
||||
| `user_id` | integer | yes | ID of the user. |
|
||||
| `impersonation_token_id` | integer | yes | ID of the impersonation token. |
|
||||
| `user_id` | integer | yes | ID of user account |
|
||||
| `impersonation_token_id` | integer | yes | ID of impersonation token |
|
||||
|
||||
Example request:
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ Prerequisites:
|
|||
You can enable the Jira issues integration by configuring your project settings in GitLab.
|
||||
You can also configure the integration for a specific
|
||||
[group](../../user/project/integrations/index.md#manage-group-default-settings-for-a-project-integration) or an entire
|
||||
[instance](../../administration/settings/project_integration_management.md#manage-instance-level-default-settings-for-a-project-integration)
|
||||
[instance](../../administration/settings/project_integration_management.md#configure-default-settings-for-an-integration)
|
||||
on self-managed GitLab.
|
||||
|
||||
With this integration, your GitLab project can interact with all Jira projects on your instance.
|
||||
|
|
|
|||
|
|
@ -29,13 +29,34 @@ NOTE:
|
|||
Subscriptions cannot be transferred between GitLab.com and GitLab self-managed.
|
||||
A new subscription must be purchased and applied as needed.
|
||||
|
||||
## Choose a GitLab tier
|
||||
## Choose a subscription tier
|
||||
|
||||
Pricing is [tier-based](https://about.gitlab.com/pricing/), allowing you to choose
|
||||
the features that fit your budget.
|
||||
|
||||
For more details, see [a comparison of features available in each tier](https://about.gitlab.com/pricing/feature-comparison/).
|
||||
|
||||
## Choose a subscription add-on
|
||||
|
||||
An add-on is an additional paid feature or service that you can purchase on top of an existing
|
||||
GitLab subscription. Add-ons provide extra functionality or resources to enhance your GitLab
|
||||
experience.
|
||||
|
||||
You can purchase the following add-ons:
|
||||
|
||||
- [GitLab Duo](subscription-add-ons.md): Get access to AI-powered features like Code Suggestions, GitLab
|
||||
Duo Chat, and more.
|
||||
- [Enterprise Agile Planning](gitlab_com/index.md#enterprise-agile-planning): Increase collaboration between
|
||||
technical and non-technical teams on a single platform. Non-engineering team members can participate in planning,
|
||||
measure impact with Value Stream Analytics, and gain visibility into software development velocity.
|
||||
- [Storage](gitlab_com/index.md#purchase-more-storage): Buy more storage when you exceed your
|
||||
free 10 GiB storage quota.
|
||||
- [Compute minutes](gitlab_com/compute_minutes.md): Buy additional compute minutes when your
|
||||
plan exceeds its allocated amount and you need to continue running automated
|
||||
builds, tests, and deployments without interruption.
|
||||
|
||||
Some add-ons are only available to specific subscription tiers and offerings.
|
||||
|
||||
## Contact Support
|
||||
|
||||
- See the tiers of [GitLab Support](https://about.gitlab.com/support/).
|
||||
|
|
|
|||
|
|
@ -125,9 +125,28 @@ When GitLab Duo is turned off for a group, project, or instance:
|
|||
|
||||
::Tabs
|
||||
|
||||
:::TabTitle In 17.4 and later
|
||||
:::TabTitle In 17.7 and later
|
||||
|
||||
In GitLab 17.4 and later, follow these instructions to turn off GitLab Duo
|
||||
In GitLab 17.7 and later, follow these instructions to turn off GitLab Duo
|
||||
for a group and its subgroups and projects.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must have the Owner role for the group.
|
||||
|
||||
To turn off GitLab Duo for a group:
|
||||
|
||||
1. On the left sidebar, select **Search or go to** and find your group.
|
||||
1. Select **Settings > GitLab Duo**.
|
||||
1. Select **Change configuration**.
|
||||
1. Choose an option:
|
||||
- To turn off GitLab Duo for the group, but let other groups or projects turn it on, select **Off by default**.
|
||||
- To turn off GitLab Duo for the group, and to prevent other groups or projects from turning it on, select **Always off**.
|
||||
1. Select **Save changes**.
|
||||
|
||||
:::TabTitle In 17.4 to 17.6
|
||||
|
||||
In GitLab 17.4 to 17.6, follow these instructions to turn off GitLab Duo
|
||||
for a group and its subgroups and projects.
|
||||
|
||||
Prerequisites:
|
||||
|
|
@ -200,9 +219,27 @@ DETAILS:
|
|||
|
||||
::Tabs
|
||||
|
||||
:::TabTitle In 17.4 and later
|
||||
:::TabTitle In 17.7 and later
|
||||
|
||||
In GitLab 17.4 and later, follow these instructions to turn off GitLab Duo for the instance.
|
||||
In GitLab 17.7 and later, follow these instructions to turn off GitLab Duo for the instance.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must be an administrator.
|
||||
|
||||
To turn off GitLab Duo for an instance:
|
||||
|
||||
1. On the left sidebar, at the bottom, select **Admin area**.
|
||||
1. On the left sidebar, select **GitLab Duo**.
|
||||
1. Select **Change configuration**.
|
||||
1. Choose an option:
|
||||
- To turn off GitLab Duo for the instance, but let groups and projects turn it on, select **Off by default**.
|
||||
- To turn off GitLab Duo for the instance, and to prevent groups or projects from ever turning it on, select **Always off**.
|
||||
1. Select **Save changes**.
|
||||
|
||||
:::TabTitle In 17.4 to 17.6
|
||||
|
||||
In GitLab 17.4 to 17.6, follow these instructions to turn off GitLab Duo for the instance.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ DETAILS:
|
|||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/443557) for direct transfer in GitLab 17.4 [with flags](../../../administration/feature_flags.md) named `importer_user_mapping` and `bulk_import_importer_user_mapping`. Disabled by default.
|
||||
> - Introduced in GitLab 17.6 [for Gitea](https://gitlab.com/gitlab-org/gitlab/-/issues/467084) [with flags](../../../administration/feature_flags.md) named `importer_user_mapping` and `gitea_user_mapping`, and [for GitHub](https://gitlab.com/gitlab-org/gitlab/-/issues/466355) with flags named `importer_user_mapping` and `github_user_mapping`. Disabled by default.
|
||||
> - Introduced in GitLab 17.7 [for Bitbucket Server](https://gitlab.com/gitlab-org/gitlab/-/issues/466356) [with flags](../../../administration/feature_flags.md) named `importer_user_mapping` and `bitbucket_server_user_mapping`. Disabled by default.
|
||||
> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/472735) for direct transfer in GitLab 17.7.
|
||||
> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/472735) for direct transfer in GitLab 17.7.
|
||||
|
||||
FLAG:
|
||||
The availability of this feature is controlled by feature flags.
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ You can integrate with external applications to add functionality to GitLab.
|
|||
|
||||
You can view and manage integrations for the:
|
||||
|
||||
- [Instance](../../../administration/settings/project_integration_management.md#manage-instance-level-default-settings-for-a-project-integration) (self-managed GitLab)
|
||||
- [Instance](../../../administration/settings/project_integration_management.md#configure-default-settings-for-an-integration) (self-managed GitLab)
|
||||
- [Group](#manage-group-default-settings-for-a-project-integration)
|
||||
|
||||
You can use:
|
||||
|
|
@ -59,7 +59,7 @@ When you make further changes to the group defaults:
|
|||
- Subgroups and projects with custom settings selected for the integration are not immediately affected and
|
||||
may choose to use the latest defaults at any time.
|
||||
|
||||
If [instance settings](../../../administration/settings/project_integration_management.md#manage-instance-level-default-settings-for-a-project-integration)
|
||||
If [instance settings](../../../administration/settings/project_integration_management.md#configure-default-settings-for-an-integration)
|
||||
have also been configured for the same integration, projects in the group inherit settings from the group.
|
||||
|
||||
Only the entire settings for an integration can be inherited. Per-field inheritance
|
||||
|
|
@ -114,6 +114,10 @@ To use custom settings for a project or group integration:
|
|||
|
||||
## Available integrations
|
||||
|
||||
The following integrations can be available on a GitLab instance.
|
||||
If an instance administrator has configured an [integration allowlist](../../../administration/settings/project_integration_management.md#integration-allowlist),
|
||||
only those integrations are available.
|
||||
|
||||
| Integration | Description | Integration hooks |
|
||||
| ------------------------------------------------------------ | ---------------------------------------------------------------------------------------- | ----------------- |
|
||||
| [Apple App Store Connect](apple_app_store.md) | Use GitLab to build and release an app in the Apple App Store. | **{dotted-circle}** No |
|
||||
|
|
|
|||
|
|
@ -122,7 +122,31 @@ To generate quality code, write clear, descriptive, specific tasks.
|
|||
|
||||
For use cases and best practices, follow the [GitLab Duo examples documentation](../../../gitlab_duo_examples.md).
|
||||
|
||||
## Open tabs as context
|
||||
## Advanced Context
|
||||
|
||||
When using Code Suggestions, the Advanced Context feature searches in the background
|
||||
to find and include relevant context from across a user's repository.
|
||||
|
||||
For more information, see the [Advanced Context Resolver project epic](https://gitlab.com/groups/gitlab-org/editor-extensions/-/epics/57).
|
||||
|
||||
### Advanced Context supported languages
|
||||
|
||||
The Advanced Context feature supports these languages:
|
||||
|
||||
- Code completion: all configured languages.
|
||||
- Code generation:
|
||||
- Go
|
||||
- Java
|
||||
- JavaScript
|
||||
- Kotlin
|
||||
- Python
|
||||
- Ruby
|
||||
- Rust
|
||||
- TypeScript (`.ts` and `.tsx` files)
|
||||
- Vue
|
||||
- YAML
|
||||
|
||||
### Open tabs as context
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/464767) in GitLab 17.1 [with a flag](../../../../administration/feature_flags.md) named `advanced_context_resolver`. Disabled by default.
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/462750) in GitLab 17.1 [with a flag](../../../../administration/feature_flags.md) named `code_suggestions_context`. Disabled by default.
|
||||
|
|
@ -140,7 +164,9 @@ To get more accurate and relevant results from Code Suggestions and code generat
|
|||
the contents of the files open in tabs in your IDE. Similar to prompt engineering, these files
|
||||
give GitLab Duo more information about the standards and practices in your code project.
|
||||
|
||||
### Enable open tabs as context
|
||||
Open tabs as context is part of the Advanced Context feature.
|
||||
|
||||
#### Enable open tabs as context
|
||||
|
||||
By default, Code Suggestions uses the open files in your IDE for context when making suggestions.
|
||||
|
||||
|
|
@ -177,7 +203,7 @@ To confirm that open tabs are used as context:
|
|||
|
||||
::EndTabs
|
||||
|
||||
### Use open tabs as context
|
||||
#### Use open tabs as context
|
||||
|
||||
Open the files you want to provide for context:
|
||||
|
||||
|
|
@ -201,17 +227,7 @@ To learn about the code that builds the prompt, see these files:
|
|||
[`ai_gateway/code_suggestions/processing/completions.py`](https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist/-/blob/fcb3f485a8f047a86a8166aad81f93b6d82106a7/ai_gateway/code_suggestions/processing/completions.py#L273)
|
||||
in the `modelops` repository.
|
||||
|
||||
Provide feedback about this feature in
|
||||
[issue 258](https://gitlab.com/gitlab-org/editor-extensions/gitlab-lsp/-/issues/258).
|
||||
|
||||
## Advanced Context supported languages
|
||||
|
||||
The Advanced Context feature supports these languages:
|
||||
|
||||
- Code completion: all configured languages.
|
||||
- Code generation: Go, Java, JavaScript, Kotlin, Python, Ruby, Rust, TypeScript (`.ts` and `.tsx` files), Vue, and YAML.
|
||||
|
||||
## Inference window context
|
||||
### Inference window context
|
||||
|
||||
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/435271) in GitLab 16.8.
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/editor-extensions/gitlab-lsp/-/issues/206) open tabs context in GitLab 17.2 [with flags](../../../../administration/feature_flags.md) named `advanced_context_resolver` and `code_suggestions_context`. Disabled by default.
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ module API
|
|||
if actor.user
|
||||
load_balancer_stick_request(::User, :user, actor.user.id)
|
||||
set_current_organization(user: actor.user)
|
||||
link_scoped_user(params)
|
||||
end
|
||||
|
||||
Gitlab::ApplicationContext.push(
|
||||
|
|
@ -39,6 +40,35 @@ module API
|
|||
container.lfs_http_url_to_repo
|
||||
end
|
||||
|
||||
def gitaly_context(params)
|
||||
return unless params[:gitaly_client_context_bin].present?
|
||||
|
||||
raw_context = Base64.decode64(params[:gitaly_client_context_bin])
|
||||
context = Gitlab::Json.parse(raw_context)
|
||||
|
||||
raise bad_request!('gitaly_client_context_bin is not a Hash') unless context.is_a?(Hash)
|
||||
|
||||
context
|
||||
rescue JSON::ParserError => e
|
||||
Gitlab::ErrorTracking.log_exception(e, gitaly_context: params[:gitaly_client_context_bin])
|
||||
bad_request!('malformed gitaly_client_context_bin')
|
||||
end
|
||||
|
||||
def link_scoped_user(params)
|
||||
context = gitaly_context(params)
|
||||
|
||||
return unless context
|
||||
|
||||
scoped_user_id = context['scoped-user-id']
|
||||
|
||||
return unless scoped_user_id.present?
|
||||
|
||||
scoped_user_id = scoped_user_id.to_i
|
||||
identity = ::Gitlab::Auth::Identity.link_from_scoped_user_id(actor.user, scoped_user_id)
|
||||
|
||||
not_found!("User ID #{scoped_user_id} not found") unless identity
|
||||
end
|
||||
|
||||
# rubocop: disable Metrics/AbcSize
|
||||
def check_allowed(params)
|
||||
# This is a separate method so that EE can alter its behaviour more
|
||||
|
|
|
|||
|
|
@ -33,6 +33,16 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
def self.link_from_scoped_user_id(user, scoped_user_id)
|
||||
scoped_user = ::User.find_by_id(scoped_user_id)
|
||||
|
||||
return unless scoped_user
|
||||
|
||||
::Gitlab::Auth::Identity.fabricate(user).tap do |identity|
|
||||
identity.link!(scoped_user) if identity&.composite?
|
||||
end
|
||||
end
|
||||
|
||||
def self.sidekiq_restore!(job)
|
||||
ids = Array(job[COMPOSITE_IDENTITY_SIDEKIQ_ARG])
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ module Gitlab
|
|||
|
||||
# The maximum number of patterned glob comparisons that will be
|
||||
# performed before the rule assumes that it has a match
|
||||
MAX_PATTERN_COMPARISONS = 10_000
|
||||
MAX_PATTERN_COMPARISONS = 50_000
|
||||
|
||||
WILDCARD_NESTED_PATTERN = "**/*"
|
||||
|
||||
|
|
@ -85,7 +85,19 @@ module Gitlab
|
|||
end
|
||||
|
||||
def pattern_matches?(paths, pattern_globs, context)
|
||||
return true if (paths.size * pattern_globs.size) > MAX_PATTERN_COMPARISONS
|
||||
comparisons = paths.size * pattern_globs.size
|
||||
|
||||
if comparisons > MAX_PATTERN_COMPARISONS
|
||||
Gitlab::AppJsonLogger.info(
|
||||
class: self.class.name,
|
||||
message: 'rules:exists pattern comparisons limit exceeded',
|
||||
project_id: context.project&.id,
|
||||
paths_size: paths.size,
|
||||
globs_size: pattern_globs.size,
|
||||
comparisons: comparisons
|
||||
)
|
||||
return true
|
||||
end
|
||||
|
||||
pattern_globs.any? do |glob|
|
||||
Gitlab::SafeRequestStore.fetch("ci_rules_exists_pattern_matches_#{context.project&.id}_#{glob}") do
|
||||
|
|
|
|||
|
|
@ -328,9 +328,13 @@ module Gitlab
|
|||
'authorization' => "Bearer #{authorization_token(storage)}",
|
||||
'client_name' => CLIENT_NAME
|
||||
}
|
||||
|
||||
gitaly_context = {}
|
||||
relative_path = fetch_relative_path
|
||||
|
||||
::Gitlab::Auth::Identity.currently_linked do |identity|
|
||||
gitaly_context['scoped-user-id'] = identity.scoped_user.id.to_s
|
||||
end
|
||||
|
||||
context_data = Gitlab::ApplicationContext.current
|
||||
|
||||
feature_stack = Thread.current[:gitaly_feature_stack]
|
||||
|
|
@ -343,6 +347,8 @@ module Gitlab
|
|||
metadata['user_id'] = context_data['meta.user_id'].to_s if context_data&.fetch('meta.user_id', nil)
|
||||
metadata['remote_ip'] = context_data['meta.remote_ip'] if context_data&.fetch('meta.remote_ip', nil)
|
||||
metadata['relative-path-bin'] = relative_path if relative_path
|
||||
metadata['gitaly-client-context-bin'] = gitaly_context.to_json if gitaly_context.present?
|
||||
|
||||
metadata.merge!(Feature::Gitaly.server_feature_flags(**feature_flag_actors))
|
||||
metadata.merge!(route_to_primary)
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ module Gitlab
|
|||
|
||||
def initialize(project, options = nil)
|
||||
@project = project
|
||||
@project_namespace, _, @project_path = project.full_path.partition('/')
|
||||
@project_namespace, _, @project_path = project.full_path.downcase.partition('/')
|
||||
@options = options || {}
|
||||
end
|
||||
|
||||
|
|
@ -34,7 +34,7 @@ module Gitlab
|
|||
# See https://docs.gitlab.com/ee/user/project/pages/getting_started_part_one.html#user-and-group-website-examples.
|
||||
def is_namespace_homepage? # rubocop:disable Naming/PredicateName -- namespace_homepage is not an
|
||||
# adjective, so adding "is_" improves understandability
|
||||
project_path.downcase == "#{project_namespace}.#{instance_pages_domain}"
|
||||
project_path == "#{project_namespace}.#{instance_pages_domain}"
|
||||
end
|
||||
|
||||
def artifact_url(artifact, job)
|
||||
|
|
|
|||
|
|
@ -5216,6 +5216,9 @@ msgstr ""
|
|||
msgid "AiPowered|Features are not available. However, any group, subgroup, or project can turn them on."
|
||||
msgstr ""
|
||||
|
||||
msgid "AiPowered|GitLab Duo settings have moved"
|
||||
msgstr ""
|
||||
|
||||
msgid "AiPowered|Help make the next releases of GitLab better. As a member of the GitLab Early Access Program, you can take part in shaping GitLab by letting us know what you think of GitLab Duo experimental and beta features. How does this program work?"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -5258,12 +5261,18 @@ msgstr ""
|
|||
msgid "AiPowered|Start date: %{startDate}"
|
||||
msgstr ""
|
||||
|
||||
msgid "AiPowered|To make it easier to configure GitLab Duo, the settings have moved to a more visible location. To access them, go to "
|
||||
msgstr ""
|
||||
|
||||
msgid "AiPowered|Turn on experiment and beta GitLab Duo features"
|
||||
msgstr ""
|
||||
|
||||
msgid "AiPowered|Turn on self-hosted models"
|
||||
msgstr ""
|
||||
|
||||
msgid "AiPowered|View GitLab Duo settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "AiPowered|When GitLab Duo is not available, experiment and beta features cannot be turned on."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -26715,6 +26724,9 @@ msgstr ""
|
|||
msgid "GroupSAML|Are you sure you want to reset the SCIM token? SCIM provisioning will stop working until the new token is updated."
|
||||
msgstr ""
|
||||
|
||||
msgid "GroupSAML|Assign GitLab Duo seats to users in this group"
|
||||
msgstr ""
|
||||
|
||||
msgid "GroupSAML|Before disabling password authentication, enable SAML authentication."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -26844,6 +26856,9 @@ msgstr ""
|
|||
msgid "GroupSAML|Use SAML group links to manage group membership using SAML."
|
||||
msgstr ""
|
||||
|
||||
msgid "GroupSAML|Users in this SAML group will be assigned a GitLab Duo Pro or GitLab Duo Enterprise seat, if available"
|
||||
msgstr ""
|
||||
|
||||
msgid "GroupSAML|Valid SAML Response"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -26856,6 +26871,9 @@ msgstr ""
|
|||
msgid "GroupSAML|recommend persistent ID instead of email"
|
||||
msgstr ""
|
||||
|
||||
msgid "GroupSAML|with GitLab Duo seat assignment"
|
||||
msgstr ""
|
||||
|
||||
msgid "GroupSaml|Copy SCIM API endpoint URL"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -35467,6 +35485,9 @@ msgstr ""
|
|||
msgid "ModelRegistry|Author"
|
||||
msgstr ""
|
||||
|
||||
msgid "ModelRegistry|CI Job"
|
||||
msgstr ""
|
||||
|
||||
msgid "ModelRegistry|Created"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -35476,6 +35497,9 @@ msgstr ""
|
|||
msgid "ModelRegistry|Latest version"
|
||||
msgstr ""
|
||||
|
||||
msgid "ModelRegistry|MLflow Run ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "ModelRegistry|Model name"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -35488,6 +35512,9 @@ msgstr ""
|
|||
msgid "ModelRegistry|New version"
|
||||
msgstr ""
|
||||
|
||||
msgid "ModelRegistry|Status"
|
||||
msgstr ""
|
||||
|
||||
msgid "ModelRegistry|Version"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -70,12 +70,12 @@
|
|||
"@gitlab/application-sdk-browser": "^0.3.3",
|
||||
"@gitlab/at.js": "1.5.7",
|
||||
"@gitlab/cluster-client": "^2.4.0",
|
||||
"@gitlab/duo-ui": "^5.0.0",
|
||||
"@gitlab/duo-ui": "^3.0.0",
|
||||
"@gitlab/favicon-overlay": "2.0.0",
|
||||
"@gitlab/fonts": "^1.3.0",
|
||||
"@gitlab/query-language-rust": "0.1.2",
|
||||
"@gitlab/svgs": "3.121.0",
|
||||
"@gitlab/ui": "105.1.1",
|
||||
"@gitlab/ui": "104.2.0",
|
||||
"@gitlab/vue-router-vue3": "npm:vue-router@4.1.6",
|
||||
"@gitlab/vuex-vue3": "npm:vuex@4.0.0",
|
||||
"@gitlab/web-ide": "^0.0.1-dev-20241112063543",
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
module Scenario
|
||||
module Test
|
||||
module Instance
|
||||
class NonBlocking < All
|
||||
tags :"~blocking", :"~smoke", *Specs::Runner::DEFAULT_SKIPPED_TAGS.map { |tag| :"~#{tag}" }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -231,7 +231,6 @@ module QA
|
|||
file_path: file_path,
|
||||
status: status(example),
|
||||
smoke: example.metadata.key?(:smoke).to_s,
|
||||
blocking: example.metadata.key?(:blocking).to_s,
|
||||
quarantined: quarantined(example),
|
||||
job_name: job_name,
|
||||
merge_request: merge_request,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ module QA
|
|||
module Tools
|
||||
class MigrateInfluxDataToGcsCsv < MigrateInfluxDataToGcs
|
||||
TEST_STATS_FIELDS = %w[id testcase file_path name product_group stage job_id job_name
|
||||
job_url pipeline_id pipeline_url merge_request merge_request_iid smoke blocking quarantined
|
||||
job_url pipeline_id pipeline_url merge_request merge_request_iid smoke quarantined
|
||||
retried retry_attempts run_time run_type status ui_fabrication api_fabrication total_fabrication].freeze
|
||||
FABRICATION_STATS_FIELDS = %w[timestamp resource fabrication_method http_method run_type
|
||||
merge_request fabrication_time info job_url].freeze
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ module QA
|
|||
# @param [String] values record's values to get the data from
|
||||
# @return [Hash]
|
||||
def tags(values)
|
||||
tags = values.slice('name', 'file_path', 'status', 'smoke', 'blocking',
|
||||
tags = values.slice('name', 'file_path', 'status', 'smoke',
|
||||
'quarantined', 'job_name', 'merge_request', 'run_type', 'stage',
|
||||
'product_group', 'testcase', 'exception_class')
|
||||
|
||||
|
|
|
|||
|
|
@ -1,649 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "influxdb-client"
|
||||
require "terminal-table"
|
||||
require "slack-notifier"
|
||||
require 'rainbow/refinement'
|
||||
|
||||
module QA
|
||||
module Tools
|
||||
class ReliableReport
|
||||
using Rainbow
|
||||
include Support::InfluxdbTools
|
||||
include Support::API
|
||||
|
||||
RELIABLE_REPORT_LABEL = "reliable test report"
|
||||
PROMOTION_BATCH_LIMIT = 10
|
||||
|
||||
ALLOWED_EXCEPTION_PATTERNS = [
|
||||
/Couldn't find option named/,
|
||||
/Waiting for [\w:]+ to be removed/,
|
||||
/503 Server Unavailable/,
|
||||
/\w+ did not appear on [\w:]+ as expected/,
|
||||
/Internal Server Error/,
|
||||
/Ambiguous match/,
|
||||
/500 Error - GitLab/,
|
||||
/Page did not fully load/,
|
||||
/Timed out reading data from server/,
|
||||
/Internal API error/,
|
||||
/Something went wrong/
|
||||
].freeze
|
||||
|
||||
# Project for report creation: https://gitlab.com/gitlab-org/gitlab
|
||||
PROJECT_ID = 278964
|
||||
BLOB_MASTER = 'https://gitlab.com/gitlab-org/gitlab/-/blob/master'
|
||||
FEATURES_DIR = '/qa/qa/specs/features'
|
||||
|
||||
# @param [Integer] range amount of days for results range
|
||||
def initialize(range)
|
||||
@range = range.to_i
|
||||
@slack_channel = "#quality-reports"
|
||||
end
|
||||
|
||||
# Run reliable reporter
|
||||
#
|
||||
# @param [Integer] range amount of days for results range
|
||||
# @param [String] report_in_issue_and_slack
|
||||
# @return [void]
|
||||
def self.run(range: 14, report_in_issue_and_slack: "false")
|
||||
reporter = new(range)
|
||||
|
||||
reporter.print_report
|
||||
|
||||
if report_in_issue_and_slack == "true"
|
||||
reporter.report_in_issue_and_slack
|
||||
reporter.write_specs_json
|
||||
reporter.close_previous_reports
|
||||
end
|
||||
rescue StandardError => e
|
||||
reporter&.notify_failure(e)
|
||||
raise(e)
|
||||
end
|
||||
|
||||
# Print top stable specs
|
||||
#
|
||||
# @return [void]
|
||||
def print_report
|
||||
puts "#{summary_table(stable: true)}\n\n"
|
||||
puts "Total amount: #{stable_test_runs.sum { |_k, v| v.count }}\n\n"
|
||||
print_results(stable_results_tables)
|
||||
return puts("No unstable blocking tests present!".yellow) if unstable_blocking_test_runs.empty?
|
||||
|
||||
puts "#{summary_table(stable: false)}\n\n"
|
||||
puts "Total amount: #{unstable_blocking_test_runs.sum { |_k, v| v.count }}\n\n"
|
||||
print_results(unstable_blocking_results_tables)
|
||||
end
|
||||
|
||||
# Create report issue
|
||||
#
|
||||
# @return [void]
|
||||
def report_in_issue_and_slack
|
||||
puts "Creating report".green
|
||||
issue = api_update(
|
||||
:post,
|
||||
"projects/#{PROJECT_ID}/issues",
|
||||
title: "Reliable e2e test report",
|
||||
description: report_issue_body,
|
||||
labels: "#{RELIABLE_REPORT_LABEL},Quality,test,type::maintenance,automation:ml"
|
||||
)
|
||||
@report_iid = issue[:iid]
|
||||
@report_web_url = issue[:web_url]
|
||||
puts "Created report issue: #{@report_web_url}"
|
||||
|
||||
puts "Sending slack notification".green
|
||||
notifier.post(
|
||||
icon_emoji: ":tanuki-protect:",
|
||||
text: <<~TEXT
|
||||
```#{summary_table(stable: true)}```
|
||||
```#{summary_table(stable: false)}```
|
||||
|
||||
#{@report_web_url}
|
||||
TEXT
|
||||
)
|
||||
puts "Done!"
|
||||
end
|
||||
|
||||
# Close previous reliable test reports
|
||||
#
|
||||
# @return [void]
|
||||
def close_previous_reports
|
||||
puts "Closing previous reports".green
|
||||
issues = api_get("projects/#{PROJECT_ID}/issues?labels=#{RELIABLE_REPORT_LABEL}&state=opened")
|
||||
|
||||
issues
|
||||
.reject { |issue| issue[:iid] == report_iid }
|
||||
.each do |issue|
|
||||
issue_iid = issue[:iid]
|
||||
issue_endpoint = "projects/#{PROJECT_ID}/issues/#{issue_iid}"
|
||||
|
||||
puts "Closing previous report '#{issue[:web_url]}'"
|
||||
api_update(:put, issue_endpoint, state_event: "close")
|
||||
api_update(:post, "#{issue_endpoint}/notes", body: "Closed issue in favor of ##{report_iid}")
|
||||
end
|
||||
end
|
||||
|
||||
# Notify failure
|
||||
#
|
||||
# @param [StandardError] error
|
||||
# @return [void]
|
||||
def notify_failure(error)
|
||||
notifier.post(
|
||||
text: "Reliable reporter failed to create report. Error: ```#{error}```",
|
||||
icon_emoji: ":sadpanda:"
|
||||
)
|
||||
end
|
||||
|
||||
def write_specs_json
|
||||
# 'unstable_specs.json' contain unstable specs tagged blocking
|
||||
# 'stable_specs.json' contain stable specs not tagged blocking
|
||||
File.write('tmp/unstable_specs.json', JSON.pretty_generate(specs_attributes(blocking: true)))
|
||||
File.write('tmp/stable_specs.json', JSON.pretty_generate(specs_attributes(blocking: false)))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :range, :slack_channel, :report_iid, :report_web_url
|
||||
|
||||
# Slack notifier
|
||||
#
|
||||
# @return [Slack::Notifier]
|
||||
def notifier
|
||||
@notifier ||= Slack::Notifier.new(
|
||||
slack_webhook_url,
|
||||
channel: slack_channel,
|
||||
username: "Reliable Report"
|
||||
)
|
||||
end
|
||||
|
||||
# Gitlab access token
|
||||
#
|
||||
# @return [String]
|
||||
def gitlab_access_token
|
||||
@gitlab_access_token ||= ENV["GITLAB_ACCESS_TOKEN"] || raise("Missing GITLAB_ACCESS_TOKEN env variable")
|
||||
end
|
||||
|
||||
# Gitlab api url
|
||||
#
|
||||
# @return [String]
|
||||
def gitlab_api_url
|
||||
@gitlab_api_url ||= ENV["CI_API_V4_URL"] || raise("Missing CI_API_V4_URL env variable")
|
||||
end
|
||||
|
||||
# Slack webhook url
|
||||
#
|
||||
# @return [String]
|
||||
def slack_webhook_url
|
||||
@slack_webhook_url ||= ENV["SLACK_WEBHOOK"] || raise("Missing SLACK_WEBHOOK env variable")
|
||||
end
|
||||
|
||||
# Markdown formatted report issue body
|
||||
#
|
||||
# @return [String]
|
||||
def report_issue_body
|
||||
execution_interval = "(#{Date.today - range} - #{Date.today})"
|
||||
|
||||
issue = []
|
||||
issue << "[[_TOC_]]"
|
||||
issue << "# Candidates for promotion to blocking #{execution_interval}"
|
||||
issue << "**Note: MRs will be auto-created for promoting the top #{PROMOTION_BATCH_LIMIT} " \
|
||||
"specs sorted by most number of successful runs**"
|
||||
issue << "Total amount: **#{test_count(stable_test_runs)}**"
|
||||
issue << summary_table(markdown: true, stable: true).to_s
|
||||
issue << results_markdown(:stable)
|
||||
return issue.join("\n\n") if unstable_blocking_test_runs.empty?
|
||||
|
||||
issue << "# Blocking specs with failures #{execution_interval}"
|
||||
issue << "**Note:**"
|
||||
issue << "* Only failures from the nightly, e2e-test-on-omnibus and e2e-test-on-gdk pipelines are considered"
|
||||
issue << "* Only specs that have a failure rate of equal or greater than 1 percent are considered"
|
||||
issue << "* Quarantine MRs will be created for all specs listed below"
|
||||
issue << "Total amount: **#{test_count(unstable_blocking_test_runs)}**"
|
||||
issue << summary_table(markdown: true, stable: false).to_s
|
||||
issue << results_markdown(:unstable)
|
||||
issue.join("\n\n")
|
||||
end
|
||||
|
||||
# Spec summary table
|
||||
#
|
||||
# @param [Boolean] markdown
|
||||
# @param [Boolean] stable
|
||||
# @return [Terminal::Table]
|
||||
def summary_table(markdown: false, stable: true)
|
||||
test_runs = stable ? stable_test_runs : unstable_blocking_test_runs
|
||||
terminal_table(
|
||||
rows: test_runs.map do |stage, stage_specs|
|
||||
[stage, stage_specs.sum { |_k, group_specs| group_specs.length }]
|
||||
end,
|
||||
title: "#{stable ? 'Stable' : 'Unstable'} spec summary for past #{range} days".ljust(50),
|
||||
headings: %w[STAGE COUNT],
|
||||
markdown: markdown
|
||||
)
|
||||
end
|
||||
|
||||
# Result tables for stable specs
|
||||
#
|
||||
# @param [Boolean] markdown
|
||||
# @return [Hash]
|
||||
def stable_results_tables(markdown: false)
|
||||
results_tables(:stable, markdown: markdown)
|
||||
end
|
||||
|
||||
# Result table for unstable specs
|
||||
#
|
||||
# @param [Boolean] markdown
|
||||
# @return [Hash]
|
||||
def unstable_blocking_results_tables(markdown: false)
|
||||
results_tables(:unstable, markdown: markdown)
|
||||
end
|
||||
|
||||
# Markdown formatted tables
|
||||
#
|
||||
# @param [Symbol] type result type - :stable, :unstable
|
||||
# @return [String]
|
||||
def results_markdown(type)
|
||||
runs = type == :stable ? stable_test_runs : unstable_blocking_test_runs
|
||||
results_tables(type, markdown: true).map do |stage, group_tables|
|
||||
markdown = "## #{stage.capitalize} (#{runs[stage].sum { |_k, group_runs| group_runs.count }})\n\n"
|
||||
|
||||
markdown << group_tables.map { |product_group, table| group_results_markdown(product_group, table) }.join
|
||||
end.join("\n\n")
|
||||
end
|
||||
|
||||
# Markdown formatted group results table
|
||||
#
|
||||
# @param [String] product_group
|
||||
# @param [Terminal::Table] table
|
||||
# @return [String]
|
||||
def group_results_markdown(product_group, table)
|
||||
<<~MARKDOWN.chomp
|
||||
<details>
|
||||
<summary>Executions table ~"group::#{product_group.tr('_', ' ')}" (#{table.rows.size})</summary>
|
||||
|
||||
#{table}
|
||||
|
||||
</details>
|
||||
MARKDOWN
|
||||
end
|
||||
|
||||
# Results table
|
||||
#
|
||||
# @param [Symbol] type result type - :stable, :unstable
|
||||
# @param [Boolean] markdown
|
||||
# @return [Hash<String, Hash<String, Terminal::Table>>] grouped by stage and product_group
|
||||
def results_tables(type, markdown: false)
|
||||
(type == :stable ? stable_test_runs : unstable_blocking_test_runs).to_h do |stage, specs|
|
||||
headings = ['NAME', 'RUNS', 'FAILURES', 'FAILURE RATE'].freeze
|
||||
[stage, specs.transform_values do |group_specs|
|
||||
terminal_table(
|
||||
title: "Top #{type} specs in '#{stage}::#{specs.key(group_specs)}' group for past #{range} days",
|
||||
headings: headings,
|
||||
markdown: markdown,
|
||||
rows: group_specs.map do |name, result|
|
||||
[
|
||||
name_column(name: name, file: result[:file], link: result[:link],
|
||||
exceptions_and_related_urls: result[:exceptions_and_related_urls], markdown: markdown),
|
||||
*table_params(result.values)
|
||||
]
|
||||
end
|
||||
)
|
||||
end]
|
||||
end
|
||||
end
|
||||
|
||||
# Stable specs
|
||||
#
|
||||
# @return [Hash]
|
||||
def stable_test_runs
|
||||
@top_stable ||= begin
|
||||
stable_specs = test_runs(blocking: false).each do |stage, stage_specs|
|
||||
stage_specs.transform_values! do |group_specs|
|
||||
group_specs.reject { |k, v| v[:failure_rate] != 0 }
|
||||
.sort_by { |k, v| -v[:runs] }
|
||||
.to_h
|
||||
end
|
||||
end
|
||||
stable_specs.transform_values { |v| v.reject { |_, v| v.empty? } }.reject { |_, v| v.empty? }
|
||||
end
|
||||
end
|
||||
|
||||
# Unstable blocking specs
|
||||
#
|
||||
# @return [Hash]
|
||||
def unstable_blocking_test_runs
|
||||
@top_unstable_blocking ||= begin
|
||||
unstable = test_runs(blocking: true).each do |_stage, stage_specs|
|
||||
stage_specs.transform_values! do |group_specs|
|
||||
group_specs.reject { |_, v| v[:failure_rate] == 0 }
|
||||
.sort_by { |_, v| -v[:failure_rate] }
|
||||
.to_h
|
||||
end
|
||||
end
|
||||
unstable.transform_values { |v| v.reject { |_, v| v.empty? } }.reject { |_, v| v.empty? }
|
||||
end
|
||||
end
|
||||
|
||||
def print_results(results)
|
||||
results.each do |_stage, stage_results|
|
||||
stage_results.each_value { |group_results_table| puts "#{group_results_table}\n\n" }
|
||||
end
|
||||
end
|
||||
|
||||
def test_count(test_runs)
|
||||
test_runs.sum do |_stage, stage_results|
|
||||
stage_results.sum { |_product_group, group_results| group_results.count }
|
||||
end
|
||||
end
|
||||
|
||||
# Terminal table for result formatting
|
||||
#
|
||||
# @param [Array] rows
|
||||
# @param [Array] headings
|
||||
# @param [String] title
|
||||
# @param [Boolean] markdown
|
||||
# @return [Terminal::Table]
|
||||
def terminal_table(rows:, headings:, title:, markdown:)
|
||||
Terminal::Table.new(
|
||||
headings: headings,
|
||||
title: markdown ? nil : title,
|
||||
rows: rows,
|
||||
style: markdown ? { border: :markdown } : { all_separators: true }
|
||||
)
|
||||
end
|
||||
|
||||
# Spec parameters for table row
|
||||
#
|
||||
# @param [Array] parameters
|
||||
# @return [Array]
|
||||
def table_params(parameters)
|
||||
[*parameters[2..3], "#{parameters.last}%"]
|
||||
end
|
||||
|
||||
# Name column content
|
||||
#
|
||||
# @param [String] name
|
||||
# @param [String] file
|
||||
# @param [String] link
|
||||
# @param [Hash] exceptions_and_related_urls
|
||||
# @param [Boolean] markdown
|
||||
# @return [String]
|
||||
def name_column(name:, file:, link:, exceptions_and_related_urls:, markdown: false)
|
||||
if markdown
|
||||
return "**Name**: #{name}<br>**File**: " \
|
||||
"[#{file}](#{link})#{exceptions_markdown(exceptions_and_related_urls)}"
|
||||
end
|
||||
|
||||
wrapped_name = name.length > 150 ? "#{name} ".scan(/.{1,150} /).map(&:strip).join("\n") : name
|
||||
"Name: '#{wrapped_name}'\nFile: #{file.ljust(160)}"
|
||||
end
|
||||
|
||||
# Formatted exceptions with link to job url
|
||||
#
|
||||
# @param [Hash] exceptions_and_related_urls
|
||||
# @return [String]
|
||||
def exceptions_markdown(exceptions_and_related_urls)
|
||||
return '' if exceptions_and_related_urls.empty?
|
||||
|
||||
"<br>**Exceptions**:#{exceptions_and_related_urls.keys.map do |e|
|
||||
"<br>- [`#{e.truncate(250).tr('`', "'")}`](#{exceptions_and_related_urls[e]})"
|
||||
end.join('')}"
|
||||
end
|
||||
|
||||
def api_query_notblocking
|
||||
@api_query_notblocking ||= begin
|
||||
log_fetching_query_data(true)
|
||||
query_api.query(query: query(false))
|
||||
end
|
||||
end
|
||||
|
||||
def api_query_blocking
|
||||
@api_query_blocking ||= begin
|
||||
log_fetching_query_data(true)
|
||||
query_api.query(query: query(true))
|
||||
end
|
||||
end
|
||||
|
||||
def log_fetching_query_data(reliable)
|
||||
puts("Fetching data on #{reliable ? 'reliable ' : ''}test execution for past #{range} days\n".green)
|
||||
end
|
||||
|
||||
def specs_attributes(blocking:)
|
||||
all_runs = query_for(blocking: blocking)
|
||||
|
||||
specs_array = all_runs.each_with_object([]) do |table, arr|
|
||||
records = table.records.sort_by { |record| record.values["_time"] }
|
||||
|
||||
next if within_execution_range(records.first.values["_time"], records.last.values["_time"])
|
||||
|
||||
result = spec_attributes_per_run(records)
|
||||
|
||||
# When collecting specs not in blocking bucket for promotion, skip specs with failures
|
||||
next if !blocking && result[:failed] != 0
|
||||
|
||||
next if blocking && skip_blocking_spec_record?(failed_count: result[:failed],
|
||||
failure_issue: result[:failure_issue],
|
||||
failed_run_type: result[:failed_run_type],
|
||||
failure_rate: result[:failure_rate])
|
||||
|
||||
arr << result
|
||||
end
|
||||
|
||||
specs_array = specs_array.sort_by { |item| item[:runs] }.reverse unless blocking
|
||||
|
||||
{
|
||||
type: blocking ? 'Unstable Specs' : 'Stable Specs',
|
||||
report_issue: report_web_url,
|
||||
specs: specs_array
|
||||
}
|
||||
end
|
||||
|
||||
def skip_blocking_spec_record?(failed_count:, failure_issue:, failed_run_type:, failure_rate:)
|
||||
# For unstable blocking specs, skip if no failures or
|
||||
return true if failed_count == 0 ||
|
||||
# skip if a failure issue does not exist or
|
||||
failure_issue&.exclude?('issues') ||
|
||||
# skip if run type is other than nightly and non-MR e2e-test-on-omnibus pipeline or
|
||||
(failed_run_type & %w[e2e-test-on-omnibus e2e-test-on-gdk nightly]).empty? ||
|
||||
# skip if failure rate of tests is less than or equal to 1 percent
|
||||
failure_rate <= 1
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def spec_attributes_per_run(records)
|
||||
failed_records = records.select do |r|
|
||||
r.values["status"] == "failed" && !allowed_failure?(r.values["failure_exception"])
|
||||
end
|
||||
|
||||
failure_issue = issue_for_most_failures(failed_records)
|
||||
last_record = records.last.values
|
||||
name = last_record["name"]
|
||||
file = last_record["file_path"].split("/").last
|
||||
link = BLOB_MASTER + FEATURES_DIR + last_record["file_path"]
|
||||
file_path = FEATURES_DIR + last_record["file_path"]
|
||||
stage = last_record["stage"] || "unknown"
|
||||
testcase = last_record["testcase"]
|
||||
run_type = records.map { |record| record.values['run_type'] }.uniq
|
||||
failed_run_type = failed_records.map { |record| record.values['run_type'] }.uniq
|
||||
product_group = last_record["product_group"] || "unknown"
|
||||
runs = records.count
|
||||
failure_rate = (failed_records.count.to_f / runs) * 100
|
||||
|
||||
{
|
||||
stage: stage,
|
||||
product_group: product_group,
|
||||
name: name,
|
||||
file: file,
|
||||
link: link,
|
||||
runs: runs,
|
||||
failed: failed_records.count,
|
||||
failure_issue: failure_issue || '',
|
||||
failure_rate: failure_rate == 0 ? failure_rate.round(0) : failure_rate.round(2),
|
||||
testcase: testcase,
|
||||
file_path: file_path,
|
||||
all_run_type: run_type,
|
||||
failed_run_type: failed_run_type
|
||||
}
|
||||
end
|
||||
|
||||
def query_for(blocking:)
|
||||
blocking ? api_query_blocking : api_query_notblocking
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/AbcSize
|
||||
# Test executions grouped by name
|
||||
#
|
||||
# @param [Boolean] blocking
|
||||
# @return [Hash<String, Hash>]
|
||||
def test_runs(blocking:)
|
||||
all_runs = query_for(blocking: blocking)
|
||||
|
||||
all_runs.each_with_object(Hash.new { |hsh, key| hsh[key] = {} }) do |table, result|
|
||||
records = table.records.sort_by { |record| record.values["_time"] }
|
||||
|
||||
next if within_execution_range(records.first.values["_time"], records.last.values["_time"])
|
||||
|
||||
last_record = records.last.values
|
||||
|
||||
name = last_record["name"]
|
||||
file = last_record["file_path"].split("/").last
|
||||
link = BLOB_MASTER + FEATURES_DIR + last_record["file_path"]
|
||||
stage = last_record["stage"] || "unknown"
|
||||
product_group = last_record["product_group"] || "unknown"
|
||||
|
||||
runs = records.count
|
||||
|
||||
failed_records = records.select do |r|
|
||||
r.values["status"] == "failed" && !allowed_failure?(r.values["failure_exception"])
|
||||
end
|
||||
|
||||
failure_issue = issue_for_most_failures(failed_records)
|
||||
|
||||
failed_run_type = failed_records.map { |record| record.values['run_type'] }.uniq
|
||||
|
||||
failure_rate = (failed_records.count.to_f / runs) * 100
|
||||
|
||||
next if blocking && skip_blocking_spec_record?(failed_count: failed_records.count,
|
||||
failure_issue: failure_issue, failed_run_type: failed_run_type, failure_rate: failure_rate)
|
||||
|
||||
result[stage][product_group] ||= {}
|
||||
result[stage][product_group][name] = {
|
||||
file: file,
|
||||
link: link,
|
||||
runs: runs,
|
||||
failed: failed_records.count,
|
||||
exceptions_and_related_urls: exceptions_and_related_urls(records),
|
||||
failure_rate: failure_rate == 0 ? failure_rate.round(0) : failure_rate.round(2)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
# rubocop:enable Metrics/AbcSize
|
||||
|
||||
# Return hash of exceptions as key and failure_issue or job_url urls as value
|
||||
#
|
||||
# @param [Array<InfluxDB2::FluxRecord>] records
|
||||
# @return [Hash]
|
||||
def exceptions_and_related_urls(records)
|
||||
records_with_exception = records.reject { |r| !r.values["failure_exception"] }
|
||||
|
||||
# Since exception is the key in the below hash, only one instance of an occurrence is kept
|
||||
records_with_exception.to_h do |r|
|
||||
[r.values["failure_exception"], r.values["failure_issue"] || r.values["job_url"]]
|
||||
end
|
||||
end
|
||||
|
||||
# Return the failure that has the most occurrence
|
||||
#
|
||||
# @param [Array<InfluxDB2::FluxRecord>] records
|
||||
# @return [String] the failure with most occurrence
|
||||
def issue_for_most_failures(records)
|
||||
return '' if records.empty?
|
||||
|
||||
issues = records.filter_map { |r| r.values["failure_issue"] }
|
||||
return '' if issues.empty?
|
||||
|
||||
issues.tally.max_by { |_, count| count }&.first
|
||||
end
|
||||
|
||||
# Check if failure is allowed
|
||||
#
|
||||
# @param [String] failure_exception
|
||||
# @return [Boolean]
|
||||
def allowed_failure?(failure_exception)
|
||||
ALLOWED_EXCEPTION_PATTERNS.any? { |pattern| pattern.match?(failure_exception) }
|
||||
end
|
||||
|
||||
# Returns true if first_time is before our range, or if last_time is before report date
|
||||
# offset 1 day due to how schedulers are configured and first run can be 1 day later
|
||||
#
|
||||
# @param [String] first_time
|
||||
# @param [String] last_time
|
||||
# @return [Boolean]
|
||||
def within_execution_range(first_time, last_time)
|
||||
(Date.today - Date.parse(first_time)).to_i < (range - 1) || (Date.today - Date.parse(last_time)).to_i > 1
|
||||
end
|
||||
|
||||
# Flux query
|
||||
#
|
||||
# @param [Boolean] blocking
|
||||
# @return [String]
|
||||
def query(blocking)
|
||||
<<~QUERY
|
||||
from(bucket: "#{Support::InfluxdbTools::INFLUX_MAIN_TEST_METRICS_BUCKET}")
|
||||
|> range(start: -#{range}d)
|
||||
|> filter(fn: (r) => r._measurement == "test-stats")
|
||||
|> filter(fn: (r) => r.run_type == "staging-full" or
|
||||
r.run_type == "staging-sanity" or
|
||||
r.run_type == "production-full" or
|
||||
r.run_type == "production-sanity" or
|
||||
r.run_type == "e2e-test-on-omnibus" or
|
||||
r.run_type == "e2e-test-on-gdk" or
|
||||
r.run_type == "nightly"
|
||||
)
|
||||
|> filter(fn: (r) => r.job_name != "airgapped" and
|
||||
r.job_name != "nplus1-instance-image"
|
||||
)
|
||||
|> filter(fn: (r) => r.status != "pending" and
|
||||
r.merge_request == "false" and
|
||||
r.quarantined == "false" and
|
||||
r.smoke == "false" and
|
||||
r.blocking == "#{blocking}"
|
||||
)
|
||||
|> filter(fn: (r) => r["_field"] == "job_url" or
|
||||
r["_field"] == "failure_exception" or
|
||||
r["_field"] == "id" or
|
||||
r["_field"] == "failure_issue"
|
||||
)
|
||||
|> pivot(rowKey: ["_time"], columnKey: ["_field"], valueColumn: "_value")
|
||||
|> group(columns: ["name"])
|
||||
QUERY
|
||||
end
|
||||
|
||||
# Api get request
|
||||
#
|
||||
# @param [String] path
|
||||
# @param [Hash] payload
|
||||
# @return [Hash, Array]
|
||||
def api_get(path)
|
||||
response = get("#{gitlab_api_url}/#{path}", { headers: { "PRIVATE-TOKEN" => gitlab_access_token } })
|
||||
parse_body(response)
|
||||
end
|
||||
|
||||
# Api update request
|
||||
#
|
||||
# @param [Symbol] verb :post or :put
|
||||
# @param [String] path
|
||||
# @param [Hash] payload
|
||||
# @return [Hash, Array]
|
||||
def api_update(verb, path, **payload)
|
||||
response = send(
|
||||
verb,
|
||||
"#{gitlab_api_url}/#{path}",
|
||||
payload,
|
||||
{ headers: { "PRIVATE-TOKEN" => gitlab_access_token } }
|
||||
)
|
||||
parse_body(response)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -32,7 +32,6 @@ describe QA::Support::Formatters::TestMetricsFormatter do
|
|||
let(:branch) { 'master' }
|
||||
let(:run_type) { 'staging-full' }
|
||||
let(:smoke) { 'false' }
|
||||
let(:blocking) { 'false' }
|
||||
let(:quarantined) { 'false' }
|
||||
let(:failure_issue) { '' }
|
||||
let(:influx_client) { instance_double('InfluxDB2::Client', create_write_api: influx_write_api) }
|
||||
|
|
@ -66,7 +65,6 @@ describe QA::Support::Formatters::TestMetricsFormatter do
|
|||
file_path: file_path.gsub('./qa/specs/features', ''),
|
||||
status: status,
|
||||
smoke: smoke,
|
||||
blocking: blocking,
|
||||
quarantined: quarantined,
|
||||
job_name: 'test-job',
|
||||
merge_request: 'false',
|
||||
|
|
@ -195,18 +193,6 @@ describe QA::Support::Formatters::TestMetricsFormatter do
|
|||
stub_env('QA_INFLUXDB_TIMEOUT', "10")
|
||||
end
|
||||
|
||||
context 'with blocking spec' do
|
||||
let(:blocking) { 'true' }
|
||||
|
||||
it 'exports data with correct blocking tag', :aggregate_failures do
|
||||
run_spec do
|
||||
it('spec', :blocking, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/1234') {}
|
||||
end
|
||||
|
||||
expect(influx_write_api).to have_received(:write).with(data: [data])
|
||||
end
|
||||
end
|
||||
|
||||
context 'with product group tag' do
|
||||
let(:expected_data) { data.tap { |d| d[:tags][:product_group] = :import } }
|
||||
|
||||
|
|
@ -222,7 +208,7 @@ describe QA::Support::Formatters::TestMetricsFormatter do
|
|||
context 'with smoke spec' do
|
||||
let(:smoke) { 'true' }
|
||||
|
||||
it 'exports data with correct blocking tag', :aggregate_failures do
|
||||
it 'exports data with correct smoke tag', :aggregate_failures do
|
||||
run_spec do
|
||||
it('spec', :smoke, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/1234') {}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,682 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
describe QA::Tools::ReliableReport do
|
||||
include QA::Support::Helpers::StubEnv
|
||||
|
||||
before do
|
||||
stub_env("QA_INFLUXDB_URL", "url")
|
||||
stub_env("QA_INFLUXDB_TOKEN", "token")
|
||||
stub_env("SLACK_WEBHOOK", "slack_url")
|
||||
stub_env("CI_API_V4_URL", "gitlab_api_url")
|
||||
stub_env("GITLAB_ACCESS_TOKEN", "gitlab_token")
|
||||
|
||||
allow(RestClient::Request).to receive(:execute)
|
||||
allow(Slack::Notifier).to receive(:new).and_return(slack_notifier)
|
||||
allow(InfluxDB2::Client).to receive(:new).and_return(influx_client)
|
||||
|
||||
allow(query_api).to receive(:query).with(query: flux_query(blocking: false)).and_return(runs)
|
||||
allow(query_api).to receive(:query).with(query: flux_query(blocking: true)).and_return(reliable_runs)
|
||||
end
|
||||
|
||||
let(:slack_notifier) { instance_double("Slack::Notifier", post: nil) }
|
||||
let(:influx_client) { instance_double("InfluxDB2::Client", create_query_api: query_api) }
|
||||
let(:query_api) { instance_double("InfluxDB2::QueryApi") }
|
||||
|
||||
let(:slack_channel) { "#quality-reports" }
|
||||
let(:range) { 14 }
|
||||
let(:issue_url) { "https://gitlab.com/issue/1" }
|
||||
let(:time) { "2021-12-07T04:05:25.000000000+00:00" }
|
||||
let(:failure_message) { 'random failure message' }
|
||||
|
||||
let(:run_values) do
|
||||
{
|
||||
"name" => "stable spec1",
|
||||
"status" => "passed",
|
||||
"file_path" => "/some/spec.rb",
|
||||
"stage" => "create",
|
||||
"product_group" => "code_review",
|
||||
"testcase" => "https://testcase/url",
|
||||
"run_type" => "e2e-test-on-omnibus",
|
||||
"_time" => time
|
||||
}
|
||||
end
|
||||
|
||||
let(:run_more_values) do
|
||||
{
|
||||
"name" => "stable spec2",
|
||||
"status" => "passed",
|
||||
"file_path" => "/some/spec.rb",
|
||||
"stage" => "manage",
|
||||
"product_group" => "import_and_integrate",
|
||||
"testcase" => "https://testcase/url",
|
||||
"run_type" => "e2e-test-on-omnibus",
|
||||
"_time" => time
|
||||
}
|
||||
end
|
||||
|
||||
let(:runs) do
|
||||
[
|
||||
instance_double(
|
||||
"InfluxDB2::FluxTable",
|
||||
records: [
|
||||
instance_double("InfluxDB2::FluxRecord", values: run_values),
|
||||
instance_double("InfluxDB2::FluxRecord", values: run_values),
|
||||
instance_double("InfluxDB2::FluxRecord", values: run_values.merge({ "_time" => Time.now.to_s }))
|
||||
]
|
||||
),
|
||||
instance_double(
|
||||
"InfluxDB2::FluxTable",
|
||||
records: [
|
||||
instance_double("InfluxDB2::FluxRecord", values: run_more_values),
|
||||
instance_double("InfluxDB2::FluxRecord", values: run_more_values),
|
||||
instance_double("InfluxDB2::FluxRecord", values: run_more_values.merge({ "_time" => Time.now.to_s }))
|
||||
]
|
||||
)
|
||||
]
|
||||
end
|
||||
|
||||
let(:reliable_run_values) do
|
||||
{
|
||||
"name" => "unstable spec",
|
||||
"status" => "failed",
|
||||
"file_path" => "/some/spec.rb",
|
||||
"stage" => "create",
|
||||
"product_group" => "code_review",
|
||||
"failure_exception" => failure_message,
|
||||
"job_url" => "https://job/url",
|
||||
"testcase" => "https://testcase/url",
|
||||
"failure_issue" => "https://issues/url",
|
||||
"run_type" => "e2e-test-on-omnibus",
|
||||
"_time" => time
|
||||
}
|
||||
end
|
||||
|
||||
let(:reliable_run_more_values) do
|
||||
{
|
||||
"name" => "unstable spec",
|
||||
"status" => "failed",
|
||||
"file_path" => "/some/spec.rb",
|
||||
"stage" => "manage",
|
||||
"product_group" => "import_and_integrate",
|
||||
"failure_exception" => failure_message,
|
||||
"job_url" => "https://job/url",
|
||||
"testcase" => "https://testcase/url",
|
||||
"failure_issue" => "https://issues/url",
|
||||
"run_type" => "e2e-test-on-omnibus",
|
||||
"_time" => time
|
||||
}
|
||||
end
|
||||
|
||||
let(:reliable_runs) do
|
||||
[
|
||||
instance_double(
|
||||
"InfluxDB2::FluxTable",
|
||||
records: [
|
||||
instance_double("InfluxDB2::FluxRecord", values: { **reliable_run_values, "status" => "passed" }),
|
||||
instance_double("InfluxDB2::FluxRecord", values: reliable_run_values),
|
||||
instance_double("InfluxDB2::FluxRecord", values: reliable_run_values.merge({ "_time" => Time.now.to_s }))
|
||||
]
|
||||
),
|
||||
instance_double(
|
||||
"InfluxDB2::FluxTable",
|
||||
records: [
|
||||
instance_double("InfluxDB2::FluxRecord", values: { **reliable_run_more_values, "status" => "passed" }),
|
||||
instance_double("InfluxDB2::FluxRecord", values: reliable_run_more_values),
|
||||
instance_double("InfluxDB2::FluxRecord", values: reliable_run_more_values.merge({ "_time" => Time.now.to_s }))
|
||||
]
|
||||
)
|
||||
]
|
||||
end
|
||||
|
||||
def flux_query(blocking:)
|
||||
<<~QUERY
|
||||
from(bucket: "e2e-test-stats-main")
|
||||
|> range(start: -#{range}d)
|
||||
|> filter(fn: (r) => r._measurement == "test-stats")
|
||||
|> filter(fn: (r) => r.run_type == "staging-full" or
|
||||
r.run_type == "staging-sanity" or
|
||||
r.run_type == "production-full" or
|
||||
r.run_type == "production-sanity" or
|
||||
r.run_type == "e2e-test-on-omnibus" or
|
||||
r.run_type == "e2e-test-on-gdk" or
|
||||
r.run_type == "nightly"
|
||||
)
|
||||
|> filter(fn: (r) => r.job_name != "airgapped" and
|
||||
r.job_name != "nplus1-instance-image"
|
||||
)
|
||||
|> filter(fn: (r) => r.status != "pending" and
|
||||
r.merge_request == "false" and
|
||||
r.quarantined == "false" and
|
||||
r.smoke == "false" and
|
||||
r.blocking == "#{blocking}"
|
||||
)
|
||||
|> filter(fn: (r) => r["_field"] == "job_url" or
|
||||
r["_field"] == "failure_exception" or
|
||||
r["_field"] == "id" or
|
||||
r["_field"] == "failure_issue"
|
||||
)
|
||||
|> pivot(rowKey: ["_time"], columnKey: ["_field"], valueColumn: "_value")
|
||||
|> group(columns: ["name"])
|
||||
QUERY
|
||||
end
|
||||
|
||||
def expected_stage_markdown(result, stage, product_group, type)
|
||||
<<~SECTION.strip
|
||||
## #{stage.capitalize} (1)
|
||||
|
||||
<details>
|
||||
<summary>Executions table ~\"group::#{product_group}\" (1)</summary>
|
||||
|
||||
#{table(result, ['NAME', 'RUNS', 'FAILURES', 'FAILURE RATE'], "Top #{type} specs in '#{stage}' stage for past #{range} days", true)}
|
||||
|
||||
</details>
|
||||
SECTION
|
||||
end
|
||||
|
||||
def expected_summary_table(summary, type, markdown = false)
|
||||
table(summary, %w[STAGE COUNT], "#{type.capitalize} spec summary for past #{range} days".ljust(50), markdown)
|
||||
end
|
||||
|
||||
def table(rows, headings, title, markdown = false)
|
||||
Terminal::Table.new(
|
||||
headings: headings,
|
||||
title: markdown ? nil : title,
|
||||
rows: rows,
|
||||
style: markdown ? { border: :markdown } : { all_separators: true }
|
||||
)
|
||||
end
|
||||
|
||||
def name_column(spec_name, exceptions_and_related_urls = {})
|
||||
"**Name**: #{spec_name}<br>**File**: [spec.rb](https://gitlab.com/gitlab-org/gitlab/-/blob/master/qa/qa/specs/features/some/spec.rb)#{exceptions_markdown(exceptions_and_related_urls)}"
|
||||
end
|
||||
|
||||
def exceptions_markdown(exceptions_and_related_urls)
|
||||
exceptions_and_related_urls.empty? ? '' : "<br>**Exceptions**:<br>- [`#{failure_message}`](https://issues/url)"
|
||||
end
|
||||
|
||||
describe '.run' do
|
||||
subject(:run) { described_class.run(range: range, report_in_issue_and_slack: create_issue) }
|
||||
|
||||
context "without report creation" do
|
||||
let(:create_issue) { "false" }
|
||||
|
||||
it "does not create report issue", :aggregate_failures do
|
||||
expect { run }.to output.to_stdout
|
||||
|
||||
expect(RestClient::Request).not_to have_received(:execute)
|
||||
expect(slack_notifier).not_to have_received(:post)
|
||||
end
|
||||
end
|
||||
|
||||
context "with report creation" do
|
||||
let(:create_issue) { "true" }
|
||||
let(:iid) { 2 }
|
||||
let(:old_iid) { 1 }
|
||||
let(:issue_endpoint) { "gitlab_api_url/projects/278964/issues" }
|
||||
|
||||
let(:common_api_args) do
|
||||
{
|
||||
verify_ssl: false,
|
||||
headers: { "PRIVATE-TOKEN" => "gitlab_token" }
|
||||
}
|
||||
end
|
||||
|
||||
let(:create_issue_response) do
|
||||
instance_double(
|
||||
"RestClient::Response",
|
||||
code: 200,
|
||||
body: { web_url: issue_url, iid: iid }.to_json
|
||||
)
|
||||
end
|
||||
|
||||
let(:open_issues_response) do
|
||||
instance_double(
|
||||
"RestClient::Response",
|
||||
code: 200,
|
||||
body: [{ web_url: issue_url, iid: iid }, { web_url: issue_url, iid: old_iid }].to_json
|
||||
)
|
||||
end
|
||||
|
||||
let(:success_response) do
|
||||
instance_double("RestClient::Response", code: 200, body: {}.to_json)
|
||||
end
|
||||
|
||||
before do
|
||||
allow(RestClient::Request).to receive(:execute).exactly(4).times.and_return(
|
||||
create_issue_response,
|
||||
open_issues_response,
|
||||
success_response,
|
||||
success_response
|
||||
)
|
||||
end
|
||||
|
||||
shared_examples 'report creation' do
|
||||
it "creates report issue" do
|
||||
expect { run }.to output.to_stdout
|
||||
|
||||
expect(RestClient::Request).to have_received(:execute).with(
|
||||
method: :post,
|
||||
url: issue_endpoint,
|
||||
payload: {
|
||||
title: "Reliable e2e test report",
|
||||
description: expected_issue_body,
|
||||
labels: "reliable test report,Quality,test,type::maintenance,automation:ml"
|
||||
},
|
||||
**common_api_args
|
||||
)
|
||||
expect(RestClient::Request).to have_received(:execute).with(
|
||||
method: :get,
|
||||
url: "#{issue_endpoint}?labels=reliable test report&state=opened",
|
||||
**common_api_args
|
||||
)
|
||||
expect(RestClient::Request).to have_received(:execute).with(
|
||||
method: :put,
|
||||
url: "#{issue_endpoint}/#{old_iid}",
|
||||
payload: {
|
||||
state_event: "close"
|
||||
},
|
||||
**common_api_args
|
||||
)
|
||||
expect(RestClient::Request).to have_received(:execute).with(
|
||||
method: :post,
|
||||
url: "#{issue_endpoint}/#{old_iid}/notes",
|
||||
payload: {
|
||||
body: "Closed issue in favor of ##{iid}"
|
||||
},
|
||||
**common_api_args
|
||||
)
|
||||
expect(slack_notifier).to have_received(:post).with(
|
||||
icon_emoji: ":tanuki-protect:",
|
||||
text: expected_slack_text
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "with disallowed exception" do
|
||||
let(:failure_message) { 'random failure message' }
|
||||
|
||||
let(:expected_issue_body) do
|
||||
<<~TXT.strip
|
||||
[[_TOC_]]
|
||||
|
||||
# Candidates for promotion to blocking (#{Date.today - range} - #{Date.today})
|
||||
|
||||
**Note: MRs will be auto-created for promoting the top #{QA::Tools::ReliableReport::PROMOTION_BATCH_LIMIT} specs sorted by most number of successful runs**
|
||||
|
||||
Total amount: **2**
|
||||
|
||||
#{expected_summary_table([['create', 1], ['manage', 1]], :stable, true)}
|
||||
|
||||
#{expected_stage_markdown([[name_column('stable spec1'), 3, 0, '0%']], 'create', 'code review', :stable)}
|
||||
|
||||
#{expected_stage_markdown([[name_column('stable spec2'), 3, 0, '0%']], 'manage', 'import and integrate', :stable)}
|
||||
|
||||
# Blocking specs with failures (#{Date.today - range} - #{Date.today})
|
||||
|
||||
**Note:**
|
||||
|
||||
* Only failures from the nightly, e2e-test-on-omnibus and e2e-test-on-gdk pipelines are considered
|
||||
|
||||
* Only specs that have a failure rate of equal or greater than 1 percent are considered
|
||||
|
||||
* Quarantine MRs will be created for all specs listed below
|
||||
|
||||
Total amount: **2**
|
||||
|
||||
#{expected_summary_table([['create', 1], ['manage', 1]], :unstable, true)}
|
||||
|
||||
#{expected_stage_markdown([[name_column('unstable spec', { failure_message => 'https://job/url' }), 3, 2, '66.67%']], 'create', 'code review', :unstable)}
|
||||
|
||||
#{expected_stage_markdown([[name_column('unstable spec', { failure_message => 'https://job/url' }), 3, 2, '66.67%']], 'manage', 'import and integrate', :unstable)}
|
||||
TXT
|
||||
end
|
||||
|
||||
let(:expected_slack_text) do
|
||||
<<~TEXT
|
||||
```#{expected_summary_table([['create', 1], ['manage', 1]], :stable)}```
|
||||
```#{expected_summary_table([['create', 1], ['manage', 1]], :unstable)}```
|
||||
|
||||
#{issue_url}
|
||||
TEXT
|
||||
end
|
||||
|
||||
it_behaves_like "report creation"
|
||||
end
|
||||
|
||||
context "with allowed exception" do
|
||||
let(:failure_message) { 'Ambiguous match' }
|
||||
|
||||
let(:expected_issue_body) do
|
||||
<<~TXT.strip
|
||||
[[_TOC_]]
|
||||
|
||||
# Candidates for promotion to blocking (#{Date.today - range} - #{Date.today})
|
||||
|
||||
**Note: MRs will be auto-created for promoting the top #{QA::Tools::ReliableReport::PROMOTION_BATCH_LIMIT} specs sorted by most number of successful runs**
|
||||
|
||||
Total amount: **2**
|
||||
|
||||
#{expected_summary_table([['create', 1], ['manage', 1]], :stable, true)}
|
||||
|
||||
#{expected_stage_markdown([[name_column('stable spec1'), 3, 0, '0%']], 'create', 'code review', :stable)}
|
||||
|
||||
#{expected_stage_markdown([[name_column('stable spec2'), 3, 0, '0%']], 'manage', 'import and integrate', :stable)}
|
||||
TXT
|
||||
end
|
||||
|
||||
let(:expected_slack_text) do
|
||||
<<~TEXT
|
||||
```#{expected_summary_table([['create', 1], ['manage', 1]], :stable)}```
|
||||
```#{expected_summary_table([], :unstable)}```
|
||||
|
||||
#{issue_url}
|
||||
TEXT
|
||||
end
|
||||
|
||||
it_behaves_like "report creation"
|
||||
end
|
||||
end
|
||||
|
||||
context "with failure" do
|
||||
let(:create_issue) { "true" }
|
||||
|
||||
before do
|
||||
allow(query_api).to receive(:query).and_raise("Connection error!")
|
||||
end
|
||||
|
||||
it "notifies failure", :aggregate_failures do
|
||||
expect { expect { run }.to raise_error("Connection error!") }.to output.to_stdout
|
||||
|
||||
expect(slack_notifier).to have_received(:post).with(
|
||||
icon_emoji: ":sadpanda:",
|
||||
text: "Reliable reporter failed to create report. Error: ```Connection error!```"
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#allowed_failure?" do
|
||||
subject(:reliable_report) { described_class.new(14) }
|
||||
|
||||
it "returns true for an allowed failure" do
|
||||
expect(reliable_report.send(:allowed_failure?, "Couldn't find option named abc")).to be true
|
||||
end
|
||||
|
||||
it "returns false for disallowed failure" do
|
||||
expect(reliable_report.send(:allowed_failure?,
|
||||
%q([Unable to find css "[data-testid=\"user_action_dropdown\"]"]))).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe "#issue_for_most_failures" do
|
||||
subject(:reliable_report) { described_class.new(14) }
|
||||
|
||||
let(:failure_message_1) { "This is a failure exception 1" }
|
||||
let(:failure_message_2) { "This is a failure exception 2" }
|
||||
let(:job_url) { "https://example.com/job/url" }
|
||||
let(:failure_issue_url_1) { "https://example.com/failure/issue_1" }
|
||||
let(:failure_issue_url_2) { "https://example.com/failure/issue_2" }
|
||||
|
||||
let(:records) do
|
||||
[
|
||||
instance_double("InfluxDB2::FluxRecord", values: values_1),
|
||||
instance_double("InfluxDB2::FluxRecord", values: values_2),
|
||||
instance_double("InfluxDB2::FluxRecord", values: values_2)
|
||||
]
|
||||
end
|
||||
|
||||
let(:values_1) do
|
||||
{
|
||||
"failure_exception" => failure_message_1,
|
||||
"failure_issue" => failure_issue_url_1,
|
||||
"job_url" => job_url
|
||||
}
|
||||
end
|
||||
|
||||
let(:values_2) do
|
||||
{
|
||||
"failure_exception" => failure_message_2,
|
||||
"failure_issue" => failure_issue_url_2,
|
||||
"job_url" => job_url
|
||||
}
|
||||
end
|
||||
|
||||
let(:values_3) do
|
||||
{
|
||||
"failure_exception" => failure_message_2,
|
||||
"failure_issue" => failure_issue_url_2,
|
||||
"job_url" => job_url
|
||||
}
|
||||
end
|
||||
|
||||
it 'returns the failure issue with most failures' do
|
||||
expect(reliable_report.send(:issue_for_most_failures, records)).to eq(failure_issue_url_2)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#exceptions_and_related_urls" do
|
||||
subject(:reliable_report) { described_class.new(14) }
|
||||
|
||||
let(:failure_message) { "This is a failure exception" }
|
||||
let(:job_url) { "https://example.com/job/url" }
|
||||
let(:failure_issue_url) { "https://example.com/failure/issue" }
|
||||
|
||||
let(:records) do
|
||||
[instance_double("InfluxDB2::FluxRecord", values: values)]
|
||||
end
|
||||
|
||||
context "without failure_exception" do
|
||||
let(:values) do
|
||||
{
|
||||
"failure_exception" => nil,
|
||||
"job_url" => job_url,
|
||||
"failure_issue" => failure_issue_url
|
||||
}
|
||||
end
|
||||
|
||||
it "returns an empty hash" do
|
||||
expect(reliable_report.send(:exceptions_and_related_urls, records)).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context "with failure_exception" do
|
||||
context "without failure_issue" do
|
||||
let(:values) do
|
||||
{
|
||||
"failure_exception" => failure_message,
|
||||
"job_url" => job_url
|
||||
}
|
||||
end
|
||||
|
||||
it "returns job_url as value" do
|
||||
expect(reliable_report.send(:exceptions_and_related_urls, records).values).to eq([job_url])
|
||||
end
|
||||
end
|
||||
|
||||
context "with failure_issue and job_url" do
|
||||
let(:values) do
|
||||
{
|
||||
"failure_exception" => failure_message,
|
||||
"failure_issue" => failure_issue_url,
|
||||
"job_url" => job_url
|
||||
}
|
||||
end
|
||||
|
||||
it "returns failure_issue as value" do
|
||||
expect(reliable_report.send(:exceptions_and_related_urls, records).values).to eq([failure_issue_url])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#specs_attributes" do
|
||||
subject(:reliable_report) { described_class.new(14) }
|
||||
|
||||
let(:promotion_batch_limit) { 10 }
|
||||
|
||||
let(:report_web_url) { 'https://report/url' }
|
||||
|
||||
before do
|
||||
allow(reliable_report).to receive(:report_web_url).and_return(report_web_url)
|
||||
end
|
||||
|
||||
shared_examples "spec attributes" do |blocking|
|
||||
it "returns #{blocking} spec attributes" do
|
||||
stub_const("QA::Tools::ReliableReport::PROMOTION_BATCH_LIMIT", promotion_batch_limit)
|
||||
|
||||
expect(reliable_report.send(:specs_attributes, blocking: blocking)).to eq(expected_specs_attributes)
|
||||
end
|
||||
end
|
||||
|
||||
context "with blocking true" do
|
||||
let(:expected_specs_attributes) do
|
||||
{ type: "Unstable Specs",
|
||||
report_issue: "https://report/url",
|
||||
specs:
|
||||
[
|
||||
{ stage: "create",
|
||||
product_group: "code_review",
|
||||
name: "unstable spec",
|
||||
file: "spec.rb",
|
||||
link: "https://gitlab.com/gitlab-org/gitlab/-/blob/master/qa/qa/specs/features/some/spec.rb",
|
||||
runs: 3,
|
||||
failed: 2,
|
||||
failure_issue: "https://issues/url",
|
||||
failure_rate: 66.67,
|
||||
testcase: "https://testcase/url",
|
||||
file_path: "/qa/qa/specs/features/some/spec.rb",
|
||||
all_run_type: ["e2e-test-on-omnibus"],
|
||||
failed_run_type: ["e2e-test-on-omnibus"] },
|
||||
{ stage: "manage",
|
||||
product_group: "import_and_integrate",
|
||||
name: "unstable spec",
|
||||
file: "spec.rb",
|
||||
link: "https://gitlab.com/gitlab-org/gitlab/-/blob/master/qa/qa/specs/features/some/spec.rb",
|
||||
runs: 3,
|
||||
failed: 2,
|
||||
failure_issue: "https://issues/url",
|
||||
failure_rate: 66.67,
|
||||
testcase: "https://testcase/url",
|
||||
file_path: "/qa/qa/specs/features/some/spec.rb",
|
||||
all_run_type: ["e2e-test-on-omnibus"],
|
||||
failed_run_type: ["e2e-test-on-omnibus"] }
|
||||
] }
|
||||
end
|
||||
|
||||
it_behaves_like "spec attributes", true
|
||||
end
|
||||
|
||||
context "with blocking false" do
|
||||
let(:expected_specs_attributes) do
|
||||
{
|
||||
type: "Stable Specs",
|
||||
report_issue: "https://report/url",
|
||||
specs:
|
||||
[
|
||||
{ stage: "manage",
|
||||
product_group: "import_and_integrate",
|
||||
name: "stable spec2",
|
||||
file: "spec.rb",
|
||||
link: "https://gitlab.com/gitlab-org/gitlab/-/blob/master/qa/qa/specs/features/some/spec.rb",
|
||||
runs: 3,
|
||||
failed: 0,
|
||||
failure_issue: "",
|
||||
failure_rate: 0,
|
||||
testcase: "https://testcase/url",
|
||||
file_path: "/qa/qa/specs/features/some/spec.rb",
|
||||
all_run_type: ["e2e-test-on-omnibus"],
|
||||
failed_run_type: [] },
|
||||
{ stage: "create",
|
||||
product_group: "code_review",
|
||||
name: "stable spec1",
|
||||
file: "spec.rb",
|
||||
link: "https://gitlab.com/gitlab-org/gitlab/-/blob/master/qa/qa/specs/features/some/spec.rb",
|
||||
runs: 3,
|
||||
failed: 0,
|
||||
failure_issue: "",
|
||||
failure_rate: 0,
|
||||
testcase: "https://testcase/url",
|
||||
file_path: "/qa/qa/specs/features/some/spec.rb",
|
||||
all_run_type: ["e2e-test-on-omnibus"],
|
||||
failed_run_type: [] }
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
it_behaves_like "spec attributes", false
|
||||
|
||||
context "with specific PROMOTION_BATCH_LIMIT" do
|
||||
let(:promotion_batch_limit) { 1 }
|
||||
|
||||
let(:runs) do
|
||||
[
|
||||
instance_double(
|
||||
"InfluxDB2::FluxTable",
|
||||
records: [
|
||||
instance_double("InfluxDB2::FluxRecord", values: run_values),
|
||||
instance_double("InfluxDB2::FluxRecord", values: run_values),
|
||||
instance_double("InfluxDB2::FluxRecord", values: run_values.merge({ "_time" => Time.now.to_s }))
|
||||
]
|
||||
),
|
||||
instance_double(
|
||||
"InfluxDB2::FluxTable",
|
||||
records: [
|
||||
instance_double("InfluxDB2::FluxRecord", values: run_more_values),
|
||||
instance_double("InfluxDB2::FluxRecord", values: run_more_values)
|
||||
]
|
||||
)
|
||||
]
|
||||
end
|
||||
|
||||
let(:expected_specs_attributes) do
|
||||
{
|
||||
type: "Stable Specs",
|
||||
report_issue: "https://report/url",
|
||||
specs:
|
||||
[
|
||||
{ stage: "create",
|
||||
product_group: "code_review",
|
||||
name: "stable spec1",
|
||||
file: "spec.rb",
|
||||
link: "https://gitlab.com/gitlab-org/gitlab/-/blob/master/qa/qa/specs/features/some/spec.rb",
|
||||
runs: 3,
|
||||
failed: 0,
|
||||
failure_issue: "",
|
||||
failure_rate: 0,
|
||||
testcase: "https://testcase/url",
|
||||
file_path: "/qa/qa/specs/features/some/spec.rb",
|
||||
all_run_type: ["e2e-test-on-omnibus"],
|
||||
failed_run_type: [] }
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
it_behaves_like "spec attributes", false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#skip_blocking_spec_record?" do
|
||||
subject(:reliable_report) { described_class.new(14) }
|
||||
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:failed_count, :failure_issue, :failed_run_type, :failure_rate, :result) do
|
||||
1 | 'https://failure/issues/url' | ['e2e-test-on-omnibus'] | 2 | false
|
||||
1 | 'https://failure/issues/url' | ['e2e-test-on-gdk'] | 2 | false
|
||||
1 | 'https://failure/issues/url' | ['nightly'] | 2 | false
|
||||
0 | 'https://failure/issues/url' | ['e2e-test-on-gdk'] | 2 | true
|
||||
1 | 'https://failure/issue/url' | ['e2e-test-on-gdk'] | 2 | true
|
||||
1 | 'https://failure/issues/url' | ['abc'] | 2 | true
|
||||
1 | 'https://failure/issues/url' | ['e2e-test-on-gdk'] | 0 | true
|
||||
end
|
||||
|
||||
with_them do
|
||||
it do
|
||||
expect(reliable_report.send(:skip_blocking_spec_record?, failed_count: failed_count,
|
||||
failure_issue: failure_issue, failed_run_type: failed_run_type, failure_rate: failure_rate))
|
||||
.to eq result
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
desc "Fetch reliable and unreliable spec data and create report"
|
||||
task :reliable_spec_report, [:range, :report_in_issue_and_slack] do |_task, args|
|
||||
QA::Tools::ReliableReport.run(**args)
|
||||
end
|
||||
|
|
@ -1,176 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Work item linked items', :js, feature_category: :team_planning do
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:project) { create(:project, :public, namespace: group) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:work_item) { create(:work_item, project: project) }
|
||||
let(:work_items_path) { project_work_item_path(project, work_item.iid) }
|
||||
let_it_be(:task) { create(:work_item, :task, project: project, title: 'Task 1') }
|
||||
let_it_be(:milestone) { create(:milestone, project: project, title: '1.0') }
|
||||
let_it_be(:label) { create(:label, project: project) }
|
||||
let_it_be(:objective) do
|
||||
create(:work_item, :objective, project: project, milestone: milestone,
|
||||
title: 'Objective 1', labels: [label])
|
||||
end
|
||||
|
||||
context 'for signed in user' do
|
||||
let(:token_input_selector) { '[data-testid="work-item-token-select-input"] .gl-token-selector-input' }
|
||||
|
||||
before_all do
|
||||
project.add_developer(user)
|
||||
end
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
|
||||
stub_feature_flags(work_items: true)
|
||||
|
||||
visit work_items_path
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
it 'are not displayed when issue does not have work item links', :aggregate_failures do
|
||||
within_testid('work-item-relationships') do
|
||||
expect(page).to have_selector('[data-testid="link-item-add-button"]')
|
||||
expect(page).not_to have_selector('[data-testid="link-work-item-form"]')
|
||||
expect(page).not_to have_selector('[data-testid="work-item-linked-items-list"]')
|
||||
end
|
||||
end
|
||||
|
||||
it 'toggles widget body', :aggregate_failures do
|
||||
within_testid('work-item-relationships') do
|
||||
expect(page).to have_selector('[data-testid="crud-empty"]')
|
||||
|
||||
click_button 'Collapse'
|
||||
|
||||
expect(page).not_to have_selector('[data-testid="crud-empty"]')
|
||||
|
||||
click_button 'Expand'
|
||||
|
||||
expect(page).to have_selector('[data-testid="crud-empty"]')
|
||||
end
|
||||
end
|
||||
|
||||
it 'toggles form', :aggregate_failures do
|
||||
within_testid('work-item-relationships') do
|
||||
expect(page).not_to have_selector('[data-testid="link-work-item-form"]')
|
||||
|
||||
click_button 'Add'
|
||||
|
||||
expect(page).to have_selector('[data-testid="link-work-item-form"]')
|
||||
|
||||
click_button 'Cancel'
|
||||
|
||||
expect(page).not_to have_selector('[data-testid="link-work-item-form"]')
|
||||
end
|
||||
end
|
||||
|
||||
it 'links a new item with work item text', :aggregate_failures,
|
||||
quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/444980' do
|
||||
verify_linked_item_added(task.title)
|
||||
end
|
||||
|
||||
it 'links a new item with work item iid', :aggregate_failures,
|
||||
quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/444751' do
|
||||
verify_linked_item_added(task.iid)
|
||||
end
|
||||
|
||||
it 'links a new item with work item wildcard iid', :aggregate_failures,
|
||||
quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/444914' do
|
||||
verify_linked_item_added("##{task.iid}")
|
||||
end
|
||||
|
||||
it 'links a new item with work item reference', :aggregate_failures,
|
||||
quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/445635' do
|
||||
verify_linked_item_added(task.to_reference(full: true))
|
||||
end
|
||||
|
||||
it 'links a new item with work item url', :aggregate_failures,
|
||||
quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/438014' do
|
||||
verify_linked_item_added("#{Gitlab.config.gitlab.url}/#{task.project.full_path}/-/work_items/#{task.iid}")
|
||||
end
|
||||
|
||||
it 'removes a linked item', :aggregate_failures do
|
||||
within_testid('work-item-relationships') do
|
||||
click_button 'Add'
|
||||
|
||||
within_testid('link-work-item-form') do
|
||||
expect(page).to have_button('Add', disabled: true)
|
||||
find_by_testid('work-item-token-select-input').set(task.title)
|
||||
wait_for_all_requests
|
||||
click_button task.title
|
||||
|
||||
expect(page).to have_button('Add', disabled: false)
|
||||
|
||||
click_button 'Add'
|
||||
|
||||
wait_for_all_requests
|
||||
end
|
||||
|
||||
expect(find('.work-items-list')).to have_content('Task 1')
|
||||
|
||||
find_by_testid('links-child').hover
|
||||
find_by_testid('remove-work-item-link').click
|
||||
|
||||
wait_for_all_requests
|
||||
|
||||
expect(page).not_to have_content('Task 1')
|
||||
end
|
||||
end
|
||||
|
||||
it 'passes axe automated accessibility testing for linked items empty state' do
|
||||
selector = '[data-testid="work-item-relationships"]'
|
||||
expect(page).to be_axe_clean.within(selector).skipping :'link-in-text-block'
|
||||
end
|
||||
|
||||
it 'passes axe automated accessibility testing for linked items' do
|
||||
within_testid('work-item-relationships') do
|
||||
click_button 'Add'
|
||||
|
||||
find_by_testid('work-item-token-select-input').set(objective.title)
|
||||
wait_for_all_requests
|
||||
|
||||
form_selector = '[data-testid="work-item-relationships"]'
|
||||
expect(page).to be_axe_clean.within(form_selector).skipping :'aria-input-field-name',
|
||||
:'aria-required-children'
|
||||
|
||||
within_testid('link-work-item-form') do
|
||||
click_button objective.title
|
||||
|
||||
click_button 'Add'
|
||||
end
|
||||
|
||||
wait_for_all_requests
|
||||
|
||||
expect(page).to be_axe_clean.within(form_selector)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def verify_linked_item_added(input)
|
||||
within_testid('work-item-relationships') do
|
||||
click_button 'Add'
|
||||
|
||||
within_testid('link-work-item-form') do
|
||||
expect(page).to have_button('Add', disabled: true)
|
||||
|
||||
find(token_input_selector).set(input)
|
||||
wait_for_all_requests
|
||||
|
||||
click_button task.title
|
||||
|
||||
expect(page).to have_button('Add', disabled: false)
|
||||
|
||||
click_button 'Add'
|
||||
|
||||
wait_for_all_requests
|
||||
end
|
||||
|
||||
expect(find('.work-items-list')).to have_content('Task 1')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -24,6 +24,8 @@ RSpec.describe 'Work item detail', :js, feature_category: :team_planning do
|
|||
let(:work_items_path) { project_work_item_path(project, work_item.iid) }
|
||||
|
||||
context 'for signed in user' do
|
||||
let(:linked_item) { task }
|
||||
|
||||
before_all do
|
||||
group.add_developer(user)
|
||||
end
|
||||
|
|
@ -46,6 +48,7 @@ RSpec.describe 'Work item detail', :js, feature_category: :team_planning do
|
|||
it_behaves_like 'work items title'
|
||||
it_behaves_like 'work items description'
|
||||
it_behaves_like 'work items award emoji'
|
||||
it_behaves_like 'work items linked items'
|
||||
it_behaves_like 'work items comments', :issue
|
||||
it_behaves_like 'work items toggle status button'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { parseConfig } from '~/glql/core/parser/config';
|
||||
import { parseYAMLConfig } from '~/glql/core/parser/config';
|
||||
|
||||
describe('parseConfig', () => {
|
||||
describe('parseYAMLConfig', () => {
|
||||
it('parses the frontmatter and returns an object', () => {
|
||||
const frontmatter = 'fields: title, assignees, dueDate\ndisplay: list';
|
||||
|
||||
expect(parseConfig(frontmatter)).toEqual({
|
||||
expect(parseYAMLConfig(frontmatter)).toEqual({
|
||||
fields: [
|
||||
{ name: 'title', label: 'Title', key: 'title' },
|
||||
{ name: 'assignees', label: 'Assignees', key: 'assignees' },
|
||||
|
|
@ -17,7 +17,7 @@ describe('parseConfig', () => {
|
|||
it('returns default fields if none are provided', () => {
|
||||
const frontmatter = 'display: list';
|
||||
|
||||
expect(parseConfig(frontmatter, { fields: ['title', 'assignees', 'dueDate'] })).toEqual({
|
||||
expect(parseYAMLConfig(frontmatter, { fields: ['title', 'assignees', 'dueDate'] })).toEqual({
|
||||
fields: [
|
||||
{ name: 'title', label: 'Title', key: 'title' },
|
||||
{ name: 'assignees', label: 'Assignees', key: 'assignees' },
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { parseQueryText } from '~/glql/core/parser';
|
||||
import { parseQueryTextWithFrontmatter, parse } from '~/glql/core/parser';
|
||||
|
||||
describe('parseQueryText', () => {
|
||||
describe('parseQueryTextWithFrontmatter', () => {
|
||||
it('separates the presentation layer from the query and returns an object', () => {
|
||||
const text = `---
|
||||
fields: title, assignees, dueDate
|
||||
|
|
@ -8,7 +8,7 @@ display: list
|
|||
---
|
||||
assignee = currentUser()`;
|
||||
|
||||
expect(parseQueryText(text)).toEqual({
|
||||
expect(parseQueryTextWithFrontmatter(text)).toEqual({
|
||||
frontmatter: 'fields: title, assignees, dueDate\ndisplay: list',
|
||||
query: 'assignee = currentUser()',
|
||||
});
|
||||
|
|
@ -17,9 +17,179 @@ assignee = currentUser()`;
|
|||
it('returns empty frontmatter if no frontmatter is present', () => {
|
||||
const text = 'assignee = currentUser()';
|
||||
|
||||
expect(parseQueryText(text)).toEqual({
|
||||
expect(parseQueryTextWithFrontmatter(text)).toEqual({
|
||||
frontmatter: '',
|
||||
query: 'assignee = currentUser()',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('parse', () => {
|
||||
beforeEach(() => {
|
||||
gon.current_username = 'root';
|
||||
});
|
||||
|
||||
it('parses a simple query correctly', async () => {
|
||||
expect(await parse('assignee = currentUser()')).toMatchInlineSnapshot(`
|
||||
{
|
||||
"config": {
|
||||
"display": "list",
|
||||
"fields": [
|
||||
{
|
||||
"key": "title",
|
||||
"label": "Title",
|
||||
"name": "title",
|
||||
},
|
||||
],
|
||||
},
|
||||
"query": "query GLQL {
|
||||
issues(assigneeUsernames: "root", first: 100) {
|
||||
nodes {
|
||||
id
|
||||
iid
|
||||
title
|
||||
webUrl
|
||||
reference
|
||||
state
|
||||
type
|
||||
}
|
||||
pageInfo {
|
||||
startCursor
|
||||
endCursor
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
}
|
||||
}
|
||||
}
|
||||
",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('parses a query with frontmatter correctly', async () => {
|
||||
expect(
|
||||
await parse(`
|
||||
---
|
||||
fields: title, assignees, dueDate
|
||||
display: table
|
||||
---
|
||||
assignee = currentUser()`),
|
||||
).toMatchInlineSnapshot(`
|
||||
{
|
||||
"config": {
|
||||
"display": "table",
|
||||
"fields": [
|
||||
{
|
||||
"key": "title",
|
||||
"label": "Title",
|
||||
"name": "title",
|
||||
},
|
||||
{
|
||||
"key": "assignees",
|
||||
"label": "Assignees",
|
||||
"name": "assignees",
|
||||
},
|
||||
{
|
||||
"key": "dueDate",
|
||||
"label": "Due date",
|
||||
"name": "dueDate",
|
||||
},
|
||||
],
|
||||
},
|
||||
"query": "query GLQL {
|
||||
issues(assigneeUsernames: "root", first: 100) {
|
||||
nodes {
|
||||
id
|
||||
iid
|
||||
title
|
||||
webUrl
|
||||
reference
|
||||
state
|
||||
type
|
||||
assignees {
|
||||
nodes {
|
||||
id
|
||||
avatarUrl
|
||||
username
|
||||
name
|
||||
webUrl}
|
||||
}
|
||||
dueDate
|
||||
}
|
||||
pageInfo {
|
||||
startCursor
|
||||
endCursor
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
}
|
||||
}
|
||||
}
|
||||
",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('parses a YAML based query correctly', async () => {
|
||||
expect(
|
||||
await parse(`
|
||||
fields: title, assignees, dueDate
|
||||
display: table
|
||||
limit: 20
|
||||
query: assignee = currentUser()
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
{
|
||||
"config": {
|
||||
"display": "table",
|
||||
"fields": [
|
||||
{
|
||||
"key": "title",
|
||||
"label": "Title",
|
||||
"name": "title",
|
||||
},
|
||||
{
|
||||
"key": "assignees",
|
||||
"label": "Assignees",
|
||||
"name": "assignees",
|
||||
},
|
||||
{
|
||||
"key": "dueDate",
|
||||
"label": "Due date",
|
||||
"name": "dueDate",
|
||||
},
|
||||
],
|
||||
"limit": 20,
|
||||
},
|
||||
"query": "query GLQL {
|
||||
issues(assigneeUsernames: "root", first: 20) {
|
||||
nodes {
|
||||
id
|
||||
iid
|
||||
title
|
||||
webUrl
|
||||
reference
|
||||
state
|
||||
type
|
||||
assignees {
|
||||
nodes {
|
||||
id
|
||||
avatarUrl
|
||||
username
|
||||
name
|
||||
webUrl}
|
||||
}
|
||||
dueDate
|
||||
}
|
||||
pageInfo {
|
||||
startCursor
|
||||
endCursor
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
}
|
||||
}
|
||||
}
|
||||
",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,8 +5,7 @@ import * as Sentry from '~/sentry/sentry_browser_wrapper';
|
|||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import CandidateList from '~/ml/model_registry/components/candidate_list.vue';
|
||||
import SearchableList from '~/ml/model_registry/components/searchable_list.vue';
|
||||
import CandidateListRow from '~/ml/model_registry/components/candidate_list_row.vue';
|
||||
import SearchableTable from '~/ml/model_registry/components/searchable_table.vue';
|
||||
import getModelCandidatesQuery from '~/ml/model_registry/graphql/queries/get_model_candidates.query.graphql';
|
||||
import { GRAPHQL_PAGE_SIZE } from '~/ml/model_registry/constants';
|
||||
import {
|
||||
|
|
@ -22,8 +21,7 @@ describe('ml/model_registry/components/candidate_list.vue', () => {
|
|||
let wrapper;
|
||||
let apolloProvider;
|
||||
|
||||
const findSearchableList = () => wrapper.findComponent(SearchableList);
|
||||
const findAllRows = () => wrapper.findAllComponents(CandidateListRow);
|
||||
const findSearchableTable = () => wrapper.findComponent(SearchableTable);
|
||||
|
||||
const mountComponent = ({
|
||||
props = {},
|
||||
|
|
@ -38,6 +36,9 @@ describe('ml/model_registry/components/candidate_list.vue', () => {
|
|||
modelId: 'gid://gitlab/Ml::Model/2',
|
||||
...props,
|
||||
},
|
||||
stubs: {
|
||||
SearchableTable,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -66,7 +67,7 @@ describe('ml/model_registry/components/candidate_list.vue', () => {
|
|||
});
|
||||
|
||||
it('is displayed', () => {
|
||||
expect(findSearchableList().props('errorMessage')).toBe(
|
||||
expect(findSearchableTable().props('errorMessage')).toBe(
|
||||
'Failed to load model candidates with error: Failure!',
|
||||
);
|
||||
});
|
||||
|
|
@ -83,21 +84,15 @@ describe('ml/model_registry/components/candidate_list.vue', () => {
|
|||
});
|
||||
|
||||
it('Passes items to list', () => {
|
||||
expect(findSearchableList().props('items')).toEqual(graphqlCandidates);
|
||||
expect(findSearchableTable().props('candidates')).toEqual(graphqlCandidates);
|
||||
});
|
||||
|
||||
it('displays package version rows', () => {
|
||||
expect(findAllRows()).toHaveLength(graphqlCandidates.length);
|
||||
expect(findSearchableTable().props('candidates')).toHaveLength(graphqlCandidates.length);
|
||||
});
|
||||
|
||||
it('binds the correct props', () => {
|
||||
expect(findAllRows().at(0).props()).toMatchObject({
|
||||
candidate: expect.objectContaining(graphqlCandidates[0]),
|
||||
});
|
||||
|
||||
expect(findAllRows().at(1).props()).toMatchObject({
|
||||
candidate: expect.objectContaining(graphqlCandidates[1]),
|
||||
});
|
||||
expect(findSearchableTable().props('candidates')).toEqual(graphqlCandidates);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -114,7 +109,7 @@ describe('ml/model_registry/components/candidate_list.vue', () => {
|
|||
});
|
||||
|
||||
it('when list emits fetch-page fetches the next set of records', async () => {
|
||||
findSearchableList().vm.$emit('fetch-page', {
|
||||
findSearchableTable().vm.$emit('fetch-page', {
|
||||
after: 'eyJpZCI6IjIifQ',
|
||||
first: 30,
|
||||
id: 'gid://gitlab/Ml::Model/2',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,94 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import { GlTableLite, GlBadge, GlLink } from '@gitlab/ui';
|
||||
import CandidatesTable from '~/ml/model_registry/components/candidates_table.vue';
|
||||
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
import { graphqlCandidates } from '../graphql_mock_data';
|
||||
|
||||
describe('CandidatesTable', () => {
|
||||
let wrapper;
|
||||
const createWrapper = (props = {}) => {
|
||||
wrapper = mount(CandidatesTable, {
|
||||
propsData: {
|
||||
items: graphqlCandidates,
|
||||
...props,
|
||||
},
|
||||
stubs: {
|
||||
GlTableLite,
|
||||
GlBadge,
|
||||
GlLink,
|
||||
TimeAgoTooltip,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findGlTable = () => wrapper.findComponent(GlTableLite);
|
||||
const findTableRows = () => wrapper.findAll('tbody tr');
|
||||
|
||||
beforeEach(() => {
|
||||
createWrapper();
|
||||
});
|
||||
|
||||
it('renders the table', () => {
|
||||
expect(findGlTable().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('has the correct columns in the table', () => {
|
||||
expect(findGlTable().props('fields')).toEqual([
|
||||
{ key: 'eid', label: 'MLflow Run ID', thClass: 'gl-w-2/8' },
|
||||
{ key: 'ciJob', label: 'CI Job', thClass: 'gl-w-1/8' },
|
||||
{ key: 'createdAt', label: 'Created', thClass: 'gl-w-1/8' },
|
||||
{ key: 'status', label: 'Status', thClass: 'gl-w-1/8' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('renders the correct number of rows', () => {
|
||||
expect(findTableRows().length).toBe(2);
|
||||
});
|
||||
|
||||
it('renders the correct information in the id column', () => {
|
||||
const idCell = findTableRows().at(0).findAll('td').at(0);
|
||||
expect(idCell.text()).toBe(graphqlCandidates[0].eid);
|
||||
expect(idCell.findComponent(GlLink).attributes('href')).toBe(
|
||||
graphqlCandidates[0]._links.showPath,
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the correct information in the CI job column', () => {
|
||||
const ciJobCell = findTableRows().at(0).findAll('td').at(1);
|
||||
expect(ciJobCell.text()).toContain(graphqlCandidates[0].ciJob.name);
|
||||
});
|
||||
|
||||
it('renders the correct information in the created column', () => {
|
||||
const createdAtCell = findTableRows().at(0).findAll('td').at(2);
|
||||
expect(createdAtCell.findComponent(TimeAgoTooltip).props('time')).toBe(
|
||||
graphqlCandidates[0].createdAt,
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the correct information in the status column', () => {
|
||||
const statusCell = findTableRows().at(0).findAll('td').at(3);
|
||||
expect(statusCell.text()).toContain(graphqlCandidates[0].status);
|
||||
expect(statusCell.findComponent(GlBadge).props('variant')).toBe('info');
|
||||
});
|
||||
|
||||
describe('when there is no CI Job information', () => {
|
||||
beforeEach(() => {
|
||||
createWrapper({ items: [{ ...graphqlCandidates[0], ciJob: null }] });
|
||||
});
|
||||
|
||||
it('does not render content in CI Job column', () => {
|
||||
const ciJobCell = findTableRows().at(0).findAll('td').at(1);
|
||||
expect(ciJobCell.text()).toBe('');
|
||||
});
|
||||
|
||||
it('does not render a badge in the CI Job column', () => {
|
||||
const ciJobCell = findTableRows().at(0).findAll('td').at(1);
|
||||
expect(ciJobCell.findComponent(GlBadge).exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('does not render a link in the CI Job column', () => {
|
||||
const ciJobCell = findTableRows().at(0).findAll('td').at(1);
|
||||
expect(ciJobCell.findComponent(GlLink).exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -3,18 +3,20 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
|||
import SearchableTable from '~/ml/model_registry/components/searchable_table.vue';
|
||||
import ModelVersionsTable from '~/ml/model_registry/components/model_versions_table.vue';
|
||||
import ModelsTable from '~/ml/model_registry/components/models_table.vue';
|
||||
import CandidatesTable from '~/ml/model_registry/components/candidates_table.vue';
|
||||
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
|
||||
import { BASE_SORT_FIELDS } from '~/ml/model_registry/constants';
|
||||
import * as urlHelpers from '~/lib/utils/url_utility';
|
||||
import LoadOrErrorOrShow from '~/ml/model_registry/components/load_or_error_or_show.vue';
|
||||
import { defaultPageInfo } from '../mock_data';
|
||||
import { graphqlModelVersions, graphqlModels } from '../graphql_mock_data';
|
||||
import { graphqlModelVersions, graphqlModels, graphqlCandidates } from '../graphql_mock_data';
|
||||
|
||||
describe('ml/model_registry/components/searchable_list.vue', () => {
|
||||
describe('ml/model_registry/components/searchable_table.vue', () => {
|
||||
let wrapper;
|
||||
|
||||
const findLoadOrErrorOrShow = () => wrapper.findComponent(LoadOrErrorOrShow);
|
||||
const findModelsTable = () => wrapper.findComponent(ModelsTable);
|
||||
const findCandidatesTable = () => wrapper.findComponent(CandidatesTable);
|
||||
const findModelVersionsTable = () => wrapper.findComponent(ModelVersionsTable);
|
||||
const findSearchableTable = () => wrapper.findComponent(SearchableTable);
|
||||
const findEmptyState = () => wrapper.findByTestId('empty-state-slot');
|
||||
|
|
@ -72,6 +74,7 @@ describe('ml/model_registry/components/searchable_list.vue', () => {
|
|||
it('does not display registry list', () => {
|
||||
expect(findModelVersionsTable().exists()).toBe(false);
|
||||
expect(findModelsTable().exists()).toBe(false);
|
||||
expect(findCandidatesTable().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('Does not display error message', () => {
|
||||
|
|
@ -101,6 +104,7 @@ describe('ml/model_registry/components/searchable_list.vue', () => {
|
|||
it('displays model versions table', () => {
|
||||
expect(findModelVersionsTable().exists()).toBe(true);
|
||||
expect(findModelsTable().exists()).toBe(false);
|
||||
expect(findCandidatesTable().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('binds the right props', () => {
|
||||
|
|
@ -133,6 +137,7 @@ describe('ml/model_registry/components/searchable_list.vue', () => {
|
|||
it('displays model table', () => {
|
||||
expect(findModelsTable().exists()).toBe(true);
|
||||
expect(findModelVersionsTable().exists()).toBe(false);
|
||||
expect(findCandidatesTable().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('binds the right props', () => {
|
||||
|
|
@ -159,6 +164,39 @@ describe('ml/model_registry/components/searchable_list.vue', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when list is loaded with candidates', () => {
|
||||
beforeEach(() => mountComponent({ candidates: graphqlCandidates }));
|
||||
|
||||
it('displays candidates table', () => {
|
||||
expect(findCandidatesTable().exists()).toBe(true);
|
||||
expect(findModelVersionsTable().exists()).toBe(false);
|
||||
expect(findModelsTable().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('binds the right props', () => {
|
||||
expect(findSearchableTable().props()).toMatchObject({
|
||||
candidates: graphqlCandidates,
|
||||
isLoading: false,
|
||||
pageInfo: defaultPageInfo,
|
||||
showSearch: false,
|
||||
sortableFields: [],
|
||||
canWriteModelRegistry: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('displays candidate rows', () => {
|
||||
expect(findCandidatesTable().props('items')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('does not display loader', () => {
|
||||
expect(findLoadOrErrorOrShow().props('isLoading')).toBe(false);
|
||||
});
|
||||
|
||||
it('does not display empty state', () => {
|
||||
expect(findEmptyState().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when user interacts with pagination', () => {
|
||||
beforeEach(() => mountComponent());
|
||||
|
||||
|
|
|
|||
|
|
@ -154,6 +154,12 @@ export const modelVersionWithCandidateAndNullAuthor = {
|
|||
export const graphqlCandidates = [
|
||||
{
|
||||
id: 'gid://gitlab/Ml::Candidate/1',
|
||||
eid: 'e9a71521-45c6-4b0a-b0c3-21f0b4528a5c',
|
||||
ciJob: {
|
||||
id: 'gid://gitlab/Ci::Build/1',
|
||||
name: 'build:linux',
|
||||
},
|
||||
status: 'running',
|
||||
name: 'narwhal-aardvark-heron-6953',
|
||||
createdAt: '2023-12-06T12:41:48Z',
|
||||
_links: {
|
||||
|
|
@ -162,6 +168,12 @@ export const graphqlCandidates = [
|
|||
},
|
||||
{
|
||||
id: 'gid://gitlab/Ml::Candidate/2',
|
||||
eid: 'e9a71521-45c6-4b0a-b0c3-21f0b4528a4c',
|
||||
ciJob: {
|
||||
id: 'gid://gitlab/Ci::Build/2',
|
||||
name: 'build:linux',
|
||||
},
|
||||
status: 'failed',
|
||||
name: 'anteater-chimpanzee-snake-1254',
|
||||
createdAt: '2023-12-06T12:41:48Z',
|
||||
_links: {
|
||||
|
|
|
|||
|
|
@ -84,6 +84,34 @@ RSpec.describe Gitlab::Auth::Identity, :request_store, feature_category: :system
|
|||
end
|
||||
end
|
||||
|
||||
describe '.link_from_scoped_user_id' do
|
||||
let(:scoped_user_id) { scoped_user.id }
|
||||
|
||||
subject(:identity) { described_class.link_from_scoped_user_id(primary_user, scoped_user_id) }
|
||||
|
||||
context 'when composite identity is required for the actor' do
|
||||
before do
|
||||
allow(primary_user).to receive(:has_composite_identity?).and_return(true)
|
||||
end
|
||||
|
||||
it 'returns an identity' do
|
||||
expect(identity).to be_composite
|
||||
expect(identity).to be_linked
|
||||
expect(identity).to be_valid
|
||||
|
||||
expect(identity.scoped_user).to eq(scoped_user)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when scoped_user_id is unknown' do
|
||||
let(:scoped_user_id) { 0 }
|
||||
|
||||
it 'returns nil' do
|
||||
expect(identity).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.fabricate' do
|
||||
subject(:identity) { described_class.fabricate(primary_user) }
|
||||
|
||||
|
|
|
|||
|
|
@ -99,6 +99,19 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists, feature_category:
|
|||
end
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
|
||||
it 'logs the pattern comparison limit exceeded' do
|
||||
expect(Gitlab::AppJsonLogger).to receive(:info).with(
|
||||
class: described_class.name,
|
||||
message: 'rules:exists pattern comparisons limit exceeded',
|
||||
project_id: project.id,
|
||||
paths_size: kind_of(Integer),
|
||||
globs_size: 1,
|
||||
comparisons: kind_of(Integer)
|
||||
)
|
||||
|
||||
satisfied_by?
|
||||
end
|
||||
end
|
||||
|
||||
context 'when rules:exists:project is provided' do
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ RSpec.describe Gitlab::Ci::Config::External::Rules, feature_category: :pipeline_
|
|||
|
||||
context 'when there is a rule with exists:' do
|
||||
let(:rule_hashes) { [{ exists: 'file.txt' }] }
|
||||
let(:pipeline) { instance_double(Ci::Pipeline, project: project, project_id: project.id, sha: 'sha', id: 1) }
|
||||
|
||||
context 'when the file exists' do
|
||||
let(:context) { double(top_level_worktree_paths: ['file.txt']) }
|
||||
|
|
|
|||
|
|
@ -540,6 +540,7 @@ protected_branches:
|
|||
- approval_project_rules_with_unique_policies
|
||||
- external_status_checks
|
||||
- required_code_owners_sections
|
||||
- squash_option
|
||||
protected_tags:
|
||||
- project
|
||||
- create_access_levels
|
||||
|
|
|
|||
|
|
@ -312,4 +312,19 @@ RSpec.describe Gitlab::Pages::UrlBuilder, feature_category: :pages do
|
|||
it { is_expected.to eq('example.com') }
|
||||
end
|
||||
end
|
||||
|
||||
describe "is_namespace_homepage?" do
|
||||
subject { described_class.new(project).is_namespace_homepage? }
|
||||
|
||||
where(:full_path, :result) do
|
||||
'group/foo' | false
|
||||
'group/group.example.com' | true
|
||||
'grOUP/group.example.com' | true
|
||||
'group/Group.example.com' | true
|
||||
end
|
||||
|
||||
with_them do
|
||||
it { is_expected.to eq(result) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ RSpec.describe API::Pages, feature_category: :pages do
|
|||
"created_at" => created_at.strftime('%Y-%m-%dT%H:%M:%S.%3LZ'),
|
||||
"path_prefix" => nil,
|
||||
"root_directory" => "public",
|
||||
"url" => "http://unique-domain.example.com/"
|
||||
"url" => "http://unique-domain.example.com"
|
||||
}
|
||||
])
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,14 +6,14 @@ RSpec.describe VerifyPagesDomainService, feature_category: :pages do
|
|||
using RSpec::Parameterized::TableSyntax
|
||||
include EmailHelpers
|
||||
|
||||
let(:error_status) { { status: :error, message: "Couldn't verify #{domain.domain}" } }
|
||||
|
||||
subject(:service) { described_class.new(domain) }
|
||||
let(:service) { described_class.new(domain) }
|
||||
|
||||
describe '#execute' do
|
||||
subject(:service_response) { service.execute }
|
||||
|
||||
where(:domain_sym, :code_sym) do
|
||||
:domain | :verification_code
|
||||
:domain | :keyed_verification_code
|
||||
:domain | :verification_code
|
||||
:domain | :keyed_verification_code
|
||||
|
||||
:verification_domain | :verification_code
|
||||
:verification_domain | :keyed_verification_code
|
||||
|
|
@ -24,8 +24,10 @@ RSpec.describe VerifyPagesDomainService, feature_category: :pages do
|
|||
let(:verification_code) { domain.send(code_sym) }
|
||||
|
||||
shared_examples 'verifies and enables the domain' do
|
||||
it_behaves_like 'returning a success service response'
|
||||
|
||||
it 'verifies and enables the domain' do
|
||||
expect(service.execute).to eq(status: :success)
|
||||
service_response
|
||||
|
||||
expect(domain).to be_verified
|
||||
expect(domain).to be_enabled
|
||||
|
|
@ -52,13 +54,19 @@ RSpec.describe VerifyPagesDomainService, feature_category: :pages do
|
|||
end
|
||||
|
||||
shared_examples 'unverifies and disables domain' do
|
||||
let(:error_message) { "Couldn't verify #{domain.domain}" }
|
||||
|
||||
it_behaves_like 'returning an error service response'
|
||||
it { is_expected.to have_attributes message: error_message }
|
||||
|
||||
it 'unverifies domain' do
|
||||
expect(service.execute).to eq(error_status)
|
||||
service_response
|
||||
|
||||
expect(domain).not_to be_verified
|
||||
end
|
||||
|
||||
it 'disables domain and shedules it for removal in 1 week' do
|
||||
service.execute
|
||||
service_response
|
||||
|
||||
expect(domain).not_to be_enabled
|
||||
|
||||
|
|
@ -66,7 +74,7 @@ RSpec.describe VerifyPagesDomainService, feature_category: :pages do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when domain is disabled(or new)' do
|
||||
context 'when domain is disabled (or new)' do
|
||||
let(:domain) { create(:pages_domain, :disabled) }
|
||||
|
||||
include_examples 'successful enablement and verification'
|
||||
|
|
@ -79,14 +87,6 @@ RSpec.describe VerifyPagesDomainService, feature_category: :pages do
|
|||
include_examples 'unverifies and disables domain'
|
||||
end
|
||||
|
||||
context 'when txt record does not contain verification code' do
|
||||
before do
|
||||
stub_resolver(domain_name => 'something else')
|
||||
end
|
||||
|
||||
include_examples 'unverifies and disables domain'
|
||||
end
|
||||
|
||||
context 'when no txt records are present' do
|
||||
before do
|
||||
stub_resolver
|
||||
|
|
@ -102,14 +102,19 @@ RSpec.describe VerifyPagesDomainService, feature_category: :pages do
|
|||
include_examples 'successful enablement and verification'
|
||||
|
||||
shared_examples 'unverifing domain' do
|
||||
it_behaves_like 'returning an error service response'
|
||||
it { is_expected.to have_attributes message: "Couldn't verify #{domain.domain}" }
|
||||
|
||||
it 'unverifies but does not disable domain' do
|
||||
expect(service.execute).to eq(error_status)
|
||||
service_response
|
||||
|
||||
expect(domain).not_to be_verified
|
||||
expect(domain).to be_enabled
|
||||
end
|
||||
|
||||
it 'does not schedule domain for removal' do
|
||||
service.execute
|
||||
service_response
|
||||
|
||||
expect(domain.remove_at).to be_nil
|
||||
end
|
||||
end
|
||||
|
|
@ -147,9 +152,9 @@ RSpec.describe VerifyPagesDomainService, feature_category: :pages do
|
|||
stub_resolver
|
||||
end
|
||||
|
||||
let(:error_status) { { status: :error, message: "Couldn't verify #{domain.domain}. It is now disabled." } }
|
||||
|
||||
include_examples 'unverifies and disables domain'
|
||||
include_examples 'unverifies and disables domain' do
|
||||
let(:error_message) { "Couldn't verify #{domain.domain}. It is now disabled." }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -161,15 +166,17 @@ RSpec.describe VerifyPagesDomainService, feature_category: :pages do
|
|||
stub_resolver(domain.domain => domain.keyed_verification_code)
|
||||
end
|
||||
|
||||
it_behaves_like 'returning a success service response'
|
||||
|
||||
it 'verifies and enables domain' do
|
||||
expect(service.execute).to eq(status: :success)
|
||||
service_response
|
||||
|
||||
expect(domain).to be_verified
|
||||
expect(domain).to be_enabled
|
||||
end
|
||||
|
||||
it 'prevent domain from being removed' do
|
||||
expect { service.execute }.to change { domain.remove_at }.to(nil)
|
||||
expect { service_response }.to change { domain.remove_at }.to(nil)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -179,7 +186,7 @@ RSpec.describe VerifyPagesDomainService, feature_category: :pages do
|
|||
end
|
||||
|
||||
it 'keeps domain scheduled for removal but does not change removal time' do
|
||||
expect { service.execute }.not_to change { domain.remove_at }
|
||||
expect { service_response }.not_to change { domain.remove_at }
|
||||
expect(domain.remove_at).to be_present
|
||||
end
|
||||
end
|
||||
|
|
@ -190,14 +197,14 @@ RSpec.describe VerifyPagesDomainService, feature_category: :pages do
|
|||
|
||||
before do
|
||||
domain.save!(validate: false)
|
||||
stub_resolver
|
||||
end
|
||||
|
||||
it_behaves_like 'returning an error service response'
|
||||
it { is_expected.to have_attributes(message: "Couldn't verify #{domain.domain}. It is now disabled.") }
|
||||
|
||||
it 'can be disabled' do
|
||||
error_status[:message] += '. It is now disabled.'
|
||||
|
||||
stub_resolver
|
||||
|
||||
expect(service.execute).to eq(error_status)
|
||||
service_response
|
||||
|
||||
expect(domain).not_to be_verified
|
||||
expect(domain).not_to be_enabled
|
||||
|
|
@ -211,7 +218,7 @@ RSpec.describe VerifyPagesDomainService, feature_category: :pages do
|
|||
it 'sets a timeout on the DNS query' do
|
||||
expect(stub_resolver).to receive(:timeouts=).with(described_class::RESOLVER_TIMEOUT_SECONDS)
|
||||
|
||||
service.execute
|
||||
service_response
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -235,7 +242,7 @@ RSpec.describe VerifyPagesDomainService, feature_category: :pages do
|
|||
let(:domain) { create(:pages_domain, *[factory].compact) }
|
||||
|
||||
before do
|
||||
allow(service).to receive(:notification_service) { notification_service }
|
||||
allow(NotificationService).to receive(:new).and_return(notification_service)
|
||||
|
||||
if verification_succeeds
|
||||
stub_resolver(domain.domain => domain.verification_code)
|
||||
|
|
@ -249,7 +256,7 @@ RSpec.describe VerifyPagesDomainService, feature_category: :pages do
|
|||
expect(notification_service).to receive(:"pages_domain_#{expected_notification}").with(domain)
|
||||
end
|
||||
|
||||
service.execute
|
||||
service_response
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -257,28 +264,28 @@ RSpec.describe VerifyPagesDomainService, feature_category: :pages do
|
|||
let(:domain) { create(:pages_domain, :disabled) }
|
||||
|
||||
before do
|
||||
allow(NotificationService).to receive(:new).and_return(notification_service)
|
||||
|
||||
stub_application_setting(pages_domain_verification_enabled: false)
|
||||
allow(service).to receive(:notification_service) { notification_service }
|
||||
end
|
||||
|
||||
it 'skips email notifications' do
|
||||
expect(notification_service).not_to receive(:pages_domain_enabled)
|
||||
|
||||
service.execute
|
||||
service_response
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'no verification code' do
|
||||
let(:domain) { create(:pages_domain) }
|
||||
|
||||
it 'returns an error' do
|
||||
domain.verification_code = ''
|
||||
let(:domain) { build(:pages_domain, verification_code: '') }
|
||||
|
||||
before do
|
||||
disallow_resolver!
|
||||
|
||||
expect(service.execute).to eq(status: :error, message: "No verification code set for #{domain.domain}")
|
||||
end
|
||||
|
||||
it_behaves_like 'returning an error service response'
|
||||
it { is_expected.to have_attributes(message: "No verification code set for #{domain.domain}") }
|
||||
end
|
||||
|
||||
context 'pages domain verification is disabled' do
|
||||
|
|
@ -291,7 +298,7 @@ RSpec.describe VerifyPagesDomainService, feature_category: :pages do
|
|||
it 'extends domain validity by unconditionally reverifying' do
|
||||
disallow_resolver!
|
||||
|
||||
service.execute
|
||||
service_response
|
||||
|
||||
expect(domain).to be_verified
|
||||
expect(domain).to be_enabled
|
||||
|
|
@ -302,7 +309,7 @@ RSpec.describe VerifyPagesDomainService, feature_category: :pages do
|
|||
domain.update!(enabled_until: grace)
|
||||
disallow_resolver!
|
||||
|
||||
service.execute
|
||||
service_response
|
||||
|
||||
expect(domain.enabled_until).to be_like_time(grace)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ module ProtectedBranchHelpers
|
|||
page.driver.browser.manage.window.maximize
|
||||
|
||||
# Make sure dropdown is in view
|
||||
execute_script('window.scrollTo(0,0)')
|
||||
execute_script("document.querySelector('#{form}').scrollIntoView({ block: 'start' })")
|
||||
|
||||
within(form) do
|
||||
within_select(".js-allowed-to-#{operation}:not([disabled])") do
|
||||
|
|
|
|||
|
|
@ -866,3 +866,115 @@ RSpec.shared_examples 'work items hierarchy' do |testid, type|
|
|||
click_button "Create #{type}"
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'work items linked items' do |is_group = false|
|
||||
it 'are not displayed when issue does not have work item links', :aggregate_failures do
|
||||
within_testid('work-item-relationships') do
|
||||
expect(page).to have_selector('[data-testid="link-item-add-button"]')
|
||||
expect(page).not_to have_selector('[data-testid="link-work-item-form"]')
|
||||
expect(page).not_to have_selector('[data-testid="work-item-linked-items-list"]')
|
||||
end
|
||||
end
|
||||
|
||||
it 'toggles widget body and form', :aggregate_failures do
|
||||
within_testid('work-item-relationships') do
|
||||
expect(page).to have_selector('[data-testid="crud-empty"]')
|
||||
|
||||
click_button 'Collapse'
|
||||
|
||||
expect(page).not_to have_selector('[data-testid="crud-empty"]')
|
||||
|
||||
click_button 'Expand'
|
||||
|
||||
expect(page).to have_selector('[data-testid="crud-empty"]')
|
||||
|
||||
expect(page).not_to have_selector('[data-testid="link-work-item-form"]')
|
||||
|
||||
click_button 'Add'
|
||||
|
||||
expect(page).to have_selector('[data-testid="link-work-item-form"]')
|
||||
|
||||
click_button 'Cancel'
|
||||
|
||||
expect(page).not_to have_selector('[data-testid="link-work-item-form"]')
|
||||
end
|
||||
end
|
||||
|
||||
it 'links a new item with work item text', :aggregate_failures do
|
||||
expect_linked_item_added(linked_item.title)
|
||||
end
|
||||
|
||||
it 'links a new item with work item iid', :aggregate_failures do
|
||||
expect_linked_item_added(linked_item.iid)
|
||||
end
|
||||
|
||||
it 'links a new item with work item wildcard iid', :aggregate_failures do
|
||||
expect_linked_item_added("##{linked_item.iid}")
|
||||
end
|
||||
|
||||
it 'links a new item with work item url', :aggregate_failures do
|
||||
url = if is_group
|
||||
"#{Gitlab.config.gitlab.url}/groups/#{linked_item.namespace.full_path}/-/work_items/#{linked_item.iid}"
|
||||
else
|
||||
"#{Gitlab.config.gitlab.url}/#{linked_item.project.full_path}/-/work_items/#{linked_item.iid}"
|
||||
end
|
||||
|
||||
expect_linked_item_added(url)
|
||||
end
|
||||
|
||||
it 'removes a linked item', :aggregate_failures do
|
||||
within_testid('work-item-relationships') do
|
||||
click_button 'Add'
|
||||
|
||||
within_testid('link-work-item-form') do
|
||||
fill_in 'Search existing items', with: linked_item.title
|
||||
click_button linked_item.title
|
||||
click_button 'Add'
|
||||
end
|
||||
|
||||
expect(page).to have_link linked_item.title
|
||||
|
||||
find_link(linked_item.title).hover
|
||||
click_button 'Remove', match: :first
|
||||
|
||||
expect(page).not_to have_link linked_item.title
|
||||
end
|
||||
end
|
||||
|
||||
it 'passes axe automated accessibility testing for linked items', :aggregate_failures do
|
||||
selector = '[data-testid="work-item-relationships"]'
|
||||
|
||||
within_testid('work-item-relationships') do
|
||||
expect(page).to be_axe_clean.within(selector).skipping :'link-in-text-block'
|
||||
|
||||
click_button 'Add'
|
||||
fill_in 'Search existing items', with: linked_item.title
|
||||
|
||||
expect(page).to be_axe_clean.within(selector).skipping :'aria-input-field-name',
|
||||
:'aria-required-children'
|
||||
|
||||
within_testid('link-work-item-form') do
|
||||
click_button linked_item.title
|
||||
click_button 'Add'
|
||||
end
|
||||
|
||||
expect(page).to be_axe_clean.within(selector)
|
||||
end
|
||||
end
|
||||
|
||||
def expect_linked_item_added(input)
|
||||
within_testid('work-item-relationships') do
|
||||
click_button 'Add'
|
||||
|
||||
within_testid('link-work-item-form') do
|
||||
expect(page).to have_button('Add', disabled: true)
|
||||
|
||||
fill_in 'Search existing items', with: input
|
||||
click_button linked_item.title, match: :first
|
||||
click_button 'Add'
|
||||
end
|
||||
|
||||
expect(page).to have_link linked_item.title
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
32
yarn.lock
32
yarn.lock
|
|
@ -1355,10 +1355,10 @@
|
|||
core-js "^3.29.1"
|
||||
mitt "^3.0.1"
|
||||
|
||||
"@gitlab/duo-ui@^5.0.0":
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/duo-ui/-/duo-ui-5.0.0.tgz#f803a8c10400b7eccaa08451ca8c5149b556b5b5"
|
||||
integrity sha512-ojeNHENJrx6MZeUskm6DisGAT2BXKRn8UjRx3zldsxZJBpwU55mVsTrzQa1iR1ilvy9RrPbGeWRT4jbAH7n3kg==
|
||||
"@gitlab/duo-ui@^3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/duo-ui/-/duo-ui-3.0.0.tgz#923ef334f88a473b32a4fce267060f23a00de9ed"
|
||||
integrity sha512-e+BjHZsefmvYRj5+uFcxZs1/FhwCVa1KvRxqKntFS9nARgwpYr+HzDfDb+30SG+CvJNsdWIebkOnuLEKfKvH1w==
|
||||
dependencies:
|
||||
"@floating-ui/dom" "1.4.3"
|
||||
echarts "^5.3.2"
|
||||
|
|
@ -1369,7 +1369,6 @@
|
|||
popper.js "^1.16.1"
|
||||
portal-vue "^2.1.7"
|
||||
vue-functional-data-merge "^3.1.0"
|
||||
vue-resizable "1.3.4"
|
||||
vue-runtime-helpers "^1.1.2"
|
||||
|
||||
"@gitlab/eslint-plugin@20.6.0":
|
||||
|
|
@ -1398,8 +1397,7 @@
|
|||
resolved "https://registry.yarnpkg.com/@gitlab/fonts/-/fonts-1.3.0.tgz#df89c1bb6714e4a8a5d3272568aa4de7fb337267"
|
||||
integrity sha512-DoMUIN3DqjEn7wvcxBg/b7Ite5fTdF5EmuOZoBRo2j0UBGweDXmNBi+9HrTZs4cBU660dOxcf1hATFcG3npbPg==
|
||||
|
||||
"@gitlab/noop@^1.0.0", jackspeak@^2.3.5, "jackspeak@npm:@gitlab/noop@1.0.0":
|
||||
name jackspeak
|
||||
"@gitlab/noop@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/noop/-/noop-1.0.0.tgz#b1ecb8ae6b2abf9b2e28927e4fbb05b7a1b2704b"
|
||||
integrity sha512-nOltttik5o2BjBo8LnyeTFzHoLpMY/XcCVOC+lm9ZwU+ivEam8wafacMF0KTbRn1KVrIoHYdo70QnqS+vJiOVw==
|
||||
|
|
@ -1423,15 +1421,17 @@
|
|||
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.121.0.tgz#57cacc895929aef4320632396373797a64b230ff"
|
||||
integrity sha512-ZekVjdMZrjrNEjdrOHsJYCu7A+ea3AkuNUxWIZ3FaNgJj4Oh21RlTP7bQKnRSXVhBbV1jg1PgzQ1ANEoCW8t4g==
|
||||
|
||||
"@gitlab/ui@105.1.1":
|
||||
version "105.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-105.1.1.tgz#deb27dbe0a00bbdf88300848d01e39b7814524e0"
|
||||
integrity sha512-5AOVdqy3NL/LIUCEnsgkpbDjYcZJbYWUOnu9qi19wSFzwuvZN+Pb94firIkB/8dwmxTwPM7RDg3HU0msUTBt7Q==
|
||||
"@gitlab/ui@104.2.0":
|
||||
version "104.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-104.2.0.tgz#074db0e83ee8a05afa69b0099c750c4df48d55e4"
|
||||
integrity sha512-CUTHvQRYFpCAyCLpY/D/eKiOMMaOzuCmjqDLNLieU6Uh6AkS49orSBNNNs3bUSS163r+LNx/NsTrHDLA5NxAcA==
|
||||
dependencies:
|
||||
"@floating-ui/dom" "1.4.3"
|
||||
echarts "^5.3.2"
|
||||
iframe-resizer "^4.3.2"
|
||||
lodash "^4.17.20"
|
||||
marked "^12.0.0"
|
||||
marked-bidi "^1.0.8"
|
||||
popper.js "^1.16.1"
|
||||
portal-vue "^2.1.7"
|
||||
vue-functional-data-merge "^3.1.0"
|
||||
|
|
@ -9261,6 +9261,11 @@ iterall@^1.2.1:
|
|||
resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.3.0.tgz#afcb08492e2915cbd8a0884eb93a8c94d0d72fea"
|
||||
integrity sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==
|
||||
|
||||
jackspeak@^2.3.5, "jackspeak@npm:@gitlab/noop@1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/noop/-/noop-1.0.0.tgz#b1ecb8ae6b2abf9b2e28927e4fbb05b7a1b2704b"
|
||||
integrity sha512-nOltttik5o2BjBo8LnyeTFzHoLpMY/XcCVOC+lm9ZwU+ivEam8wafacMF0KTbRn1KVrIoHYdo70QnqS+vJiOVw==
|
||||
|
||||
jed@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/jed/-/jed-1.1.1.tgz#7a549bbd9ffe1585b0cd0a191e203055bee574b4"
|
||||
|
|
@ -14924,11 +14929,6 @@ vue-observe-visibility@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/vue-observe-visibility/-/vue-observe-visibility-1.0.0.tgz#17cf1b2caf74022f0f3c95371468ddf2b9573152"
|
||||
integrity sha512-s5TFh3s3h3Mhd3jaz3zGzkVHKHnc/0C/gNr30olO99+yw2hl3WBhK3ng3/f9OF+qkW4+l7GkmwfAzDAcY3lCFg==
|
||||
|
||||
vue-resizable@1.3.4:
|
||||
version "1.3.4"
|
||||
resolved "https://registry.yarnpkg.com/vue-resizable/-/vue-resizable-1.3.4.tgz#04cbaeb7f90ebb92970b774f81f590832c640e5e"
|
||||
integrity sha512-Ub28/auJPaXObqpakCZABxoiZnJlSyJx65SJ7EwCu54rOJda2fgWj5ZiqvHa+xOuBGH6yemYPKwuO761Cey0Bg==
|
||||
|
||||
vue-resize@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/vue-resize/-/vue-resize-1.0.1.tgz#c120bed4e09938771d622614f57dbcf58a5147ee"
|
||||
|
|
|
|||
Loading…
Reference in New Issue