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--GITLAB_SHELL_VERSION2
-rw-r--r--app/assets/javascripts/admin/deploy_keys/components/table.vue175
-rw-r--r--app/assets/javascripts/organizations/groups_and_projects/components/app.vue63
-rw-r--r--app/assets/javascripts/organizations/groups_and_projects/components/groups_page.vue9
-rw-r--r--app/assets/javascripts/organizations/groups_and_projects/components/projects_page.vue46
-rw-r--r--app/assets/javascripts/organizations/groups_and_projects/constants.js4
-rw-r--r--app/assets/javascripts/organizations/groups_and_projects/graphql/resolvers.js2
-rw-r--r--app/assets/javascripts/organizations/groups_and_projects/index.js17
-rw-r--r--app/assets/javascripts/organizations/groups_and_projects/utils.js13
-rw-r--r--app/assets/javascripts/pages/groups/work_items/index.js3
-rw-r--r--app/assets/javascripts/projects/components/shared/delete_button.vue148
-rw-r--r--app/assets/javascripts/projects/components/shared/delete_modal.vue163
-rw-r--r--app/assets/javascripts/work_items/list/components/work_items_list_app.vue37
-rw-r--r--app/assets/javascripts/work_items/list/index.js16
-rw-r--r--app/controllers/graphql_controller.rb6
-rw-r--r--app/controllers/projects/tracing_controller.rb4
-rw-r--r--app/finders/deployments_finder.rb3
-rw-r--r--app/graphql/resolvers/concerns/resolves_merge_requests.rb2
-rw-r--r--app/graphql/types/ci/detailed_status_type.rb20
-rw-r--r--app/helpers/projects/observability_helper.rb10
-rw-r--r--app/policies/ci/build_policy.rb11
-rw-r--r--app/views/groups/work_items/index.html.haml2
-rw-r--r--app/views/projects/tracing/show.html.haml5
-rw-r--r--config/feature_flags/development/remove_deployments_api_ref_sort.yml8
-rw-r--r--config/metrics/counts_28d/20210216184454_code_review_total_unique_counts_monthly.yml1
-rw-r--r--config/metrics/counts_28d/20210427103010_code_review_extension_category_monthly_active_users.yml3
-rw-r--r--config/metrics/counts_28d/20210427103119_code_review_group_monthly_active_users.yml1
-rw-r--r--config/metrics/counts_28d/20230725222604_user_neovim_plugin_api_request_monthly.yml25
-rw-r--r--config/metrics/counts_7d/20210216184452_code_review_total_unique_counts_weekly.yml1
-rw-r--r--config/metrics/counts_7d/20210427103328_code_review_group_monthly_active_users.yml1
-rw-r--r--config/metrics/counts_7d/20210427103452_code_review_extension_category_monthly_active_users.yml1
-rw-r--r--config/metrics/counts_7d/20230725222603_user_neovim_plugin_api_request_weekly.yml25
-rw-r--r--config/routes/project.rb2
-rw-r--r--doc/user/clusters/agent/gitops/flux_tutorial.md40
-rw-r--r--lib/api/api.rb4
-rw-r--r--lib/gitlab/usage_data_counters/known_events/code_review_events.yml2
-rw-r--r--lib/gitlab/usage_data_counters/neovim_plugin_activity_unique_counter.rb29
-rw-r--r--locale/gitlab.pot5
-rw-r--r--package.json2
-rw-r--r--qa/qa/page/component/delete_modal.rb2
-rw-r--r--spec/controllers/graphql_controller_spec.rb20
-rw-r--r--spec/features/dashboard/todos/todos_filtering_spec.rb2
-rw-r--r--spec/finders/deployments_finder_spec.rb33
-rw-r--r--spec/frontend/admin/deploy_keys/components/table_spec.js20
-rw-r--r--spec/frontend/organizations/groups_and_projects/components/app_spec.js112
-rw-r--r--spec/frontend/organizations/groups_and_projects/components/projects_page_spec.js88
-rw-r--r--spec/frontend/organizations/groups_and_projects/mock_data.js (renamed from spec/frontend/organizations/groups_and_projects/components/mock_data.js)0
-rw-r--r--spec/frontend/organizations/groups_and_projects/utils_spec.js22
-rw-r--r--spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap97
-rw-r--r--spec/frontend/projects/components/shared/__snapshots__/delete_button_spec.js.snap116
-rw-r--r--spec/frontend/projects/components/shared/delete_button_spec.js114
-rw-r--r--spec/frontend/projects/components/shared/delete_modal_spec.js167
-rw-r--r--spec/frontend/work_items/list/components/work_items_list_app_spec.js29
-rw-r--r--spec/helpers/projects/observability_helper_spec.rb22
-rw-r--r--spec/lib/gitlab/usage_data_counters/neovim_plugin_activity_unique_counter_spec.rb19
-rw-r--r--spec/policies/ci/build_policy_spec.rb25
-rw-r--r--spec/requests/api/graphql/project/merge_requests_spec.rb10
-rw-r--r--spec/requests/projects/tracing_controller_spec.rb60
-rw-r--r--spec/services/ci/retry_pipeline_service_spec.rb16
-rw-r--r--yarn.lock8
60 files changed, 1187 insertions, 706 deletions
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 1e0795f98af..6c3bb88f439 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-14.24.1
+14.23.0
diff --git a/app/assets/javascripts/admin/deploy_keys/components/table.vue b/app/assets/javascripts/admin/deploy_keys/components/table.vue
index 134498af348..ac963a12afb 100644
--- a/app/assets/javascripts/admin/deploy_keys/components/table.vue
+++ b/app/assets/javascripts/admin/deploy_keys/components/table.vue
@@ -1,5 +1,14 @@
<script>
-import { GlTable, GlButton, GlPagination, GlLoadingIcon, GlEmptyState, GlModal } from '@gitlab/ui';
+import {
+ GlCard,
+ GlTable,
+ GlButton,
+ GlPagination,
+ GlIcon,
+ GlLoadingIcon,
+ GlEmptyState,
+ GlModal,
+} from '@gitlab/ui';
import { __ } from '~/locale';
import Api, { DEFAULT_PER_PAGE } from '~/api';
@@ -15,7 +24,7 @@ export default {
newDeployKeyButtonText: __('New deploy key'),
emptyStateTitle: __('No public deploy keys'),
emptyStateDescription: __(
- 'Deploy keys grant read/write access to all repositories in your instance',
+ 'Deploy keys grant read/write access to all repositories in your instance, start by creating a new one above.',
),
delete: __('Delete deploy key'),
edit: __('Edit deploy key'),
@@ -37,10 +46,12 @@ export default {
{
key: 'fingerprint_sha256',
label: __('Fingerprint (SHA256)'),
+ tdClass: 'gl-md-max-w-26',
},
{
key: 'fingerprint',
label: __('Fingerprint (MD5)'),
+ tdClass: 'gl-md-max-w-26',
},
{
key: 'projects',
@@ -75,10 +86,12 @@ export default {
csrf,
DEFAULT_PER_PAGE,
components: {
+ GlCard,
GlTable,
GlButton,
GlPagination,
TimeAgoTooltip,
+ GlIcon,
GlLoadingIcon,
GlEmptyState,
GlModal,
@@ -177,85 +190,105 @@ export default {
</script>
<template>
- <div>
- <div class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-py-5">
- <h4 class="gl-m-0">
- {{ $options.i18n.pageTitle }}
- </h4>
- <gl-button variant="confirm" :href="createPath" data-testid="new-deploy-key-button">{{
- $options.i18n.newDeployKeyButtonText
- }}</gl-button>
- </div>
- <template v-if="shouldShowTable">
- <gl-table
- :busy="loading"
- :items="items"
- :fields="$options.fields"
- stacked="lg"
- data-testid="deploy-keys-list"
- >
- <template #table-busy>
- <gl-loading-icon size="lg" class="gl-my-5" />
- </template>
+ <gl-card
+ class="gl-new-card gl-overflow-hidden"
+ header-class="gl-new-card-header"
+ body-class="gl-new-card-body gl-overflow-hidden gl-px-0"
+ >
+ <template #header>
+ <div class="gl-new-card-title-wrapper">
+ <h3 class="gl-new-card-title">{{ $options.i18n.pageTitle }}</h3>
+ <span class="gl-new-card-count">
+ <gl-icon name="key" class="gl-mr-2" />
+ {{ totalItems }}
+ </span>
+ </div>
+ <div class="gl-new-card-actions">
+ <gl-button size="small" :href="createPath" data-testid="new-deploy-key-button">{{
+ $options.i18n.newDeployKeyButtonText
+ }}</gl-button>
+ </div>
+ </template>
- <template #cell(projects)="{ item: { projects } }">
- <a
- v-for="project in projects"
- :key="project.id"
- :href="projectHref(project)"
- class="gl-display-block"
- >{{ project.name_with_namespace }}</a
- >
- </template>
+ <gl-table
+ v-if="shouldShowTable"
+ :busy="loading"
+ :items="items"
+ :fields="$options.fields"
+ stacked="md"
+ data-testid="deploy-keys-list"
+ class="gl-mt-n1 gl-mb-n2"
+ >
+ <template #table-busy>
+ <gl-loading-icon size="sm" class="gl-my-5" />
+ </template>
- <template #cell(fingerprint_sha256)="{ item: { fingerprint_sha256 } }">
- <span v-if="fingerprint_sha256" class="monospace">{{ fingerprint_sha256 }}</span>
- </template>
+ <template #cell(projects)="{ item: { projects } }">
+ <a
+ v-for="project in projects"
+ :key="project.id"
+ :href="projectHref(project)"
+ class="gl-display-block"
+ >{{ project.name_with_namespace }}</a
+ >
+ </template>
+ <template #cell(fingerprint_sha256)="{ item: { fingerprint_sha256 } }">
+ <div
+ v-if="fingerprint_sha256"
+ class="gl-font-monospace gl-text-truncate"
+ :title="fingerprint_sha256"
+ >
+ {{ fingerprint_sha256 }}
+ </div>
+ </template>
- <template #cell(fingerprint)="{ item: { fingerprint } }">
- <span v-if="fingerprint" class="monospace">{{ fingerprint }}</span>
- </template>
+ <template #cell(fingerprint)="{ item: { fingerprint } }">
+ <div v-if="fingerprint" class="gl-font-monospace gl-text-truncate" :title="fingerprint">
+ {{ fingerprint }}
+ </div>
+ </template>
- <template #cell(created)="{ item: { created } }">
- <time-ago-tooltip :time="created" />
- </template>
+ <template #cell(created)="{ item: { created } }">
+ <time-ago-tooltip :time="created" />
+ </template>
- <template #head(actions)="{ label }">
- <span class="gl-sr-only">{{ label }}</span>
- </template>
+ <template #head(actions)="{ label }">
+ <span class="gl-sr-only">{{ label }}</span>
+ </template>
- <template #cell(actions)="{ item: { id } }">
- <gl-button
- icon="pencil"
- :aria-label="$options.i18n.edit"
- :href="editHref(id)"
- class="gl-mr-2"
- />
- <gl-button
- variant="danger"
- icon="remove"
- :aria-label="$options.i18n.delete"
- @click="handleDeleteClick(id)"
- />
- </template>
- </gl-table>
- <gl-pagination
- v-if="!loading"
- v-model="page"
- :per-page="$options.DEFAULT_PER_PAGE"
- :total-items="totalItems"
- :next-text="$options.i18n.pagination.next"
- :prev-text="$options.i18n.pagination.prev"
- align="center"
- />
- </template>
+ <template #cell(actions)="{ item: { id } }">
+ <gl-button
+ icon="pencil"
+ size="small"
+ :aria-label="$options.i18n.edit"
+ :href="editHref(id)"
+ class="gl-mr-2"
+ />
+ <gl-button
+ variant="danger"
+ category="secondary"
+ icon="remove"
+ size="small"
+ :aria-label="$options.i18n.delete"
+ @click="handleDeleteClick(id)"
+ />
+ </template>
+ </gl-table>
<gl-empty-state
v-else
:svg-path="emptyStateSvgPath"
:title="$options.i18n.emptyStateTitle"
:description="$options.i18n.emptyStateDescription"
- :primary-button-text="$options.i18n.newDeployKeyButtonText"
- :primary-button-link="createPath"
+ />
+ <gl-pagination
+ v-if="!loading"
+ v-model="page"
+ :per-page="$options.DEFAULT_PER_PAGE"
+ :total-items="totalItems"
+ :next-text="$options.i18n.pagination.next"
+ :prev-text="$options.i18n.pagination.prev"
+ align="center"
+ class="gl-mt-5"
/>
<gl-modal
:modal-id="$options.modal.id"
@@ -273,5 +306,5 @@ export default {
</form>
{{ $options.i18n.modal.body }}
</gl-modal>
- </div>
+ </gl-card>
</template>
diff --git a/app/assets/javascripts/organizations/groups_and_projects/components/app.vue b/app/assets/javascripts/organizations/groups_and_projects/components/app.vue
index 2b42c821cd5..43620872cd7 100644
--- a/app/assets/javascripts/organizations/groups_and_projects/components/app.vue
+++ b/app/assets/javascripts/organizations/groups_and_projects/components/app.vue
@@ -1,53 +1,27 @@
<script>
-import { GlLoadingIcon } from '@gitlab/ui';
-import { __, s__ } from '~/locale';
-import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { createAlert } from '~/alert';
-import projectsQuery from '../graphql/queries/projects.query.graphql';
+import { __ } from '~/locale';
+import { DISPLAY_QUERY_GROUPS, DISPLAY_QUERY_PROJECTS } from '../constants';
+import GroupsPage from './groups_page.vue';
+import ProjectsPage from './projects_page.vue';
export default {
i18n: {
pageTitle: __('Groups and projects'),
- errorMessage: s__(
- 'Organization|An error occurred loading the projects. Please refresh the page to try again.',
- ),
- },
- components: {
- ProjectsList,
- GlLoadingIcon,
- },
- data() {
- return {
- projects: [],
- };
- },
- apollo: {
- projects: {
- query: projectsQuery,
- update(data) {
- return data.organization.projects.nodes;
- },
- error(error) {
- createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true });
- },
- },
},
computed: {
- formattedProjects() {
- return this.projects.map(({ id, nameWithNamespace, accessLevel, ...project }) => ({
- ...project,
- id: getIdFromGraphQLId(id),
- name: nameWithNamespace,
- permissions: {
- projectAccess: {
- accessLevel: accessLevel.integerValue,
- },
- },
- }));
- },
- isLoading() {
- return this.$apollo.queries.projects?.loading;
+ routerView() {
+ const { display } = this.$route.query;
+
+ switch (display) {
+ case DISPLAY_QUERY_GROUPS:
+ return GroupsPage;
+
+ case DISPLAY_QUERY_PROJECTS:
+ return ProjectsPage;
+
+ default:
+ return GroupsPage;
+ }
},
},
};
@@ -56,7 +30,6 @@ export default {
<template>
<div>
<h1 class="gl-font-size-h-display">{{ $options.i18n.pageTitle }}</h1>
- <gl-loading-icon v-if="isLoading" class="gl-mt-5" size="md" />
- <projects-list v-else :projects="formattedProjects" show-project-icon />
+ <component :is="routerView" />
</div>
</template>
diff --git a/app/assets/javascripts/organizations/groups_and_projects/components/groups_page.vue b/app/assets/javascripts/organizations/groups_and_projects/components/groups_page.vue
new file mode 100644
index 00000000000..b723cd98ce4
--- /dev/null
+++ b/app/assets/javascripts/organizations/groups_and_projects/components/groups_page.vue
@@ -0,0 +1,9 @@
+<script>
+export default {};
+</script>
+
+<template>
+ <div>
+ <!-- Intentionally empty. Will be implemented in future commits. -->
+ </div>
+</template>
diff --git a/app/assets/javascripts/organizations/groups_and_projects/components/projects_page.vue b/app/assets/javascripts/organizations/groups_and_projects/components/projects_page.vue
new file mode 100644
index 00000000000..d6958ee996e
--- /dev/null
+++ b/app/assets/javascripts/organizations/groups_and_projects/components/projects_page.vue
@@ -0,0 +1,46 @@
+<script>
+import { GlLoadingIcon } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue';
+import { createAlert } from '~/alert';
+import projectsQuery from '../graphql/queries/projects.query.graphql';
+import { formatProjects } from '../utils';
+
+export default {
+ i18n: {
+ errorMessage: s__(
+ 'Organization|An error occurred loading the projects. Please refresh the page to try again.',
+ ),
+ },
+ components: {
+ ProjectsList,
+ GlLoadingIcon,
+ },
+ data() {
+ return {
+ projects: [],
+ };
+ },
+ apollo: {
+ projects: {
+ query: projectsQuery,
+ update(data) {
+ return formatProjects(data.organization.projects.nodes);
+ },
+ error(error) {
+ createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true });
+ },
+ },
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.projects.loading;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-loading-icon v-if="isLoading" class="gl-mt-5" size="md" />
+ <projects-list v-else :projects="projects" show-project-icon />
+</template>
diff --git a/app/assets/javascripts/organizations/groups_and_projects/constants.js b/app/assets/javascripts/organizations/groups_and_projects/constants.js
new file mode 100644
index 00000000000..c816ea27342
--- /dev/null
+++ b/app/assets/javascripts/organizations/groups_and_projects/constants.js
@@ -0,0 +1,4 @@
+export const DISPLAY_QUERY_GROUPS = 'groups';
+export const DISPLAY_QUERY_PROJECTS = 'projects';
+
+export const ORGANIZATION_ROOT_ROUTE_NAME = 'root';
diff --git a/app/assets/javascripts/organizations/groups_and_projects/graphql/resolvers.js b/app/assets/javascripts/organizations/groups_and_projects/graphql/resolvers.js
index 794410c2a78..e3e0529d6d9 100644
--- a/app/assets/javascripts/organizations/groups_and_projects/graphql/resolvers.js
+++ b/app/assets/javascripts/organizations/groups_and_projects/graphql/resolvers.js
@@ -1,4 +1,4 @@
-import { organizationProjects } from 'jest/organizations/groups_and_projects/components/mock_data';
+import { organizationProjects } from 'jest/organizations/groups_and_projects/mock_data';
export default {
Query: {
diff --git a/app/assets/javascripts/organizations/groups_and_projects/index.js b/app/assets/javascripts/organizations/groups_and_projects/index.js
index d0790bcc040..f3f15c635f1 100644
--- a/app/assets/javascripts/organizations/groups_and_projects/index.js
+++ b/app/assets/javascripts/organizations/groups_and_projects/index.js
@@ -1,22 +1,39 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import VueRouter from 'vue-router';
import createDefaultClient from '~/lib/graphql';
import resolvers from './graphql/resolvers';
import App from './components/app.vue';
+import { ORGANIZATION_ROOT_ROUTE_NAME } from './constants';
+
+export const createRouter = () => {
+ const routes = [{ path: '/', name: ORGANIZATION_ROOT_ROUTE_NAME }];
+
+ const router = new VueRouter({
+ routes,
+ base: '/',
+ mode: 'history',
+ });
+
+ return router;
+};
export const initOrganizationsGroupsAndProjects = () => {
const el = document.getElementById('js-organizations-groups-and-projects');
if (!el) return false;
+ Vue.use(VueRouter);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(resolvers),
});
+ const router = createRouter();
return new Vue({
el,
name: 'OrganizationsGroupsAndProjects',
apolloProvider,
+ router,
render(createElement) {
return createElement(App);
},
diff --git a/app/assets/javascripts/organizations/groups_and_projects/utils.js b/app/assets/javascripts/organizations/groups_and_projects/utils.js
new file mode 100644
index 00000000000..853a8543c1b
--- /dev/null
+++ b/app/assets/javascripts/organizations/groups_and_projects/utils.js
@@ -0,0 +1,13 @@
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+
+export const formatProjects = (projects) =>
+ projects.map(({ id, nameWithNamespace, accessLevel, ...project }) => ({
+ ...project,
+ id: getIdFromGraphQLId(id),
+ name: nameWithNamespace,
+ permissions: {
+ projectAccess: {
+ accessLevel: accessLevel.integerValue,
+ },
+ },
+ }));
diff --git a/app/assets/javascripts/pages/groups/work_items/index.js b/app/assets/javascripts/pages/groups/work_items/index.js
new file mode 100644
index 00000000000..a95070b1857
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/work_items/index.js
@@ -0,0 +1,3 @@
+import { mountWorkItemsListApp } from '~/work_items/list';
+
+mountWorkItemsListApp();
diff --git a/app/assets/javascripts/projects/components/shared/delete_button.vue b/app/assets/javascripts/projects/components/shared/delete_button.vue
index 06c0230c8e0..c749034d2a8 100644
--- a/app/assets/javascripts/projects/components/shared/delete_button.vue
+++ b/app/assets/javascripts/projects/components/shared/delete_button.vue
@@ -1,19 +1,14 @@
<script>
-import { GlModal, GlModalDirective, GlFormInput, GlButton, GlAlert, GlSprintf } from '@gitlab/ui';
-import { uniqueId } from 'lodash';
+import { GlButton, GlForm } from '@gitlab/ui';
import csrf from '~/lib/utils/csrf';
import { __ } from '~/locale';
+import DeleteModal from './delete_modal.vue';
export default {
components: {
- GlAlert,
- GlModal,
- GlFormInput,
GlButton,
- GlSprintf,
- },
- directives: {
- GlModal: GlModalDirective,
+ GlForm,
+ DeleteModal,
},
props: {
confirmPhrase: {
@@ -47,139 +42,54 @@ export default {
},
data() {
return {
- userInput: null,
- modalId: uniqueId('delete-project-modal-'),
+ isModalVisible: false,
};
},
computed: {
- confirmDisabled() {
- return this.userInput !== this.confirmPhrase;
- },
csrfToken() {
return csrf.token;
},
- modalActionProps() {
- return {
- primary: {
- text: __('Yes, delete project'),
- attributes: {
- variant: 'danger',
- disabled: this.confirmDisabled,
- 'data-qa-selector': 'confirm_delete_button',
- },
- },
- cancel: {
- text: __('Cancel, keep project'),
- },
- };
- },
},
methods: {
submitForm() {
- this.$refs.form.submit();
+ this.$refs.form.$el.submit();
+ },
+ onButtonClick() {
+ this.isModalVisible = true;
},
},
- strings: {
+ i18n: {
deleteProject: __('Delete project'),
- title: __('Are you absolutely sure?'),
- confirmText: __('Enter the following to confirm:'),
- isForkAlertTitle: __('You are about to delete this forked project containing:'),
- isNotForkAlertTitle: __('You are about to delete this project containing:'),
- isForkAlertBody: __('This process deletes the project repository and all related resources.'),
- isNotForkAlertBody: __(
- 'This project is %{strongStart}NOT%{strongEnd} a fork. This process deletes the project repository and all related resources.',
- ),
- isNotForkMessage: __(
- 'This project is %{strongStart}NOT%{strongEnd} a fork, and has the following:',
- ),
},
};
</script>
<template>
- <form ref="form" :action="formPath" method="post">
+ <gl-form ref="form" :action="formPath" method="post">
<input type="hidden" name="_method" value="delete" />
<input :value="csrfToken" type="hidden" name="authenticity_token" />
+ <delete-modal
+ v-model="isModalVisible"
+ :confirm-phrase="confirmPhrase"
+ :is-fork="isFork"
+ :issues-count="issuesCount"
+ :merge-requests-count="mergeRequestsCount"
+ :forks-count="forksCount"
+ :stars-count="starsCount"
+ @primary="submitForm"
+ >
+ <template #modal-footer>
+ <slot name="modal-footer"></slot>
+ </template>
+ </delete-modal>
+
<gl-button
- v-gl-modal="modalId"
category="primary"
variant="danger"
data-qa-selector="delete_button"
- >{{ $options.strings.deleteProject }}</gl-button
+ @click="onButtonClick"
+ >{{ $options.i18n.deleteProject }}</gl-button
>
-
- <gl-modal
- ref="removeModal"
- :modal-id="modalId"
- ok-variant="danger"
- footer-class="gl-bg-gray-10 gl-p-5"
- title-class="gl-text-red-500"
- :action-primary="modalActionProps.primary"
- :action-cancel="modalActionProps.cancel"
- @ok="submitForm"
- >
- <template #modal-title>{{ $options.strings.title }}</template>
- <div>
- <gl-alert class="gl-mb-5" variant="danger" :dismissible="false">
- <h4 v-if="isFork" data-testid="delete-alert-title" class="gl-alert-title">
- {{ $options.strings.isForkAlertTitle }}
- </h4>
- <h4 v-else data-testid="delete-alert-title" class="gl-alert-title">
- {{ $options.strings.isNotForkAlertTitle }}
- </h4>
- <ul>
- <li>
- <gl-sprintf :message="n__('%d issue', '%d issues', issuesCount)">
- <template #issuesCount>{{ issuesCount }}</template>
- </gl-sprintf>
- </li>
- <li>
- <gl-sprintf
- :message="n__('%d merge requests', '%d merge requests', mergeRequestsCount)"
- >
- <template #mergeRequestsCount>{{ mergeRequestsCount }}</template>
- </gl-sprintf>
- </li>
- <li>
- <gl-sprintf :message="n__('%d fork', '%d forks', forksCount)">
- <template #forksCount>{{ forksCount }}</template>
- </gl-sprintf>
- </li>
- <li>
- <gl-sprintf :message="n__('%d star', '%d stars', starsCount)">
- <template #starsCount>{{ starsCount }}</template>
- </gl-sprintf>
- </li>
- </ul>
- <gl-sprintf
- v-if="isFork"
- data-testid="delete-alert-body"
- :message="$options.strings.isForkAlertBody"
- />
- <gl-sprintf
- v-else
- data-testid="delete-alert-body"
- :message="$options.strings.isNotForkAlertBody"
- >
- <template #strong="{ content }">
- <strong>{{ content }}</strong>
- </template>
- </gl-sprintf>
- </gl-alert>
- <p class="gl-mb-1">{{ $options.strings.confirmText }}</p>
- <p>
- <code class="gl-white-space-pre-wrap">{{ confirmPhrase }}</code>
- </p>
- <gl-form-input
- id="confirm_name_input"
- v-model="userInput"
- name="confirm_name_input"
- type="text"
- data-qa-selector="confirm_name_field"
- />
- <slot name="modal-footer"></slot>
- </div>
- </gl-modal>
- </form>
+ </gl-form>
</template>
diff --git a/app/assets/javascripts/projects/components/shared/delete_modal.vue b/app/assets/javascripts/projects/components/shared/delete_modal.vue
new file mode 100644
index 00000000000..aded11ca92c
--- /dev/null
+++ b/app/assets/javascripts/projects/components/shared/delete_modal.vue
@@ -0,0 +1,163 @@
+<script>
+import { GlModal, GlAlert, GlSprintf, GlFormInput } from '@gitlab/ui';
+import uniqueId from 'lodash/uniqueId';
+import { __ } from '~/locale';
+
+export default {
+ i18n: {
+ deleteProject: __('Delete project'),
+ title: __('Are you absolutely sure?'),
+ confirmText: __('Enter the following to confirm:'),
+ isForkAlertTitle: __('You are about to delete this forked project containing:'),
+ isNotForkAlertTitle: __('You are about to delete this project containing:'),
+ isForkAlertBody: __('This process deletes the project repository and all related resources.'),
+ isNotForkAlertBody: __(
+ 'This project is %{strongStart}NOT%{strongEnd} a fork. This process deletes the project repository and all related resources.',
+ ),
+ isNotForkMessage: __(
+ 'This project is %{strongStart}NOT%{strongEnd} a fork, and has the following:',
+ ),
+ },
+ components: { GlModal, GlAlert, GlSprintf, GlFormInput },
+ model: {
+ prop: 'visible',
+ event: 'change',
+ },
+ props: {
+ visible: {
+ type: Boolean,
+ required: true,
+ },
+ confirmPhrase: {
+ type: String,
+ required: true,
+ },
+ isFork: {
+ type: Boolean,
+ required: true,
+ },
+ issuesCount: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ mergeRequestsCount: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ forksCount: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ starsCount: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ userInput: null,
+ modalId: uniqueId('delete-project-modal-'),
+ };
+ },
+ computed: {
+ confirmDisabled() {
+ return this.userInput !== this.confirmPhrase;
+ },
+ modalActionProps() {
+ return {
+ primary: {
+ text: __('Yes, delete project'),
+ attributes: {
+ variant: 'danger',
+ disabled: this.confirmDisabled,
+ 'data-qa-selector': 'confirm_delete_button',
+ },
+ },
+ cancel: {
+ text: __('Cancel, keep project'),
+ },
+ };
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-modal
+ :visible="visible"
+ :modal-id="modalId"
+ footer-class="gl-bg-gray-10 gl-p-5"
+ title-class="gl-text-red-500"
+ :action-primary="modalActionProps.primary"
+ :action-cancel="modalActionProps.cancel"
+ @primary="$emit('primary', $event)"
+ @change="$emit('change', $event)"
+ >
+ <template #modal-title>{{ $options.i18n.title }}</template>
+ <div>
+ <gl-alert class="gl-mb-5" variant="danger" :dismissible="false">
+ <h4 v-if="isFork" data-testid="delete-alert-title" class="gl-alert-title">
+ {{ $options.i18n.isForkAlertTitle }}
+ </h4>
+ <h4 v-else data-testid="delete-alert-title" class="gl-alert-title">
+ {{ $options.i18n.isNotForkAlertTitle }}
+ </h4>
+ <ul>
+ <li v-if="issuesCount !== null">
+ <gl-sprintf :message="n__('%d issue', '%d issues', issuesCount)">
+ <template #issuesCount>{{ issuesCount }}</template>
+ </gl-sprintf>
+ </li>
+ <li v-if="mergeRequestsCount !== null">
+ <gl-sprintf
+ :message="n__('%d merge requests', '%d merge requests', mergeRequestsCount)"
+ >
+ <template #mergeRequestsCount>{{ mergeRequestsCount }}</template>
+ </gl-sprintf>
+ </li>
+ <li v-if="forksCount !== null">
+ <gl-sprintf :message="n__('%d fork', '%d forks', forksCount)">
+ <template #forksCount>{{ forksCount }}</template>
+ </gl-sprintf>
+ </li>
+ <li v-if="starsCount !== null">
+ <gl-sprintf :message="n__('%d star', '%d stars', starsCount)">
+ <template #starsCount>{{ starsCount }}</template>
+ </gl-sprintf>
+ </li>
+ </ul>
+ <gl-sprintf
+ v-if="isFork"
+ data-testid="delete-alert-body"
+ :message="$options.i18n.isForkAlertBody"
+ />
+ <gl-sprintf
+ v-else
+ data-testid="delete-alert-body"
+ :message="$options.i18n.isNotForkAlertBody"
+ >
+ <template #strong="{ content }">
+ <strong>{{ content }}</strong>
+ </template>
+ </gl-sprintf>
+ </gl-alert>
+ <p class="gl-mb-1">{{ $options.i18n.confirmText }}</p>
+ <p>
+ <code class="gl-white-space-pre-wrap">{{ confirmPhrase }}</code>
+ </p>
+
+ <gl-form-input
+ id="confirm_name_input"
+ v-model="userInput"
+ name="confirm_name_input"
+ type="text"
+ data-qa-selector="confirm_name_field"
+ />
+ <slot name="modal-footer"></slot>
+ </div>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/work_items/list/components/work_items_list_app.vue b/app/assets/javascripts/work_items/list/components/work_items_list_app.vue
new file mode 100644
index 00000000000..4180d484357
--- /dev/null
+++ b/app/assets/javascripts/work_items/list/components/work_items_list_app.vue
@@ -0,0 +1,37 @@
+<script>
+import { STATUS_OPEN } from '~/issues/constants';
+import { __ } from '~/locale';
+import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
+import { issuableListTabs } from '~/vue_shared/issuable/list/constants';
+
+export default {
+ i18n: {
+ searchPlaceholder: __('Search or filter results...'),
+ },
+ issuableListTabs,
+ components: {
+ IssuableList,
+ },
+ data() {
+ return {
+ issues: [],
+ searchTokens: [],
+ sortOptions: [],
+ state: STATUS_OPEN,
+ };
+ },
+};
+</script>
+
+<template>
+ <issuable-list
+ :current-tab="state"
+ :issuables="issues"
+ namespace="work-items"
+ recent-searches-storage-key="issues"
+ :search-input-placeholder="$options.i18n.searchPlaceholder"
+ :search-tokens="searchTokens"
+ :sort-options="sortOptions"
+ :tabs="$options.issuableListTabs"
+ />
+</template>
diff --git a/app/assets/javascripts/work_items/list/index.js b/app/assets/javascripts/work_items/list/index.js
new file mode 100644
index 00000000000..5b701893471
--- /dev/null
+++ b/app/assets/javascripts/work_items/list/index.js
@@ -0,0 +1,16 @@
+import Vue from 'vue';
+import WorkItemsListApp from '~/work_items/list/components/work_items_list_app.vue';
+
+export const mountWorkItemsListApp = () => {
+ const el = document.querySelector('.js-work-items-list-root');
+
+ if (!el) {
+ return null;
+ }
+
+ return new Vue({
+ el,
+ name: 'WorkItemsListRoot',
+ render: (createComponent) => createComponent(WorkItemsListApp),
+ });
+};
diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb
index e6e8cfc4020..29bc48f93e9 100644
--- a/app/controllers/graphql_controller.rb
+++ b/app/controllers/graphql_controller.rb
@@ -39,6 +39,7 @@ class GraphqlController < ApplicationController
before_action :track_jetbrains_bundled_usage
before_action :track_gitlab_cli_usage
before_action :track_visual_studio_usage
+ before_action :track_neovim_plugin_usage
before_action :disable_query_limiting
before_action :limit_query_size
@@ -190,6 +191,11 @@ class GraphqlController < ApplicationController
.track_api_request_when_trackable(user_agent: request.user_agent, user: current_user)
end
+ def track_neovim_plugin_usage
+ Gitlab::UsageDataCounters::NeovimPluginActivityUniqueCounter
+ .track_api_request_when_trackable(user_agent: request.user_agent, user: current_user)
+ end
+
def track_gitlab_cli_usage
Gitlab::UsageDataCounters::GitLabCliActivityUniqueCounter
.track_api_request_when_trackable(user_agent: request.user_agent, user: current_user)
diff --git a/app/controllers/projects/tracing_controller.rb b/app/controllers/projects/tracing_controller.rb
index d1218ebf344..45e773bf62b 100644
--- a/app/controllers/projects/tracing_controller.rb
+++ b/app/controllers/projects/tracing_controller.rb
@@ -10,6 +10,10 @@ module Projects
def index; end
+ def show
+ @trace_id = params[:id]
+ end
+
private
def check_tracing_enabled
diff --git a/app/finders/deployments_finder.rb b/app/finders/deployments_finder.rb
index 800158dfd0a..9881cb3fc74 100644
--- a/app/finders/deployments_finder.rb
+++ b/app/finders/deployments_finder.rb
@@ -25,7 +25,7 @@ class DeploymentsFinder
# performant with the other filtering/sorting parameters.
# The composed query could be significantly slower when the filtering and sorting columns are different.
# See https://gitlab.com/gitlab-org/gitlab/-/issues/325627 for example.
- ALLOWED_SORT_VALUES = %w[id iid created_at updated_at ref finished_at].freeze
+ ALLOWED_SORT_VALUES = %w[id iid created_at updated_at finished_at].freeze
DEFAULT_SORT_VALUE = 'id'
ALLOWED_SORT_DIRECTIONS = %w[asc desc].freeze
@@ -128,7 +128,6 @@ class DeploymentsFinder
def build_sort_params
order_by = ALLOWED_SORT_VALUES.include?(params[:order_by]) ? params[:order_by] : DEFAULT_SORT_VALUE
- order_by = DEFAULT_SORT_VALUE if order_by == 'ref' && Feature.enabled?(:remove_deployments_api_ref_sort)
order_direction = ALLOWED_SORT_DIRECTIONS.include?(params[:sort]) ? params[:sort] : DEFAULT_SORT_DIRECTION
{ order_by => order_direction }
diff --git a/app/graphql/resolvers/concerns/resolves_merge_requests.rb b/app/graphql/resolvers/concerns/resolves_merge_requests.rb
index 1470129187d..e9e7ea9f77f 100644
--- a/app/graphql/resolvers/concerns/resolves_merge_requests.rb
+++ b/app/graphql/resolvers/concerns/resolves_merge_requests.rb
@@ -60,7 +60,7 @@ module ResolvesMergeRequests
pipelines: [:merge_request_diffs], # used by `recent_diff_head_shas` to load pipelines
committers: [merge_request_diff: [:merge_request_diff_commits]],
suggested_reviewers: [:predictions],
- diff_stats: [:latest_merge_request_diff]
+ diff_stats: [latest_merge_request_diff: [:merge_request_diff_commits]]
}
end
end
diff --git a/app/graphql/types/ci/detailed_status_type.rb b/app/graphql/types/ci/detailed_status_type.rb
index 8bc50e974bb..e18770c2708 100644
--- a/app/graphql/types/ci/detailed_status_type.rb
+++ b/app/graphql/types/ci/detailed_status_type.rb
@@ -39,17 +39,15 @@ module Types
end
def action
- if object.has_action?
- {
- button_title: object.action_button_title,
- icon: object.action_icon,
- method: object.action_method,
- path: object.action_path,
- title: object.action_title
- }
- else
- nil
- end
+ return unless object.has_action?
+
+ {
+ button_title: object.action_button_title,
+ icon: object.action_icon,
+ method: object.action_method,
+ path: object.action_path,
+ title: object.action_title
+ }
end
end
# rubocop: enable Graphql/AuthorizeTypes
diff --git a/app/helpers/projects/observability_helper.rb b/app/helpers/projects/observability_helper.rb
index 24bc1928a36..4515fdb1bc3 100644
--- a/app/helpers/projects/observability_helper.rb
+++ b/app/helpers/projects/observability_helper.rb
@@ -9,5 +9,15 @@ module Projects
oauthUrl: Gitlab::Observability.oauth_url
})
end
+
+ def observability_tracing_details_model(project, trace_id)
+ Gitlab::Json.generate({
+ tracingIndexUrl: namespace_project_tracing_index_path(project.group, project),
+ traceId: trace_id,
+ tracingUrl: Gitlab::Observability.tracing_url(project),
+ provisioningUrl: Gitlab::Observability.provisioning_url(project),
+ oauthUrl: Gitlab::Observability.oauth_url
+ })
+ end
end
end
diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb
index 73e4cbee54a..0f551234efe 100644
--- a/app/policies/ci/build_policy.rb
+++ b/app/policies/ci/build_policy.rb
@@ -73,17 +73,22 @@ module Ci
# Use admin_ci_minutes for detailed quota and usage reporting
# this is limited to total usage and total quota for a builds namespace
- rule { can_read_project_build }.enable :read_ci_minutes_limited_summary
+ rule { can_read_project_build }.policy do
+ enable :read_ci_minutes_limited_summary
+ enable :read_build_trace
+ end
- rule { can_read_project_build }.enable :read_build_trace
rule { debug_mode & ~project_update_build }.prevent :read_build_trace
# Authorizing the user to access to protected entities.
# There is a "jailbreak" mode to exceptionally bypass the authorization,
# however, you should NEVER allow it, rather suspect it's a wrong feature/product design.
rule { ~can?(:jailbreak) & (archived | (protected_ref & ~admin) | protected_environment) }.policy do
- prevent :update_build
prevent :update_commit_status
+ end
+
+ rule { ~can?(:jailbreak) & (archived | protected_ref | protected_environment) }.policy do
+ prevent :update_build
prevent :erase_build
end
diff --git a/app/views/groups/work_items/index.html.haml b/app/views/groups/work_items/index.html.haml
index 9aa90b913a2..1d508289b21 100644
--- a/app/views/groups/work_items/index.html.haml
+++ b/app/views/groups/work_items/index.html.haml
@@ -1 +1,3 @@
- page_title s_('WorkItem|Work items')
+
+.js-work-items-list-root
diff --git a/app/views/projects/tracing/show.html.haml b/app/views/projects/tracing/show.html.haml
new file mode 100644
index 00000000000..4ba316a0b5c
--- /dev/null
+++ b/app/views/projects/tracing/show.html.haml
@@ -0,0 +1,5 @@
+- page_title _('Trace Details')
+- add_to_breadcrumbs _('Tracing'), project_tracing_index_path(@project)
+
+#js-tracing-details{ data: { view_model: observability_tracing_details_model(@project, @trace_id) } }
+
diff --git a/config/feature_flags/development/remove_deployments_api_ref_sort.yml b/config/feature_flags/development/remove_deployments_api_ref_sort.yml
deleted file mode 100644
index 584012ba2bf..00000000000
--- a/config/feature_flags/development/remove_deployments_api_ref_sort.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: remove_deployments_api_ref_sort
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/124229
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/416305
-milestone: '16.2'
-type: development
-group: group::environments
-default_enabled: true
diff --git a/config/metrics/counts_28d/20210216184454_code_review_total_unique_counts_monthly.yml b/config/metrics/counts_28d/20210216184454_code_review_total_unique_counts_monthly.yml
index 0b75e7faa13..8ad0494e775 100644
--- a/config/metrics/counts_28d/20210216184454_code_review_total_unique_counts_monthly.yml
+++ b/config/metrics/counts_28d/20210216184454_code_review_total_unique_counts_monthly.yml
@@ -109,6 +109,7 @@ options:
- i_code_review_user_jetbrains_api_request
- i_editor_extensions_user_jetbrains_bundled_api_request
- i_editor_extensions_user_visual_studio_api_request
+ - i_editor_extensions_user_neovim_plugin_api_request
- i_code_review_user_labels_changed
- i_code_review_user_load_conflict_ui
- i_code_review_user_marked_as_draft
diff --git a/config/metrics/counts_28d/20210427103010_code_review_extension_category_monthly_active_users.yml b/config/metrics/counts_28d/20210427103010_code_review_extension_category_monthly_active_users.yml
index 1a30a8558f3..f61e065033a 100644
--- a/config/metrics/counts_28d/20210427103010_code_review_extension_category_monthly_active_users.yml
+++ b/config/metrics/counts_28d/20210427103010_code_review_extension_category_monthly_active_users.yml
@@ -1,7 +1,7 @@
---
data_category: optional
key_path: counts_monthly.aggregated_metrics.code_review_extension_category_monthly_active_users
-description: Number of users performing i_code_review_user_vs_code_api_request event
+description: Number of users performing api requests with editor extensions
product_section: dev
product_stage: create
product_group: code_review
@@ -22,6 +22,7 @@ options:
- 'i_editor_extensions_user_jetbrains_bundled_api_request'
- 'i_code_review_user_gitlab_cli_api_request'
- 'i_editor_extensions_user_visual_studio_api_request'
+ - 'i_editor_extensions_user_neovim_plugin_api_request'
distribution:
- ce
- ee
diff --git a/config/metrics/counts_28d/20210427103119_code_review_group_monthly_active_users.yml b/config/metrics/counts_28d/20210427103119_code_review_group_monthly_active_users.yml
index 0fab23fc035..9140ab4cc5f 100644
--- a/config/metrics/counts_28d/20210427103119_code_review_group_monthly_active_users.yml
+++ b/config/metrics/counts_28d/20210427103119_code_review_group_monthly_active_users.yml
@@ -89,6 +89,7 @@ options:
- 'i_code_review_user_jetbrains_api_request'
- 'i_editor_extensions_user_jetbrains_bundled_api_request'
- 'i_editor_extensions_user_visual_studio_api_request'
+ - 'i_editor_extensions_user_neovim_plugin_api_request'
- 'i_code_review_user_gitlab_cli_api_request'
- 'i_code_review_user_create_note_in_ipynb_diff'
- 'i_code_review_user_create_note_in_ipynb_diff_mr'
diff --git a/config/metrics/counts_28d/20230725222604_user_neovim_plugin_api_request_monthly.yml b/config/metrics/counts_28d/20230725222604_user_neovim_plugin_api_request_monthly.yml
new file mode 100644
index 00000000000..0f2048f6b0b
--- /dev/null
+++ b/config/metrics/counts_28d/20230725222604_user_neovim_plugin_api_request_monthly.yml
@@ -0,0 +1,25 @@
+---
+key_path: redis_hll_counters.editor_extensions.user_neovim_plugin_api_request_monthly
+description: Count of unique users per month who use the GitLab plugin for Neovim
+product_section: dev
+product_stage: create
+product_group: code_review
+value_type: number
+status: active
+milestone: '16.3'
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/127561
+time_frame: 28d
+data_source: redis_hll
+data_category: optional
+instrumentation_class: RedisHLLMetric
+options:
+ events:
+ - i_editor_extensions_user_neovim_plugin_api_request
+performance_indicator_type: []
+distribution:
+ - ce
+ - ee
+tier:
+ - free
+ - premium
+ - ultimate
diff --git a/config/metrics/counts_7d/20210216184452_code_review_total_unique_counts_weekly.yml b/config/metrics/counts_7d/20210216184452_code_review_total_unique_counts_weekly.yml
index 1dd12324481..e8670a1fe1e 100644
--- a/config/metrics/counts_7d/20210216184452_code_review_total_unique_counts_weekly.yml
+++ b/config/metrics/counts_7d/20210216184452_code_review_total_unique_counts_weekly.yml
@@ -109,6 +109,7 @@ options:
- i_code_review_user_jetbrains_api_request
- i_editor_extensions_user_jetbrains_bundled_api_request
- i_editor_extensions_user_visual_studio_api_request
+ - i_editor_extensions_user_neovim_plugin_api_request
- i_code_review_user_labels_changed
- i_code_review_user_load_conflict_ui
- i_code_review_user_marked_as_draft
diff --git a/config/metrics/counts_7d/20210427103328_code_review_group_monthly_active_users.yml b/config/metrics/counts_7d/20210427103328_code_review_group_monthly_active_users.yml
index 2f9ed569234..7e209470ac9 100644
--- a/config/metrics/counts_7d/20210427103328_code_review_group_monthly_active_users.yml
+++ b/config/metrics/counts_7d/20210427103328_code_review_group_monthly_active_users.yml
@@ -87,6 +87,7 @@ options:
- 'i_code_review_user_jetbrains_api_request'
- 'i_editor_extensions_user_jetbrains_bundled_api_request'
- 'i_editor_extensions_user_visual_studio_api_request'
+ - 'i_editor_extensions_user_neovim_plugin_api_request'
- 'i_code_review_user_gitlab_cli_api_request'
- 'i_code_review_user_create_note_in_ipynb_diff'
- 'i_code_review_user_create_note_in_ipynb_diff_mr'
diff --git a/config/metrics/counts_7d/20210427103452_code_review_extension_category_monthly_active_users.yml b/config/metrics/counts_7d/20210427103452_code_review_extension_category_monthly_active_users.yml
index 1b27089b9cd..7b1d58caeca 100644
--- a/config/metrics/counts_7d/20210427103452_code_review_extension_category_monthly_active_users.yml
+++ b/config/metrics/counts_7d/20210427103452_code_review_extension_category_monthly_active_users.yml
@@ -22,6 +22,7 @@ options:
- 'i_editor_extensions_user_jetbrains_bundled_api_request'
- 'i_code_review_user_gitlab_cli_api_request'
- 'i_editor_extensions_user_visual_studio_api_request'
+ - 'i_editor_extensions_user_neovim_plugin_api_request'
distribution:
- ce
- ee
diff --git a/config/metrics/counts_7d/20230725222603_user_neovim_plugin_api_request_weekly.yml b/config/metrics/counts_7d/20230725222603_user_neovim_plugin_api_request_weekly.yml
new file mode 100644
index 00000000000..abe7720152d
--- /dev/null
+++ b/config/metrics/counts_7d/20230725222603_user_neovim_plugin_api_request_weekly.yml
@@ -0,0 +1,25 @@
+---
+key_path: redis_hll_counters.editor_extensions.user_neovim_plugin_api_request_weekly
+description: Count of unique users per week who use the GitLab plugin for Neovim
+product_section: dev
+product_stage: create
+product_group: code_review
+value_type: number
+status: active
+milestone: '16.3'
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/127561
+time_frame: 7d
+data_source: redis_hll
+data_category: optional
+instrumentation_class: RedisHLLMetric
+options:
+ events:
+ - i_editor_extensions_user_neovim_plugin_api_request
+performance_indicator_type: []
+distribution:
+ - ce
+ - ee
+tier:
+ - free
+ - premium
+ - ultimate
diff --git a/config/routes/project.rb b/config/routes/project.rb
index d0d7442857b..5831d5d331d 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -393,7 +393,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
- resources :tracing, only: [:index], controller: :tracing
+ resources :tracing, only: [:index, :show], controller: :tracing
namespace :design_management do
namespace :designs, path: 'designs/:design_id(/:sha)', constraints: -> (params) { params[:sha].nil? || Gitlab::Git.commit_id?(params[:sha]) } do
diff --git a/doc/user/clusters/agent/gitops/flux_tutorial.md b/doc/user/clusters/agent/gitops/flux_tutorial.md
index 8aee0c01d65..e6e5f2d764c 100644
--- a/doc/user/clusters/agent/gitops/flux_tutorial.md
+++ b/doc/user/clusters/agent/gitops/flux_tutorial.md
@@ -83,8 +83,19 @@ You must register `agentk` before you install it in your cluster.
To register `agentk`:
-- Complete the steps in [Register the agent with GitLab](../install/index.md#register-the-agent-with-gitlab).
- Be sure to save the agent registration token and `kas` address.
+1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project.
+ If you have an [agent configuration file](../install/index.md#create-an-agent-configuration-file),
+ it must be in this project. Your cluster manifest files should also be in this project.
+1. Select **Operate > Kubernetes clusters**.
+1. Select **Connect a cluster (agent)**.
+ - If you want to create a configuration with CI/CD defaults, type a name.
+ - If you already have an agent configuration file, select it from the list.
+1. Select **Register an agent**.
+1. Securely store the agent access token and `kasAddress` for later.
+
+The agent is registered for your project. You don't need to run any commands yet.
+
+In the next step, you'll use Flux to install `agentk` in your cluster.
## Install `agentk`
@@ -103,10 +114,24 @@ To install `agentk`:
name: gitlab
```
-1. Apply the agent registration token as a secret in the cluster:
+1. Create a file called `secret.yaml` that contains your agent access token as a secret:
+
+ ```yaml
+ apiVersion: v1
+ kind: Secret
+ metadata:
+ name: gitlab-agent-token
+ type: Opaque
+ stringData:
+ values.yaml: |-
+ config:
+ token: "<your-token-here>"
+ ```
+
+1. Apply `secret.yaml` to your cluster:
```shell
- kubectl create secret generic gitlab-agent-token -n gitlab --from-literal=token=YOUR-TOKEN-HERE
+ kubectl apply -f secret.yaml -n gitlab
```
Although this step does not follow GitOps principles, it simplifies configuration for new Flux users.
@@ -147,8 +172,11 @@ To install `agentk`:
interval: 1h0m0s
values:
config:
- kasAddress: "wss://kas.gitlab.example.com"
- secretName: "gitlab-agent-token"
+ kasAddress: "wss://kas.gitlab.com"
+ valuesFrom:
+ - kind: Secret
+ name: gitlab-agent-token
+ valuesKey: values.yaml
```
1. To verify that `agentk` is installed and running in the cluster, run the following command:
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 6e57174c1dd..2506680fe35 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -99,6 +99,10 @@ module API
end
after do
+ Gitlab::UsageDataCounters::NeovimPluginActivityUniqueCounter.track_api_request_when_trackable(user_agent: request&.user_agent, user: @current_user)
+ end
+
+ after do
Gitlab::UsageDataCounters::GitLabCliActivityUniqueCounter.track_api_request_when_trackable(user_agent: request&.user_agent, user: @current_user)
end
diff --git a/lib/gitlab/usage_data_counters/known_events/code_review_events.yml b/lib/gitlab/usage_data_counters/known_events/code_review_events.yml
index 342446e3e28..b51a991df67 100644
--- a/lib/gitlab/usage_data_counters/known_events/code_review_events.yml
+++ b/lib/gitlab/usage_data_counters/known_events/code_review_events.yml
@@ -83,6 +83,8 @@
aggregation: weekly
- name: i_editor_extensions_user_visual_studio_api_request
aggregation: weekly
+- name: i_editor_extensions_user_neovim_plugin_api_request
+ aggregation: weekly
- name: i_code_review_user_gitlab_cli_api_request
aggregation: weekly
- name: i_code_review_user_create_mr_from_issue
diff --git a/lib/gitlab/usage_data_counters/neovim_plugin_activity_unique_counter.rb b/lib/gitlab/usage_data_counters/neovim_plugin_activity_unique_counter.rb
new file mode 100644
index 00000000000..7cf89a96e5d
--- /dev/null
+++ b/lib/gitlab/usage_data_counters/neovim_plugin_activity_unique_counter.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module UsageDataCounters
+ module NeovimPluginActivityUniqueCounter
+ NEOVIM_PLUGIN_API_REQUEST_ACTION = 'i_editor_extensions_user_neovim_plugin_api_request'
+ NEOVIM_PLUGIN_USER_AGENT_REGEX = /gitlab.vim/
+
+ class << self
+ def track_api_request_when_trackable(user_agent:, user:)
+ user_agent&.match?(NEOVIM_PLUGIN_USER_AGENT_REGEX) &&
+ track_unique_action_by_user(NEOVIM_PLUGIN_API_REQUEST_ACTION, user)
+ end
+
+ private
+
+ def track_unique_action_by_user(action, user)
+ return unless user
+
+ track_unique_action(action, user.id)
+ end
+
+ def track_unique_action(action, value)
+ Gitlab::UsageDataCounters::HLLRedisCounter.track_usage_event(action, value)
+ end
+ end
+ end
+ end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 2e597b2901b..513942e34a6 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -15660,7 +15660,7 @@ msgstr ""
msgid "Deploy keys"
msgstr ""
-msgid "Deploy keys grant read/write access to all repositories in your instance"
+msgid "Deploy keys grant read/write access to all repositories in your instance, start by creating a new one above."
msgstr ""
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
@@ -48902,6 +48902,9 @@ msgstr ""
msgid "TotalRefCountIndicator|1000+"
msgstr ""
+msgid "Trace Details"
+msgstr ""
+
msgid "Traces"
msgstr ""
diff --git a/package.json b/package.json
index fa6ce046851..b6ab860198e 100644
--- a/package.json
+++ b/package.json
@@ -124,7 +124,7 @@
"clipboard": "^2.0.8",
"compression-webpack-plugin": "^5.0.2",
"copy-webpack-plugin": "^6.4.1",
- "core-js": "^3.31.0",
+ "core-js": "^3.31.1",
"cron-validator": "^1.1.1",
"cronstrue": "^1.122.0",
"cropper": "^2.3.0",
diff --git a/qa/qa/page/component/delete_modal.rb b/qa/qa/page/component/delete_modal.rb
index 18bb2b1bb1b..9fbbd9930a9 100644
--- a/qa/qa/page/component/delete_modal.rb
+++ b/qa/qa/page/component/delete_modal.rb
@@ -9,7 +9,7 @@ module QA
def self.included(base)
super
- base.view 'app/assets/javascripts/projects/components/shared/delete_button.vue' do
+ base.view 'app/assets/javascripts/projects/components/shared/delete_modal.vue' do
element :confirm_name_field
element :confirm_delete_button
end
diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb
index 6a09c5cb823..8fcbf4049a5 100644
--- a/spec/controllers/graphql_controller_spec.rb
+++ b/spec/controllers/graphql_controller_spec.rb
@@ -244,6 +244,16 @@ RSpec.describe GraphqlController, feature_category: :integrations do
post :execute
end
+ it 'calls the track neovim plugin api when trackable method' do
+ agent = 'code-completions-language-server-experiment (Neovim:0.9.0; gitlab.vim (v0.1.0); arch:amd64; os:darwin)'
+ request.env['HTTP_USER_AGENT'] = agent
+
+ expect(Gitlab::UsageDataCounters::NeovimPluginActivityUniqueCounter)
+ .to receive(:track_api_request_when_trackable).with(user_agent: agent, user: user)
+
+ post :execute
+ end
+
context 'if using the GitLab CLI' do
it 'call trackable for the old UserAgent' do
agent = 'GLab - GitLab CLI'
@@ -399,6 +409,16 @@ RSpec.describe GraphqlController, feature_category: :integrations do
subject
end
+ it 'calls the track neovim plugin api when trackable method' do
+ agent = 'code-completions-language-server-experiment (Neovim:0.9.0; gitlab.vim (v0.1.0); arch:amd64; os:darwin)'
+ request.env['HTTP_USER_AGENT'] = agent
+
+ expect(Gitlab::UsageDataCounters::NeovimPluginActivityUniqueCounter)
+ .to receive(:track_api_request_when_trackable).with(user_agent: agent, user: user)
+
+ subject
+ end
+
it 'calls the track gitlab cli when trackable method' do
agent = 'GLab - GitLab CLI'
request.env['HTTP_USER_AGENT'] = agent
diff --git a/spec/features/dashboard/todos/todos_filtering_spec.rb b/spec/features/dashboard/todos/todos_filtering_spec.rb
index ea8c7e800c5..990b2f18120 100644
--- a/spec/features/dashboard/todos/todos_filtering_spec.rb
+++ b/spec/features/dashboard/todos/todos_filtering_spec.rb
@@ -130,7 +130,7 @@ RSpec.describe 'Dashboard > User filters todos', :js, feature_category: :team_pl
before do
create(:todo, :build_failed, user: user_1, author: user_2, project: project_1, target: merge_request)
create(:todo, :marked, user: user_1, author: user_2, project: project_1, target: issue1)
- create(:todo, :review_requested, user: user_1, author: user_2, project: project_1, target: issue1)
+ create(:todo, :review_requested, user: user_1, author: user_2, project: project_1, target: merge_request)
end
it 'filters by Assigned' do
diff --git a/spec/finders/deployments_finder_spec.rb b/spec/finders/deployments_finder_spec.rb
index 65003ea97ef..5a803ee2a0d 100644
--- a/spec/finders/deployments_finder_spec.rb
+++ b/spec/finders/deployments_finder_spec.rb
@@ -185,39 +185,6 @@ RSpec.describe DeploymentsFinder, feature_category: :deployment_management do
end
end
end
-
- context 'when remove_deployments_api_ref_sort is disabled' do
- before do
- stub_feature_flags(remove_deployments_api_ref_sort: false)
- end
-
- where(:order_by, :sort, :ordered_deployments) do
- 'created_at' | 'asc' | [:deployment_1, :deployment_2, :deployment_3]
- 'created_at' | 'desc' | [:deployment_3, :deployment_2, :deployment_1]
- 'id' | 'asc' | [:deployment_1, :deployment_2, :deployment_3]
- 'id' | 'desc' | [:deployment_3, :deployment_2, :deployment_1]
- 'iid' | 'asc' | [:deployment_1, :deployment_2, :deployment_3]
- 'iid' | 'desc' | [:deployment_3, :deployment_2, :deployment_1]
- 'ref' | 'asc' | [:deployment_2, :deployment_1, :deployment_3] # ref sorts when remove_deployments_api_ref_sort feature flag is disabled
- 'ref' | 'desc' | [:deployment_3, :deployment_1, :deployment_2] # ref sorts when remove_deployments_api_ref_sort feature flag is disabled
- 'updated_at' | 'asc' | [:deployment_2, :deployment_3, :deployment_1]
- 'updated_at' | 'desc' | [:deployment_1, :deployment_3, :deployment_2]
- 'finished_at' | 'asc' | described_class::InefficientQueryError
- 'finished_at' | 'desc' | described_class::InefficientQueryError
- 'invalid' | 'asc' | [:deployment_1, :deployment_2, :deployment_3]
- 'iid' | 'err' | [:deployment_1, :deployment_2, :deployment_3]
- end
-
- with_them do
- it 'returns the deployments ordered' do
- if ordered_deployments == described_class::InefficientQueryError
- expect { subject }.to raise_error(described_class::InefficientQueryError)
- else
- expect(subject).to eq(ordered_deployments.map { |name| public_send(name) })
- end
- end
- end
- end
end
describe 'transform `created_at` sorting to `id` sorting' do
diff --git a/spec/frontend/admin/deploy_keys/components/table_spec.js b/spec/frontend/admin/deploy_keys/components/table_spec.js
index a05654a1d25..07d0f045509 100644
--- a/spec/frontend/admin/deploy_keys/components/table_spec.js
+++ b/spec/frontend/admin/deploy_keys/components/table_spec.js
@@ -1,5 +1,5 @@
import { merge } from 'lodash';
-import { GlLoadingIcon, GlEmptyState, GlPagination, GlModal } from '@gitlab/ui';
+import { GlCard, GlLoadingIcon, GlEmptyState, GlPagination, GlModal } from '@gitlab/ui';
import { nextTick } from 'vue';
import responseBody from 'test_fixtures/api/deploy_keys/index.json';
@@ -45,6 +45,8 @@ describe('DeployKeysTable', () => {
});
};
+ const findCard = () => wrapper.findComponent(GlCard);
+ const findCardTitle = () => findCard().find('.gl-new-card-title-wrapper');
const findEditButton = (index) =>
wrapper.findAllByLabelText(DeployKeysTable.i18n.edit, { selector: 'a' }).at(index);
const findRemoveButton = (index) =>
@@ -60,7 +62,7 @@ describe('DeployKeysTable', () => {
expect(wrapper.findByText(expectedDeployKey.title).exists()).toBe(true);
expect(
- wrapper.findByText(expectedDeployKey.fingerprint_sha256, { selector: 'span' }).exists(),
+ wrapper.findByText(expectedDeployKey.fingerprint_sha256, { selector: 'div' }).exists(),
).toBe(true);
expect(timeAgoTooltip.exists()).toBe(true);
expect(timeAgoTooltip.props('time')).toBe(expectedDeployKey.created_at);
@@ -70,7 +72,7 @@ describe('DeployKeysTable', () => {
};
const expectDeployKeyWithFingerprintIsRendered = (expectedDeployKey, expectedRowIndex) => {
- expect(wrapper.findByText(expectedDeployKey.fingerprint, { selector: 'span' }).exists()).toBe(
+ expect(wrapper.findByText(expectedDeployKey.fingerprint, { selector: 'div' }).exists()).toBe(
true,
);
expectDeployKeyIsRendered(expectedDeployKey, expectedRowIndex);
@@ -85,8 +87,6 @@ describe('DeployKeysTable', () => {
svgPath: defaultProvide.emptyStateSvgPath,
title: DeployKeysTable.i18n.emptyStateTitle,
description: DeployKeysTable.i18n.emptyStateDescription,
- primaryButtonText: DeployKeysTable.i18n.newDeployKeyButtonText,
- primaryButtonLink: defaultProvide.createPath,
});
});
};
@@ -131,6 +131,16 @@ describe('DeployKeysTable', () => {
createComponent();
});
+ it('renders card with the deploy keys', () => {
+ expect(findCard().exists()).toBe(true);
+ });
+
+ it('shows the correct number of deploy keys', () => {
+ expect(findCardTitle().text()).toMatchInterpolatedText(
+ `Public deploy keys ${responseBody.length}`,
+ );
+ });
+
it('renders deploy keys in table', () => {
expectDeployKeyWithFingerprintIsRendered(deployKey, 0);
expectDeployKeyWithFingerprintIsRendered(deployKey2, 1);
diff --git a/spec/frontend/organizations/groups_and_projects/components/app_spec.js b/spec/frontend/organizations/groups_and_projects/components/app_spec.js
index 24e1a26336c..36fa1c75ab0 100644
--- a/spec/frontend/organizations/groups_and_projects/components/app_spec.js
+++ b/spec/frontend/organizations/groups_and_projects/components/app_spec.js
@@ -1,99 +1,37 @@
-import VueApollo from 'vue-apollo';
-import Vue from 'vue';
-import { GlLoadingIcon } from '@gitlab/ui';
import App from '~/organizations/groups_and_projects/components/app.vue';
-import resolvers from '~/organizations/groups_and_projects/graphql/resolvers';
-import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { createAlert } from '~/alert';
+import GroupsPage from '~/organizations/groups_and_projects/components/groups_page.vue';
+import ProjectsPage from '~/organizations/groups_and_projects/components/projects_page.vue';
+import {
+ DISPLAY_QUERY_GROUPS,
+ DISPLAY_QUERY_PROJECTS,
+} from '~/organizations/groups_and_projects/constants';
+import { createRouter } from '~/organizations/groups_and_projects';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import { organizationProjects } from './mock_data';
-
-jest.mock('~/alert');
-
-Vue.use(VueApollo);
-jest.useFakeTimers();
describe('GroupsAndProjectsApp', () => {
- let wrapper;
- let mockApollo;
-
- const createComponent = ({ mockResolvers = resolvers } = {}) => {
- mockApollo = createMockApollo([], mockResolvers);
-
- wrapper = shallowMountExtended(App, { apolloProvider: mockApollo });
+ const router = createRouter();
+ const routerMock = {
+ push: jest.fn(),
};
+ let wrapper;
- afterEach(() => {
- mockApollo = null;
- });
-
- describe('when API call is loading', () => {
- beforeEach(() => {
- const mockResolvers = {
- Query: {
- organization: jest.fn().mockReturnValueOnce(new Promise(() => {})),
- },
- };
-
- createComponent({ mockResolvers });
- });
-
- it('renders loading icon', () => {
- expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
- });
- });
-
- describe('when API call is successful', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('renders `ProjectsList` component and passes correct props', async () => {
- jest.runAllTimers();
- await waitForPromises();
-
- expect(wrapper.findComponent(ProjectsList).props()).toEqual({
- projects: organizationProjects.projects.nodes.map(
- ({ id, nameWithNamespace, accessLevel, ...project }) => ({
- ...project,
- id: getIdFromGraphQLId(id),
- name: nameWithNamespace,
- permissions: {
- projectAccess: {
- accessLevel: accessLevel.integerValue,
- },
- },
- }),
- ),
- showProjectIcon: true,
- });
- });
- });
-
- describe('when API call is not successful', () => {
- const error = new Error();
-
- beforeEach(() => {
- const mockResolvers = {
- Query: {
- organization: jest.fn().mockRejectedValueOnce(error),
- },
- };
-
- createComponent({ mockResolvers });
+ const createComponent = ({ routeQuery = {} } = {}) => {
+ wrapper = shallowMountExtended(App, {
+ router,
+ mocks: { $route: { path: '/', query: routeQuery }, $router: routerMock },
});
+ };
- it('displays error alert', async () => {
- await waitForPromises();
+ describe.each`
+ display | expectedComponent
+ ${null} | ${GroupsPage}
+ ${DISPLAY_QUERY_GROUPS} | ${GroupsPage}
+ ${DISPLAY_QUERY_PROJECTS} | ${ProjectsPage}
+ `('when `display` query string is $display', ({ display, expectedComponent }) => {
+ it('renders expected component', () => {
+ createComponent({ routeQuery: { display } });
- expect(createAlert).toHaveBeenCalledWith({
- message: App.i18n.errorMessage,
- error,
- captureError: true,
- });
+ expect(wrapper.findComponent(expectedComponent).exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/organizations/groups_and_projects/components/projects_page_spec.js b/spec/frontend/organizations/groups_and_projects/components/projects_page_spec.js
new file mode 100644
index 00000000000..07f9f0da7c7
--- /dev/null
+++ b/spec/frontend/organizations/groups_and_projects/components/projects_page_spec.js
@@ -0,0 +1,88 @@
+import VueApollo from 'vue-apollo';
+import Vue from 'vue';
+import { GlLoadingIcon } from '@gitlab/ui';
+import ProjectsPage from '~/organizations/groups_and_projects/components/projects_page.vue';
+import { formatProjects } from '~/organizations/groups_and_projects/utils';
+import resolvers from '~/organizations/groups_and_projects/graphql/resolvers';
+import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue';
+import { createAlert } from '~/alert';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { organizationProjects } from '../mock_data';
+
+jest.mock('~/alert');
+
+Vue.use(VueApollo);
+jest.useFakeTimers();
+
+describe('ProjectsPage', () => {
+ let wrapper;
+ let mockApollo;
+
+ const createComponent = ({ mockResolvers = resolvers } = {}) => {
+ mockApollo = createMockApollo([], mockResolvers);
+
+ wrapper = shallowMountExtended(ProjectsPage, { apolloProvider: mockApollo });
+ };
+
+ afterEach(() => {
+ mockApollo = null;
+ });
+
+ describe('when API call is loading', () => {
+ beforeEach(() => {
+ const mockResolvers = {
+ Query: {
+ organization: jest.fn().mockReturnValueOnce(new Promise(() => {})),
+ },
+ };
+
+ createComponent({ mockResolvers });
+ });
+
+ it('renders loading icon', () => {
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+ });
+ });
+
+ describe('when API call is successful', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders `ProjectsList` component and passes correct props', async () => {
+ jest.runAllTimers();
+ await waitForPromises();
+
+ expect(wrapper.findComponent(ProjectsList).props()).toEqual({
+ projects: formatProjects(organizationProjects.projects.nodes),
+ showProjectIcon: true,
+ });
+ });
+ });
+
+ describe('when API call is not successful', () => {
+ const error = new Error();
+
+ beforeEach(() => {
+ const mockResolvers = {
+ Query: {
+ organization: jest.fn().mockRejectedValueOnce(error),
+ },
+ };
+
+ createComponent({ mockResolvers });
+ });
+
+ it('displays error alert', async () => {
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: ProjectsPage.i18n.errorMessage,
+ error,
+ captureError: true,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/organizations/groups_and_projects/components/mock_data.js b/spec/frontend/organizations/groups_and_projects/mock_data.js
index c3276450745..c3276450745 100644
--- a/spec/frontend/organizations/groups_and_projects/components/mock_data.js
+++ b/spec/frontend/organizations/groups_and_projects/mock_data.js
diff --git a/spec/frontend/organizations/groups_and_projects/utils_spec.js b/spec/frontend/organizations/groups_and_projects/utils_spec.js
new file mode 100644
index 00000000000..5aae26802ac
--- /dev/null
+++ b/spec/frontend/organizations/groups_and_projects/utils_spec.js
@@ -0,0 +1,22 @@
+import { formatProjects } from '~/organizations/groups_and_projects/utils';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { organizationProjects } from './mock_data';
+
+describe('formatProjects', () => {
+ it('correctly formats the projects', () => {
+ const [firstMockProject] = organizationProjects.projects.nodes;
+ const formattedProjects = formatProjects(organizationProjects.projects.nodes);
+ const [firstFormattedProject] = formattedProjects;
+
+ expect(firstFormattedProject).toMatchObject({
+ id: getIdFromGraphQLId(firstMockProject.id),
+ name: firstMockProject.nameWithNamespace,
+ permissions: {
+ projectAccess: {
+ accessLevel: firstMockProject.accessLevel.integerValue,
+ },
+ },
+ });
+ expect(formattedProjects.length).toBe(organizationProjects.projects.nodes.length);
+ });
+});
diff --git a/spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap b/spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap
index 974650a2c7c..4893ee26178 100644
--- a/spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap
+++ b/spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Project remove modal initialized matches the snapshot 1`] = `
-<form
+<gl-form-stub
action="some/path"
method="post"
>
@@ -16,100 +16,23 @@ exports[`Project remove modal initialized matches the snapshot 1`] = `
type="hidden"
/>
+ <delete-modal-stub
+ confirmphrase="foo"
+ forkscount="3"
+ issuescount="1"
+ mergerequestscount="2"
+ starscount="4"
+ />
+
<gl-button-stub
buttontextclasses=""
category="primary"
data-qa-selector="delete_button"
icon=""
- role="button"
size="medium"
- tabindex="0"
variant="danger"
>
Delete project
</gl-button-stub>
-
- <gl-modal-stub
- actioncancel="[object Object]"
- actionprimary="[object Object]"
- arialabel=""
- dismisslabel="Close"
- footer-class="gl-bg-gray-10 gl-p-5"
- modalclass=""
- modalid="fakeUniqueId"
- ok-variant="danger"
- size="md"
- title-class="gl-text-red-500"
- titletag="h4"
- >
-
- <div>
- <gl-alert-stub
- class="gl-mb-5"
- dismisslabel="Dismiss"
- primarybuttonlink=""
- primarybuttontext=""
- secondarybuttonlink=""
- secondarybuttontext=""
- showicon="true"
- title=""
- variant="danger"
- >
- <h4
- class="gl-alert-title"
- data-testid="delete-alert-title"
- >
-
- You are about to delete this project containing:
-
- </h4>
-
- <ul>
- <li>
- 1 issue
- </li>
-
- <li>
- 2 merge requests
- </li>
-
- <li>
- 3 forks
- </li>
-
- <li>
- 4 stars
- </li>
- </ul>
- This project is
- <strong>
- NOT
- </strong>
- a fork. This process deletes the project repository and all related resources.
- </gl-alert-stub>
-
- <p
- class="gl-mb-1"
- >
- Enter the following to confirm:
- </p>
-
- <p>
- <code
- class="gl-white-space-pre-wrap"
- >
- foo
- </code>
- </p>
-
- <gl-form-input-stub
- data-qa-selector="confirm_name_field"
- id="confirm_name_input"
- name="confirm_name_input"
- type="text"
- />
-
- </div>
- </gl-modal-stub>
-</form>
+</gl-form-stub>
`;
diff --git a/spec/frontend/projects/components/shared/__snapshots__/delete_button_spec.js.snap b/spec/frontend/projects/components/shared/__snapshots__/delete_button_spec.js.snap
deleted file mode 100644
index ac020fe6915..00000000000
--- a/spec/frontend/projects/components/shared/__snapshots__/delete_button_spec.js.snap
+++ /dev/null
@@ -1,116 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Project remove modal intialized matches the snapshot 1`] = `
-<form
- action="some/path"
- method="post"
->
- <input
- name="_method"
- type="hidden"
- value="delete"
- />
-
- <input
- name="authenticity_token"
- type="hidden"
- value="test-csrf-token"
- />
-
- <gl-button-stub
- buttontextclasses=""
- category="primary"
- data-qa-selector="delete_button"
- icon=""
- role="button"
- size="medium"
- tabindex="0"
- variant="danger"
- >
- Delete project
- </gl-button-stub>
-
- <div
- footer-class="gl-bg-gray-10 gl-p-5"
- ok-variant="danger"
- title-class="gl-text-red-500"
- >
- Are you absolutely sure?
- <div>
- <gl-alert-stub
- class="gl-mb-5"
- dismisslabel="Dismiss"
- primarybuttonlink=""
- primarybuttontext=""
- secondarybuttonlink=""
- secondarybuttontext=""
- showicon="true"
- title=""
- variant="danger"
- >
- <h4
- class="gl-alert-title"
- data-testid="delete-alert-title"
- >
-
- You are about to delete this project containing:
-
- </h4>
-
- <ul>
- <li>
- <gl-sprintf-stub
- message="1 issue"
- />
- </li>
-
- <li>
- <gl-sprintf-stub
- message="2 merge requests"
- />
- </li>
-
- <li>
- <gl-sprintf-stub
- message="3 forks"
- />
- </li>
-
- <li>
- <gl-sprintf-stub
- message="4 stars"
- />
- </li>
- </ul>
-
- <gl-sprintf-stub
- data-testid="delete-alert-body"
- message="This project is %{strongStart}NOT%{strongEnd} a fork. This process deletes the project repository and all related resources."
- />
- </gl-alert-stub>
-
- <p
- class="gl-mb-1"
- >
- Enter the following to confirm:
- </p>
-
- <p>
- <code
- class="gl-white-space-pre-wrap"
- >
- foo
- </code>
- </p>
-
- <gl-form-input-stub
- data-qa-selector="confirm_name_field"
- id="confirm_name_input"
- name="confirm_name_input"
- type="text"
- />
-
- </div>
- </div>
-</form>
-`;
diff --git a/spec/frontend/projects/components/shared/delete_button_spec.js b/spec/frontend/projects/components/shared/delete_button_spec.js
index 6b4ef341b0c..556c1ae7084 100644
--- a/spec/frontend/projects/components/shared/delete_button_spec.js
+++ b/spec/frontend/projects/components/shared/delete_button_spec.js
@@ -1,21 +1,17 @@
-import { GlModal } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { stubComponent } from 'helpers/stub_component';
-import SharedDeleteButton from '~/projects/components/shared/delete_button.vue';
+import { GlForm, GlButton } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import DeleteButton from '~/projects/components/shared/delete_button.vue';
+import DeleteModal from '~/projects/components/shared/delete_modal.vue';
jest.mock('~/lib/utils/csrf', () => ({ token: 'test-csrf-token' }));
-describe('Project remove modal', () => {
+describe('DeleteButton', () => {
let wrapper;
- const findFormElement = () => wrapper.find('form');
- const findConfirmButton = () => wrapper.find('.js-modal-action-primary');
- const findAuthenticityTokenInput = () => findFormElement().find('input[name=authenticity_token]');
- const findModal = () => wrapper.findComponent(GlModal);
- const findTitle = () => wrapper.find('[data-testid="delete-alert-title"]');
- const findAlertBody = () => wrapper.find('[data-testid="delete-alert-body"]');
+ const findForm = () => wrapper.findComponent(GlForm);
+ const findModal = () => wrapper.findComponent(DeleteModal);
- const defaultProps = {
+ const defaultPropsData = {
confirmPhrase: 'foo',
formPath: 'some/path',
isFork: false,
@@ -25,88 +21,68 @@ describe('Project remove modal', () => {
starsCount: 4,
};
- const createComponent = (data = {}, stubs = {}, props = {}) => {
- wrapper = shallowMount(SharedDeleteButton, {
+ const createComponent = (propsData) => {
+ wrapper = shallowMountExtended(DeleteButton, {
propsData: {
- ...defaultProps,
- ...props,
+ ...defaultPropsData,
+ ...propsData,
},
- data: () => data,
- stubs: {
- GlModal: stubComponent(GlModal, {
- template: `
- <div>
- <slot name="modal-title"></slot>
- <slot></slot>
- </div>`,
- }),
- ...stubs,
+ scopedSlots: {
+ 'modal-footer': '<div data-testid="modal-footer-slot"></div>',
},
});
};
- describe('intialized', () => {
- beforeEach(() => {
- createComponent();
- });
+ it('renders modal and passes correct props', () => {
+ createComponent();
- it('matches the snapshot', () => {
- expect(wrapper.element).toMatchSnapshot();
- });
-
- it('sets a csrf token on the authenticity form input', () => {
- expect(findAuthenticityTokenInput().element.value).toEqual('test-csrf-token');
- });
+ const { formPath, ...expectedProps } = defaultPropsData;
- it('sets the form action to the provided path', () => {
- expect(findFormElement().attributes('action')).toEqual(defaultProps.formPath);
+ expect(findModal().props()).toMatchObject({
+ visible: false,
+ ...expectedProps,
});
});
- describe('when the user input does not match the confirmPhrase', () => {
- beforeEach(() => {
- createComponent({ userInput: 'bar' }, { GlModal });
- });
+ it('renders form with required inputs', () => {
+ createComponent();
- it('the confirm button is disabled', () => {
- expect(findConfirmButton().attributes('disabled')).toBeDefined();
- });
+ const form = findForm();
+
+ expect(form.find('input[name="_method"]').attributes('value')).toBe('delete');
+ expect(form.find('input[name="authenticity_token"]').attributes('value')).toBe(
+ 'test-csrf-token',
+ );
});
- describe('when the user input matches the confirmPhrase', () => {
+ describe('when button is clicked', () => {
beforeEach(() => {
- createComponent({ userInput: defaultProps.confirmPhrase }, { GlModal });
+ createComponent();
+ wrapper.findComponent(GlButton).vm.$emit('click');
});
- it('the confirm button is not disabled', () => {
- expect(findConfirmButton().attributes('disabled')).toBe(undefined);
+ it('opens modal', () => {
+ expect(findModal().props('visible')).toBe(true);
});
});
- describe('when the modal is confirmed', () => {
- beforeEach(() => {
+ describe('when modal emits `primary` event', () => {
+ it('submits the form', () => {
createComponent();
- findModal().vm.$emit('ok');
- });
- it('submits the form element', () => {
- expect(findFormElement().element.submit).toHaveBeenCalled();
- });
- });
+ const submitMock = jest.fn();
- describe('when project is a fork', () => {
- beforeEach(() => {
- createComponent({}, {}, { isFork: true });
- });
+ findForm().element.submit = submitMock;
- it('matches the fork title', () => {
- expect(findTitle().text()).toEqual('You are about to delete this forked project containing:');
- });
+ findModal().vm.$emit('primary');
- it('matches the fork body', () => {
- expect(findAlertBody().attributes().message).toEqual(
- 'This process deletes the project repository and all related resources.',
- );
+ expect(submitMock).toHaveBeenCalled();
});
});
+
+ it('renders `modal-footer` slot', () => {
+ createComponent();
+
+ expect(wrapper.findByTestId('modal-footer-slot').exists()).toBe(true);
+ });
});
diff --git a/spec/frontend/projects/components/shared/delete_modal_spec.js b/spec/frontend/projects/components/shared/delete_modal_spec.js
new file mode 100644
index 00000000000..c6213fd4b6d
--- /dev/null
+++ b/spec/frontend/projects/components/shared/delete_modal_spec.js
@@ -0,0 +1,167 @@
+import { GlFormInput, GlModal, GlAlert } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import DeleteModal from '~/projects/components/shared/delete_modal.vue';
+import { __, sprintf } from '~/locale';
+import { stubComponent } from 'helpers/stub_component';
+
+jest.mock('lodash/uniqueId', () => () => 'fake-id');
+
+describe('DeleteModal', () => {
+ let wrapper;
+
+ const defaultPropsData = {
+ visible: false,
+ confirmPhrase: 'foo',
+ isFork: false,
+ issuesCount: 1,
+ mergeRequestsCount: 2,
+ forksCount: 3,
+ starsCount: 4,
+ };
+
+ const createComponent = (propsData) => {
+ wrapper = mountExtended(DeleteModal, {
+ propsData: {
+ ...defaultPropsData,
+ ...propsData,
+ },
+ stubs: {
+ GlModal: stubComponent(GlModal),
+ },
+ scopedSlots: {
+ 'modal-footer': '<div data-testid="modal-footer-slot"></div>',
+ },
+ });
+ };
+
+ const findGlModal = () => wrapper.findComponent(GlModal);
+ const alertText = () => wrapper.findComponent(GlAlert).text();
+ const findFormInput = () => wrapper.findComponent(GlFormInput);
+
+ it('renders modal with correct props', () => {
+ createComponent();
+
+ expect(findGlModal().props()).toMatchObject({
+ visible: defaultPropsData.visible,
+ modalId: 'fake-id',
+ actionPrimary: {
+ text: __('Yes, delete project'),
+ attributes: {
+ variant: 'danger',
+ disabled: true,
+ 'data-qa-selector': 'confirm_delete_button',
+ },
+ },
+ actionCancel: {
+ text: __('Cancel, keep project'),
+ },
+ });
+ });
+
+ describe('when resource counts are set', () => {
+ it('displays resource counts', () => {
+ createComponent();
+
+ expect(alertText()).toContain(`${defaultPropsData.issuesCount} issue`);
+ expect(alertText()).toContain(`${defaultPropsData.mergeRequestsCount} merge requests`);
+ expect(alertText()).toContain(`${defaultPropsData.forksCount} forks`);
+ expect(alertText()).toContain(`${defaultPropsData.starsCount} stars`);
+ });
+ });
+
+ describe('when resource counts are not set', () => {
+ it('does not display resource counts', () => {
+ createComponent({
+ issuesCount: null,
+ mergeRequestsCount: null,
+ forksCount: null,
+ starsCount: null,
+ });
+
+ expect(alertText()).not.toContain('issue');
+ expect(alertText()).not.toContain('merge requests');
+ expect(alertText()).not.toContain('forks');
+ expect(alertText()).not.toContain('stars');
+ });
+ });
+
+ describe('when project is a fork', () => {
+ beforeEach(() => {
+ createComponent({
+ isFork: true,
+ });
+ });
+
+ it('displays correct alert title', () => {
+ expect(alertText()).toContain(DeleteModal.i18n.isForkAlertTitle);
+ });
+
+ it('displays correct alert body', () => {
+ expect(alertText()).toContain(DeleteModal.i18n.isForkAlertBody);
+ });
+ });
+
+ describe('when project is not a fork', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('displays correct alert title', () => {
+ expect(alertText()).toContain(
+ sprintf(DeleteModal.i18n.isNotForkAlertTitle, { strongStart: '', strongEnd: '' }),
+ );
+ });
+
+ it('displays correct alert body', () => {
+ expect(alertText()).toContain(
+ sprintf(DeleteModal.i18n.isNotForkAlertBody, { strongStart: '', strongEnd: '' }),
+ );
+ });
+ });
+
+ describe('when correct confirm phrase is used', () => {
+ beforeEach(() => {
+ createComponent();
+
+ findFormInput().vm.$emit('input', defaultPropsData.confirmPhrase);
+ });
+
+ it('enables the primary action', () => {
+ expect(findGlModal().props('actionPrimary').attributes.disabled).toBe(false);
+ });
+ });
+
+ describe('when correct confirm phrase is not used', () => {
+ beforeEach(() => {
+ createComponent();
+
+ findFormInput().vm.$emit('input', 'bar');
+ });
+
+ it('keeps the primary action disabled', () => {
+ expect(findGlModal().props('actionPrimary').attributes.disabled).toBe(true);
+ });
+ });
+
+ it('emits `primary` event', () => {
+ createComponent();
+
+ findGlModal().vm.$emit('primary');
+
+ expect(wrapper.emitted('primary')).toEqual([[]]);
+ });
+
+ it('emits `change` event', () => {
+ createComponent();
+
+ findGlModal().vm.$emit('change', true);
+
+ expect(wrapper.emitted('change')).toEqual([[true]]);
+ });
+
+ it('renders `modal-footer` slot', () => {
+ createComponent();
+
+ expect(wrapper.findByTestId('modal-footer-slot').exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/work_items/list/components/work_items_list_app_spec.js b/spec/frontend/work_items/list/components/work_items_list_app_spec.js
new file mode 100644
index 00000000000..10d9fea8a06
--- /dev/null
+++ b/spec/frontend/work_items/list/components/work_items_list_app_spec.js
@@ -0,0 +1,29 @@
+import { shallowMount } from '@vue/test-utils';
+import { STATUS_OPEN } from '~/issues/constants';
+import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
+import WorkItemsListApp from '~/work_items/list/components/work_items_list_app.vue';
+
+describe('WorkItemsListApp component', () => {
+ let wrapper;
+
+ const findIssuableList = () => wrapper.findComponent(IssuableList);
+
+ const mountComponent = () => {
+ wrapper = shallowMount(WorkItemsListApp);
+ };
+
+ it('renders IssuableList component', () => {
+ mountComponent();
+
+ expect(findIssuableList().props()).toMatchObject({
+ currentTab: STATUS_OPEN,
+ issuables: [],
+ namespace: 'work-items',
+ recentSearchesStorageKey: 'issues',
+ searchInputPlaceholder: 'Search or filter results...',
+ searchTokens: [],
+ sortOptions: [],
+ tabs: WorkItemsListApp.issuableListTabs,
+ });
+ });
+});
diff --git a/spec/helpers/projects/observability_helper_spec.rb b/spec/helpers/projects/observability_helper_spec.rb
index 65b6ddf04ec..0f47cdb8be2 100644
--- a/spec/helpers/projects/observability_helper_spec.rb
+++ b/spec/helpers/projects/observability_helper_spec.rb
@@ -4,10 +4,12 @@ require 'spec_helper'
require 'json'
RSpec.describe Projects::ObservabilityHelper, type: :helper, feature_category: :tracing do
- describe '#observability_tracing_view_model' do
- let_it_be(:group) { build_stubbed(:group) }
- let_it_be(:project) { build_stubbed(:project, group: group) }
+ include Gitlab::Routing.url_helpers
+
+ let_it_be(:group) { build_stubbed(:group) }
+ let_it_be(:project) { build_stubbed(:project, group: group) }
+ describe '#observability_tracing_view_model' do
it 'generates the correct JSON' do
expected_json = {
tracingUrl: Gitlab::Observability.tracing_url(project),
@@ -18,4 +20,18 @@ RSpec.describe Projects::ObservabilityHelper, type: :helper, feature_category: :
expect(helper.observability_tracing_view_model(project)).to eq(expected_json)
end
end
+
+ describe '#observability_tracing_details_model' do
+ it 'generates the correct JSON' do
+ expected_json = {
+ tracingIndexUrl: namespace_project_tracing_index_path(project.group, project),
+ traceId: "trace-id",
+ tracingUrl: Gitlab::Observability.tracing_url(project),
+ provisioningUrl: Gitlab::Observability.provisioning_url(project),
+ oauthUrl: Gitlab::Observability.oauth_url
+ }.to_json
+
+ expect(helper.observability_tracing_details_model(project, "trace-id")).to eq(expected_json)
+ end
+ end
end
diff --git a/spec/lib/gitlab/usage_data_counters/neovim_plugin_activity_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/neovim_plugin_activity_unique_counter_spec.rb
new file mode 100644
index 00000000000..274a3ffc843
--- /dev/null
+++ b/spec/lib/gitlab/usage_data_counters/neovim_plugin_activity_unique_counter_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::UsageDataCounters::NeovimPluginActivityUniqueCounter, :clean_gitlab_redis_shared_state, feature_category: :editor_extensions do
+ let(:user1) { build(:user, id: 1) }
+ let(:user2) { build(:user, id: 2) }
+ let(:time) { Time.current }
+ let(:action) { described_class::NEOVIM_PLUGIN_API_REQUEST_ACTION }
+ let(:user_agent_string) do
+ 'code-completions-language-server-experiment (Neovim:0.9.0; gitlab.vim (v0.1.0); arch:amd64; os:darwin)'
+ end
+
+ let(:user_agent) { { user_agent: user_agent_string } }
+
+ context 'when tracking a neovim plugin api request' do
+ it_behaves_like 'a request from an extension'
+ end
+end
diff --git a/spec/policies/ci/build_policy_spec.rb b/spec/policies/ci/build_policy_spec.rb
index ec3b3fde719..041b70b10d4 100644
--- a/spec/policies/ci/build_policy_spec.rb
+++ b/spec/policies/ci/build_policy_spec.rb
@@ -99,12 +99,15 @@ RSpec.describe Ci::BuildPolicy do
context 'when maintainer is allowed to push to pipeline branch' do
let(:project) { create(:project, :public) }
- let(:owner) { user }
- it 'enables update_build if user is maintainer' do
- allow_any_instance_of(Project).to receive(:empty_repo?).and_return(false)
- allow_any_instance_of(Project).to receive(:branch_allows_collaboration?).and_return(true)
+ before do
+ project.add_maintainer(user)
+
+ allow(project).to receive(:empty_repo?).and_return(false)
+ allow(project).to receive(:branch_allows_collaboration?).and_return(true)
+ end
+ it 'enables update_build if user is maintainer' do
expect(policy).to be_allowed :update_build
expect(policy).to be_allowed :update_commit_status
end
@@ -127,6 +130,16 @@ RSpec.describe Ci::BuildPolicy do
it 'does not include ability to update build' do
expect(policy).to be_disallowed :update_build
end
+
+ context 'when the user is admin', :enable_admin_mode do
+ before do
+ user.update!(admin: true)
+ end
+
+ it 'does not include ability to update build' do
+ expect(policy).to be_disallowed :update_build
+ end
+ end
end
context 'when developers can push to the branch' do
@@ -252,7 +265,7 @@ RSpec.describe Ci::BuildPolicy do
create(:protected_branch, :developers_can_push, name: build.ref, project: project)
end
- it { expect(policy).to be_allowed :erase_build }
+ it { expect(policy).to be_disallowed :erase_build }
end
context 'when the build was created for a protected tag' do
@@ -262,7 +275,7 @@ RSpec.describe Ci::BuildPolicy do
build.update!(tag: true)
end
- it { expect(policy).to be_allowed :erase_build }
+ it { expect(policy).to be_disallowed :erase_build }
end
context 'when the build was created for an unprotected ref' do
diff --git a/spec/requests/api/graphql/project/merge_requests_spec.rb b/spec/requests/api/graphql/project/merge_requests_spec.rb
index 3ad98ee09aa..05ed0ed8729 100644
--- a/spec/requests/api/graphql/project/merge_requests_spec.rb
+++ b/spec/requests/api/graphql/project/merge_requests_spec.rb
@@ -407,6 +407,16 @@ RSpec.describe 'getting merge request listings nested in a project', feature_cat
end
include_examples 'N+1 query check', skip_cached: false
+
+ context 'when each merge request diff has no head_commit_sha' do
+ before do
+ [merge_request_a, merge_request_b, merge_request_c].each do |mr|
+ mr.merge_request_diffs.update!(head_commit_sha: nil)
+ end
+ end
+
+ include_examples 'N+1 query check', skip_cached: false
+ end
end
end
diff --git a/spec/requests/projects/tracing_controller_spec.rb b/spec/requests/projects/tracing_controller_spec.rb
index eecaa0d962a..8996ea7f8d6 100644
--- a/spec/requests/projects/tracing_controller_spec.rb
+++ b/spec/requests/projects/tracing_controller_spec.rb
@@ -14,14 +14,12 @@ RSpec.describe Projects::TracingController, feature_category: :tracing do
response
end
- describe 'GET #index' do
- before do
- stub_feature_flags(observability_tracing: observability_tracing_ff)
- sign_in(user)
- end
-
- let(:path) { project_tracing_index_path(project) }
+ before do
+ stub_feature_flags(observability_tracing: observability_tracing_ff)
+ sign_in(user)
+ end
+ shared_examples 'tracing route request' do
it_behaves_like 'observability csp policy' do
before_all do
project.add_developer(user)
@@ -45,6 +43,26 @@ RSpec.describe Projects::TracingController, feature_category: :tracing do
expect(subject).to have_gitlab_http_status(:ok)
end
+ context 'when feature is disabled' do
+ let(:observability_tracing_ff) { false }
+
+ it 'returns 404' do
+ expect(subject).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+ end
+
+ describe 'GET #index' do
+ let(:path) { project_tracing_index_path(project) }
+
+ it_behaves_like 'tracing route request'
+
+ describe 'html response' do
+ before_all do
+ project.add_developer(user)
+ end
+
it 'renders the js-tracing element correctly' do
element = Nokogiri::HTML.parse(subject.body).at_css('#js-tracing')
@@ -55,13 +73,31 @@ RSpec.describe Projects::TracingController, feature_category: :tracing do
}.to_json
expect(element.attributes['data-view-model'].value).to eq(expected_view_model)
end
+ end
+ end
- context 'when feature is disabled' do
- let(:observability_tracing_ff) { false }
+ describe 'GET #show' do
+ let(:path) { project_tracing_path(project, id: "test-trace-id") }
- it 'returns 404' do
- expect(subject).to have_gitlab_http_status(:not_found)
- end
+ it_behaves_like 'tracing route request'
+
+ describe 'html response' do
+ before_all do
+ project.add_developer(user)
+ end
+
+ it 'renders the js-tracing element correctly' do
+ element = Nokogiri::HTML.parse(subject.body).at_css('#js-tracing-details')
+
+ expected_view_model = {
+ tracingIndexUrl: project_tracing_index_path(project),
+ traceId: 'test-trace-id',
+ tracingUrl: Gitlab::Observability.tracing_url(project),
+ provisioningUrl: Gitlab::Observability.provisioning_url(project),
+ oauthUrl: Gitlab::Observability.oauth_url
+ }.to_json
+
+ expect(element.attributes['data-view-model'].value).to eq(expected_view_model)
end
end
end
diff --git a/spec/services/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb
index fc2c66e7f73..6d991baafd0 100644
--- a/spec/services/ci/retry_pipeline_service_spec.rb
+++ b/spec/services/ci/retry_pipeline_service_spec.rb
@@ -451,22 +451,18 @@ RSpec.describe Ci::RetryPipelineService, '#execute', feature_category: :continuo
before do
project.add_maintainer(user)
- create(:merge_request,
- source_project: forked_project,
- target_project: project,
- source_branch: 'fixes',
- allow_collaboration: true)
- create_build('rspec 1', :failed, test_stage)
- end
- it 'allows to retry failed pipeline' do
- allow_any_instance_of(Project).to receive(:branch_allows_collaboration?).and_return(true)
+ create_build('rspec 1', :failed, test_stage, project: project, ref: pipeline.ref)
+
allow_any_instance_of(Project).to receive(:empty_repo?).and_return(false)
+ allow_any_instance_of(Project).to receive(:branch_allows_collaboration?).and_return(true)
+ end
+ it 'allows to retry failed pipeline' do
service.execute(pipeline)
expect(build('rspec 1')).to be_pending
- expect(pipeline.reload).to be_running
+ expect(pipeline).to be_running
end
end
diff --git a/yarn.lock b/yarn.lock
index 3c47c5ec061..f1a03c8cdb0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4267,10 +4267,10 @@ core-js-pure@^3.0.0:
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813"
integrity sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA==
-core-js@^3.29.1, core-js@^3.31.0, core-js@^3.6.5:
- version "3.31.0"
- resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.31.0.tgz#4471dd33e366c79d8c0977ed2d940821719db344"
- integrity sha512-NIp2TQSGfR6ba5aalZD+ZQ1fSxGhDo/s1w0nx3RYzf2pnJxt7YynxFlFScP6eV7+GZsKO95NSjGxyJsU3DZgeQ==
+core-js@^3.29.1, core-js@^3.31.1, core-js@^3.6.5:
+ version "3.31.1"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.31.1.tgz#f2b0eea9be9da0def2c5fece71064a7e5d687653"
+ integrity sha512-2sKLtfq1eFST7l7v62zaqXacPc7uG8ZAya8ogijLhTtaKNcpzpB4TMoTw2Si+8GYKRwFPMMtUT0263QFWFfqyQ==
core-util-is@~1.0.0:
version "1.0.3"