diff options
Diffstat (limited to 'app')
22 files changed, 506 insertions, 253 deletions
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) } } + |