Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/blob/components/blob_edit_header.vue2
-rw-r--r--app/assets/javascripts/graphql_shared/issuable_client.js10
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/modal.vue1
-rw-r--r--app/assets/javascripts/lazy_loader.js2
-rw-r--r--app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/components/candidate_detail_row.vue6
-rw-r--r--app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/ml_candidates_show.vue167
-rw-r--r--app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/translations.js7
-rw-r--r--app/assets/javascripts/organizations/index/components/app.vue32
-rw-r--r--app/assets/javascripts/organizations/index/components/organizations_list.vue26
-rw-r--r--app/assets/javascripts/organizations/index/components/organizations_list_item.vue35
-rw-r--r--app/assets/javascripts/organizations/index/components/organizations_view.vue66
-rw-r--r--app/assets/javascripts/organizations/index/graphql/organizations.query.graphql14
-rw-r--r--app/assets/javascripts/organizations/index/index.js33
-rw-r--r--app/assets/javascripts/organizations/mock_data.js30
-rw-r--r--app/assets/javascripts/organizations/shared/graphql/resolvers.js23
-rw-r--r--app/assets/javascripts/pages/organizations/organizations/index/index.js3
-rw-r--r--app/assets/javascripts/snippets/components/edit.vue5
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue3
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_edit.vue4
-rw-r--r--app/assets/javascripts/snippets/components/snippet_description_edit.vue4
-rw-r--r--app/assets/javascripts/snippets/components/snippet_visibility_edit.vue2
-rw-r--r--app/assets/javascripts/super_sidebar/components/counter.vue2
-rw-r--r--app/assets/javascripts/super_sidebar/components/pinned_section.vue2
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_metadata_widgets.fragment.graphql8
-rw-r--r--app/assets/stylesheets/page_bundles/build.scss2
-rw-r--r--app/assets/stylesheets/page_bundles/ml_experiment_tracking.scss2
-rw-r--r--app/helpers/organizations/organization_helper.rb7
-rw-r--r--app/models/application_setting.rb2
-rw-r--r--app/models/ci/catalog/listing.rb2
-rw-r--r--app/models/ci/catalog/resource.rb2
-rw-r--r--app/presenters/ml/candidate_details_presenter.rb2
-rw-r--r--app/views/dashboard/_projects_head.html.haml2
-rw-r--r--app/views/dashboard/projects/_blank_state_admin_welcome.html.haml2
-rw-r--r--app/views/dashboard/projects/_blank_state_welcome.html.haml2
-rw-r--r--app/views/groups/_home_panel.html.haml2
-rw-r--r--app/views/layouts/_img_loader.html.haml2
-rw-r--r--app/views/organizations/organizations/index.html.haml2
-rw-r--r--app/views/shared/empty_states/_snippets.html.haml4
-rw-r--r--app/views/shared/projects/_project.html.haml4
-rw-r--r--app/views/shared/projects/_search_form.html.haml2
-rw-r--r--app/views/shared/snippets/_snippet.html.haml6
-rw-r--r--doc/administration/terraform_state.md26
-rw-r--r--doc/api/graphql/reference/index.md3
-rw-r--r--doc/user/group/saml_sso/index.md1
-rw-r--r--lib/gitlab/experiment/rollout/feature.rb17
-rw-r--r--locale/gitlab.pot43
-rw-r--r--qa/qa/page/component/lazy_loader.rb2
-rw-r--r--qa/qa/page/component/new_snippet.rb38
-rw-r--r--qa/qa/page/component/snippet.rb8
-rw-r--r--qa/qa/page/component/web_ide/modal/create_new_file.rb2
-rw-r--r--qa/qa/page/component/wiki.rb2
-rw-r--r--qa/qa/page/dashboard/projects.rb22
-rw-r--r--qa/qa/page/dashboard/snippet/edit.rb30
-rw-r--r--qa/qa/page/dashboard/snippet/index.rb16
-rw-r--r--qa/qa/page/group/show.rb6
-rw-r--r--qa/qa/page/label/index.rb2
-rw-r--r--qa/qa/page/project/snippet/index.rb6
-rw-r--r--qa/qa/page/project/snippet/new.rb10
-rw-r--r--qa/qa/page/project/web_ide/edit.rb8
-rw-r--r--qa/qa/page/search/results.rb2
-rwxr-xr-xscripts/generate_rspec_pipeline.rb21
-rw-r--r--spec/factories/ml/candidate_metrics.rb2
-rw-r--r--spec/frontend/ml/experiment_tracking/routes/candidates/show/components/candidate_detail_row_spec.js11
-rw-r--r--spec/frontend/ml/experiment_tracking/routes/candidates/show/ml_candidates_show_spec.js138
-rw-r--r--spec/frontend/ml/experiment_tracking/routes/candidates/show/mock_data.js7
-rw-r--r--spec/frontend/organizations/index/components/app_spec.js34
-rw-r--r--spec/frontend/organizations/index/components/organizations_list_item_spec.js45
-rw-r--r--spec/frontend/organizations/index/components/organizations_list_spec.js28
-rw-r--r--spec/frontend/organizations/index/components/organizations_view_spec.js130
-rw-r--r--spec/frontend/organizations/index/mock_data.js3
-rw-r--r--spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap4
-rw-r--r--spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap4
-rw-r--r--spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap6
-rw-r--r--spec/frontend/snippets/components/edit_spec.js3
-rw-r--r--spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js2
-rw-r--r--spec/helpers/organizations/organization_helper_spec.rb14
-rw-r--r--spec/lib/gitlab/experiment/rollout/feature_spec.rb8
-rw-r--r--spec/models/ci/catalog/listing_spec.rb43
-rw-r--r--spec/models/ci/catalog/resource_spec.rb32
-rw-r--r--spec/presenters/ml/candidate_details_presenter_spec.rb12
-rw-r--r--spec/scripts/generate_rspec_pipeline_spec.rb30
81 files changed, 1044 insertions, 311 deletions
diff --git a/app/assets/javascripts/blob/components/blob_edit_header.vue b/app/assets/javascripts/blob/components/blob_edit_header.vue
index 8fd3f03ff71..cd2872026c1 100644
--- a/app/assets/javascripts/blob/components/blob_edit_header.vue
+++ b/app/assets/javascripts/blob/components/blob_edit_header.vue
@@ -48,7 +48,7 @@ export default {
variant="danger"
category="secondary"
:disabled="!canDelete"
- data-qa-selector="delete_file_button"
+ data-testid="delete-file-button"
@click="$emit('delete')"
>{{ s__('Snippets|Delete file') }}</gl-button
>
diff --git a/app/assets/javascripts/graphql_shared/issuable_client.js b/app/assets/javascripts/graphql_shared/issuable_client.js
index 94385b6c214..78d58261589 100644
--- a/app/assets/javascripts/graphql_shared/issuable_client.js
+++ b/app/assets/javascripts/graphql_shared/issuable_client.js
@@ -87,14 +87,6 @@ export const config = {
const incomingWidget = incoming.find(
(w) => w.type && w.type === existingWidget.type,
);
- // We don't want to override existing notes or award emojis with empty widget on work item updates
- if (
- (incomingWidget?.type === WIDGET_TYPE_NOTES ||
- incomingWidget?.type === WIDGET_TYPE_AWARD_EMOJI) &&
- !context.variables.pageSize
- ) {
- return existingWidget;
- }
// we want to concat next page of awardEmoji to the existing ones
if (incomingWidget?.type === WIDGET_TYPE_AWARD_EMOJI && context.variables.after) {
@@ -126,7 +118,7 @@ export const config = {
};
}
- return incomingWidget || existingWidget;
+ return { ...existingWidget, ...incomingWidget };
});
},
},
diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
index 741845e3325..ba1258f8b50 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
@@ -176,7 +176,6 @@ export default {
type="text"
class="form-control"
data-testid="file-name-field"
- data-qa-selector="file_name_field"
:placeholder="placeholder"
/>
</form>
diff --git a/app/assets/javascripts/lazy_loader.js b/app/assets/javascripts/lazy_loader.js
index 36f387205f8..4354785e585 100644
--- a/app/assets/javascripts/lazy_loader.js
+++ b/app/assets/javascripts/lazy_loader.js
@@ -170,7 +170,7 @@ export default class LazyLoader {
img.classList.remove('lazy');
img.classList.add('js-lazy-loaded');
// eslint-disable-next-line no-param-reassign
- img.dataset.qa_selector = 'js_lazy_loaded_content';
+ img.dataset.testid = 'js-lazy-loaded-content';
}
}
}
diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/components/candidate_detail_row.vue b/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/components/candidate_detail_row.vue
index 747e92b9e85..8c7460940a0 100644
--- a/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/components/candidate_detail_row.vue
+++ b/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/components/candidate_detail_row.vue
@@ -6,18 +6,12 @@ export default {
type: String,
required: true,
},
- sectionLabel: {
- type: String,
- required: false,
- default: '',
- },
},
};
</script>
<template>
<tr>
- <td class="gl-text-secondary gl-font-weight-bold">{{ sectionLabel }}</td>
<td class="gl-font-weight-bold">{{ label }}</td>
<td>
<slot></slot>
diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/ml_candidates_show.vue b/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/ml_candidates_show.vue
index a68fb7d340a..43d28e3d699 100644
--- a/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/ml_candidates_show.vue
+++ b/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/ml_candidates_show.vue
@@ -1,7 +1,9 @@
<script>
-import { GlAvatarLabeled, GlLink } from '@gitlab/ui';
+import { GlAvatarLabeled, GlLink, GlTableLite } from '@gitlab/ui';
+import { isEmpty, maxBy, range } from 'lodash';
import ModelExperimentsHeader from '~/ml/experiment_tracking/components/model_experiments_header.vue';
import DeleteButton from '~/ml/experiment_tracking/components/delete_button.vue';
+import { __, sprintf } from '~/locale';
import DetailRow from './components/candidate_detail_row.vue';
import {
@@ -22,6 +24,11 @@ import {
JOB_LABEL,
CI_USER_LABEL,
CI_MR_LABEL,
+ PERFORMANCE_LABEL,
+ NO_PARAMETERS_MESSAGE,
+ NO_METRICS_MESSAGE,
+ NO_METADATA_MESSAGE,
+ NO_CI_MESSAGE,
} from './translations';
export default {
@@ -32,6 +39,7 @@ export default {
DetailRow,
GlAvatarLabeled,
GlLink,
+ GlTableLite,
},
props: {
candidate: {
@@ -54,6 +62,14 @@ export default {
JOB_LABEL,
CI_USER_LABEL,
CI_MR_LABEL,
+ PARAMETERS_LABEL,
+ METRICS_LABEL,
+ METADATA_LABEL,
+ PERFORMANCE_LABEL,
+ NO_PARAMETERS_MESSAGE,
+ NO_METRICS_MESSAGE,
+ NO_METADATA_MESSAGE,
+ NO_CI_MESSAGE,
},
computed: {
info() {
@@ -62,21 +78,38 @@ export default {
ciJob() {
return Object.freeze(this.info.ci_job);
},
- sections() {
- return [
- {
- sectionName: PARAMETERS_LABEL,
- sectionValues: this.candidate.params,
- },
- {
- sectionName: METRICS_LABEL,
- sectionValues: this.candidate.metrics,
- },
- {
- sectionName: METADATA_LABEL,
- sectionValues: this.candidate.metadata,
- },
- ];
+ hasMetadata() {
+ return !isEmpty(this.candidate.metadata);
+ },
+ hasParameters() {
+ return !isEmpty(this.candidate.params);
+ },
+ hasMetrics() {
+ return !isEmpty(this.candidate.metrics);
+ },
+ metricsTableFields() {
+ const maxStep = maxBy(this.candidate.metrics, 'step').step;
+ const rowClass = 'gl-p-3!';
+
+ const cssClasses = { thClass: rowClass, tdClass: rowClass };
+
+ const fields = range(maxStep + 1).map((step) => ({
+ key: step.toString(),
+ label: sprintf(__('Step %{step}'), { step }),
+ ...cssClasses,
+ }));
+
+ return [{ key: 'name', label: __('Metric'), ...cssClasses }, ...fields];
+ },
+ metricsTableItems() {
+ const items = {};
+ this.candidate.metrics.forEach((metric) => {
+ const metricRow = items[metric.name] || { name: metric.name };
+ metricRow[metric.step] = metric.value;
+ items[metric.name] = metricRow;
+ });
+
+ return Object.values(items);
},
},
};
@@ -93,33 +126,37 @@ export default {
/>
</model-experiments-header>
- <table class="candidate-details gl-w-full">
- <tbody>
- <tr class="divider"></tr>
-
- <detail-row :label="$options.i18n.ID_LABEL" :section-label="$options.i18n.INFO_LABEL">
- {{ info.iid }}
- </detail-row>
+ <section class="gl-mb-6">
+ <table class="candidate-details">
+ <tbody>
+ <detail-row :label="$options.i18n.ID_LABEL">
+ {{ info.iid }}
+ </detail-row>
- <detail-row :label="$options.i18n.MLFLOW_ID_LABEL">{{ info.eid }}</detail-row>
+ <detail-row :label="$options.i18n.MLFLOW_ID_LABEL">{{ info.eid }}</detail-row>
- <detail-row :label="$options.i18n.STATUS_LABEL">{{ info.status }}</detail-row>
+ <detail-row :label="$options.i18n.STATUS_LABEL">{{ info.status }}</detail-row>
- <detail-row :label="$options.i18n.EXPERIMENT_LABEL">
- <gl-link :href="info.path_to_experiment">
- {{ info.experiment_name }}
- </gl-link>
- </detail-row>
+ <detail-row :label="$options.i18n.EXPERIMENT_LABEL">
+ <gl-link :href="info.path_to_experiment">
+ {{ info.experiment_name }}
+ </gl-link>
+ </detail-row>
- <detail-row v-if="info.path_to_artifact" :label="$options.i18n.ARTIFACTS_LABEL">
- <gl-link :href="info.path_to_artifact">
- {{ $options.i18n.ARTIFACTS_LABEL }}
- </gl-link>
- </detail-row>
+ <detail-row v-if="info.path_to_artifact" :label="$options.i18n.ARTIFACTS_LABEL">
+ <gl-link :href="info.path_to_artifact">
+ {{ $options.i18n.ARTIFACTS_LABEL }}
+ </gl-link>
+ </detail-row>
+ </tbody>
+ </table>
+ </section>
- <template v-if="ciJob">
- <tr class="divider"></tr>
+ <section class="gl-mb-6">
+ <h4>{{ $options.i18n.CI_SECTION_LABEL }}</h4>
+ <table v-if="ciJob" class="candidate-details">
+ <tbody>
<detail-row
:label="$options.i18n.JOB_LABEL"
:section-label="$options.i18n.CI_SECTION_LABEL"
@@ -142,21 +179,53 @@ export default {
!{{ ciJob.merge_request.iid }} {{ ciJob.merge_request.title }}
</gl-link>
</detail-row>
- </template>
+ </tbody>
+ </table>
- <template v-for="{ sectionName, sectionValues } in sections">
- <tr v-if="sectionValues" :key="sectionName" class="divider"></tr>
+ <div v-else class="gl-text-secondary">{{ $options.i18n.NO_CI_MESSAGE }}</div>
+ </section>
- <detail-row
- v-for="(item, index) in sectionValues"
- :key="item.name"
- :label="item.name"
- :section-label="index === 0 ? sectionName : ''"
- >
+ <section class="gl-mb-6">
+ <h4>{{ $options.i18n.PARAMETERS_LABEL }}</h4>
+
+ <table v-if="hasParameters" class="candidate-details">
+ <tbody>
+ <detail-row v-for="item in candidate.params" :key="item.name" :label="item.name">
{{ item.value }}
</detail-row>
- </template>
- </tbody>
- </table>
+ </tbody>
+ </table>
+
+ <div v-else class="gl-text-secondary">{{ $options.i18n.NO_PARAMETERS_MESSAGE }}</div>
+ </section>
+
+ <section class="gl-mb-6">
+ <h4>{{ $options.i18n.METADATA_LABEL }}</h4>
+
+ <table v-if="hasMetadata" class="candidate-details">
+ <tbody>
+ <detail-row v-for="item in candidate.metadata" :key="item.name" :label="item.name">
+ {{ item.value }}
+ </detail-row>
+ </tbody>
+ </table>
+
+ <div v-else class="gl-text-secondary">{{ $options.i18n.NO_METADATA_MESSAGE }}</div>
+ </section>
+
+ <section class="gl-mb-6">
+ <h4>{{ $options.i18n.PERFORMANCE_LABEL }}</h4>
+
+ <div v-if="hasMetrics" class="gl-overflow-x-auto">
+ <gl-table-lite
+ :items="metricsTableItems"
+ :fields="metricsTableFields"
+ class="gl-w-auto"
+ hover
+ />
+ </div>
+
+ <div v-else class="gl-text-secondary">{{ $options.i18n.NO_METRICS_MESSAGE }}</div>
+ </section>
</div>
</template>
diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/translations.js b/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/translations.js
index fa9518f3e27..98988e1db35 100644
--- a/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/translations.js
+++ b/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/translations.js
@@ -9,13 +9,18 @@ export const EXPERIMENT_LABEL = s__('MlExperimentTracking|Experiment');
export const ARTIFACTS_LABEL = s__('MlExperimentTracking|Artifacts');
export const PARAMETERS_LABEL = s__('MlExperimentTracking|Parameters');
export const METRICS_LABEL = s__('MlExperimentTracking|Metrics');
+export const PERFORMANCE_LABEL = s__('MlExperimentTracking|Model performance');
export const METADATA_LABEL = s__('MlExperimentTracking|Metadata');
+export const NO_PARAMETERS_MESSAGE = s__('MlExperimentTracking|No logged parameters');
+export const NO_METRICS_MESSAGE = s__('MlExperimentTracking|No logged metrics');
+export const NO_METADATA_MESSAGE = s__('MlExperimentTracking|No logged metadata');
+export const NO_CI_MESSAGE = s__('MlExperimentTracking|Candidate not linked to a CI build');
export const DELETE_CANDIDATE_CONFIRMATION_MESSAGE = s__(
'MlExperimentTracking|Deleting this candidate will delete the associated parameters, metrics, and metadata.',
);
export const DELETE_CANDIDATE_PRIMARY_ACTION_LABEL = s__('MlExperimentTracking|Delete candidate');
export const DELETE_CANDIDATE_MODAL_TITLE = s__('MLExperimentTracking|Delete candidate?');
-export const CI_SECTION_LABEL = __('CI');
+export const CI_SECTION_LABEL = s__('MLExperimentTracking|CI Info');
export const JOB_LABEL = __('Job');
export const CI_USER_LABEL = s__('MlExperimentTracking|Triggered by');
export const CI_MR_LABEL = __('Merge request');
diff --git a/app/assets/javascripts/organizations/index/components/app.vue b/app/assets/javascripts/organizations/index/components/app.vue
new file mode 100644
index 00000000000..21a11c82196
--- /dev/null
+++ b/app/assets/javascripts/organizations/index/components/app.vue
@@ -0,0 +1,32 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
+import OrganizationsView from './organizations_view.vue';
+
+export default {
+ name: 'OrganizationsIndexApp',
+ i18n: {
+ organizations: __('Organizations'),
+ newOrganization: s__('Organization|New organization'),
+ },
+ components: {
+ GlButton,
+ OrganizationsView,
+ },
+ inject: ['newOrganizationUrl'],
+};
+</script>
+
+<template>
+ <section>
+ <div class="gl-display-flex gl-align-items-center">
+ <h1 class="gl-my-4 gl-font-size-h-display">{{ $options.i18n.organizations }}</h1>
+ <div class="gl-ml-auto">
+ <gl-button :href="newOrganizationUrl" variant="confirm">{{
+ $options.i18n.newOrganization
+ }}</gl-button>
+ </div>
+ </div>
+ <organizations-view />
+ </section>
+</template>
diff --git a/app/assets/javascripts/organizations/index/components/organizations_list.vue b/app/assets/javascripts/organizations/index/components/organizations_list.vue
new file mode 100644
index 00000000000..539a4fcfe29
--- /dev/null
+++ b/app/assets/javascripts/organizations/index/components/organizations_list.vue
@@ -0,0 +1,26 @@
+<script>
+import OrganizationsListItem from './organizations_list_item.vue';
+
+export default {
+ name: 'OrganizationsList',
+ components: {
+ OrganizationsListItem,
+ },
+ props: {
+ organizations: {
+ type: Array,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <ul class="gl-p-0 gl-list-style-none">
+ <organizations-list-item
+ v-for="organization in organizations"
+ :key="organization.id"
+ :organization="organization"
+ />
+ </ul>
+</template>
diff --git a/app/assets/javascripts/organizations/index/components/organizations_list_item.vue b/app/assets/javascripts/organizations/index/components/organizations_list_item.vue
new file mode 100644
index 00000000000..99fa41e3af8
--- /dev/null
+++ b/app/assets/javascripts/organizations/index/components/organizations_list_item.vue
@@ -0,0 +1,35 @@
+<script>
+import { GlAvatarLabeled } from '@gitlab/ui';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+
+export default {
+ name: 'OrganizationsListItem',
+ components: {
+ GlAvatarLabeled,
+ },
+ props: {
+ organization: {
+ type: Object,
+ required: true,
+ },
+ },
+ avatarSize: { default: 32, md: 48 },
+ getIdFromGraphQLId,
+};
+</script>
+
+<template>
+ <li class="gl-py-3 gl-border-b gl-display-flex gl-align-items-flex-start">
+ <gl-avatar-labeled
+ :size="$options.avatarSize"
+ :src="organization.avatarUrl"
+ :entity-id="$options.getIdFromGraphQLId(organization.id)"
+ :entity-name="organization.name"
+ :label="organization.name"
+ :label-link="organization.webUrl"
+ shape="rect"
+ >
+ <span class="gl-mt-2 gl-text-gray-500">{{ organization.description }}</span>
+ </gl-avatar-labeled>
+ </li>
+</template>
diff --git a/app/assets/javascripts/organizations/index/components/organizations_view.vue b/app/assets/javascripts/organizations/index/components/organizations_view.vue
new file mode 100644
index 00000000000..51aff482c8a
--- /dev/null
+++ b/app/assets/javascripts/organizations/index/components/organizations_view.vue
@@ -0,0 +1,66 @@
+<script>
+import { GlLoadingIcon, GlEmptyState } from '@gitlab/ui';
+import { createAlert } from '~/alert';
+import { s__ } from '~/locale';
+import organizationsQuery from '../graphql/organizations.query.graphql';
+import OrganizationsList from './organizations_list.vue';
+
+export default {
+ name: 'OrganizationsView',
+ i18n: {
+ errorMessage: s__(
+ 'Organization|An error occurred loading user organizations. Please refresh the page to try again.',
+ ),
+ emptyStateTitle: s__('Organization|Get started with organizations'),
+ emptyStateDescription: s__(
+ 'Organization|Create an organization to contain all of your groups and projects.',
+ ),
+ emptyStateButtonText: s__('Organization|New organization'),
+ },
+ components: {
+ GlLoadingIcon,
+ OrganizationsList,
+ GlEmptyState,
+ },
+ inject: ['newOrganizationUrl', 'organizationsEmptyStateSvgPath'],
+ data() {
+ return {
+ organizations: [],
+ };
+ },
+ apollo: {
+ organizations: {
+ query: organizationsQuery,
+ update(data) {
+ return data.currentUser.organizations.nodes;
+ },
+ error(error) {
+ createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true });
+ },
+ },
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.organizations.loading;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-loading-icon v-if="isLoading" class="gl-mt-5" size="md" />
+ <organizations-list
+ v-else-if="organizations.length"
+ :organizations="organizations"
+ class="gl-border-t"
+ />
+ <gl-empty-state
+ v-else
+ :svg-height="144"
+ :svg-path="organizationsEmptyStateSvgPath"
+ :title="$options.i18n.emptyStateTitle"
+ :description="$options.i18n.emptyStateDescription"
+ :primary-button-link="newOrganizationUrl"
+ :primary-button-text="$options.i18n.emptyStateButtonText"
+ />
+</template>
diff --git a/app/assets/javascripts/organizations/index/graphql/organizations.query.graphql b/app/assets/javascripts/organizations/index/graphql/organizations.query.graphql
new file mode 100644
index 00000000000..560cd9dd578
--- /dev/null
+++ b/app/assets/javascripts/organizations/index/graphql/organizations.query.graphql
@@ -0,0 +1,14 @@
+query getCurrentUserOrganizations {
+ currentUser {
+ id
+ organizations @client {
+ nodes {
+ id
+ name
+ description
+ avatarUrl
+ webUrl
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/organizations/index/index.js b/app/assets/javascripts/organizations/index/index.js
new file mode 100644
index 00000000000..7cbb9c9165d
--- /dev/null
+++ b/app/assets/javascripts/organizations/index/index.js
@@ -0,0 +1,33 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import resolvers from '../shared/graphql/resolvers';
+import OrganizationsIndexApp from './components/app.vue';
+
+export const initOrganizationsIndex = () => {
+ const el = document.getElementById('js-organizations-index');
+
+ if (!el) return false;
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(resolvers),
+ });
+
+ const { newOrganizationUrl, organizationsEmptyStateSvgPath } = convertObjectPropsToCamelCase(
+ el.dataset,
+ );
+
+ return new Vue({
+ el,
+ name: 'OrganizationsIndexRoot',
+ apolloProvider,
+ provide: {
+ newOrganizationUrl,
+ organizationsEmptyStateSvgPath,
+ },
+ render(createElement) {
+ return createElement(OrganizationsIndexApp);
+ },
+ });
+};
diff --git a/app/assets/javascripts/organizations/mock_data.js b/app/assets/javascripts/organizations/mock_data.js
index 17ab7bd1d34..60c446900c9 100644
--- a/app/assets/javascripts/organizations/mock_data.js
+++ b/app/assets/javascripts/organizations/mock_data.js
@@ -4,10 +4,32 @@
// https://gitlab.com/gitlab-org/gitlab/-/issues/420777
// https://gitlab.com/gitlab-org/gitlab/-/issues/421441
-export const organization = {
- id: 'gid://gitlab/Organization/1',
- __typename: 'Organization',
-};
+export const organizations = [
+ {
+ id: 'gid://gitlab/Organization/1',
+ name: 'My First Organization',
+ description: 'This is where an organization can be explained in detail',
+ avatarUrl: null,
+ webUrl: null,
+ __typename: 'Organization',
+ },
+ {
+ id: 'gid://gitlab/Organization/2',
+ name: 'Vegetation Co.',
+ description: 'Lorem ipsum dolor sit amet',
+ avatarUrl: null,
+ webUrl: null,
+ __typename: 'Organization',
+ },
+ {
+ id: 'gid://gitlab/Organization/3',
+ name: 'Dude where is my car?',
+ description: 'Bacon ipsum dolor amet short ribs',
+ avatarUrl: null,
+ webUrl: null,
+ __typename: 'Organization',
+ },
+];
export const organizationProjects = {
nodes: [
diff --git a/app/assets/javascripts/organizations/shared/graphql/resolvers.js b/app/assets/javascripts/organizations/shared/graphql/resolvers.js
index c78266b0476..318b41647bc 100644
--- a/app/assets/javascripts/organizations/shared/graphql/resolvers.js
+++ b/app/assets/javascripts/organizations/shared/graphql/resolvers.js
@@ -1,18 +1,31 @@
-import { organization, organizationProjects, organizationGroups } from '../../mock_data';
+import { organizations, organizationProjects, organizationGroups } from '../../mock_data';
+
+const simulateLoading = () => {
+ return new Promise((resolve) => {
+ setTimeout(resolve, 1000);
+ });
+};
export default {
Query: {
organization: async () => {
// Simulate API loading
- await new Promise((resolve) => {
- setTimeout(resolve, 1000);
- });
+ await simulateLoading();
return {
- ...organization,
+ ...organizations[0],
projects: organizationProjects,
groups: organizationGroups,
};
},
},
+ UserCore: {
+ organizations: async () => {
+ await simulateLoading();
+
+ return {
+ nodes: organizations,
+ };
+ },
+ },
};
diff --git a/app/assets/javascripts/pages/organizations/organizations/index/index.js b/app/assets/javascripts/pages/organizations/organizations/index/index.js
new file mode 100644
index 00000000000..c7e087b81c6
--- /dev/null
+++ b/app/assets/javascripts/pages/organizations/organizations/index/index.js
@@ -0,0 +1,3 @@
+import { initOrganizationsIndex } from '~/organizations/index';
+
+initOrganizationsIndex();
diff --git a/app/assets/javascripts/snippets/components/edit.vue b/app/assets/javascripts/snippets/components/edit.vue
index 9e80210de51..aa3f33989c8 100644
--- a/app/assets/javascripts/snippets/components/edit.vue
+++ b/app/assets/javascripts/snippets/components/edit.vue
@@ -232,8 +232,7 @@ export default {
<gl-form-input
id="snippet-title"
v-model="snippet.title"
- data-testid="snippet-title-input"
- data-qa-selector="snippet_title_field"
+ data-testid="snippet-title-input-field"
:autofocus="true"
/>
</gl-form-group>
@@ -261,7 +260,7 @@ export default {
category="primary"
type="submit"
variant="confirm"
- data-qa-selector="submit_button"
+ data-testid="submit-button"
:disabled="isUpdating"
>{{ saveButtonLabel }}</gl-button
>
diff --git a/app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue b/app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue
index 59f7c8d8d97..ca1d9f858a5 100644
--- a/app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue
+++ b/app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue
@@ -157,10 +157,9 @@ export default {
</gl-form-group>
<gl-button
:disabled="!canAdd"
- data-testid="add_button"
+ data-testid="add-button"
class="gl-my-3"
variant="dashed"
- data-qa-selector="add_file_button"
@click="addBlob"
>{{ addLabel }}</gl-button
>
diff --git a/app/assets/javascripts/snippets/components/snippet_blob_edit.vue b/app/assets/javascripts/snippets/components/snippet_blob_edit.vue
index 021bd23781e..9b0a1db23f2 100644
--- a/app/assets/javascripts/snippets/components/snippet_blob_edit.vue
+++ b/app/assets/javascripts/snippets/components/snippet_blob_edit.vue
@@ -69,11 +69,11 @@ export default {
};
</script>
<template>
- <div class="file-holder snippet" data-qa-selector="file_holder_container">
+ <div class="file-holder snippet" data-testid="file-holder-container">
<blob-header-edit
:id="inputId"
:value="blob.path"
- data-qa-selector="file_name_field"
+ data-testid="file-name-field"
:can-delete="canDelete"
:show-delete="showDelete"
@input="notifyAboutUpdates({ path: $event })"
diff --git a/app/assets/javascripts/snippets/components/snippet_description_edit.vue b/app/assets/javascripts/snippets/components/snippet_description_edit.vue
index 3ce7ea231ff..93d52890675 100644
--- a/app/assets/javascripts/snippets/components/snippet_description_edit.vue
+++ b/app/assets/javascripts/snippets/components/snippet_description_edit.vue
@@ -36,7 +36,7 @@ export default {
<gl-form-input
class="form-control"
:placeholder="s__('Snippets|Describe what your snippet does or how to use it…')"
- data-qa-selector="description_placeholder"
+ data-testid="description-placeholder"
/>
</div>
<markdown-field
@@ -54,7 +54,7 @@ export default {
:value="value"
class="note-textarea js-gfm-input js-autosize markdown-area"
dir="auto"
- data-qa-selector="snippet_description_field"
+ data-testid="snippet-description-field"
data-supports-quick-actions="false"
:aria-label="__('Description')"
:placeholder="__('Write a comment or drag your files here…')"
diff --git a/app/assets/javascripts/snippets/components/snippet_visibility_edit.vue b/app/assets/javascripts/snippets/components/snippet_visibility_edit.vue
index 24dd978585c..37d10cffc78 100644
--- a/app/assets/javascripts/snippets/components/snippet_visibility_edit.vue
+++ b/app/assets/javascripts/snippets/components/snippet_visibility_edit.vue
@@ -57,7 +57,7 @@ export default {
<gl-icon :size="16" :name="option.icon" />
<span
class="font-weight-bold ml-1 js-visibility-option"
- data-qa-selector="visibility_content"
+ data-testid="visibility-content"
:data-qa-visibility="option.label"
>{{ option.label }}</span
>
diff --git a/app/assets/javascripts/super_sidebar/components/counter.vue b/app/assets/javascripts/super_sidebar/components/counter.vue
index c0e1959fba4..49efc5ab5b9 100644
--- a/app/assets/javascripts/super_sidebar/components/counter.vue
+++ b/app/assets/javascripts/super_sidebar/components/counter.vue
@@ -15,7 +15,7 @@ export default {
href: {
type: String,
required: false,
- default: '',
+ default: null,
},
icon: {
type: String,
diff --git a/app/assets/javascripts/super_sidebar/components/pinned_section.vue b/app/assets/javascripts/super_sidebar/components/pinned_section.vue
index 5da45b52bf4..ea3e9e9df1f 100644
--- a/app/assets/javascripts/super_sidebar/components/pinned_section.vue
+++ b/app/assets/javascripts/super_sidebar/components/pinned_section.vue
@@ -102,7 +102,7 @@ export default {
<draggable
v-if="items.length > 0"
v-model="draggableItems"
- class="gl-p-0 gl-m-0"
+ class="gl-p-0 gl-m-0 gl-list-style-none"
data-testid="pinned-nav-items"
handle=".js-draggable-icon"
tag="ul"
diff --git a/app/assets/javascripts/work_items/graphql/work_item_metadata_widgets.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item_metadata_widgets.fragment.graphql
index f303a797e9c..d15e3086560 100644
--- a/app/assets/javascripts/work_items/graphql/work_item_metadata_widgets.fragment.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item_metadata_widgets.fragment.graphql
@@ -52,4 +52,12 @@ fragment WorkItemMetadataWidgets on WorkItemWidget {
... on WorkItemWidgetAwardEmoji {
type
}
+
+ ... on WorkItemWidgetLinkedItems {
+ type
+ }
+
+ ... on WorkItemWidgetHierarchy {
+ type
+ }
}
diff --git a/app/assets/stylesheets/page_bundles/build.scss b/app/assets/stylesheets/page_bundles/build.scss
index 24bd8b0fd7f..16fc0e7ebae 100644
--- a/app/assets/stylesheets/page_bundles/build.scss
+++ b/app/assets/stylesheets/page_bundles/build.scss
@@ -102,7 +102,7 @@
.sidebar-container {
@include gl-sticky;
top: #{$top-bar-height - 1px};
- max-height: calc(100vh - #{$top-bar-height - 1px});
+ max-height: calc(100vh - #{$top-bar-height - 1px} - var(--performance-bar-height));
overflow-y: scroll;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
diff --git a/app/assets/stylesheets/page_bundles/ml_experiment_tracking.scss b/app/assets/stylesheets/page_bundles/ml_experiment_tracking.scss
index d6f71b12cd9..685719071b5 100644
--- a/app/assets/stylesheets/page_bundles/ml_experiment_tracking.scss
+++ b/app/assets/stylesheets/page_bundles/ml_experiment_tracking.scss
@@ -15,6 +15,6 @@ table.ml-candidate-table {
table.candidate-details {
td {
- padding: $gl-spacing-scale-3;
+ padding: $gl-spacing-scale-3 $gl-spacing-scale-3 $gl-spacing-scale-3 0;
}
}
diff --git a/app/helpers/organizations/organization_helper.rb b/app/helpers/organizations/organization_helper.rb
index 6b5c4342c5c..0f760d87173 100644
--- a/app/helpers/organizations/organization_helper.rb
+++ b/app/helpers/organizations/organization_helper.rb
@@ -20,6 +20,13 @@ module Organizations
shared_groups_and_projects_app_data.to_json
end
+ def organization_index_app_data
+ {
+ new_organization_url: new_organization_path,
+ organizations_empty_state_svg_path: image_path('illustrations/empty-state/empty-organizations-md.svg')
+ }
+ end
+
private
def shared_groups_and_projects_app_data
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 9a5765d8074..c7088908de8 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -30,7 +30,7 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
jitsu_project_xid
jitsu_administrator_email
], remove_with: '16.5', remove_after: '2023-09-22'
- ignore_columns %i[ai_access_token ai_access_token_iv], remove_with: '16.6', remove_after: '2023-10-22'
+ ignore_columns %i[encrypted_ai_access_token encrypted_ai_access_token_iv], remove_with: '16.6', remove_after: '2023-10-22'
INSTANCE_REVIEW_MIN_USERS = 50
GRAFANA_URL_ERROR_MESSAGE = 'Please check your Grafana URL setting in ' \
diff --git a/app/models/ci/catalog/listing.rb b/app/models/ci/catalog/listing.rb
index 1cb030c67c3..c3b18af8c3f 100644
--- a/app/models/ci/catalog/listing.rb
+++ b/app/models/ci/catalog/listing.rb
@@ -18,6 +18,8 @@ module Ci
case sort.to_s
when 'name_desc' then all_resources.order_by_name_desc
when 'name_asc' then all_resources.order_by_name_asc
+ when 'latest_released_at_desc' then all_resources.order_by_latest_released_at_desc
+ when 'latest_released_at_asc' then all_resources.order_by_latest_released_at_asc
else
all_resources.order_by_created_at_desc
end
diff --git a/app/models/ci/catalog/resource.rb b/app/models/ci/catalog/resource.rb
index 799cdce4af7..8ffc0292a69 100644
--- a/app/models/ci/catalog/resource.rb
+++ b/app/models/ci/catalog/resource.rb
@@ -18,6 +18,8 @@ module Ci
scope :order_by_created_at_desc, -> { reorder(created_at: :desc) }
scope :order_by_name_desc, -> { joins(:project).merge(Project.sorted_by_name_desc) }
scope :order_by_name_asc, -> { joins(:project).merge(Project.sorted_by_name_asc) }
+ scope :order_by_latest_released_at_desc, -> { reorder(arel_table[:latest_released_at].desc.nulls_last) }
+ scope :order_by_latest_released_at_asc, -> { reorder(arel_table[:latest_released_at].asc.nulls_last) }
delegate :avatar_path, :description, :name, :star_count, :forks_count, to: :project
diff --git a/app/presenters/ml/candidate_details_presenter.rb b/app/presenters/ml/candidate_details_presenter.rb
index 29d4617903f..057d3bd19d9 100644
--- a/app/presenters/ml/candidate_details_presenter.rb
+++ b/app/presenters/ml/candidate_details_presenter.rb
@@ -23,7 +23,7 @@ module Ml
ci_job: job_info
},
params: candidate.params,
- metrics: candidate.latest_metrics,
+ metrics: candidate.metrics,
metadata: candidate.metadata
}
}
diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml
index b72b252a852..74dc2277f54 100644
--- a/app/views/dashboard/_projects_head.html.haml
+++ b/app/views/dashboard/_projects_head.html.haml
@@ -7,7 +7,7 @@
.page-title-controls.gl-display-flex.gl-align-items-center.gl-gap-5
= link_to _("Explore projects"), explore_projects_path
- if current_user.can_create_project?
- = render Pajamas::ButtonComponent.new(href: new_project_path, variant: :confirm, button_options: { data: { qa_selector: 'new_project_button' } }) do
+ = render Pajamas::ButtonComponent.new(href: new_project_path, variant: :confirm, button_options: { data: { testid: 'new-project-button' } }) do
= _("New project")
.top-area
diff --git a/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml b/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml
index 94f956896d6..1d2e6e1e332 100644
--- a/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml
+++ b/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml
@@ -4,7 +4,7 @@
- if has_start_trial?
= render_if_exists "dashboard/projects/blank_state_ee_trial"
- = link_to new_project_path, class: link_classes, data: { qa_selector: 'new_project_button' } do
+ = link_to new_project_path, class: link_classes, data: { testid: 'new-project-button' } do
.blank-state-icon
= custom_icon("add_new_project", size: 50)
.blank-state-body.gl-sm-pl-6
diff --git a/app/views/dashboard/projects/_blank_state_welcome.html.haml b/app/views/dashboard/projects/_blank_state_welcome.html.haml
index 08b914a218d..032c5206d99 100644
--- a/app/views/dashboard/projects/_blank_state_welcome.html.haml
+++ b/app/views/dashboard/projects/_blank_state_welcome.html.haml
@@ -2,7 +2,7 @@
.gl-display-flex.gl-flex-wrap.gl-justify-content-space-between
- if current_user.can_create_project?
- = link_to new_project_path, class: link_classes, data: { qa_selector: 'new_project_button' } do
+ = link_to new_project_path, class: link_classes, data: { testid: 'new-project-button' } do
.blank-state-icon
= custom_icon("add_new_project", size: 50)
.blank-state-body.gl-sm-pl-6
diff --git a/app/views/groups/_home_panel.html.haml b/app/views/groups/_home_panel.html.haml
index ca7798257cb..544acd5ae56 100644
--- a/app/views/groups/_home_panel.html.haml
+++ b/app/views/groups/_home_panel.html.haml
@@ -35,7 +35,7 @@
- if can_create_projects
.gl-sm-w-auto.gl-w-full
- = render Pajamas::ButtonComponent.new(href: new_project_path(namespace_id: @group.id), variant: :confirm, button_options: { data: { qa_selector: 'new_project_button' }, class: 'gl-sm-w-auto gl-w-full' }) do
+ = render Pajamas::ButtonComponent.new(href: new_project_path(namespace_id: @group.id), variant: :confirm, button_options: { data: { testid: 'new-project-button' }, class: 'gl-sm-w-auto gl-w-full' }) do
= _('New project')
- if @group.description.present?
diff --git a/app/views/layouts/_img_loader.html.haml b/app/views/layouts/_img_loader.html.haml
index c1fe3ae0924..ac00a18f0bc 100644
--- a/app/views/layouts/_img_loader.html.haml
+++ b/app/views/layouts/_img_loader.html.haml
@@ -13,6 +13,6 @@
img.removeAttribute('data-src');
img.classList.remove('lazy');
img.classList.add('js-lazy-loaded');
- img.dataset.testid = 'js_lazy_loaded_content';
+ img.dataset.testid = 'js-lazy-loaded-content';
});
}
diff --git a/app/views/organizations/organizations/index.html.haml b/app/views/organizations/organizations/index.html.haml
index 04a90b7589f..e108878a15d 100644
--- a/app/views/organizations/organizations/index.html.haml
+++ b/app/views/organizations/organizations/index.html.haml
@@ -1,2 +1,4 @@
- page_title s_('Organization|Organizations')
- header_title _("Your work"), root_path
+
+#js-organizations-index{ data: organization_index_app_data }
diff --git a/app/views/shared/empty_states/_snippets.html.haml b/app/views/shared/empty_states/_snippets.html.haml
index 6fe36d75453..a2457fb0810 100644
--- a/app/views/shared/empty_states/_snippets.html.haml
+++ b/app/views/shared/empty_states/_snippets.html.haml
@@ -2,7 +2,7 @@
.row.empty-state
.col-12
- .svg-content.svg-150{ data: { qa_selector: 'svg_content' } }
+ .svg-content.svg-150{ data: { testid: 'svg-content' } }
= image_tag 'illustrations/empty-state/empty-snippets-md.svg'
.text-content.gl-text-center.gl-pt-0
- if current_user
@@ -12,7 +12,7 @@
= s_('SnippetsEmptyState|Store, share, and embed small pieces of code and text.')
.gl-mt-3<
- if button_path
- = link_button_to s_('SnippetsEmptyState|New snippet'), button_path, title: s_('SnippetsEmptyState|New snippet'), id: 'new_snippet_link', data: { qa_selector: 'create_first_snippet_link' }, variant: :confirm
+ = link_button_to s_('SnippetsEmptyState|New snippet'), button_path, title: s_('SnippetsEmptyState|New snippet'), id: 'new_snippet_link', data: { testid: 'create-first-snippet-link' }, variant: :confirm
= link_button_to s_('SnippetsEmptyState|Documentation'), help_page_path('user/snippets.md'), title: s_('SnippetsEmptyState|Documentation')
- else
%h4.gl-text-center= s_('SnippetsEmptyState|There are no snippets to show.')
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 75151da8071..2de4a9d7780 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -24,7 +24,7 @@
- else
= render Pajamas::AvatarComponent.new(project, size: 48, alt: '', class: 'gl-mr-5')
.project-cell{ class: css_class }
- .project-details.gl-pr-9.gl-sm-pr-0.gl-w-full.gl-display-flex.gl-flex-direction-column{ data: { qa_selector: 'project_content', qa_project_name: project.name } }
+ .project-details.gl-pr-9.gl-sm-pr-0.gl-w-full.gl-display-flex.gl-flex-direction-column{ data: { testid: 'project-content', qa_project_name: project.name } }
.gl-display-flex.gl-align-items-center.gl-flex-wrap
%h2.gl-font-base.gl-line-height-20.gl-my-0.gl-overflow-wrap-anywhere
= link_to project_path(project), class: 'text-plain gl-mr-3 js-prefetch-document', title: project.name do
@@ -45,7 +45,7 @@
- if !explore_projects_tab? && access&.nonzero?
-# haml-lint:disable UnnecessaryStringOutput
= ' ' # prevent haml from eating the space between elements
- %span.user-access-role.gl-display-block.gl-m-0{ data: { qa_selector: 'user_role_content' } }= localized_project_human_access(access)
+ %span.user-access-role.gl-display-block.gl-m-0{ data: { testid: 'user-role-content' } }= localized_project_human_access(access)
- if !explore_projects_tab?
= render_if_exists 'compliance_management/compliance_framework/compliance_framework_badge', project: project, additional_classes: 'gl-ml-3!'
diff --git a/app/views/shared/projects/_search_form.html.haml b/app/views/shared/projects/_search_form.html.haml
index 2388bf2f0be..de54cc2810b 100644
--- a/app/views/shared/projects/_search_form.html.haml
+++ b/app/views/shared/projects/_search_form.html.haml
@@ -2,7 +2,7 @@
- admin_view ||= false
- top_padding = admin_view ? 'gl-lg-pt-3' : ''
-= form_tag filter_projects_path, method: :get, class: "project-filter-form gl-display-flex! gl-flex-wrap gl-w-full gl-gap-3 #{top_padding}", data: { qa_selector: 'project_filter_form_container' }, id: 'project-filter-form' do |f|
+= form_tag filter_projects_path, method: :get, class: "project-filter-form gl-display-flex! gl-flex-wrap gl-w-full gl-gap-3 #{top_padding}", data: { testid: 'project-filter-form-container' }, id: 'project-filter-form' do |f|
= search_field_tag :name, params[:name],
placeholder: placeholder,
class: "project-filter-form-field form-control input-short js-projects-list-filter gl-m-0!",
diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml
index 6caadeb0ba4..9767f7929d0 100644
--- a/app/views/shared/snippets/_snippet.html.haml
+++ b/app/views/shared/snippets/_snippet.html.haml
@@ -1,13 +1,13 @@
- link_project = local_assigns.fetch(:link_project, false)
- notes_count = @noteable_meta_data[snippet.id].user_notes_count
-%li.snippet-row.py-3{ data: { qa_selector: 'snippet_link', qa_snippet_title: snippet.title } }
+%li.snippet-row.py-3{ data: { testid: 'snippet-link', qa_snippet_title: snippet.title } }
= render Pajamas::AvatarComponent.new(snippet.author, size: 48, alt: "", class: 'gl-display-none gl-sm-display-block gl-float-left gl-mr-3')
= link_to gitlab_snippet_path(snippet), class: "title" do
= snippet.title
- %ul.controls{ data: { qa_selector: 'snippet_file_count_content', qa_snippet_files: snippet.statistics&.file_count } }
+ %ul.controls{ data: { testid: 'snippet-file-count-content', qa_snippet_files: snippet.statistics&.file_count } }
%li
= snippet_file_count(snippet)
%li
@@ -15,7 +15,7 @@
= sprite_icon('comments', css_class: 'gl-vertical-align-text-bottom')
= notes_count
%li
- %span.sr-only{ data: { qa_selector: 'snippet_visibility_content', qa_snippet_visibility: visibility_level_label(snippet.visibility_level) } }
+ %span.sr-only{ data: { testid: 'snippet-visibility-content', qa_snippet_visibility: visibility_level_label(snippet.visibility_level) } }
= visibility_level_label(snippet.visibility_level)
= visibility_level_icon(snippet.visibility_level)
diff --git a/doc/administration/terraform_state.md b/doc/administration/terraform_state.md
index 90b030e6e13..bf0774478ee 100644
--- a/doc/administration/terraform_state.md
+++ b/doc/administration/terraform_state.md
@@ -234,3 +234,29 @@ See [the available connection settings for different providers](object_storage.m
1. [Migrate any existing local states to the object storage](#migrate-to-object-storage)
::EndTabs
+
+### Find a Terraform state file path
+
+Terraform state files are stored in the hashed directory path of the relevant project.
+
+The format of the path is `/var/opt/gitlab/gitlab-rails/shared/terraform_state/<path>/<to>/<projectHashDirectory>/<UUID>/0.tfstate`, where [UUID](https://gitlab.com/gitlab-org/gitlab/-/blob/dcc47a95c7e1664cb15bef9a70f2a4eefa9bd99a/app/models/terraform/state.rb#L33) is randomly defined.
+
+To find a state file path:
+
+1. Add `get-terraform-path` to your shell:
+
+ ```shell
+ get-terraform-path() {
+ PROJECT_HASH=$(echo -n $1 | openssl dgst -sha256 | sed 's/^.* //')
+ echo "${PROJECT_HASH:0:2}/${PROJECT_HASH:2:2}/${PROJECT_HASH}"
+ }
+ ```
+
+1. Run `get-terraform-path <project_id>`.
+
+ ```shell
+ $ get-terraform-path 650
+ 20/99/2099a9b5f777e242d1f9e19d27e232cc71e2fa7964fc988a319fce5671ca7f73
+ ```
+
+The relative path is displayed.
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 6dd8fbdad50..b4f57847408 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -14126,6 +14126,7 @@ Represents the total number of issues and their weights for a particular day.
| <a id="cicatalogresourceforkscount"></a>`forksCount` **{warning-solid}** | [`Int!`](#int) | **Introduced** in 16.1. This feature is an Experiment. It can be changed or removed at any time. Number of times the catalog resource has been forked. |
| <a id="cicatalogresourceicon"></a>`icon` **{warning-solid}** | [`String`](#string) | **Introduced** in 15.11. This feature is an Experiment. It can be changed or removed at any time. Icon for the catalog resource. |
| <a id="cicatalogresourceid"></a>`id` **{warning-solid}** | [`ID!`](#id) | **Introduced** in 15.11. This feature is an Experiment. It can be changed or removed at any time. ID of the catalog resource. |
+| <a id="cicatalogresourcelatestreleasedat"></a>`latestReleasedAt` **{warning-solid}** | [`Time`](#time) | **Introduced** in 16.5. This feature is an Experiment. It can be changed or removed at any time. Release date of the catalog resource's latest version. |
| <a id="cicatalogresourcelatestversion"></a>`latestVersion` **{warning-solid}** | [`Release`](#release) | **Introduced** in 16.1. This feature is an Experiment. It can be changed or removed at any time. Latest version of the catalog resource. |
| <a id="cicatalogresourcename"></a>`name` **{warning-solid}** | [`String`](#string) | **Introduced** in 15.11. This feature is an Experiment. It can be changed or removed at any time. Name of the catalog resource. |
| <a id="cicatalogresourceopenissuescount"></a>`openIssuesCount` **{warning-solid}** | [`Int!`](#int) | **Introduced** in 16.3. This feature is an Experiment. It can be changed or removed at any time. Count of open issues that belong to the the catalog resource. |
@@ -26964,6 +26965,8 @@ Values for sorting catalog resources.
| ----- | ----------- |
| <a id="cicatalogresourcesortcreated_asc"></a>`CREATED_ASC` | Created at ascending order. |
| <a id="cicatalogresourcesortcreated_desc"></a>`CREATED_DESC` | Created at descending order. |
+| <a id="cicatalogresourcesortlatest_released_at_asc"></a>`LATEST_RELEASED_AT_ASC` | Latest release date by ascending order. |
+| <a id="cicatalogresourcesortlatest_released_at_desc"></a>`LATEST_RELEASED_AT_DESC` | Latest release date by descending order. |
| <a id="cicatalogresourcesortname_asc"></a>`NAME_ASC` | Name by ascending order. |
| <a id="cicatalogresourcesortname_desc"></a>`NAME_DESC` | Name by descending order. |
| <a id="cicatalogresourcesortupdated_asc"></a>`UPDATED_ASC` | Updated at ascending order. |
diff --git a/doc/user/group/saml_sso/index.md b/doc/user/group/saml_sso/index.md
index 734679cf331..0449848671b 100644
--- a/doc/user/group/saml_sso/index.md
+++ b/doc/user/group/saml_sso/index.md
@@ -532,6 +532,7 @@ immediately. If the user:
- [SAML SSO for self-managed GitLab instances](../../../integration/saml.md)
- [Glossary](../../../integration/saml.md#glossary)
+- [Blog post: The ultimate guide to enabling SAML and SSO on GitLab.com](https://about.gitlab.com/blog/2023/09/14/the-ultimate-guide-to-enabling-saml/)
- [Authentication comparison between SaaS and self-managed](../../../administration/auth/index.md#saas-vs-self-managed-comparison)
- [Passwords for users created through integrated authentication](../../../security/passwords_for_integrated_authentication_methods.md)
- [SAML Group Sync](group_sync.md)
diff --git a/lib/gitlab/experiment/rollout/feature.rb b/lib/gitlab/experiment/rollout/feature.rb
index 102d979ecfb..4ff61aa3551 100644
--- a/lib/gitlab/experiment/rollout/feature.rb
+++ b/lib/gitlab/experiment/rollout/feature.rb
@@ -30,18 +30,7 @@ module Gitlab
# assign a variant based on our provided distribution rules.
# Otherwise we will assign a variant evenly across the behaviours without control.
def execute_assignment
- return unless ::Feature.enabled?(feature_flag_name, self, type: :experiment)
-
- # non-control distribution.
- if distribution_rules.present?
- # In our setup with Flipper use, we'll want to use distribution rules with control set to 0% since
- # the Feature being disabled is the default control experience.
- super
- else
- crc = normalized_id
- eligible_behaviors = behavior_names - [:control]
- eligible_behaviors.empty? ? nil : eligible_behaviors[crc % eligible_behaviors.length]
- end
+ super if ::Feature.enabled?(feature_flag_name, self, type: :experiment)
end
# This is what's provided to the `Feature.enabled?` call that will be
@@ -79,6 +68,10 @@ module Gitlab
def feature_flag_name
experiment.name.tr('/', '_')
end
+
+ def behavior_names
+ super - [:control]
+ end
end
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 726ce285cba..90170834546 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -442,6 +442,11 @@ msgid_plural "%d tags per image name"
msgstr[0] ""
msgstr[1] ""
+msgid "%d template found"
+msgid_plural "%d templates found"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d unresolved thread"
msgid_plural "%d unresolved threads"
msgstr[0] ""
@@ -3596,7 +3601,7 @@ msgstr ""
msgid "AdminSettings|New CI/CD variables in projects and groups default to protected."
msgstr ""
-msgid "AdminSettings|No required pipeline"
+msgid "AdminSettings|No required configuration"
msgstr ""
msgid "AdminSettings|Only enable search after installing the plugin, enabling indexing, and recreating the index."
@@ -3755,6 +3760,9 @@ msgstr ""
msgid "AdminSettings|You can't delete projects before the warning email is sent."
msgstr ""
+msgid "AdminSettings|templates found"
+msgstr ""
+
msgid "AdminStatistics|Active Users"
msgstr ""
@@ -9010,9 +9018,6 @@ msgstr ""
msgid "CHANGELOG"
msgstr ""
-msgid "CI"
-msgstr ""
-
msgid "CI Lint"
msgstr ""
@@ -28364,6 +28369,9 @@ msgstr ""
msgid "MD5"
msgstr ""
+msgid "MLExperimentTracking|CI Info"
+msgstr ""
+
msgid "MLExperimentTracking|Delete candidate?"
msgstr ""
@@ -30220,6 +30228,9 @@ msgstr ""
msgid "MlExperimentTracking|CI Job"
msgstr ""
+msgid "MlExperimentTracking|Candidate not linked to a CI build"
+msgstr ""
+
msgid "MlExperimentTracking|Candidate removed"
msgstr ""
@@ -30292,6 +30303,9 @@ msgstr ""
msgid "MlExperimentTracking|Model experiments"
msgstr ""
+msgid "MlExperimentTracking|Model performance"
+msgstr ""
+
msgid "MlExperimentTracking|Model registry"
msgstr ""
@@ -30307,6 +30321,15 @@ msgstr ""
msgid "MlExperimentTracking|No candidates logged for the query. Create new candidates using the MLflow client."
msgstr ""
+msgid "MlExperimentTracking|No logged metadata"
+msgstr ""
+
+msgid "MlExperimentTracking|No logged metrics"
+msgstr ""
+
+msgid "MlExperimentTracking|No logged parameters"
+msgstr ""
+
msgid "MlExperimentTracking|No name"
msgstr ""
@@ -32968,15 +32991,24 @@ msgstr ""
msgid "Organization|An error occurred loading the projects. Please refresh the page to try again."
msgstr ""
+msgid "Organization|An error occurred loading user organizations. Please refresh the page to try again."
+msgstr ""
+
msgid "Organization|Copy organization ID"
msgstr ""
+msgid "Organization|Create an organization to contain all of your groups and projects."
+msgstr ""
+
msgid "Organization|Frequently visited groups"
msgstr ""
msgid "Organization|Frequently visited projects"
msgstr ""
+msgid "Organization|Get started with organizations"
+msgstr ""
+
msgid "Organization|Manage"
msgstr ""
@@ -45745,6 +45777,9 @@ msgstr ""
msgid "Step %{currentStep} of %{stepCount}"
msgstr ""
+msgid "Step %{step}"
+msgstr ""
+
msgid "Step 1."
msgstr ""
diff --git a/qa/qa/page/component/lazy_loader.rb b/qa/qa/page/component/lazy_loader.rb
index 1b166efbbff..2e2d99b28d5 100644
--- a/qa/qa/page/component/lazy_loader.rb
+++ b/qa/qa/page/component/lazy_loader.rb
@@ -10,7 +10,7 @@ module QA
super
base.view 'app/views/layouts/_img_loader.html.haml' do
- element :js_lazy_loaded_content
+ element 'js-lazy-loaded-content'
end
end
end
diff --git a/qa/qa/page/component/new_snippet.rb b/qa/qa/page/component/new_snippet.rb
index 657008749f2..b939c2ca750 100644
--- a/qa/qa/page/component/new_snippet.rb
+++ b/qa/qa/page/component/new_snippet.rb
@@ -10,22 +10,22 @@ module QA
super
base.view 'app/assets/javascripts/snippets/components/edit.vue' do
- element :snippet_title_field, required: true
- element :submit_button
+ element 'snippet-title-input-field', required: true
+ element 'submit-button'
end
base.view 'app/assets/javascripts/snippets/components/snippet_description_edit.vue' do
- element :snippet_description_field
- element :description_placeholder, required: true
+ element 'snippet-description-field'
+ element 'description-placeholder', required: true
end
base.view 'app/assets/javascripts/snippets/components/snippet_blob_edit.vue' do
- element :file_name_field
- element :file_holder_container
+ element 'file-name-field'
+ element 'file-holder-container'
end
base.view 'app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue' do
- element :add_file_button
+ element 'add-button'
end
base.view 'app/views/shared/_zen.html.haml' do
@@ -34,36 +34,36 @@ module QA
end
base.view 'app/assets/javascripts/snippets/components/snippet_visibility_edit.vue' do
- element :visibility_content
+ element 'visibility-content'
end
end
def fill_title(title)
- fill_element :snippet_title_field, title
+ fill_element 'snippet-title-input-field', title
end
def fill_description(description)
- click_element :description_placeholder
- fill_element :snippet_description_field, description
+ click_element 'description-placeholder'
+ fill_element 'snippet-description-field', description
end
def set_visibility(visibility)
- click_element(:visibility_content, visibility: visibility)
+ click_element('visibility-content', visibility: visibility)
end
def fill_file_name(name, file_number = nil)
if file_number
- within_element_by_index(:file_holder_container, file_number - 1) do
- fill_element(:file_name_field, name)
+ within_element_by_index('file-holder-container', file_number - 1) do
+ fill_element('file-name-field', name)
end
else
- fill_element(:file_name_field, name)
+ fill_element('file-name-field', name)
end
end
def fill_file_content(content, file_number = nil)
if file_number
- within_element_by_index(:file_holder_container, file_number - 1) do
+ within_element_by_index('file-holder-container', file_number - 1) do
text_area.set(content)
end
else
@@ -72,13 +72,13 @@ module QA
end
def click_add_file
- click_element(:add_file_button)
+ click_element('add-button')
end
def click_create_snippet_button
- click_element_coordinates(:submit_button)
+ click_element_coordinates('submit-button')
wait_until(reload: false) do
- has_no_element?(:snippet_title_field)
+ has_no_element?('snippet-title-input-field')
end
end
diff --git a/qa/qa/page/component/snippet.rb b/qa/qa/page/component/snippet.rb
index 807f0e5f2fe..91c915877e2 100644
--- a/qa/qa/page/component/snippet.rb
+++ b/qa/qa/page/component/snippet.rb
@@ -201,7 +201,7 @@ module QA
click_element('comment-button')
unless has_element?('note-author-content')
- raise ElementNotFound, "Comment did not appear as expected"
+ raise QA::Page::Base::ElementNotFound, "Comment did not appear as expected"
end
end
@@ -233,7 +233,7 @@ module QA
click_element('save-comment-button')
unless has_element?('note-author-content')
- raise ElementNotFound, "Comment did not appear as expected"
+ raise QA::Page::Base::ElementNotFound, "Comment did not appear as expected"
end
end
@@ -243,7 +243,7 @@ module QA
click_confirmation_ok_button
unless has_no_element?('note-content', text: comment)
- raise ElementNotFound, "Comment was not removed as expected"
+ raise QA::Page::Base::ElementNotFound, "Comment was not removed as expected"
end
end
@@ -265,7 +265,7 @@ module QA
click_element('comment-button')
unless has_element?('note-author-content')
- raise ElementNotFound, "Comment did not appear as expected"
+ raise QA::Page::Base::ElementNotFound, "Comment did not appear as expected"
end
end
diff --git a/qa/qa/page/component/web_ide/modal/create_new_file.rb b/qa/qa/page/component/web_ide/modal/create_new_file.rb
index 2869bb9c331..22cac83d913 100644
--- a/qa/qa/page/component/web_ide/modal/create_new_file.rb
+++ b/qa/qa/page/component/web_ide/modal/create_new_file.rb
@@ -7,7 +7,7 @@ module QA
module Modal
class CreateNewFile < Page::Base
view 'app/assets/javascripts/ide/components/new_dropdown/modal.vue' do
- element :file_name_field, required: true
+ element 'file-name-field', required: true
element :new_file_modal, required: true
element :template_list_content
end
diff --git a/qa/qa/page/component/wiki.rb b/qa/qa/page/component/wiki.rb
index ed68052f997..e34e1f4b589 100644
--- a/qa/qa/page/component/wiki.rb
+++ b/qa/qa/page/component/wiki.rb
@@ -38,7 +38,7 @@ module QA
# webdriver to miss the hit so we wait for the svg to load before
# clicking the button.
within_element(:svg_content) do
- has_element?(:js_lazy_loaded_content)
+ has_element?('js-lazy-loaded-content')
end
click_element(:create_first_page_link)
diff --git a/qa/qa/page/dashboard/projects.rb b/qa/qa/page/dashboard/projects.rb
index 4a81d34b32e..ff822a6b117 100644
--- a/qa/qa/page/dashboard/projects.rb
+++ b/qa/qa/page/dashboard/projects.rb
@@ -5,34 +5,34 @@ module QA
module Dashboard
class Projects < Page::Base
view 'app/views/shared/projects/_search_form.html.haml' do
- element :project_filter_form_container, required: true
+ element 'project-filter-form-container', required: true
end
view 'app/views/shared/projects/_project.html.haml' do
- element :project_content
- element :user_role_content
+ element 'project-content'
+ element 'user-role-content'
end
view 'app/views/dashboard/_projects_head.html.haml' do
- element :new_project_button
+ element 'new-project-button'
end
view 'app/views/dashboard/projects/_blank_state_welcome.html.haml' do
- element :new_project_button
+ element 'new-project-button'
end
view 'app/views/dashboard/projects/_blank_state_admin_welcome.html.haml' do
- element :new_project_button
+ element 'new-project-button'
end
def has_project_with_access_role?(project_name, access_role)
- within_element(:project_content, text: project_name) do
- has_element?(:user_role_content, text: access_role)
+ within_element('project-content', text: project_name) do
+ has_element?('user-role-content', text: access_role)
end
end
def filter_by_name(name)
- within_element(:project_filter_form_container) do
+ within_element('project-filter-form-container') do
fill_in :name, with: name
end
end
@@ -44,7 +44,7 @@ module QA
end
def click_new_project_button
- click_element(:new_project_button, Page::Project::New)
+ click_element('new-project-button', Page::Project::New)
end
def self.path
@@ -52,7 +52,7 @@ module QA
end
def clear_project_filter
- fill_element(:project_filter_form_container, "")
+ fill_element('project-filter-form-container', "")
end
end
end
diff --git a/qa/qa/page/dashboard/snippet/edit.rb b/qa/qa/page/dashboard/snippet/edit.rb
index 8af3eb5693c..f930e35994c 100644
--- a/qa/qa/page/dashboard/snippet/edit.rb
+++ b/qa/qa/page/dashboard/snippet/edit.rb
@@ -6,20 +6,20 @@ module QA
module Snippet
class Edit < Page::Base
view 'app/assets/javascripts/snippets/components/edit.vue' do
- element :submit_button, required: true
+ element 'submit-button', required: true
end
view 'app/assets/javascripts/snippets/components/snippet_blob_edit.vue' do
- element :file_name_field
- element :file_holder_container
+ element 'file-name-field'
+ element 'file-holder-container'
end
view 'app/assets/javascripts/blob/components/blob_edit_header.vue' do
- element :delete_file_button
+ element 'delete-file-button'
end
view 'app/assets/javascripts/snippets/components/snippet_visibility_edit.vue' do
- element :visibility_content
+ element 'visibility-content'
end
def add_to_file_content(content)
@@ -29,26 +29,26 @@ module QA
end
def change_visibility_to(visibility_type)
- click_element(:visibility_content, visibility: visibility_type)
+ click_element('visibility-content', visibility: visibility_type)
end
def click_add_file
- click_element(:add_file_button)
+ click_element('add-button')
end
def fill_file_name(name, file_number = nil)
if file_number
- within_element_by_index(:file_holder_container, file_number - 1) do
- fill_element(:file_name_field, name)
+ within_element_by_index('file-holder-container', file_number - 1) do
+ fill_element('file-name-field', name)
end
else
- fill_element(:file_name_field, name)
+ fill_element('file-name-field', name)
end
end
def fill_file_content(content, file_number = nil)
if file_number
- within_element_by_index(:file_holder_container, file_number - 1) do
+ within_element_by_index('file-holder-container', file_number - 1) do
text_area.set(content)
end
else
@@ -57,15 +57,15 @@ module QA
end
def click_delete_file(file_number)
- within_element_by_index(:file_holder_container, file_number - 1) do
- click_element(:delete_file_button)
+ within_element_by_index('file-holder-container', file_number - 1) do
+ click_element('delete-file-button')
end
end
def save_changes
- click_element_coordinates(:submit_button)
+ click_element_coordinates('submit-button')
wait_until(reload: false) do
- has_no_element?(:file_name_field)
+ has_no_element?('file-name-field')
end
end
diff --git a/qa/qa/page/dashboard/snippet/index.rb b/qa/qa/page/dashboard/snippet/index.rb
index 51d7bd3f20b..3b479ddebb1 100644
--- a/qa/qa/page/dashboard/snippet/index.rb
+++ b/qa/qa/page/dashboard/snippet/index.rb
@@ -6,25 +6,25 @@ module QA
module Snippet
class Index < Page::Base
view 'app/views/shared/snippets/_snippet.html.haml' do
- element :snippet_link
- element :snippet_visibility_content
- element :snippet_file_count_content
+ element 'snippet-link'
+ element 'snippet-visibility-content'
+ element 'snippet-file-count-content'
end
def has_snippet_title?(snippet_title)
- has_element?(:snippet_link, snippet_title: snippet_title)
+ has_element?('snippet-link', snippet_title: snippet_title)
end
def has_visibility_level?(snippet_title, visibility)
- within_element(:snippet_link, snippet_title: snippet_title) do
- has_element?(:snippet_visibility_content, snippet_visibility: visibility)
+ within_element('snippet-link', snippet_title: snippet_title) do
+ has_element?('snippet-visibility-content', snippet_visibility: visibility)
end
end
def has_number_of_files?(snippet_title, number)
retry_until(max_attempts: 5, reload: true, sleep_interval: 1) do # snippet statistics computation can take a few moments
- within_element(:snippet_link, snippet_title: snippet_title) do
- has_element?(:snippet_file_count_content, snippet_files: number, wait: 5)
+ within_element('snippet-link', snippet_title: snippet_title) do
+ has_element?('snippet-file-count-content', snippet_files: number, wait: 5)
end
end
end
diff --git a/qa/qa/page/group/show.rb b/qa/qa/page/group/show.rb
index 0eaab078950..4dd0f3a5704 100644
--- a/qa/qa/page/group/show.rb
+++ b/qa/qa/page/group/show.rb
@@ -8,7 +8,7 @@ module QA
include QA::Page::Component::ConfirmModal
view 'app/views/groups/_home_panel.html.haml' do
- element :new_project_button
+ element 'new-project-button'
element :new_subgroup_button
element :group_id_content
end
@@ -23,7 +23,7 @@ module QA
end
def has_new_project_and_new_subgroup_buttons?
- has_element?(:new_project_button)
+ has_element?('new_project_button')
has_element?(:new_subgroup_button)
end
@@ -36,7 +36,7 @@ module QA
end
def go_to_new_project
- click_element :new_project_button
+ click_element 'new-project-button'
end
def group_id
diff --git a/qa/qa/page/label/index.rb b/qa/qa/page/label/index.rb
index 70df2e4dc9e..788c0a6378a 100644
--- a/qa/qa/page/label/index.rb
+++ b/qa/qa/page/label/index.rb
@@ -23,7 +23,7 @@ module QA
# This can cause webdriver to miss the hit so we wait for the svg to load (implicitly with has_element?)
# before clicking the button.
within_element(:label_svg_content) do
- has_element?(:js_lazy_loaded_content)
+ has_element?('js-lazy-loaded-content')
end
click_element :create_new_label_button
diff --git a/qa/qa/page/project/snippet/index.rb b/qa/qa/page/project/snippet/index.rb
index 704698dc9d8..0ed2453bf77 100644
--- a/qa/qa/page/project/snippet/index.rb
+++ b/qa/qa/page/project/snippet/index.rb
@@ -9,15 +9,15 @@ module QA
include Page::Component::BlobContent
view 'app/views/shared/snippets/_snippet.html.haml' do
- element :snippet_link
+ element 'snippet-link'
end
def has_project_snippet?(title)
- has_element?(:snippet_link, snippet_title: title)
+ has_element?('snippet-link', snippet_title: title)
end
def click_snippet_link(title)
- within_element(:snippet_link, text: title) do
+ within_element('snippet-link', text: title) do
click_link(title)
end
end
diff --git a/qa/qa/page/project/snippet/new.rb b/qa/qa/page/project/snippet/new.rb
index 4a13e597e15..1a7e771ce7b 100644
--- a/qa/qa/page/project/snippet/new.rb
+++ b/qa/qa/page/project/snippet/new.rb
@@ -8,8 +8,8 @@ module QA
include Page::Component::NewSnippet
include Component::LazyLoader
view 'app/views/shared/empty_states/_snippets.html.haml' do
- element :create_first_snippet_link
- element :svg_content
+ element 'create-first-snippet-link'
+ element 'svg-content'
end
def click_create_first_snippet
@@ -19,10 +19,10 @@ module QA
# "New snippet" button shifts up a bit. This can cause
# webdriver to miss the hit so we wait for the svg to load before
# clicking the button.
- within_element(:svg_content) do
- has_element?(:js_lazy_loaded_content)
+ within_element('svg-content') do
+ has_element?('js-lazy-loaded-content')
end
- click_element(:create_first_snippet_link)
+ click_element('create-first-snippet-link')
end
end
end
diff --git a/qa/qa/page/project/web_ide/edit.rb b/qa/qa/page/project/web_ide/edit.rb
index 16eaa7efba9..a835ba9e85f 100644
--- a/qa/qa/page/project/web_ide/edit.rb
+++ b/qa/qa/page/project/web_ide/edit.rb
@@ -267,13 +267,13 @@ module QA
def create_first_file(file_name)
click_element(:first_file_button, Page::Component::WebIDE::Modal::CreateNewFile)
- fill_element(:file_name_field, file_name)
+ fill_element('file-name-field', file_name)
click_button('Create file')
end
def add_file(file_name, file_text)
click_element(:new_file_button, Page::Component::WebIDE::Modal::CreateNewFile)
- fill_element(:file_name_field, file_name)
+ fill_element('file-name-field', file_name)
click_button('Create file')
wait_until(reload: false) { has_file?(file_name) }
within_element(:editor_container) do
@@ -283,7 +283,7 @@ module QA
def add_directory(directory_name)
click_element(:new_directory_button, Page::Component::WebIDE::Modal::CreateNewFile)
- fill_element(:file_name_field, directory_name)
+ fill_element('file-name-field', directory_name)
click_button('Create directory')
wait_until(reload: false) { has_file?(directory_name) }
end
@@ -292,7 +292,7 @@ module QA
click_element('file-row-name-container', file_name: file_name)
click_element(:dropdown_button)
click_element(:rename_move_button, Page::Component::WebIDE::Modal::CreateNewFile)
- fill_element(:file_name_field, new_file_name)
+ fill_element('file-name-field', new_file_name)
click_button('Rename file')
end
diff --git a/qa/qa/page/search/results.rb b/qa/qa/page/search/results.rb
index a04fd9092d1..1fbd9a75dc5 100644
--- a/qa/qa/page/search/results.rb
+++ b/qa/qa/page/search/results.rb
@@ -16,7 +16,7 @@ module QA
end
view 'app/views/shared/projects/_project.html.haml' do
- element :project_content
+ element 'project-content'
end
def switch_to_code
diff --git a/scripts/generate_rspec_pipeline.rb b/scripts/generate_rspec_pipeline.rb
index 292b3d85b20..1fc37374ba5 100755
--- a/scripts/generate_rspec_pipeline.rb
+++ b/scripts/generate_rspec_pipeline.rb
@@ -110,7 +110,7 @@ class GenerateRspecPipeline
end
def optimal_nodes_count(test_level, rspec_files)
- nodes_count = (rspec_files.size / optimal_test_file_count_per_node_per_test_level(test_level)).ceil
+ nodes_count = (rspec_files.size / optimal_test_file_count_per_node_per_test_level(test_level, rspec_files)).ceil
info "Optimal node count for #{rspec_files.size} #{test_level} RSpec files is #{nodes_count}."
if nodes_count > MAX_NODES_COUNT
@@ -123,14 +123,27 @@ class GenerateRspecPipeline
end
end
- def optimal_test_file_count_per_node_per_test_level(test_level)
+ def optimal_test_file_count_per_node_per_test_level(test_level, rspec_files)
[
- (OPTIMAL_TEST_RUNTIME_DURATION_IN_SECONDS / average_test_file_duration_in_seconds_per_test_level[test_level]),
+ (OPTIMAL_TEST_RUNTIME_DURATION_IN_SECONDS / average_test_file_duration(test_level, rspec_files)),
1
].max
end
- def average_test_file_duration_in_seconds_per_test_level
+ def average_test_file_duration(test_level, rspec_files)
+ if rspec_files.any? && knapsack_report.any?
+ rspec_files_duration = rspec_files.sum do |rspec_file|
+ knapsack_report.fetch(
+ rspec_file, average_test_file_duration_per_test_level[test_level])
+ end
+
+ rspec_files_duration / rspec_files.size
+ else
+ average_test_file_duration_per_test_level[test_level]
+ end
+ end
+
+ def average_test_file_duration_per_test_level
@optimal_test_file_count_per_node_per_test_level ||=
if knapsack_report.any?
remaining_knapsack_report = knapsack_report.dup
diff --git a/spec/factories/ml/candidate_metrics.rb b/spec/factories/ml/candidate_metrics.rb
index 28e3974d39f..633234e5962 100644
--- a/spec/factories/ml/candidate_metrics.rb
+++ b/spec/factories/ml/candidate_metrics.rb
@@ -6,7 +6,7 @@ FactoryBot.define do
sequence(:name) { |n| "metric#{n}" }
value { 2.0 }
- step { 1 }
+ step { 0 }
tracked_at { 1234 }
end
end
diff --git a/spec/frontend/ml/experiment_tracking/routes/candidates/show/components/candidate_detail_row_spec.js b/spec/frontend/ml/experiment_tracking/routes/candidates/show/components/candidate_detail_row_spec.js
index 53dbd796d85..cd252560590 100644
--- a/spec/frontend/ml/experiment_tracking/routes/candidates/show/components/candidate_detail_row_spec.js
+++ b/spec/frontend/ml/experiment_tracking/routes/candidates/show/components/candidate_detail_row_spec.js
@@ -2,15 +2,14 @@ import { shallowMount } from '@vue/test-utils';
import DetailRow from '~/ml/experiment_tracking/routes/candidates/show/components/candidate_detail_row.vue';
describe('CandidateDetailRow', () => {
- const SECTION_LABEL_CELL = 0;
- const ROW_LABEL_CELL = 1;
- const ROW_VALUE_CELL = 2;
+ const ROW_LABEL_CELL = 0;
+ const ROW_VALUE_CELL = 1;
let wrapper;
const createWrapper = ({ slots = {} } = {}) => {
wrapper = shallowMount(DetailRow, {
- propsData: { sectionLabel: 'Section', label: 'Item' },
+ propsData: { label: 'Item' },
slots,
});
};
@@ -19,10 +18,6 @@ describe('CandidateDetailRow', () => {
beforeEach(() => createWrapper());
- it('renders section label', () => {
- expect(findCellAt(SECTION_LABEL_CELL).text()).toBe('Section');
- });
-
it('renders row label', () => {
expect(findCellAt(ROW_LABEL_CELL).text()).toBe('Item');
});
diff --git a/spec/frontend/ml/experiment_tracking/routes/candidates/show/ml_candidates_show_spec.js b/spec/frontend/ml/experiment_tracking/routes/candidates/show/ml_candidates_show_spec.js
index 0b3b780cb3f..296728af46a 100644
--- a/spec/frontend/ml/experiment_tracking/routes/candidates/show/ml_candidates_show_spec.js
+++ b/spec/frontend/ml/experiment_tracking/routes/candidates/show/ml_candidates_show_spec.js
@@ -1,32 +1,51 @@
-import { shallowMount } from '@vue/test-utils';
-import { GlAvatarLabeled, GlLink } from '@gitlab/ui';
+import { GlAvatarLabeled, GlLink, GlTableLite } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import MlCandidatesShow from '~/ml/experiment_tracking/routes/candidates/show';
import DetailRow from '~/ml/experiment_tracking/routes/candidates/show/components/candidate_detail_row.vue';
-import { TITLE_LABEL } from '~/ml/experiment_tracking/routes/candidates/show/translations';
+import {
+ TITLE_LABEL,
+ NO_PARAMETERS_MESSAGE,
+ NO_METRICS_MESSAGE,
+ NO_METADATA_MESSAGE,
+ NO_CI_MESSAGE,
+} from '~/ml/experiment_tracking/routes/candidates/show/translations';
import DeleteButton from '~/ml/experiment_tracking/components/delete_button.vue';
import ModelExperimentsHeader from '~/ml/experiment_tracking/components/model_experiments_header.vue';
+import { stubComponent } from 'helpers/stub_component';
import { newCandidate } from './mock_data';
describe('MlCandidatesShow', () => {
let wrapper;
const CANDIDATE = newCandidate();
- const USER_ROW = 6;
+ const USER_ROW = 1;
+
+ const INFO_SECTION = 0;
+ const CI_SECTION = 1;
+ const PARAMETER_SECTION = 2;
+ const METADATA_SECTION = 3;
const createWrapper = (createCandidate = () => CANDIDATE) => {
- wrapper = shallowMount(MlCandidatesShow, {
+ wrapper = shallowMountExtended(MlCandidatesShow, {
propsData: { candidate: createCandidate() },
+ stubs: {
+ GlTableLite: { ...stubComponent(GlTableLite), props: ['items', 'fields'] },
+ },
});
};
const findDeleteButton = () => wrapper.findComponent(DeleteButton);
const findHeader = () => wrapper.findComponent(ModelExperimentsHeader);
- const findNthDetailRow = (index) => wrapper.findAllComponents(DetailRow).at(index);
- const findLinkInNthDetailRow = (index) => findNthDetailRow(index).findComponent(GlLink);
- const findSectionLabel = (label) => wrapper.find(`[sectionLabel='${label}']`);
+ const findSection = (section) => wrapper.findAll('section').at(section);
+ const findRowInSection = (section, row) =>
+ findSection(section).findAllComponents(DetailRow).at(row);
+ const findLinkAtRow = (section, rowIndex) =>
+ findRowInSection(section, rowIndex).findComponent(GlLink);
+ const findNoDataMessage = (label) => wrapper.findByText(label);
const findLabel = (label) => wrapper.find(`[label='${label}']`);
- const findCiUserDetailRow = () => findNthDetailRow(USER_ROW);
+ const findCiUserDetailRow = () => findRowInSection(CI_SECTION, USER_ROW);
const findCiUserAvatar = () => findCiUserDetailRow().findComponent(GlAvatarLabeled);
const findCiUserAvatarNameLink = () => findCiUserAvatar().findComponent(GlLink);
+ const findMetricsTable = () => wrapper.findComponent(GlTableLite);
describe('Header', () => {
beforeEach(() => createWrapper());
@@ -50,42 +69,57 @@ describe('MlCandidatesShow', () => {
const mrText = `!${CANDIDATE.info.ci_job.merge_request.iid} ${CANDIDATE.info.ci_job.merge_request.title}`;
const expectedTable = [
- ['Info', 'ID', CANDIDATE.info.iid],
- ['', 'MLflow run ID', CANDIDATE.info.eid],
- ['', 'Status', CANDIDATE.info.status],
- ['', 'Experiment', CANDIDATE.info.experiment_name],
- ['', 'Artifacts', 'Artifacts'],
- ['CI', 'Job', CANDIDATE.info.ci_job.name],
- ['', 'Triggered by', 'CI User'],
- ['', 'Merge request', mrText],
- ['Parameters', CANDIDATE.params[0].name, CANDIDATE.params[0].value],
- ['', CANDIDATE.params[1].name, CANDIDATE.params[1].value],
- ['Metrics', CANDIDATE.metrics[0].name, CANDIDATE.metrics[0].value],
- ['', CANDIDATE.metrics[1].name, CANDIDATE.metrics[1].value],
- ['Metadata', CANDIDATE.metadata[0].name, CANDIDATE.metadata[0].value],
- ['', CANDIDATE.metadata[1].name, CANDIDATE.metadata[1].value],
- ].map((row, index) => [index, ...row]);
-
- it.each(expectedTable)(
- 'row %s is created correctly',
- (rowIndex, sectionLabel, label, text) => {
- const row = findNthDetailRow(rowIndex);
-
- expect(row.props()).toMatchObject({ sectionLabel, label });
- expect(row.text()).toBe(text);
- },
- );
+ [INFO_SECTION, 0, 'ID', CANDIDATE.info.iid],
+ [INFO_SECTION, 1, 'MLflow run ID', CANDIDATE.info.eid],
+ [INFO_SECTION, 2, 'Status', CANDIDATE.info.status],
+ [INFO_SECTION, 3, 'Experiment', CANDIDATE.info.experiment_name],
+ [INFO_SECTION, 4, 'Artifacts', 'Artifacts'],
+ [CI_SECTION, 0, 'Job', CANDIDATE.info.ci_job.name],
+ [CI_SECTION, 1, 'Triggered by', 'CI User'],
+ [CI_SECTION, 2, 'Merge request', mrText],
+ [PARAMETER_SECTION, 0, CANDIDATE.params[0].name, CANDIDATE.params[0].value],
+ [PARAMETER_SECTION, 1, CANDIDATE.params[1].name, CANDIDATE.params[1].value],
+ [METADATA_SECTION, 0, CANDIDATE.metadata[0].name, CANDIDATE.metadata[0].value],
+ [METADATA_SECTION, 1, CANDIDATE.metadata[1].name, CANDIDATE.metadata[1].value],
+ ];
+
+ it.each(expectedTable)('row %s is created correctly', (section, rowIndex, label, text) => {
+ const row = findRowInSection(section, rowIndex);
+
+ expect(row.props()).toMatchObject({ label });
+ expect(row.text()).toBe(text);
+ });
describe('Table links', () => {
const linkRows = [
- [3, CANDIDATE.info.path_to_experiment],
- [4, CANDIDATE.info.path_to_artifact],
- [5, CANDIDATE.info.ci_job.path],
- [7, CANDIDATE.info.ci_job.merge_request.path],
+ [INFO_SECTION, 3, CANDIDATE.info.path_to_experiment],
+ [INFO_SECTION, 4, CANDIDATE.info.path_to_artifact],
+ [CI_SECTION, 0, CANDIDATE.info.ci_job.path],
+ [CI_SECTION, 2, CANDIDATE.info.ci_job.merge_request.path],
];
- it.each(linkRows)('row %s is created correctly', (rowIndex, href) => {
- expect(findLinkInNthDetailRow(rowIndex).attributes().href).toBe(href);
+ it.each(linkRows)('row %s is created correctly', (section, rowIndex, href) => {
+ expect(findLinkAtRow(section, rowIndex).attributes().href).toBe(href);
+ });
+ });
+
+ describe('Metrics table', () => {
+ it('computes metrics table items correctly', () => {
+ expect(findMetricsTable().props('items')).toEqual([
+ { name: 'AUC', 0: '.55' },
+ { name: 'Accuracy', 1: '.99', 2: '.98', 3: '.97' },
+ { name: 'F1', 3: '.1' },
+ ]);
+ });
+
+ it('computes metrics table fields correctly', () => {
+ expect(findMetricsTable().props('fields')).toEqual([
+ expect.objectContaining({ key: 'name', label: 'Metric' }),
+ expect.objectContaining({ key: '0', label: 'Step 0' }),
+ expect.objectContaining({ key: '1', label: 'Step 1' }),
+ expect.objectContaining({ key: '2', label: 'Step 2' }),
+ expect.objectContaining({ key: '3', label: 'Step 3' }),
+ ]);
});
});
@@ -105,22 +139,6 @@ describe('MlCandidatesShow', () => {
expect(nameLink.text()).toEqual('CI User');
});
});
-
- it('does not render params', () => {
- expect(findSectionLabel('Parameters').exists()).toBe(true);
- });
-
- it('renders all conditional rows', () => {
- // This is a bit of a duplicated test from the above table test, but having this makes sure that the
- // tests that test the negatives are implemented correctly
- expect(findLabel('Artifacts').exists()).toBe(true);
- expect(findSectionLabel('Parameters').exists()).toBe(true);
- expect(findSectionLabel('Metadata').exists()).toBe(true);
- expect(findSectionLabel('Metrics').exists()).toBe(true);
- expect(findSectionLabel('CI').exists()).toBe(true);
- expect(findLabel('Merge request').exists()).toBe(true);
- expect(findLabel('Triggered by').exists()).toBe(true);
- });
});
describe('No artifact path', () => {
@@ -150,19 +168,19 @@ describe('MlCandidatesShow', () => {
);
it('does not render params', () => {
- expect(findSectionLabel('Parameters').exists()).toBe(false);
+ expect(findNoDataMessage(NO_PARAMETERS_MESSAGE).exists()).toBe(true);
});
it('does not render metadata', () => {
- expect(findSectionLabel('Metadata').exists()).toBe(false);
+ expect(findNoDataMessage(NO_METADATA_MESSAGE).exists()).toBe(true);
});
it('does not render metrics', () => {
- expect(findSectionLabel('Metrics').exists()).toBe(false);
+ expect(findNoDataMessage(NO_METRICS_MESSAGE).exists()).toBe(true);
});
it('does not render CI info', () => {
- expect(findSectionLabel('CI').exists()).toBe(false);
+ expect(findNoDataMessage(NO_CI_MESSAGE).exists()).toBe(true);
});
});
diff --git a/spec/frontend/ml/experiment_tracking/routes/candidates/show/mock_data.js b/spec/frontend/ml/experiment_tracking/routes/candidates/show/mock_data.js
index 3fbcf122997..4ea23ed2513 100644
--- a/spec/frontend/ml/experiment_tracking/routes/candidates/show/mock_data.js
+++ b/spec/frontend/ml/experiment_tracking/routes/candidates/show/mock_data.js
@@ -4,8 +4,11 @@ export const newCandidate = () => ({
{ name: 'MaxDepth', value: '3' },
],
metrics: [
- { name: 'AUC', value: '.55' },
- { name: 'Accuracy', value: '.99' },
+ { name: 'AUC', value: '.55', step: 0 },
+ { name: 'Accuracy', value: '.99', step: 1 },
+ { name: 'Accuracy', value: '.98', step: 2 },
+ { name: 'Accuracy', value: '.97', step: 3 },
+ { name: 'F1', value: '.1', step: 3 },
],
metadata: [
{ name: 'FileName', value: 'test.py' },
diff --git a/spec/frontend/organizations/index/components/app_spec.js b/spec/frontend/organizations/index/components/app_spec.js
new file mode 100644
index 00000000000..4bb90903725
--- /dev/null
+++ b/spec/frontend/organizations/index/components/app_spec.js
@@ -0,0 +1,34 @@
+import { GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import OrganizationsIndexApp from '~/organizations/index/components/app.vue';
+import OrganizationsView from '~/organizations/index/components/organizations_view.vue';
+import { MOCK_NEW_ORG_URL } from '../mock_data';
+
+describe('OrganizationsIndexApp', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(OrganizationsIndexApp, {
+ provide: {
+ newOrganizationUrl: MOCK_NEW_ORG_URL,
+ },
+ });
+ };
+
+ const findNewOrganizationButton = () => wrapper.findComponent(GlButton);
+ const findOrganizationsView = () => wrapper.findComponent(OrganizationsView);
+
+ describe('template', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders new organization button with correct link', () => {
+ expect(findNewOrganizationButton().attributes('href')).toBe(MOCK_NEW_ORG_URL);
+ });
+
+ it('renders the organizations view', () => {
+ expect(findOrganizationsView().exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/organizations/index/components/organizations_list_item_spec.js b/spec/frontend/organizations/index/components/organizations_list_item_spec.js
new file mode 100644
index 00000000000..77d07a93775
--- /dev/null
+++ b/spec/frontend/organizations/index/components/organizations_list_item_spec.js
@@ -0,0 +1,45 @@
+import { GlAvatarLabeled } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import OrganizationsListItem from '~/organizations/index/components/organizations_list_item.vue';
+import { organizations } from '~/organizations/mock_data';
+
+const MOCK_ORGANIZATION = organizations[0];
+
+describe('OrganizationsListItem', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(OrganizationsListItem, {
+ propsData: {
+ organization: MOCK_ORGANIZATION,
+ },
+ });
+ };
+
+ const findGlAvatarLabeled = () => wrapper.findComponent(GlAvatarLabeled);
+
+ describe('template', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders GlAvatarLabeled with correct description', () => {
+ expect(findGlAvatarLabeled().text()).toBe(MOCK_ORGANIZATION.description);
+ });
+
+ it('renders GlAvatarLabeled with correct data', () => {
+ expect(findGlAvatarLabeled().attributes('entity-id')).toBe(
+ getIdFromGraphQLId(MOCK_ORGANIZATION.id).toString(),
+ );
+ expect(findGlAvatarLabeled().attributes('src')).toBe(
+ MOCK_ORGANIZATION.avatarUrl || undefined,
+ );
+ expect(findGlAvatarLabeled().attributes('entity-name')).toBe(MOCK_ORGANIZATION.name);
+ expect(findGlAvatarLabeled().attributes('label')).toBe(MOCK_ORGANIZATION.name);
+ expect(findGlAvatarLabeled().attributes('label-link')).toBe(
+ MOCK_ORGANIZATION.webUrl || undefined,
+ );
+ });
+ });
+});
diff --git a/spec/frontend/organizations/index/components/organizations_list_spec.js b/spec/frontend/organizations/index/components/organizations_list_spec.js
new file mode 100644
index 00000000000..0b59c212314
--- /dev/null
+++ b/spec/frontend/organizations/index/components/organizations_list_spec.js
@@ -0,0 +1,28 @@
+import { shallowMount } from '@vue/test-utils';
+import OrganizationsList from '~/organizations/index/components/organizations_list.vue';
+import OrganizationsListItem from '~/organizations/index/components/organizations_list_item.vue';
+import { organizations } from '~/organizations/mock_data';
+
+describe('OrganizationsList', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(OrganizationsList, {
+ propsData: {
+ organizations,
+ },
+ });
+ };
+
+ const findAllOrganizationsListItem = () => wrapper.findAllComponents(OrganizationsListItem);
+
+ describe('template', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders a list item for each organization', () => {
+ expect(findAllOrganizationsListItem()).toHaveLength(organizations.length);
+ });
+ });
+});
diff --git a/spec/frontend/organizations/index/components/organizations_view_spec.js b/spec/frontend/organizations/index/components/organizations_view_spec.js
new file mode 100644
index 00000000000..9c694fdd508
--- /dev/null
+++ b/spec/frontend/organizations/index/components/organizations_view_spec.js
@@ -0,0 +1,130 @@
+import { GlLoadingIcon, GlEmptyState } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import Vue from 'vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/alert';
+import resolvers from '~/organizations/shared/graphql/resolvers';
+import { organizations } from '~/organizations/mock_data';
+import organizationsQuery from '~/organizations/index/graphql/organizations.query.graphql';
+import OrganizationsView from '~/organizations/index/components/organizations_view.vue';
+import OrganizationsList from '~/organizations/index/components/organizations_list.vue';
+import { MOCK_NEW_ORG_URL, MOCK_ORG_EMPTY_STATE_SVG } from '../mock_data';
+
+jest.mock('~/alert');
+
+Vue.use(VueApollo);
+
+describe('OrganizationsView', () => {
+ let wrapper;
+ let mockApollo;
+
+ const createComponent = (mockResolvers = resolvers) => {
+ mockApollo = createMockApollo([[organizationsQuery, mockResolvers]]);
+
+ wrapper = shallowMount(OrganizationsView, {
+ apolloProvider: mockApollo,
+ provide: {
+ newOrganizationUrl: MOCK_NEW_ORG_URL,
+ organizationsEmptyStateSvgPath: MOCK_ORG_EMPTY_STATE_SVG,
+ },
+ });
+ };
+
+ afterEach(() => {
+ mockApollo = null;
+ });
+
+ const findGlLoading = () => wrapper.findComponent(GlLoadingIcon);
+ const findOrganizationsList = () => wrapper.findComponent(OrganizationsList);
+ const findGlEmptyState = () => wrapper.findComponent(GlEmptyState);
+
+ describe('when API call is loading', () => {
+ beforeEach(() => {
+ const mockResolvers = jest.fn().mockReturnValue(new Promise(() => {}));
+
+ createComponent(mockResolvers);
+ });
+
+ it('renders loading icon', () => {
+ expect(findGlLoading().exists()).toBe(true);
+ });
+
+ it('does not render organizations list', () => {
+ expect(findOrganizationsList().exists()).toBe(false);
+ });
+
+ it('does not render empty state', () => {
+ expect(findGlEmptyState().exists()).toBe(false);
+ });
+ });
+
+ describe('when API returns successful with results', () => {
+ beforeEach(async () => {
+ const mockResolvers = jest.fn().mockResolvedValue({
+ data: { currentUser: { id: 1, organizations: { nodes: organizations } } },
+ });
+
+ createComponent(mockResolvers);
+ await waitForPromises();
+ });
+
+ it('does not render loading icon', () => {
+ expect(findGlLoading().exists()).toBe(false);
+ });
+
+ it('renders organizations list', () => {
+ expect(findOrganizationsList().exists()).toBe(true);
+ });
+
+ it('does not render empty state', () => {
+ expect(findGlEmptyState().exists()).toBe(false);
+ });
+ });
+
+ describe('when API returns successful without results', () => {
+ beforeEach(async () => {
+ const mockResolvers = jest
+ .fn()
+ .mockResolvedValue({ data: { currentUser: { id: 1, organizations: { nodes: [] } } } });
+
+ createComponent(mockResolvers);
+ await waitForPromises();
+ });
+
+ it('does not render loading icon', () => {
+ expect(findGlLoading().exists()).toBe(false);
+ });
+
+ it('does not render organizations list', () => {
+ expect(findOrganizationsList().exists()).toBe(false);
+ });
+
+ it('does render empty state with correct SVG and URL', () => {
+ expect(findGlEmptyState().exists()).toBe(true);
+ expect(findGlEmptyState().attributes('svgpath')).toBe(MOCK_ORG_EMPTY_STATE_SVG);
+ expect(findGlEmptyState().attributes('primarybuttonlink')).toBe(MOCK_NEW_ORG_URL);
+ });
+ });
+
+ describe('when API returns error', () => {
+ const error = new Error();
+
+ beforeEach(async () => {
+ const mockResolvers = jest.fn().mockRejectedValue(error);
+
+ createComponent(mockResolvers);
+ await waitForPromises();
+ });
+
+ it('creates a flash message', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message:
+ 'An error occurred loading user organizations. Please refresh the page to try again.',
+ error,
+ captureError: true,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/organizations/index/mock_data.js b/spec/frontend/organizations/index/mock_data.js
new file mode 100644
index 00000000000..50b20b4f79c
--- /dev/null
+++ b/spec/frontend/organizations/index/mock_data.js
@@ -0,0 +1,3 @@
+export const MOCK_NEW_ORG_URL = 'gitlab.com/organizations/new';
+
+export const MOCK_ORG_EMPTY_STATE_SVG = 'illustrations/empty-state/empty-organizations-md.svg';
diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap
index 1c60c3af310..6414ab6dfba 100644
--- a/spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap
+++ b/spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap
@@ -3,11 +3,11 @@
exports[`Snippet Blob Edit component with loaded blob matches snapshot 1`] = `
<div
class="file-holder snippet"
- data-qa-selector="file_holder_container"
+ data-testid="file-holder-container"
>
<blob-header-edit-stub
candelete="true"
- data-qa-selector="file_name_field"
+ data-testid="file-name-field"
id="reference-0"
showdelete="true"
value="foo/bar/test.md"
diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
index 5ed3b520b70..92511acc4f8 100644
--- a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
+++ b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
@@ -17,7 +17,7 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] =
>
<gl-form-input-stub
class="form-control"
- data-qa-selector="description_placeholder"
+ data-testid="description-placeholder"
placeholder="Describe what your snippet does or how to use it…"
/>
</div>
@@ -46,8 +46,8 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] =
<textarea
aria-label="Description"
class="js-autosize js-gfm-input js-gfm-input-initialized markdown-area note-textarea"
- data-qa-selector="snippet_description_field"
data-supports-quick-actions="false"
+ data-testid="snippet-description-field"
dir="auto"
id="reference-0"
placeholder="Write a comment or drag your files here…"
diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap
index 3274f41e4af..ab96d1a3653 100644
--- a/spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap
+++ b/spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap
@@ -44,8 +44,8 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
/>
<span
class="font-weight-bold js-visibility-option ml-1"
- data-qa-selector="visibility_content"
data-qa-visibility="Private"
+ data-testid="visibility-content"
>
Private
</span>
@@ -64,8 +64,8 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
/>
<span
class="font-weight-bold js-visibility-option ml-1"
- data-qa-selector="visibility_content"
data-qa-visibility="Internal"
+ data-testid="visibility-content"
>
Internal
</span>
@@ -84,8 +84,8 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
/>
<span
class="font-weight-bold js-visibility-option ml-1"
- data-qa-selector="visibility_content"
data-qa-visibility="Public"
+ data-testid="visibility-content"
>
Public
</span>
diff --git a/spec/frontend/snippets/components/edit_spec.js b/spec/frontend/snippets/components/edit_spec.js
index 17862953920..5fbc16ff430 100644
--- a/spec/frontend/snippets/components/edit_spec.js
+++ b/spec/frontend/snippets/components/edit_spec.js
@@ -117,7 +117,8 @@ describe('Snippet Edit app', () => {
.map((path) => `<input name="files[]" value="${path}">`)
.join('');
};
- const setTitle = (val) => wrapper.findByTestId('snippet-title-input').vm.$emit('input', val);
+ const setTitle = (val) =>
+ wrapper.findByTestId('snippet-title-input-field').vm.$emit('input', val);
const setDescription = (val) =>
wrapper.findComponent(SnippetDescriptionEdit).vm.$emit('input', val);
diff --git a/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js b/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js
index cb11e98cd35..fab65434c3a 100644
--- a/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js
+++ b/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js
@@ -40,7 +40,7 @@ describe('snippets/components/snippet_blob_actions_edit', () => {
classes: x.classes(),
}));
const findFirstBlobEdit = () => findBlobEdits().at(0);
- const findAddButton = () => wrapper.find('[data-testid="add_button"]');
+ const findAddButton = () => wrapper.find('[data-testid="add-button"]');
const findLimitationsText = () => wrapper.find('[data-testid="limitations_text"]');
const getLastActions = () => {
const events = wrapper.emitted().actions;
diff --git a/spec/helpers/organizations/organization_helper_spec.rb b/spec/helpers/organizations/organization_helper_spec.rb
index ec99d928059..67aa57dcad7 100644
--- a/spec/helpers/organizations/organization_helper_spec.rb
+++ b/spec/helpers/organizations/organization_helper_spec.rb
@@ -6,12 +6,15 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do
let_it_be(:organization) { build_stubbed(:organization) }
let_it_be(:new_group_path) { '/groups/new' }
let_it_be(:new_project_path) { '/projects/new' }
+ let_it_be(:organizations_empty_state_svg_path) { 'illustrations/empty-state/empty-organizations-md.svg' }
let_it_be(:groups_empty_state_svg_path) { 'illustrations/empty-state/empty-groups-md.svg' }
let_it_be(:projects_empty_state_svg_path) { 'illustrations/empty-state/empty-projects-md.svg' }
before do
allow(helper).to receive(:new_group_path).and_return(new_group_path)
allow(helper).to receive(:new_project_path).and_return(new_project_path)
+ allow(helper).to receive(:image_path).with(organizations_empty_state_svg_path)
+ .and_return(organizations_empty_state_svg_path)
allow(helper).to receive(:image_path).with(groups_empty_state_svg_path).and_return(groups_empty_state_svg_path)
allow(helper).to receive(:image_path).with(projects_empty_state_svg_path).and_return(projects_empty_state_svg_path)
end
@@ -62,4 +65,15 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do
)
end
end
+
+ describe '#organization_index_app_data' do
+ it 'returns expected data object' do
+ expect(helper.organization_index_app_data).to eq(
+ {
+ new_organization_url: new_organization_path,
+ organizations_empty_state_svg_path: organizations_empty_state_svg_path
+ }
+ )
+ end
+ end
end
diff --git a/spec/lib/gitlab/experiment/rollout/feature_spec.rb b/spec/lib/gitlab/experiment/rollout/feature_spec.rb
index bc90b4d74b4..6d01b7a175f 100644
--- a/spec/lib/gitlab/experiment/rollout/feature_spec.rb
+++ b/spec/lib/gitlab/experiment/rollout/feature_spec.rb
@@ -111,7 +111,7 @@ RSpec.describe Gitlab::Experiment::Rollout::Feature, :experiment, feature_catego
context "when distribution is specified as an array" do
before do
- subject_experiment.rollout(described_class, distribution: [0, 32, 25, 43])
+ subject_experiment.rollout(described_class, distribution: [32, 25, 43])
end
it "rolls out with the expected distribution" do
@@ -119,19 +119,19 @@ RSpec.describe Gitlab::Experiment::Rollout::Feature, :experiment, feature_catego
100.times { |i| run_cycle(subject_experiment, value: i) }
- expect(counts).to eq(control: 2, variant1: 37, variant2: 24, variant3: 37)
+ expect(counts).to eq(variant1: 39, variant2: 24, variant3: 37)
end
end
context "when distribution is specified as a hash" do
before do
- subject_experiment.rollout(described_class, distribution: { control: 0, variant1: 90, variant2: 10 })
+ subject_experiment.rollout(described_class, distribution: { variant1: 90, variant2: 10 })
end
it "rolls out with the expected distribution" do
100.times { |i| run_cycle(subject_experiment, value: i) }
- expect(counts).to eq(control: 2, variant1: 93, variant2: 5)
+ expect(counts).to eq(variant1: 95, variant2: 5)
end
end
diff --git a/spec/models/ci/catalog/listing_spec.rb b/spec/models/ci/catalog/listing_spec.rb
index f28a0e82bbd..7524d908252 100644
--- a/spec/models/ci/catalog/listing_spec.rb
+++ b/spec/models/ci/catalog/listing_spec.rb
@@ -6,7 +6,8 @@ RSpec.describe Ci::Catalog::Listing, feature_category: :pipeline_composition do
let_it_be(:namespace) { create(:group) }
let_it_be(:project_1) { create(:project, namespace: namespace, name: 'X Project') }
let_it_be(:project_2) { create(:project, namespace: namespace, name: 'B Project') }
- let_it_be(:project_3) { create(:project) }
+ let_it_be(:project_3) { create(:project, namespace: namespace, name: 'A Project') }
+ let_it_be(:project_4) { create(:project) }
let_it_be(:user) { create(:user) }
let(:list) { described_class.new(namespace, user) }
@@ -34,12 +35,20 @@ RSpec.describe Ci::Catalog::Listing, feature_category: :pipeline_composition do
end
context 'when the namespace has catalog resources' do
- let_it_be(:resource) { create(:ci_catalog_resource, project: project_1) }
- let_it_be(:resource_2) { create(:ci_catalog_resource, project: project_2) }
- let_it_be(:other_namespace_resource) { create(:ci_catalog_resource, project: project_3) }
+ let_it_be(:today) { Time.zone.now }
+ let_it_be(:yesterday) { today - 1.day }
+ let_it_be(:tomorrow) { today + 1.day }
+
+ let_it_be(:resource) { create(:ci_catalog_resource, project: project_1, latest_released_at: yesterday) }
+ let_it_be(:resource_2) { create(:ci_catalog_resource, project: project_2, latest_released_at: today) }
+ let_it_be(:resource_3) { create(:ci_catalog_resource, project: project_3, latest_released_at: nil) }
+
+ let_it_be(:other_namespace_resource) do
+ create(:ci_catalog_resource, project: project_4, latest_released_at: tomorrow)
+ end
it 'contains only catalog resources for projects in that namespace' do
- is_expected.to contain_exactly(resource, resource_2)
+ is_expected.to contain_exactly(resource, resource_2, resource_3)
end
context 'with a sort parameter' do
@@ -48,16 +57,32 @@ RSpec.describe Ci::Catalog::Listing, feature_category: :pipeline_composition do
context 'when the sort is name ascending' do
let_it_be(:sort) { :name_asc }
- it 'contains catalog resources for projects sorted by name' do
- is_expected.to eq([resource_2, resource])
+ it 'contains catalog resources for projects sorted by name ascending' do
+ is_expected.to eq([resource_3, resource_2, resource])
end
end
context 'when the sort is name descending' do
let_it_be(:sort) { :name_desc }
- it 'contains catalog resources for projects sorted by name' do
- is_expected.to eq([resource, resource_2])
+ it 'contains catalog resources for projects sorted by name descending' do
+ is_expected.to eq([resource, resource_2, resource_3])
+ end
+ end
+
+ context 'when the sort is latest_released_at ascending' do
+ let_it_be(:sort) { :latest_released_at_asc }
+
+ it 'contains catalog resources sorted by latest_released_at ascending with nulls last' do
+ is_expected.to eq([resource, resource_2, resource_3])
+ end
+ end
+
+ context 'when the sort is latest_released_at descending' do
+ let_it_be(:sort) { :latest_released_at_desc }
+
+ it 'contains catalog resources sorted by latest_released_at descending with nulls last' do
+ is_expected.to eq([resource_2, resource, resource_3])
end
end
end
diff --git a/spec/models/ci/catalog/resource_spec.rb b/spec/models/ci/catalog/resource_spec.rb
index 082283bb7bc..4ce1433e015 100644
--- a/spec/models/ci/catalog/resource_spec.rb
+++ b/spec/models/ci/catalog/resource_spec.rb
@@ -3,16 +3,20 @@
require 'spec_helper'
RSpec.describe Ci::Catalog::Resource, feature_category: :pipeline_composition do
+ let_it_be(:today) { Time.zone.now }
+ let_it_be(:yesterday) { today - 1.day }
+ let_it_be(:tomorrow) { today + 1.day }
+
let_it_be(:project) { create(:project, name: 'A') }
let_it_be(:project_2) { build(:project, name: 'Z') }
let_it_be(:project_3) { build(:project, name: 'L') }
- let_it_be(:resource) { create(:ci_catalog_resource, project: project) }
- let_it_be(:resource_2) { create(:ci_catalog_resource, project: project_2) }
- let_it_be(:resource_3) { create(:ci_catalog_resource, project: project_3) }
+ let_it_be(:resource) { create(:ci_catalog_resource, project: project, latest_released_at: tomorrow) }
+ let_it_be(:resource_2) { create(:ci_catalog_resource, project: project_2, latest_released_at: today) }
+ let_it_be(:resource_3) { create(:ci_catalog_resource, project: project_3, latest_released_at: nil) }
- let_it_be(:release1) { create(:release, project: project, released_at: Time.zone.now - 2.days) }
- let_it_be(:release2) { create(:release, project: project, released_at: Time.zone.now - 1.day) }
- let_it_be(:release3) { create(:release, project: project, released_at: Time.zone.now) }
+ let_it_be(:release1) { create(:release, project: project, released_at: yesterday) }
+ let_it_be(:release2) { create(:release, project: project, released_at: today) }
+ let_it_be(:release3) { create(:release, project: project, released_at: tomorrow) }
it { is_expected.to belong_to(:project) }
it { is_expected.to have_many(:components).class_name('Ci::Catalog::Resources::Component') }
@@ -58,6 +62,22 @@ RSpec.describe Ci::Catalog::Resource, feature_category: :pipeline_composition do
end
end
+ describe '.order_by_latest_released_at_desc' do
+ it 'returns catalog resources sorted by latest_released_at descending with nulls last' do
+ ordered_resources = described_class.order_by_latest_released_at_desc
+
+ expect(ordered_resources).to eq([resource, resource_2, resource_3])
+ end
+ end
+
+ describe '.order_by_latest_released_at_asc' do
+ it 'returns catalog resources sorted by latest_released_at ascending with nulls last' do
+ ordered_resources = described_class.order_by_latest_released_at_asc
+
+ expect(ordered_resources).to eq([resource_2, resource, resource_3])
+ end
+ end
+
describe '#versions' do
it 'returns releases ordered by released date descending' do
expect(resource.versions).to eq([release3, release2, release1])
diff --git a/spec/presenters/ml/candidate_details_presenter_spec.rb b/spec/presenters/ml/candidate_details_presenter_spec.rb
index 0ecf80b683e..34de1e66a8a 100644
--- a/spec/presenters/ml/candidate_details_presenter_spec.rb
+++ b/spec/presenters/ml/candidate_details_presenter_spec.rb
@@ -13,6 +13,8 @@ RSpec.describe ::Ml::CandidateDetailsPresenter, feature_category: :mlops do
let_it_be(:metrics) do
[
build_stubbed(:ml_candidate_metrics, name: 'metric1', value: 0.1, candidate: candidate),
+ build_stubbed(:ml_candidate_metrics, name: 'metric1', value: 0.2, step: 1, candidate: candidate),
+ build_stubbed(:ml_candidate_metrics, name: 'metric1', value: 0.3, step: 2, candidate: candidate),
build_stubbed(:ml_candidate_metrics, name: 'metric2', value: 0.2, candidate: candidate),
build_stubbed(:ml_candidate_metrics, name: 'metric3', value: 0.3, candidate: candidate)
]
@@ -30,7 +32,7 @@ RSpec.describe ::Ml::CandidateDetailsPresenter, feature_category: :mlops do
subject { Gitlab::Json.parse(described_class.new(candidate, include_ci_job).present)['candidate'] }
before do
- allow(candidate).to receive(:latest_metrics).and_return(metrics)
+ allow(candidate).to receive(:metrics).and_return(metrics)
allow(candidate).to receive(:params).and_return(params)
end
@@ -45,9 +47,11 @@ RSpec.describe ::Ml::CandidateDetailsPresenter, feature_category: :mlops do
it 'generates the correct metrics' do
expect(subject['metrics']).to include(
- hash_including('name' => 'metric1', 'value' => 0.1),
- hash_including('name' => 'metric2', 'value' => 0.2),
- hash_including('name' => 'metric3', 'value' => 0.3)
+ hash_including('name' => 'metric1', 'value' => 0.1, 'step' => 0),
+ hash_including('name' => 'metric1', 'value' => 0.2, 'step' => 1),
+ hash_including('name' => 'metric1', 'value' => 0.3, 'step' => 2),
+ hash_including('name' => 'metric2', 'value' => 0.2, 'step' => 0),
+ hash_including('name' => 'metric3', 'value' => 0.3, 'step' => 0)
)
end
diff --git a/spec/scripts/generate_rspec_pipeline_spec.rb b/spec/scripts/generate_rspec_pipeline_spec.rb
index 91b5739cf63..894c33968b8 100644
--- a/spec/scripts/generate_rspec_pipeline_spec.rb
+++ b/spec/scripts/generate_rspec_pipeline_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe GenerateRspecPipeline, :silence_stdout, feature_category: :toolin
describe '#generate!' do
let!(:rspec_files) { Tempfile.new(['rspec_files_path', '.txt']) }
let(:rspec_files_content) do
- "spec/migrations/a_spec.rb spec/migrations/b_spec.rb " \
+ "spec/migrations/a_spec.rb spec/migrations/b_spec.rb spec/migrations/c_spec.rb spec/migrations/d_spec.rb " \
"spec/lib/gitlab/background_migration/a_spec.rb spec/lib/gitlab/background_migration/b_spec.rb " \
"spec/models/a_spec.rb spec/models/b_spec.rb " \
"spec/controllers/a_spec.rb spec/controllers/b_spec.rb " \
@@ -63,8 +63,13 @@ RSpec.describe GenerateRspecPipeline, :silence_stdout, feature_category: :toolin
let(:knapsack_report_content) do
<<~JSON
{
- "spec/migrations/a_spec.rb": 360.3,
- "spec/migrations/b_spec.rb": 180.1,
+ "spec/migrations/a_spec.rb": 620.3,
+ "spec/migrations/b_spec.rb": 610.1,
+ "spec/migrations/c_spec.rb": 20.1,
+ "spec/migrations/d_spec.rb": 20.1,
+ "spec/migrations/e_spec.rb": 20.1,
+ "spec/migrations/f_spec.rb": 20.1,
+ "spec/migrations/g_spec.rb": 20.1,
"spec/lib/gitlab/background_migration/a_spec.rb": 60.5,
"spec/lib/gitlab/background_migration/b_spec.rb": 180.3,
"spec/models/a_spec.rb": 360.2,
@@ -123,7 +128,7 @@ RSpec.describe GenerateRspecPipeline, :silence_stdout, feature_category: :toolin
expect(File.read("#{pipeline_template.path}.yml"))
.to eq(
- "rspec migration:\n parallel: 2\nrspec background_migration:\n parallel: 2\n" \
+ "rspec migration:\n parallel: 4\nrspec background_migration:\n parallel: 2\n" \
"rspec unit:\n parallel: 2\nrspec integration:\n parallel: 2\n" \
"rspec system:\n parallel: 2"
)
@@ -164,12 +169,27 @@ RSpec.describe GenerateRspecPipeline, :silence_stdout, feature_category: :toolin
expect(File.read("#{pipeline_template.path}.yml"))
.to eq(
- "rspec migration:\n parallel: 2\nrspec background_migration:\n" \
+ "rspec migration:\n parallel: 4\nrspec background_migration:\n" \
"rspec unit:\n parallel: 2\nrspec integration:\n" \
"rspec system:\n parallel: 2"
)
end
+ context 'and RSpec files have a high duration' do
+ let(:rspec_files_content) do
+ "spec/migrations/a_spec.rb spec/migrations/b_spec.rb"
+ end
+
+ it 'generates the pipeline config with parallelization based on Knapsack' do
+ subject.generate!
+
+ expect(File.read("#{pipeline_template.path}.yml"))
+ .to eq(
+ "rspec migration:\n parallel: 2"
+ )
+ end
+ end
+
context 'and Knapsack report does not contain valid JSON' do
let(:knapsack_report_content) { "#{super()}," }