diff options
Diffstat (limited to 'app')
30 files changed, 418 insertions, 171 deletions
diff --git a/app/assets/javascripts/group.js b/app/assets/javascripts/group.js index 39c8a88d485..c1fc75fbea6 100644 --- a/app/assets/javascripts/group.js +++ b/app/assets/javascripts/group.js @@ -16,9 +16,7 @@ export default class Group { if (groupName.value === '') { groupName.addEventListener('keyup', this.updateHandler); - if (!this.parentId.value) { - groupName.addEventListener('blur', this.updateGroupPathSlugHandler); - } + groupName.addEventListener('blur', this.updateGroupPathSlugHandler); } }); @@ -53,7 +51,7 @@ export default class Group { const slug = this.groupPaths[0]?.value || slugify(value); if (!slug) return; - fetchGroupPathAvailability(slug) + fetchGroupPathAvailability(slug, this.parentId?.value) .then(({ data }) => data) .then(({ exists, suggests }) => { if (exists && suggests.length) { diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue index 22d401c451d..f2c608a8912 100644 --- a/app/assets/javascripts/groups/components/group_item.vue +++ b/app/assets/javascripts/groups/components/group_item.vue @@ -182,7 +182,10 @@ export default { </div> <div class="metadata d-flex flex-grow-1 flex-shrink-0 flex-wrap justify-content-md-between"> <item-actions v-if="isGroup" :group="group" :parent-group="parentGroup" /> - <item-stats :item="group" class="group-stats gl-mt-2 d-none d-md-flex" /> + <item-stats + :item="group" + class="group-stats gl-mt-2 d-none d-md-flex gl-align-items-center" + /> </div> </div> </div> diff --git a/app/assets/javascripts/jira_connect/components/app.vue b/app/assets/javascripts/jira_connect/components/app.vue index ffc95130783..ff4dfb23687 100644 --- a/app/assets/javascripts/jira_connect/components/app.vue +++ b/app/assets/javascripts/jira_connect/components/app.vue @@ -3,7 +3,6 @@ import { GlAlert, GlButton, GlLink, GlModal, GlModalDirective, GlSprintf } from import { mapState, mapMutations } from 'vuex'; import { retrieveAlert, getLocation } from '~/jira_connect/utils'; import { __ } from '~/locale'; -import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { SET_ALERT } from '../store/mutation_types'; import GroupsList from './groups_list.vue'; import SubscriptionsList from './subscriptions_list.vue'; @@ -22,7 +21,6 @@ export default { directives: { GlModalDirective, }, - mixins: [glFeatureFlagsMixin()], inject: { usersPath: { default: '', diff --git a/app/assets/javascripts/jira_connect/components/group_item_name.vue b/app/assets/javascripts/jira_connect/components/group_item_name.vue index c5ce24b3de3..e6c172dae9e 100644 --- a/app/assets/javascripts/jira_connect/components/group_item_name.vue +++ b/app/assets/javascripts/jira_connect/components/group_item_name.vue @@ -23,13 +23,10 @@ export default { </div> <div> - <span - class="gl-mr-3 gl-text-gray-900! gl-font-weight-bold" - data-testid="group-list-item-name" - > + <span class="gl-mr-3 gl-text-gray-900! gl-font-weight-bold"> {{ group.full_name }} </span> - <div v-if="group.description" data-testid="group-list-item-description"> + <div v-if="group.description"> <p class="gl-mt-2! gl-mb-0 gl-text-gray-600" v-text="group.description"></p> </div> </div> diff --git a/app/assets/javascripts/jira_connect/components/subscriptions_list.vue b/app/assets/javascripts/jira_connect/components/subscriptions_list.vue index 4b0f9acd6ca..a606e2edbbb 100644 --- a/app/assets/javascripts/jira_connect/components/subscriptions_list.vue +++ b/app/assets/javascripts/jira_connect/components/subscriptions_list.vue @@ -1,10 +1,12 @@ <script> import { GlButton, GlEmptyState, GlTable } from '@gitlab/ui'; import { isEmpty } from 'lodash'; +import { mapMutations } from 'vuex'; import { removeSubscription } from '~/jira_connect/api'; import { reloadPage } from '~/jira_connect/utils'; import { __, s__ } from '~/locale'; import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; +import { SET_ALERT } from '../store/mutation_types'; import GroupItemName from './group_item_name.vue'; export default { @@ -46,8 +48,12 @@ export default { emptyDescription: s__( 'Integrations|Namespaces are the GitLab groups and subgroups you link to this Jira instance.', ), + unlinkError: s__('Integrations|Failed to unlink namespace. Please try again.'), }, methods: { + ...mapMutations({ + setAlert: SET_ALERT, + }), isEmpty, isLoadingItem(item) { return this.loadingItem === item; @@ -62,7 +68,11 @@ export default { .then(() => { reloadPage(); }) - .catch(() => { + .catch((error) => { + this.setAlert({ + message: error?.response?.data?.error || this.$options.i18n.unlinkError, + variant: 'danger', + }); this.loadingItem = null; }); }, @@ -89,6 +99,7 @@ export default { :class="unlinkBtnClass(item)" category="secondary" :loading="isLoadingItem(item)" + :disabled="!isEmpty(loadingItem)" @click.prevent="onClick(item)" >{{ __('Unlink') }}</gl-button > diff --git a/app/assets/javascripts/jira_connect/index.js b/app/assets/javascripts/jira_connect/index.js index 24828e2f1d1..dc8bb3b0c77 100644 --- a/app/assets/javascripts/jira_connect/index.js +++ b/app/assets/javascripts/jira_connect/index.js @@ -1,22 +1,14 @@ import setConfigs from '@gitlab/ui/dist/config'; import Vue from 'vue'; -import { addSubscription, removeSubscription } from '~/jira_connect/api'; -import { getLocation, reloadPage, sizeToParent } from '~/jira_connect/utils'; +import { getLocation, sizeToParent } from '~/jira_connect/utils'; import GlFeatureFlagsPlugin from '~/vue_shared/gl_feature_flags_plugin'; import Translate from '~/vue_shared/translate'; import JiraConnectApp from './components/app.vue'; import createStore from './store'; -import { SET_ALERT } from './store/mutation_types'; const store = createStore(); -const reqFailed = (res, fallbackErrorMessage) => { - const { error = fallbackErrorMessage } = res || {}; - - store.commit(SET_ALERT, { message: error, variant: 'danger' }); -}; - const updateSignInLinks = async () => { const location = await getLocation(); Array.from(document.querySelectorAll('.js-jira-connect-sign-in')).forEach((el) => { @@ -25,43 +17,7 @@ const updateSignInLinks = async () => { }); }; -const initRemoveSubscriptionButtonHandlers = () => { - Array.from(document.querySelectorAll('.js-jira-connect-remove-subscription')).forEach((el) => { - el.addEventListener('click', function onRemoveSubscriptionClick(e) { - e.preventDefault(); - - const removePath = e.target.getAttribute('href'); - removeSubscription(removePath) - .then(reloadPage) - .catch((err) => - reqFailed(err.response.data, 'Failed to remove namespace. Please try again.'), - ); - }); - }); -}; - -const initAddSubscriptionFormHandler = () => { - const formEl = document.querySelector('#add-subscription-form'); - if (!formEl) { - return; - } - - formEl.addEventListener('submit', function onAddSubscriptionForm(e) { - e.preventDefault(); - - const addPath = e.target.getAttribute('action'); - const namespace = (e.target.querySelector('#namespace-input') || {}).value; - - addSubscription(addPath, namespace) - .then(reloadPage) - .catch((err) => reqFailed(err.response.data, 'Failed to add namespace. Please try again.')); - }); -}; - export async function initJiraConnect() { - initAddSubscriptionFormHandler(); - initRemoveSubscriptionButtonHandlers(); - await updateSignInLinks(); const el = document.querySelector('.js-jira-connect-app'); diff --git a/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql b/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql new file mode 100644 index 00000000000..d9e51b0345a --- /dev/null +++ b/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql @@ -0,0 +1,52 @@ +query getJobs($fullPath: ID!, $statuses: [CiJobStatus!]) { + project(fullPath: $fullPath) { + jobs(first: 20, statuses: $statuses) { + pageInfo { + endCursor + hasNextPage + hasPreviousPage + startCursor + } + nodes { + detailedStatus { + icon + label + text + tooltip + action { + buttonTitle + icon + method + path + title + } + } + id + refName + refPath + tags + shortSha + commitPath + pipeline { + id + path + user { + webPath + avatarUrl + } + } + stage { + name + } + name + duration + finishedAt + coverage + retryable + playable + cancelable + active + } + } + } +} diff --git a/app/assets/javascripts/jobs/components/table/index.js b/app/assets/javascripts/jobs/components/table/index.js new file mode 100644 index 00000000000..b6b3bb6d379 --- /dev/null +++ b/app/assets/javascripts/jobs/components/table/index.js @@ -0,0 +1,33 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import JobsTableApp from '~/jobs/components/table/jobs_table_app.vue'; +import createDefaultClient from '~/lib/graphql'; + +Vue.use(VueApollo); + +const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), +}); + +export default (containerId = 'js-jobs-table') => { + const containerEl = document.getElementById(containerId); + + if (!containerEl) { + return false; + } + + const { fullPath, jobCounts, jobStatuses } = containerEl.dataset; + + return new Vue({ + el: containerEl, + apolloProvider, + provide: { + fullPath, + jobStatuses: JSON.parse(jobStatuses), + jobCounts: JSON.parse(jobCounts), + }, + render(createElement) { + return createElement(JobsTableApp); + }, + }); +}; diff --git a/app/assets/javascripts/jobs/components/table/jobs_table.vue b/app/assets/javascripts/jobs/components/table/jobs_table.vue new file mode 100644 index 00000000000..32b26d45dfe --- /dev/null +++ b/app/assets/javascripts/jobs/components/table/jobs_table.vue @@ -0,0 +1,67 @@ +<script> +import { GlTable } from '@gitlab/ui'; +import { __ } from '~/locale'; + +const defaultTableClasses = { + tdClass: 'gl-p-5!', + thClass: 'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-100! gl-p-5! gl-border-b-1!', +}; + +export default { + fields: [ + { + key: 'status', + label: __('Status'), + ...defaultTableClasses, + }, + { + key: 'job', + label: __('Job'), + ...defaultTableClasses, + }, + { + key: 'pipeline', + label: __('Pipeline'), + ...defaultTableClasses, + }, + { + key: 'stage', + label: __('Stage'), + ...defaultTableClasses, + }, + { + key: 'name', + label: __('Name'), + ...defaultTableClasses, + }, + { + key: 'duration', + label: __('Duration'), + ...defaultTableClasses, + }, + { + key: 'coverage', + label: __('Coverage'), + ...defaultTableClasses, + }, + { + key: 'actions', + label: '', + ...defaultTableClasses, + }, + ], + components: { + GlTable, + }, + props: { + jobs: { + type: Array, + required: true, + }, + }, +}; +</script> + +<template> + <gl-table :items="jobs" :fields="$options.fields" /> +</template> diff --git a/app/assets/javascripts/jobs/components/table/jobs_table_app.vue b/app/assets/javascripts/jobs/components/table/jobs_table_app.vue new file mode 100644 index 00000000000..55954e31654 --- /dev/null +++ b/app/assets/javascripts/jobs/components/table/jobs_table_app.vue @@ -0,0 +1,85 @@ +<script> +import { GlAlert, GlSkeletonLoader } from '@gitlab/ui'; +import { __ } from '~/locale'; +import GetJobs from './graphql/queries/get_jobs.query.graphql'; +import JobsTable from './jobs_table.vue'; +import JobsTableTabs from './jobs_table_tabs.vue'; + +export default { + i18n: { + errorMsg: __('There was an error fetching the jobs for your project.'), + }, + components: { + GlAlert, + GlSkeletonLoader, + JobsTable, + JobsTableTabs, + }, + inject: { + fullPath: { + default: '', + }, + }, + apollo: { + jobs: { + query: GetJobs, + variables() { + return { + fullPath: this.fullPath, + }; + }, + update({ project }) { + return project?.jobs; + }, + error() { + this.hasError = true; + }, + }, + }, + data() { + return { + jobs: null, + hasError: false, + isAlertDismissed: false, + }; + }, + computed: { + shouldShowAlert() { + return this.hasError && !this.isAlertDismissed; + }, + }, + methods: { + fetchJobsByStatus(scope) { + this.$apollo.queries.jobs.refetch({ statuses: scope }); + }, + }, +}; +</script> + +<template> + <div> + <gl-alert + v-if="shouldShowAlert" + class="gl-mt-2" + variant="danger" + dismissible + @dismiss="isAlertDismissed = true" + > + {{ $options.i18n.errorMsg }} + </gl-alert> + + <jobs-table-tabs @fetchJobsByStatus="fetchJobsByStatus" /> + + <div v-if="$apollo.loading" class="gl-mt-5"> + <gl-skeleton-loader + preserve-aspect-ratio="none" + equal-width-lines + :lines="5" + :width="600" + :height="66" + /> + </div> + + <jobs-table v-else :jobs="jobs.nodes" /> + </div> +</template> diff --git a/app/assets/javascripts/jobs/components/table/jobs_table_tabs.vue b/app/assets/javascripts/jobs/components/table/jobs_table_tabs.vue new file mode 100644 index 00000000000..95d265fce60 --- /dev/null +++ b/app/assets/javascripts/jobs/components/table/jobs_table_tabs.vue @@ -0,0 +1,66 @@ +<script> +import { GlBadge, GlTab, GlTabs } from '@gitlab/ui'; +import { __ } from '~/locale'; + +export default { + components: { + GlBadge, + GlTab, + GlTabs, + }, + inject: { + jobCounts: { + default: {}, + }, + jobStatuses: { + default: {}, + }, + }, + computed: { + tabs() { + return [ + { + text: __('All'), + count: this.jobCounts.all, + scope: null, + testId: 'jobs-all-tab', + }, + { + text: __('Pending'), + count: this.jobCounts.pending, + scope: this.jobStatuses.pending, + testId: 'jobs-pending-tab', + }, + { + text: __('Running'), + count: this.jobCounts.running, + scope: this.jobStatuses.running, + testId: 'jobs-running-tab', + }, + { + text: __('Finished'), + count: this.jobCounts.finished, + scope: [this.jobStatuses.success, this.jobStatuses.failed, this.jobStatuses.canceled], + testId: 'jobs-finished-tab', + }, + ]; + }, + }, +}; +</script> + +<template> + <gl-tabs> + <gl-tab + v-for="tab in tabs" + :key="tab.text" + :title-link-attributes="{ 'data-testid': tab.testId }" + @click="$emit('fetchJobsByStatus', tab.scope)" + > + <template #title> + <span>{{ tab.text }}</span> + <gl-badge size="sm" class="gl-tab-counter-badge">{{ tab.count }}</gl-badge> + </template> + </gl-tab> + </gl-tabs> +</template> diff --git a/app/assets/javascripts/pages/groups/new/fetch_group_path_availability.js b/app/assets/javascripts/pages/groups/new/fetch_group_path_availability.js index 1d68ccd724d..301e0b4f7a2 100644 --- a/app/assets/javascripts/pages/groups/new/fetch_group_path_availability.js +++ b/app/assets/javascripts/pages/groups/new/fetch_group_path_availability.js @@ -1,7 +1,12 @@ +import { buildApiUrl } from '~/api/api_utils'; import axios from '~/lib/utils/axios_utils'; -const rootUrl = gon.relative_url_root; +const NAMESPACE_EXISTS_PATH = '/api/:version/namespaces/:id/exists'; -export default function fetchGroupPathAvailability(groupPath) { - return axios.get(`${rootUrl}/users/${groupPath}/suggests`); +export default function fetchGroupPathAvailability(groupPath, parentId) { + const url = buildApiUrl(NAMESPACE_EXISTS_PATH).replace(':id', encodeURIComponent(groupPath)); + + return axios.get(url, { + params: { parent_id: parentId }, + }); } diff --git a/app/assets/javascripts/pages/groups/new/group_path_validator.js b/app/assets/javascripts/pages/groups/new/group_path_validator.js index 89dccea2812..a0ff98645fb 100644 --- a/app/assets/javascripts/pages/groups/new/group_path_validator.js +++ b/app/assets/javascripts/pages/groups/new/group_path_validator.js @@ -8,6 +8,7 @@ import fetchGroupPathAvailability from './fetch_group_path_availability'; const debounceTimeoutDuration = 1000; const invalidInputClass = 'gl-field-error-outline'; const successInputClass = 'gl-field-success-outline'; +const parentIdSelector = 'group_parent_id'; const successMessageSelector = '.validation-success'; const pendingMessageSelector = '.validation-pending'; const unavailableMessageSelector = '.validation-error'; @@ -20,9 +21,10 @@ export default class GroupPathValidator extends InputValidator { const container = opts.container || ''; const validateElements = document.querySelectorAll(`${container} .js-validate-group-path`); + const parentIdElement = document.getElementById(parentIdSelector); this.debounceValidateInput = debounce((inputDomElement) => { - GroupPathValidator.validateGroupPathInput(inputDomElement); + GroupPathValidator.validateGroupPathInput(inputDomElement, parentIdElement); }, debounceTimeoutDuration); validateElements.forEach((element) => @@ -37,13 +39,14 @@ export default class GroupPathValidator extends InputValidator { this.debounceValidateInput(inputDomElement); } - static validateGroupPathInput(inputDomElement) { + static validateGroupPathInput(inputDomElement, parentIdElement) { const groupPath = inputDomElement.value; + const parentId = parentIdElement.value; if (inputDomElement.checkValidity() && groupPath.length > 1) { GroupPathValidator.setMessageVisibility(inputDomElement, pendingMessageSelector); - fetchGroupPathAvailability(groupPath) + fetchGroupPathAvailability(groupPath, parentId) .then(({ data }) => data) .then((data) => { GroupPathValidator.setInputState(inputDomElement, !data.exists); diff --git a/app/assets/javascripts/pages/groups/new/index.js b/app/assets/javascripts/pages/groups/new/index.js index 322ad2c79e7..569b5afd676 100644 --- a/app/assets/javascripts/pages/groups/new/index.js +++ b/app/assets/javascripts/pages/groups/new/index.js @@ -5,10 +5,8 @@ import Group from '~/group'; import LinkedTabs from '~/lib/utils/bootstrap_linked_tabs'; import GroupPathValidator from './group_path_validator'; -const parentId = $('#group_parent_id'); -if (!parentId.val()) { - new GroupPathValidator(); // eslint-disable-line no-new -} +new GroupPathValidator(); // eslint-disable-line no-new + BindInOut.initAll(); initFilePickers(); diff --git a/app/assets/javascripts/pages/projects/jobs/index/index.js b/app/assets/javascripts/pages/projects/jobs/index/index.js index 681d151b77f..75194499a7f 100644 --- a/app/assets/javascripts/pages/projects/jobs/index/index.js +++ b/app/assets/javascripts/pages/projects/jobs/index/index.js @@ -1,17 +1,23 @@ import Vue from 'vue'; +import initJobsTable from '~/jobs/components/table'; import GlCountdown from '~/vue_shared/components/gl_countdown.vue'; -const remainingTimeElements = document.querySelectorAll('.js-remaining-time'); -remainingTimeElements.forEach( - (el) => - new Vue({ - el, - render(h) { - return h(GlCountdown, { - props: { - endDateString: el.dateTime, - }, - }); - }, - }), -); +if (gon.features?.jobsTableVue) { + initJobsTable(); +} else { + const remainingTimeElements = document.querySelectorAll('.js-remaining-time'); + + remainingTimeElements.forEach( + (el) => + new Vue({ + el, + render(h) { + return h(GlCountdown, { + props: { + endDateString: el.dateTime, + }, + }); + }, + }), + ); +} diff --git a/app/assets/javascripts/tooltips/index.js b/app/assets/javascripts/tooltips/index.js index f60c0759c72..49a43b120e0 100644 --- a/app/assets/javascripts/tooltips/index.js +++ b/app/assets/javascripts/tooltips/index.js @@ -44,10 +44,7 @@ const addTooltips = (elements, config) => { const handleTooltipEvent = (rootTarget, e, selector, config = {}) => { for (let { target } = e; target && target !== rootTarget; target = target.parentNode) { if (isTooltip(target, selector)) { - addTooltips([target], { - show: true, - ...config, - }); + addTooltips([target], config); break; } } diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue index 84a21a25552..6d68c15cf2d 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue @@ -71,11 +71,11 @@ export default { return (this.glFeatures.mergeRequestWidgetGraphql ? this.state : this.mr).targetBranch; }, shouldRemoveSourceBranch() { - if (this.glFeatures.mergeRequestWidgetGraphql) { - return this.state.shouldRemoveSourceBranch || this.state.forceRemoveSourceBranch; - } + if (!this.glFeatures.mergeRequestWidgetGraphql) return this.mr.shouldRemoveSourceBranch; + + if (!this.state.shouldRemoveSourceBranch) return false; - return this.mr.shouldRemoveSourceBranch; + return this.state.shouldRemoveSourceBranch || this.state.forceRemoveSourceBranch; }, autoMergeStrategy() { return (this.glFeatures.mergeRequestWidgetGraphql ? this.state : this.mr).autoMergeStrategy; diff --git a/app/assets/stylesheets/page_bundles/jira_connect.scss b/app/assets/stylesheets/page_bundles/jira_connect.scss index 33a7e8c8dda..db4be3f18e8 100644 --- a/app/assets/stylesheets/page_bundles/jira_connect.scss +++ b/app/assets/stylesheets/page_bundles/jira_connect.scss @@ -8,7 +8,6 @@ // We should only import styles that we actually use. @import '@gitlab/ui/src/components/base/alert/alert'; @import '@gitlab/ui/src/components/base/avatar/avatar'; -@import '@gitlab/ui/src/components/base/badge/badge'; @import '@gitlab/ui/src/components/base/button/button'; @import '@gitlab/ui/src/components/base/icon/icon'; @import '@gitlab/ui/src/components/base/link/link'; @@ -19,22 +18,8 @@ @import '@gitlab/ui/src/components/base/tooltip/tooltip'; @import '@gitlab/ui/src/components/base/search_box_by_type/search_box_by_type'; -$atlaskit-border-color: #dfe1e6; $header-height: 40px; -.subscription-form { - .field-group-input { - display: flex; - padding-top: $gl-padding-4; - - .ak-button { - align-items: center; - height: auto; - margin-left: $btn-margin-5; - } - } -} - .jira-connect-header { min-height: $header-height; position: fixed; @@ -60,41 +45,3 @@ $header-height: 40px; margin-left: auto; margin-right: auto; } - -// for external_link buttons -svg { - fill: currentColor; - - &.s16 { - height: 16px; - width: 16px; - } -} - -.ak-field-group label { - text-align: left; -} - -.ak-button__appearance-primary { - &:hover { - color: $white; - text-decoration: none; - } - - svg { - align-self: center; - margin-left: 4px; - } -} - -.subscriptions { - tbody { - tr { - border-bottom: 1px solid $atlaskit-border-color; - } - - td { - padding: $gl-padding-8; - } - } -} diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb index f19a86209fc..92442fd4e28 100644 --- a/app/controllers/projects/jobs_controller.rb +++ b/app/controllers/projects/jobs_controller.rb @@ -15,6 +15,7 @@ class Projects::JobsController < Projects::ApplicationController before_action :verify_api_request!, only: :terminal_websocket_authorize before_action :authorize_create_proxy_build!, only: :proxy_websocket_authorize before_action :verify_proxy_request!, only: :proxy_websocket_authorize + before_action :push_jobs_table_vue, only: [:index] layout 'project' @@ -256,4 +257,8 @@ class Projects::JobsController < Projects::ApplicationController ::Gitlab::Workhorse.channel_websocket(service) end + + def push_jobs_table_vue + push_frontend_feature_flag(:jobs_table_vue, @project, default_enabled: :yaml) + end end diff --git a/app/helpers/ci/jobs_helper.rb b/app/helpers/ci/jobs_helper.rb index ec17eccf693..a0d169c1358 100644 --- a/app/helpers/ci/jobs_helper.rb +++ b/app/helpers/ci/jobs_helper.rb @@ -18,6 +18,21 @@ module Ci "retry_outdated_job_docs_url" => help_page_path('ci/pipelines/settings', anchor: 'retry-outdated-jobs') } end + + def job_counts + { + "all" => limited_counter_with_delimiter(@all_builds), + "pending" => limited_counter_with_delimiter(@all_builds.pending), + "running" => limited_counter_with_delimiter(@all_builds.running), + "finished" => limited_counter_with_delimiter(@all_builds.finished) + } + end + + def job_statuses + statuses = Ci::HasStatus::AVAILABLE_STATUSES + + statuses.to_h { |status| [status, status.upcase] } + end end end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 393580e0554..455429608b4 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -92,6 +92,8 @@ class Namespace < ApplicationRecord scope :for_user, -> { where('type IS NULL') } scope :sort_by_type, -> { order(Gitlab::Database.nulls_first_order(:type)) } scope :include_route, -> { includes(:route) } + scope :by_parent, -> (parent) { where(parent_id: parent) } + scope :filter_by_path, -> (query) { where('lower(path) = :query', query: query.downcase) } scope :with_statistics, -> do joins('LEFT JOIN project_statistics ps ON ps.namespace_id = namespaces.id') diff --git a/app/views/groups/_activities.html.haml b/app/views/groups/_activities.html.haml index 769455dc951..b1a40bfc96b 100644 --- a/app/views/groups/_activities.html.haml +++ b/app/views/groups/_activities.html.haml @@ -1,7 +1,7 @@ .nav-block.activities = render 'shared/event_filter', show_group_events: @group.supports_events? .controls - = link_to group_path(@group, rss_url_options), class: 'btn gl-button btn-default btn-icon d-none d-sm-inline-flex has-tooltip' , title: 'Subscribe' do + = link_to group_path(@group, rss_url_options), class: 'btn gl-button btn-default btn-icon d-none d-sm-inline-flex has-tooltip' , title: _('Subscribe') do = sprite_icon('rss', css_class: 'qa-rss-icon gl-icon') .content_list diff --git a/app/views/groups/activity.html.haml b/app/views/groups/activity.html.haml index bc75fada937..6ba6dab96ae 100644 --- a/app/views/groups/activity.html.haml +++ b/app/views/groups/activity.html.haml @@ -1,5 +1,5 @@ = content_for :meta_tags do - = auto_discovery_link_tag(:atom, group_url(@group, rss_url_options), title: "#{@group.name} activity") + = auto_discovery_link_tag(:atom, group_url(@group, rss_url_options), title: _("%{group_name} activity") % { group_name: @group.name }) - page_title _("Activity") diff --git a/app/views/groups/milestones/_form.html.haml b/app/views/groups/milestones/_form.html.haml index 52060e2be16..d4d8a7a57ef 100644 --- a/app/views/groups/milestones/_form.html.haml +++ b/app/views/groups/milestones/_form.html.haml @@ -4,23 +4,23 @@ .col-md-6 .form-group.row .col-form-label.col-sm-2 - = f.label :title, "Title" + = f.label :title, _("Title") .col-sm-10 = f.text_field :title, maxlength: 255, class: "form-control", data: { qa_selector: "milestone_title_field" }, required: true, autofocus: true .form-group.row.milestone-description .col-form-label.col-sm-2 - = f.label :description, "Description" + = f.label :description, _("Description") .col-sm-10 = render layout: 'shared/md_preview', locals: { url: group_preview_markdown_path } do - = render 'shared/zen', f: f, attr: :description, classes: 'note-textarea', qa_selector: 'milestone_description_field', placeholder: 'Write milestone description...', supports_autocomplete: false + = render 'shared/zen', f: f, attr: :description, classes: 'note-textarea', qa_selector: 'milestone_description_field', placeholder: _('Write milestone description...'), supports_autocomplete: false .clearfix .error-alert = render "shared/milestones/form_dates", f: f .form-actions - if @milestone.new_record? - = f.submit 'Create milestone', class: "btn-confirm gl-button btn", data: { qa_selector: "create_milestone_button" } - = link_to "Cancel", group_milestones_path(@group), class: "btn gl-button btn-cancel" + = f.submit _('Create milestone'), class: "btn-confirm gl-button btn", data: { qa_selector: "create_milestone_button" } + = link_to _("Cancel"), group_milestones_path(@group), class: "btn gl-button btn-cancel" - else - = f.submit 'Update milestone', class: "btn-confirm gl-button btn" - = link_to "Cancel", group_milestone_path(@group, @milestone), class: "btn gl-button btn-cancel" + = f.submit _('Update milestone'), class: "btn-confirm gl-button btn" + = link_to _("Cancel"), group_milestone_path(@group, @milestone), class: "btn gl-button btn-cancel" diff --git a/app/views/groups/milestones/edit.html.haml b/app/views/groups/milestones/edit.html.haml index c703d5f7f93..187c2d24b56 100644 --- a/app/views/groups/milestones/edit.html.haml +++ b/app/views/groups/milestones/edit.html.haml @@ -4,7 +4,7 @@ - render "header_title" %h3.page-title - Edit Milestone + = _('Edit Milestone') %hr = render "form" diff --git a/app/views/help/instance_configuration/_gitlab_ci.html.haml b/app/views/help/instance_configuration/_gitlab_ci.html.haml index 7fa8bd086d4..53fa3f89873 100644 --- a/app/views/help/instance_configuration/_gitlab_ci.html.haml +++ b/app/views/help/instance_configuration/_gitlab_ci.html.haml @@ -1,24 +1,24 @@ - content_for :table_content do - %li= link_to 'GitLab CI', '#gitlab-ci' + %li= link_to _('GitLab CI'), '#gitlab-ci' - content_for :settings_content do %h2#gitlab-ci - GitLab CI + = _('GitLab CI') %p - Below are the current settings regarding - = succeed('.') { link_to('GitLab CI', 'https://about.gitlab.com/gitlab-ci', target: '_blank') } + = _('Below are the current settings regarding') + = succeed('.') { link_to(_('GitLab CI'), 'https://about.gitlab.com/gitlab-ci', target: '_blank') } .table-responsive %table %thead %tr - %th Setting + %th= _('Setting') %th= instance_configuration_host(@instance_configuration.settings[:host]) - %th Default + %th= _('Default') %tbody %tr - artifacts_size = @instance_configuration.settings[:gitlab_ci][:artifacts_max_size] - %td Artifacts maximum size + %td= _('Artifacts maximum size') %td= instance_configuration_human_size_cell(artifacts_size[:value]) %td= instance_configuration_human_size_cell(artifacts_size[:default]) diff --git a/app/views/profiles/gpg_keys/_form.html.haml b/app/views/profiles/gpg_keys/_form.html.haml index a86de0681f7..9804a3b7735 100644 --- a/app/views/profiles/gpg_keys/_form.html.haml +++ b/app/views/profiles/gpg_keys/_form.html.haml @@ -4,7 +4,7 @@ .form-group = f.label :key, s_('Profiles|Key'), class: 'label-bold' - = f.text_area :key, class: "form-control", rows: 8, required: true, placeholder: _("Don't paste the private part of the GPG key. Paste the public part which begins with '-----BEGIN PGP PUBLIC KEY BLOCK-----'.") + = f.text_area :key, class: "form-control gl-form-input", rows: 8, required: true, placeholder: _("Don't paste the private part of the GPG key. Paste the public part which begins with '-----BEGIN PGP PUBLIC KEY BLOCK-----'.") .gl-mt-3 = f.submit s_('Profiles|Add key'), class: "gl-button btn btn-confirm" diff --git a/app/views/projects/jobs/index.html.haml b/app/views/projects/jobs/index.html.haml index 0f9cf1c511e..f2aab3d9394 100644 --- a/app/views/projects/jobs/index.html.haml +++ b/app/views/projects/jobs/index.html.haml @@ -1,9 +1,12 @@ - page_title _("Jobs") - add_page_specific_style 'page_bundles/ci_status' -.top-area - - build_path_proc = ->(scope) { project_jobs_path(@project, scope: scope) } - = render "shared/builds/tabs", build_path_proc: build_path_proc, all_builds: @all_builds, scope: @scope +- if Feature.enabled?(:jobs_table_vue, @project, default_enabled: :yaml) + #js-jobs-table{ data: { full_path: @project.full_path, job_counts: job_counts.to_json, job_statuses: job_statuses.to_json } } +- else + .top-area + - build_path_proc = ->(scope) { project_jobs_path(@project, scope: scope) } + = render "shared/builds/tabs", build_path_proc: build_path_proc, all_builds: @all_builds, scope: @scope -.content-list.builds-content-list - = render "table", builds: @builds, project: @project + .content-list.builds-content-list + = render "table", builds: @builds, project: @project diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index 92bc1874c97..416cb932ec9 100644 --- a/app/views/projects/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml @@ -26,21 +26,21 @@ = render "projects/merge_requests/tabs/tab", class: "notes-tab", qa_selector: "notes_tab" do = tab_link_for @merge_request, :show, force_link: @commit.present? do = _("Overview") - %span.badge.badge-pill= @merge_request.related_notes.user.count + %span.badge.badge-pill.gl-badge.badge-muted.sm= @merge_request.related_notes.user.count - if @merge_request.source_project = render "projects/merge_requests/tabs/tab", name: "commits", class: "commits-tab", qa_selector: "commits_tab" do = tab_link_for @merge_request, :commits do = _("Commits") - %span.badge.badge-pill= @commits_count + %span.badge.badge-pill.gl-badge.badge-muted.sm= @commits_count - if number_of_pipelines.nonzero? = render "projects/merge_requests/tabs/tab", name: "pipelines", class: "pipelines-tab" do = tab_link_for @merge_request, :pipelines do = _("Pipelines") - %span.badge.badge-pill.js-pipelines-mr-count= number_of_pipelines + %span.badge.badge-pill.gl-badge.badge-muted.sm.js-pipelines-mr-count= number_of_pipelines = render "projects/merge_requests/tabs/tab", name: "diffs", class: "diffs-tab", id: "diffs-tab", qa_selector: "diffs_tab" do = tab_link_for @merge_request, :diffs do = _("Changes") - %span.badge.badge-pill= @merge_request.diff_size + %span.badge.badge-pill.gl-badge.badge-muted.sm= @merge_request.diff_size .d-flex.flex-wrap.align-items-center.justify-content-lg-end #js-vue-discussion-counter diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml index a3d6a2c8e04..cff50eef88b 100644 --- a/app/views/shared/issuable/_nav.html.haml +++ b/app/views/shared/issuable/_nav.html.haml @@ -1,6 +1,6 @@ - type = local_assigns.fetch(:type, :issues) - page_context_word = type.to_s.humanize(capitalize: false) -- display_count = local_assigns.fetch(:display_count, :true) +- display_count = local_assigns.fetch(:display_count, true) %ul.nav-links.issues-state-filters.mobile-separator.nav.nav-tabs %li{ class: active_when(params[:state] == 'opened') }> |