diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-07-28 00:07:46 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-07-28 00:07:46 +0300 |
commit | d74abe41c00eedb95738a74b9bbdea67e423103b (patch) | |
tree | 754f93e47d205f0cb6908b6abb4cf7713bdef879 | |
parent | 7dd130e2cae40514f02b02922251b62302f2fdd5 (diff) |
Add latest changes from gitlab-org/gitlab@master
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" |