diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-30 12:09:36 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-30 12:09:36 +0300 |
commit | 491c773c7296a6e989d021c139d1bbce447bf709 (patch) | |
tree | d22f8b0f3aa11a61dab456aaf184b7be9be2e714 | |
parent | 0629b103246d6c8b24e753f62ccfc8649df2c315 (diff) |
Add latest changes from gitlab-org/gitlab@master
40 files changed, 854 insertions, 354 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 4c1cd9518f4..416385915b6 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -8e731456707441f9e22bfb3b668885f0f983c449 +a8e416c51179f80bb9fe213c657f14d950279c75 @@ -133,8 +133,10 @@ gem 'seed-fu', '~> 2.3.7' gem 'elasticsearch-model', '~> 6.1' gem 'elasticsearch-rails', '~> 6.1', require: 'elasticsearch/rails/instrumentation' gem 'elasticsearch-api', '~> 6.8' -gem 'aws-sdk' -gem 'faraday_middleware-aws-signers-v4' +gem 'aws-sdk-core', '~> 3' +gem 'aws-sdk-cloudformation', '~> 1' +gem 'aws-sdk-s3', '~> 1' +gem 'faraday_middleware-aws-sigv4', '~>0.3.0' # Markdown and HTML processing gem 'html-pipeline', '~> 2.12' diff --git a/Gemfile.lock b/Gemfile.lock index 15b0faa02ee..3cce9f9a1e0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -93,16 +93,25 @@ GEM encryptor (~> 3.0.0) attr_required (1.0.1) awesome_print (1.8.0) - aws-eventstream (1.0.3) - aws-sdk (2.11.374) - aws-sdk-resources (= 2.11.374) - aws-sdk-core (2.11.374) - aws-sigv4 (~> 1.0) + aws-eventstream (1.1.0) + aws-partitions (1.345.0) + aws-sdk-cloudformation (1.41.0) + aws-sdk-core (~> 3, >= 3.99.0) + aws-sigv4 (~> 1.1) + aws-sdk-core (3.104.3) + aws-eventstream (~> 1, >= 1.0.2) + aws-partitions (~> 1, >= 1.239.0) + aws-sigv4 (~> 1.1) jmespath (~> 1.0) - aws-sdk-resources (2.11.374) - aws-sdk-core (= 2.11.374) - aws-sigv4 (1.1.0) - aws-eventstream (~> 1.0, >= 1.0.2) + aws-sdk-kms (1.36.0) + aws-sdk-core (~> 3, >= 3.99.0) + aws-sigv4 (~> 1.1) + aws-sdk-s3 (1.75.0) + aws-sdk-core (~> 3, >= 3.104.1) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.1) + aws-sigv4 (1.2.1) + aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.2) base32 (0.3.2) batch-loader (1.4.0) @@ -306,9 +315,9 @@ GEM faraday (~> 0.8) faraday_middleware (0.14.0) faraday (>= 0.7.4, < 1.0) - faraday_middleware-aws-signers-v4 (0.1.7) - aws-sdk-resources (~> 2) - faraday (~> 0.9) + faraday_middleware-aws-sigv4 (0.3.0) + aws-sigv4 (~> 1.0) + faraday (>= 0.15) faraday_middleware-multi_json (0.0.6) faraday_middleware multi_json @@ -1183,7 +1192,9 @@ DEPENDENCIES atlassian-jwt (~> 0.2.0) attr_encrypted (~> 3.1.0) awesome_print - aws-sdk + aws-sdk-cloudformation (~> 1) + aws-sdk-core (~> 3) + aws-sdk-s3 (~> 1) babosa (~> 1.0.2) base32 (~> 0.3.0) batch-loader (~> 1.4.0) @@ -1230,7 +1241,7 @@ DEPENDENCIES escape_utils (~> 1.1) factory_bot_rails (~> 5.1.0) faraday (~> 0.12) - faraday_middleware-aws-signers-v4 + faraday_middleware-aws-sigv4 (~> 0.3.0) fast_blank ffaker (~> 2.10) flipper (~> 0.17.1) diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue index 3bba4fbc906..77b319aa2de 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue @@ -138,7 +138,7 @@ export default { @input="updateCommitMessage" @submit="commit" /> - <div class="clearfix prepend-top-15"> + <div class="clearfix gl-mt-5"> <actions /> <loading-button :loading="submitCommitLoading" diff --git a/app/assets/javascripts/monitoring/components/dashboard_panel_builder.vue b/app/assets/javascripts/monitoring/components/dashboard_panel_builder.vue new file mode 100644 index 00000000000..7e6ebfe26c0 --- /dev/null +++ b/app/assets/javascripts/monitoring/components/dashboard_panel_builder.vue @@ -0,0 +1,92 @@ +<script> +import { mapActions, mapState } from 'vuex'; +import { GlCard, GlForm, GlFormGroup, GlFormTextarea, GlButton, GlAlert } from '@gitlab/ui'; +import DashboardPanel from './dashboard_panel.vue'; + +const initialYml = `title: +y_label: +type: area-chart +metrics: +- query_range: + label: +`; + +export default { + components: { + GlCard, + GlForm, + GlFormGroup, + GlFormTextarea, + GlButton, + GlAlert, + DashboardPanel, + }, + data() { + return { + yml: initialYml, + }; + }, + computed: { + ...mapState('monitoringDashboard', [ + 'panelPreviewIsLoading', + 'panelPreviewError', + 'panelPreviewGraphData', + ]), + }, + methods: { + ...mapActions('monitoringDashboard', ['fetchPanelPreview']), + onSubmit() { + this.fetchPanelPreview(this.yml); + }, + }, +}; +</script> +<template> + <div> + <gl-card> + <template #header> + <h2 class="gl-font-size-h2 gl-my-3">{{ s__('Metrics|Define and preview panel') }}</h2> + </template> + <template #default> + <gl-form @submit.prevent="onSubmit"> + <gl-form-group + :label="s__('Metrics|Panel YAML')" + :description="s__('Metrics|Define panel YAML to preview panel.')" + label-for="panel-yml-input" + > + <gl-form-textarea + id="panel-yml-input" + v-model="yml" + class="gl-h-200! gl-font-monospace! gl-font-size-monospace!" + /> + </gl-form-group> + <div class="gl-text-right"> + <gl-button + ref="clipboardCopyBtn" + variant="success" + category="secondary" + :data-clipboard-text="yml" + @click="$toast.show(s__('Metrics|Panel YAML copied'))" + > + {{ s__('Metrics|Copy YAML') }} + </gl-button> + <gl-button + type="submit" + variant="success" + :disabled="panelPreviewIsLoading" + class="js-no-auto-disable" + > + {{ s__('Metrics|Preview panel') }} + </gl-button> + </div> + </gl-form> + </template> + </gl-card> + + <gl-alert v-if="panelPreviewError" variant="warning" :dismissible="false"> + {{ panelPreviewError }} + </gl-alert> + + <dashboard-panel :graph-data="panelPreviewGraphData" /> + </div> +</template> diff --git a/app/assets/javascripts/monitoring/pages/panel_new_page.vue b/app/assets/javascripts/monitoring/pages/panel_new_page.vue index d2ca0bf02d4..8ff6adb47ca 100644 --- a/app/assets/javascripts/monitoring/pages/panel_new_page.vue +++ b/app/assets/javascripts/monitoring/pages/panel_new_page.vue @@ -1,33 +1,45 @@ <script> +import { mapState } from 'vuex'; import { GlButton, GlTooltipDirective } from '@gitlab/ui'; import { s__ } from '~/locale'; -import routes from '../router/constants'; +import { DASHBOARD_PAGE } from '../router/constants'; +import DashboardPanelBuilder from '../components/dashboard_panel_builder.vue'; export default { components: { GlButton, + DashboardPanelBuilder, }, directives: { GlTooltip: GlTooltipDirective, }, + computed: { + ...mapState('monitoringDashboard', ['panelPreviewYml']), + dashboardPageLocation() { + return { + ...this.$route, + name: DASHBOARD_PAGE, + }; + }, + }, i18n: { backToDashboard: s__('Metrics|Back to dashboard'), }, - routes, }; </script> <template> - <div class="gl-display-flex gl-align-items-baseline"> - <gl-button - v-gl-tooltip - icon="go-back" - :to="{ name: $options.routes.DASHBOARD_PAGE, params: { dashboard: $route.params.dashboard } }" - :aria-label="$options.i18n.backToDashboard" - :title="$options.i18n.backToDashboard" - class="gl-mr-5" - /> - <h1 class="gl-mt-5 gl-font-size-h1">{{ s__('Metrics|Add panel') }}</h1> - - <!-- TODO: Add components. See https://gitlab.com/groups/gitlab-org/-/epics/2882 --> + <div class="gl-mt-5"> + <div class="gl-display-flex gl-align-items-baseline gl-mb-5"> + <gl-button + v-gl-tooltip + icon="go-back" + :to="dashboardPageLocation" + :aria-label="$options.i18n.backToDashboard" + :title="$options.i18n.backToDashboard" + class="gl-mr-5" + /> + <h1 class="gl-font-size-h1 gl-my-0">{{ s__('Metrics|Add panel') }}</h1> + </div> + <dashboard-panel-builder /> </div> </template> diff --git a/app/assets/javascripts/monitoring/requests/index.js b/app/assets/javascripts/monitoring/requests/index.js index 2be8048327f..962620862e6 100644 --- a/app/assets/javascripts/monitoring/requests/index.js +++ b/app/assets/javascripts/monitoring/requests/index.js @@ -41,3 +41,12 @@ export const getPrometheusQueryData = (prometheusEndpoint, params) => } throw error; }); + +// eslint-disable-next-line no-unused-vars +export function getPanelJson(panelPreviewEndpoint, panelPreviewYml) { + // TODO Use a real backend when it's available + // https://gitlab.com/gitlab-org/gitlab/-/issues/228758 + + // eslint-disable-next-line @gitlab/require-i18n-strings + return Promise.reject(new Error('API Not implemented.')); +} diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js index 466d791f9b2..0c8f97f2e29 100644 --- a/app/assets/javascripts/monitoring/stores/actions.js +++ b/app/assets/javascripts/monitoring/stores/actions.js @@ -15,7 +15,7 @@ import getAnnotations from '../queries/getAnnotations.query.graphql'; import getDashboardValidationWarnings from '../queries/getDashboardValidationWarnings.query.graphql'; import { convertObjectPropsToCamelCase } from '../../lib/utils/common_utils'; import { s__, sprintf } from '../../locale'; -import { getDashboard, getPrometheusQueryData } from '../requests'; +import { getDashboard, getPrometheusQueryData, getPanelJson } from '../requests'; import { ENVIRONMENT_AVAILABLE_STATE, DEFAULT_DASHBOARD_PATH, VARIABLE_TYPES } from '../constants'; @@ -473,3 +473,30 @@ export const fetchVariableMetricLabelValues = ({ state, commit }, { defaultQuery return Promise.all(optionsRequests); }; + +// Panel Builder + +export const fetchPanelPreview = ({ state, commit, dispatch }, panelPreviewYml) => { + if (!panelPreviewYml) { + return null; + } + + commit(types.REQUEST_PANEL_PREVIEW, panelPreviewYml); + return getPanelJson(state.panelPreviewEndpoint, panelPreviewYml) + .then(data => { + commit(types.RECEIVE_PANEL_PREVIEW_SUCCESS, data); + + dispatch('fetchPanelPreviewMetrics'); + }) + .catch(error => { + commit(types.RECEIVE_PANEL_PREVIEW_FAILURE, error); + }); +}; + +export const fetchPanelPreviewMetrics = () => { + // TODO Use a axios mock instead of spy when backend is implemented + // https://gitlab.com/gitlab-org/gitlab/-/issues/228758 + + // eslint-disable-next-line @gitlab/require-i18n-strings + throw new Error('Not implemented'); +}; diff --git a/app/assets/javascripts/monitoring/stores/mutation_types.js b/app/assets/javascripts/monitoring/stores/mutation_types.js index d408628fc4d..657ceea8644 100644 --- a/app/assets/javascripts/monitoring/stores/mutation_types.js +++ b/app/assets/javascripts/monitoring/stores/mutation_types.js @@ -46,3 +46,8 @@ export const SET_SHOW_ERROR_BANNER = 'SET_SHOW_ERROR_BANNER'; export const SET_PANEL_GROUP_METRICS = 'SET_PANEL_GROUP_METRICS'; export const SET_ENVIRONMENTS_FILTER = 'SET_ENVIRONMENTS_FILTER'; export const SET_EXPANDED_PANEL = 'SET_EXPANDED_PANEL'; + +// Panel preview +export const REQUEST_PANEL_PREVIEW = 'REQUEST_PANEL_PREVIEW'; +export const RECEIVE_PANEL_PREVIEW_SUCCESS = 'RECEIVE_PANEL_PREVIEW_SUCCESS'; +export const RECEIVE_PANEL_PREVIEW_FAILURE = 'RECEIVE_PANEL_PREVIEW_FAILURE'; diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js index 744441c8935..d71b9eaa928 100644 --- a/app/assets/javascripts/monitoring/stores/mutations.js +++ b/app/assets/javascripts/monitoring/stores/mutations.js @@ -1,9 +1,9 @@ import Vue from 'vue'; import { pick } from 'lodash'; import * as types from './mutation_types'; -import { mapToDashboardViewModel, normalizeQueryResponseData } from './utils'; +import { mapToDashboardViewModel, mapPanelToViewModel, normalizeQueryResponseData } from './utils'; import httpStatusCodes from '~/lib/utils/http_status'; -import { BACKOFF_TIMEOUT } from '../../lib/utils/common_utils'; +import { BACKOFF_TIMEOUT } from '~/lib/utils/common_utils'; import { dashboardEmptyStates, endpointKeys, initialStateKeys, metricStates } from '../constants'; import { optionsFromSeriesData } from './variable_mapping'; @@ -218,4 +218,24 @@ export default { // Add new options with assign to ensure Vue reactivity Object.assign(variable.options, { values }); }, + + [types.REQUEST_PANEL_PREVIEW](state, panelPreviewYml) { + state.panelPreviewIsLoading = true; + + state.panelPreviewYml = panelPreviewYml; + state.panelPreviewGraphData = null; + state.panelPreviewError = null; + }, + [types.RECEIVE_PANEL_PREVIEW_SUCCESS](state, payload) { + state.panelPreviewIsLoading = false; + + state.panelPreviewGraphData = mapPanelToViewModel(payload); + state.panelPreviewError = null; + }, + [types.RECEIVE_PANEL_PREVIEW_FAILURE](state, error) { + state.panelPreviewIsLoading = false; + + state.panelPreviewGraphData = null; + state.panelPreviewError = error; + }, }; diff --git a/app/assets/javascripts/monitoring/stores/state.js b/app/assets/javascripts/monitoring/stores/state.js index 89738756ffe..dbd4a68ed1d 100644 --- a/app/assets/javascripts/monitoring/stores/state.js +++ b/app/assets/javascripts/monitoring/stores/state.js @@ -59,6 +59,13 @@ export default () => ({ * via the dashboard yml file. */ links: [], + + // Panel editor / builder + panelPreviewYml: '', + panelPreviewIsLoading: false, + panelPreviewGraphData: null, + panelPreviewError: null, + // Other project data dashboardTimezone: timezones.LOCAL, annotations: [], diff --git a/app/assets/javascripts/notes/components/discussion_actions.vue b/app/assets/javascripts/notes/components/discussion_actions.vue index 251199f1778..878a748e99a 100644 --- a/app/assets/javascripts/notes/components/discussion_actions.vue +++ b/app/assets/javascripts/notes/components/discussion_actions.vue @@ -3,6 +3,7 @@ import ReplyPlaceholder from './discussion_reply_placeholder.vue'; import ResolveDiscussionButton from './discussion_resolve_button.vue'; import ResolveWithIssueButton from './discussion_resolve_with_issue_button.vue'; import JumpToNextDiscussionButton from './discussion_jump_to_next_button.vue'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; export default { name: 'DiscussionActions', @@ -12,6 +13,7 @@ export default { ResolveWithIssueButton, JumpToNextDiscussionButton, }, + mixins: [glFeatureFlagsMixin()], props: { discussion: { type: Object, @@ -36,6 +38,9 @@ export default { }, }, computed: { + hideJumpToNextUnresolvedInThreads() { + return this.glFeatures.hideJumpToNextUnresolvedInThreads; + }, resolvableNotes() { return this.discussion.notes.filter(x => x.resolvable); }, @@ -70,7 +75,11 @@ export default { /> </div> <div - v-if="discussion.resolvable && shouldShowJumpToNextDiscussion" + v-if=" + !hideJumpToNextUnresolvedInThreads && + discussion.resolvable && + shouldShowJumpToNextDiscussion + " class="btn-group discussion-actions ml-sm-2" > <jump-to-next-discussion-button :from-discussion-id="discussion.id" /> diff --git a/app/assets/javascripts/packages/details/components/app.vue b/app/assets/javascripts/packages/details/components/app.vue index 8d1d3a786dc..d2b2610892f 100644 --- a/app/assets/javascripts/packages/details/components/app.vue +++ b/app/assets/javascripts/packages/details/components/app.vue @@ -2,7 +2,6 @@ import { GlBadge, GlButton, - GlIcon, GlModal, GlModalDirective, GlTooltipDirective, @@ -27,6 +26,7 @@ import PackageListRow from '../../shared/components/package_list_row.vue'; import DependencyRow from './dependency_row.vue'; import { numberToHumanSize } from '~/lib/utils/number_utils'; import timeagoMixin from '~/vue_shared/mixins/timeago'; +import FileIcon from '~/vue_shared/components/file_icon.vue'; import { generatePackageInfo } from '../utils'; import { __, s__ } from '~/locale'; import { PackageType, TrackingActions } from '../../shared/constants'; @@ -44,7 +44,7 @@ export default { GlTab, GlTabs, GlTable, - GlIcon, + FileIcon, GlSprintf, PackageActivity, PackageInformation, @@ -243,19 +243,24 @@ export default { <package-activity /> + <h3 class="gl-font-lg">{{ __('Files') }}</h3> <gl-table :fields="$options.filesTableHeaderFields" :items="filesTableRows" tbody-tr-class="js-file-row" > <template #cell(name)="items"> - <gl-icon name="doc-code" class="space-right" /> <gl-link :href="items.item.downloadPath" - class="js-file-download" + class="js-file-download gl-relative" @click="track($options.trackingActions.PULL_PACKAGE)" > - {{ items.item.name }} + <file-icon + :file-name="items.item.name" + css-classes="gl-relative file-icon" + class="gl-mr-1 gl-relative" + /> + <span class="gl-relative">{{ items.item.name }}</span> </gl-link> </template> diff --git a/app/assets/stylesheets/fontawesome_custom.scss b/app/assets/stylesheets/fontawesome_custom.scss index 1fadc618bd1..6070ab92974 100644 --- a/app/assets/stylesheets/fontawesome_custom.scss +++ b/app/assets/stylesheets/fontawesome_custom.scss @@ -260,10 +260,6 @@ content: '\f0a3'; } -.fa-minus-circle::before { - content: '\f056'; -} - .fa-bitbucket::before { content: '\f171'; } diff --git a/app/assets/stylesheets/framework/ci_variable_list.scss b/app/assets/stylesheets/framework/ci_variable_list.scss index cae7b9b5e46..b9c8395dd37 100644 --- a/app/assets/stylesheets/framework/ci_variable_list.scss +++ b/app/assets/stylesheets/framework/ci_variable_list.scss @@ -86,7 +86,6 @@ height: $input-height; padding: 0; background: transparent; - border: 0; color: $gl-text-color-secondary; &:hover, diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss index 38842ec167e..f99efa743c0 100644 --- a/app/assets/stylesheets/utilities.scss +++ b/app/assets/stylesheets/utilities.scss @@ -82,6 +82,10 @@ .gl-h-32 { height: px-to-rem($grid-size * 4); } .gl-h-64 { height: px-to-rem($grid-size * 8); } +// Migrate this to Gitlab UI when FF is removed +// https://gitlab.com/groups/gitlab-org/-/epics/2882 +.gl-h-200\! { height: px-to-rem($grid-size * 25) !important; } + .d-sm-table-column { @include media-breakpoint-up(sm) { display: table-column !important; diff --git a/app/models/service.rb b/app/models/service.rb index 1edf1cb5830..f6e9761b8c2 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -10,6 +10,7 @@ class Service < ApplicationRecord include IgnorableColumns ignore_columns %i[title description], remove_with: '13.4', remove_after: '2020-09-22' + ignore_columns %i[default], remove_with: '13.5', remove_after: '2020-10-22' SERVICE_NAMES = %w[ alerts asana assembla bamboo bugzilla buildkite campfire confluence custom_issue_tracker discord diff --git a/app/services/admin/propagate_integration_service.rb b/app/services/admin/propagate_integration_service.rb index e21bb03ed68..9a5ce58ee2c 100644 --- a/app/services/admin/propagate_integration_service.rb +++ b/app/services/admin/propagate_integration_service.rb @@ -96,7 +96,7 @@ module Admin # rubocop: disable CodeReuse/ActiveRecord def run_callbacks(batch) - if active_external_issue_tracker? + if integration.issue_tracker? Project.where(id: batch).update_all(has_external_issue_tracker: true) end @@ -106,10 +106,6 @@ module Admin end # rubocop: enable CodeReuse/ActiveRecord - def active_external_issue_tracker? - integration.issue_tracker? && !integration.default - end - def active_external_wiki? integration.type == 'ExternalWikiService' end diff --git a/app/services/projects/propagate_service_template.rb b/app/services/projects/propagate_service_template.rb index b6465810fde..54d09b354a1 100644 --- a/app/services/projects/propagate_service_template.rb +++ b/app/services/projects/propagate_service_template.rb @@ -66,7 +66,7 @@ module Projects # rubocop: disable CodeReuse/ActiveRecord def run_callbacks(batch) - if active_external_issue_tracker? + if template.issue_tracker? Project.where(id: batch).update_all(has_external_issue_tracker: true) end @@ -76,10 +76,6 @@ module Projects end # rubocop: enable CodeReuse/ActiveRecord - def active_external_issue_tracker? - template.issue_tracker? && !template.default - end - def active_external_wiki? template.type == 'ExternalWikiService' end diff --git a/app/views/ci/variables/_url_query_variable_row.html.haml b/app/views/ci/variables/_url_query_variable_row.html.haml index 6672a8e5ea0..4c6eeb17c07 100644 --- a/app/views/ci/variables/_url_query_variable_row.html.haml +++ b/app/views/ci/variables/_url_query_variable_row.html.haml @@ -24,5 +24,5 @@ name: value_input_name, placeholder: s_('CiVariables|Input variable value') } = value - %button.js-row-remove-button.ci-variable-row-remove-button.table-section.section-5.border-top-0{ type: 'button', 'aria-label': s_('CiVariables|Remove variable row') } - = icon('minus-circle') + %button.btn.btn-svg.btn-item-remove.js-row-remove-button.ci-variable-row-remove-button.table-section{ type: 'button', 'aria-label': s_('CiVariables|Remove variable row') } + = sprite_icon('close') diff --git a/app/views/ci/variables/_variable_row.html.haml b/app/views/ci/variables/_variable_row.html.haml index c69a3adb0e9..542a41c2f7d 100644 --- a/app/views/ci/variables/_variable_row.html.haml +++ b/app/views/ci/variables/_variable_row.html.haml @@ -60,5 +60,5 @@ value: is_masked, data: { default: is_masked_default.to_s } } = render_if_exists 'ci/variables/environment_scope', form_field: form_field, variable: variable - %button.js-row-remove-button.ci-variable-row-remove-button.table-section.section-5.border-top-0{ type: 'button', 'aria-label': s_('CiVariables|Remove variable row') } - = icon('minus-circle') + %button.btn.btn-svg.js-row-remove-button.ci-variable-row-remove-button.table-section{ type: 'button', 'aria-label': s_('CiVariables|Remove variable row') } + = sprite_icon('close') diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml index e99d0ac1105..6cf48f89876 100644 --- a/app/views/devise/shared/_omniauth_box.html.haml +++ b/app/views/devise/shared/_omniauth_box.html.haml @@ -1,6 +1,6 @@ - hide_remember_me = local_assigns.fetch(:hide_remember_me, false) -.omniauth-container.prepend-top-15 +.omniauth-container.gl-mt-5 %label.label-bold.d-block Sign in with - providers = enabled_button_based_providers diff --git a/app/views/groups/settings/ci_cd/_auto_devops_form.html.haml b/app/views/groups/settings/ci_cd/_auto_devops_form.html.haml index e7efc0237c8..2b5019222f8 100644 --- a/app/views/groups/settings/ci_cd/_auto_devops_form.html.haml +++ b/app/views/groups/settings/ci_cd/_auto_devops_form.html.haml @@ -12,4 +12,4 @@ .form-text.text-muted = s_('GroupSettings|The Auto DevOps pipeline will run if no alternative CI configuration file is found.') = link_to _('More information'), help_page_path('topics/autodevops/index.md'), target: '_blank' - = f.submit _('Save changes'), class: 'btn btn-success prepend-top-15' + = f.submit _('Save changes'), class: 'btn btn-success gl-mt-5' diff --git a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml index 2786621352f..b4c9e51f53a 100644 --- a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml +++ b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml @@ -54,4 +54,4 @@ = s_('CICD|Automatic deployment to staging, manual deployment to production') = link_to icon('question-circle'), help_page_path('topics/autodevops/customize.md', anchor: 'incremental-rollout-to-production-premium'), target: '_blank' - = f.submit _('Save changes'), class: "btn btn-success prepend-top-15", data: { qa_selector: 'save_changes_button' } + = f.submit _('Save changes'), class: "btn btn-success gl-mt-5", data: { qa_selector: 'save_changes_button' } diff --git a/changelogs/unreleased/225652-update-file-table-in-package-details-ui.yml b/changelogs/unreleased/225652-update-file-table-in-package-details-ui.yml new file mode 100644 index 00000000000..94efe16e08f --- /dev/null +++ b/changelogs/unreleased/225652-update-file-table-in-package-details-ui.yml @@ -0,0 +1,5 @@ +--- +title: Updates to file table in package details UI +merge_request: 36723 +author: Adam Alvis (@adamalvis) +type: fixed diff --git a/doc/api/README.md b/doc/api/README.md index a7f549a3f42..f640149b248 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -156,7 +156,7 @@ for example, without needing to explicitly pass an access token. With a few API endpoints you can use a [GitLab CI/CD job token](../user/project/new_ci_build_permissions_model.md#job-token) to authenticate with the API: -- [Get job artifacts](jobs.md#get-job-artifacts) +- [Get job artifacts](job_artifacts.md#get-job-artifacts) - [Pipeline triggers](pipeline_triggers.md) - [Release creation](releases/index.md#create-a-release) diff --git a/doc/api/job_artifacts.md b/doc/api/job_artifacts.md new file mode 100644 index 00000000000..5df7915ad5c --- /dev/null +++ b/doc/api/job_artifacts.md @@ -0,0 +1,265 @@ +# Job Artifacts API + +## Get job artifacts + +> The use of `CI_JOB_TOKEN` in the artifacts download API was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/2346) in [GitLab Premium](https://about.gitlab.com/pricing/) 9.5. + +Get the job's artifacts zipped archive of a project. + +```plaintext +GET /projects/:id/jobs/:job_id/artifacts +``` + +| Attribute | Type | Required | Description | +|-------------|----------------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | +| `job_id` | integer | yes | ID of a job. | +| `job_token` **(PREMIUM)** | string | no | To be used with [triggers](../ci/triggers/README.md#when-a-pipeline-depends-on-the-artifacts-of-another-pipeline-premium) for multi-project pipelines. It should be invoked only inside `.gitlab-ci.yml`. Its value is always `$CI_JOB_TOKEN`. | + +Example request using the `PRIVATE-TOKEN` header: + +```shell +curl --output artifacts.zip --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/jobs/42/artifacts" +``` + +To use this in a [`script` definition](../ci/yaml/README.md#script) inside +`.gitlab-ci.yml` **(PREMIUM)**, you can use either: + +- The `JOB-TOKEN` header with the GitLab-provided `CI_JOB_TOKEN` variable. + For example, the following job will download the artifacts of the job with ID + `42`. Note that the command is wrapped into single quotes since it contains a + colon (`:`): + + ```yaml + artifact_download: + stage: test + script: + - 'curl --location --output artifacts.zip --header "JOB-TOKEN: $CI_JOB_TOKEN" "https://gitlab.example.com/api/v4/projects/1/jobs/42/artifacts"' + ``` + +- Or the `job_token` attribute with the GitLab-provided `CI_JOB_TOKEN` variable. + For example, the following job will download the artifacts of the job with ID `42`: + + ```yaml + artifact_download: + stage: test + script: + - 'curl --location --output artifacts.zip "https://gitlab.example.com/api/v4/projects/1/jobs/42/artifacts?job_token=$CI_JOB_TOKEN"' + ``` + +Possible response status codes: + +| Status | Description | +|-----------|---------------------------------| +| 200 | Serves the artifacts file. | +| 404 | Build not found or no artifacts.| + +## Download the artifacts archive + +> The use of `CI_JOB_TOKEN` in the artifacts download API was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/2346) in [GitLab Premium](https://about.gitlab.com/pricing/) 9.5. + +Download the artifacts zipped archive from the latest successful pipeline for +the given reference name and job, provided the job finished successfully. This +is the same as [getting the job's artifacts](#get-job-artifacts), but by +defining the job's name instead of its ID. + +```plaintext +GET /projects/:id/jobs/artifacts/:ref_name/download?job=name +``` + +Parameters + +| Attribute | Type | Required | Description | +|-------------|----------------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | +| `ref_name` | string | yes | Branch or tag name in repository. HEAD or SHA references are not supported. | +| `job` | string | yes | The name of the job. | +| `job_token` **(PREMIUM)** | string | no | To be used with [triggers](../ci/triggers/README.md#when-a-pipeline-depends-on-the-artifacts-of-another-pipeline-premium) for multi-project pipelines. It should be invoked only inside `.gitlab-ci.yml`. Its value is always `$CI_JOB_TOKEN`. | + +Example request using the `PRIVATE-TOKEN` header: + +```shell +curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/jobs/artifacts/master/download?job=test" +``` + +To use this in a [`script` definition](../ci/yaml/README.md#script) inside +`.gitlab-ci.yml` **(PREMIUM)**, you can use either: + +- The `JOB-TOKEN` header with the GitLab-provided `CI_JOB_TOKEN` variable. + For example, the following job will download the artifacts of the `test` job + of the `master` branch. Note that the command is wrapped into single quotes + since it contains a colon (`:`): + + ```yaml + artifact_download: + stage: test + script: + - 'curl --location --output artifacts.zip --header "JOB-TOKEN: $CI_JOB_TOKEN" "https://gitlab.example.com/api/v4/projects/$CI_PROJECT_ID/jobs/artifacts/master/download?job=test"' + ``` + +- Or the `job_token` attribute with the GitLab-provided `CI_JOB_TOKEN` variable. + For example, the following job will download the artifacts of the `test` job + of the `master` branch: + + ```yaml + artifact_download: + stage: test + script: + - 'curl --location --output artifacts.zip "https://gitlab.example.com/api/v4/projects/$CI_PROJECT_ID/jobs/artifacts/master/download?job=test&job_token=$CI_JOB_TOKEN"' + ``` + +Possible response status codes: + +| Status | Description | +|-----------|---------------------------------| +| 200 | Serves the artifacts file. | +| 404 | Build not found or no artifacts.| + +## Download a single artifact file by job ID + +> Introduced in GitLab 10.0 + +Download a single artifact file from a job with a specified ID from within +the job's artifacts zipped archive. The file is extracted from the archive and +streamed to the client. + +```plaintext +GET /projects/:id/jobs/:job_id/artifacts/*artifact_path +``` + +Parameters + +| Attribute | Type | Required | Description | +|-----------------|----------------|----------|------------------------------------------------------------------------------------------------------------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | +| `job_id` | integer | yes | The unique job identifier. | +| `artifact_path` | string | yes | Path to a file inside the artifacts archive. | + +Example request: + +```shell +curl --location --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/jobs/5/artifacts/some/release/file.pdf" +``` + +Possible response status codes: + +| Status | Description | +|-----------|--------------------------------------| +| 200 | Sends a single artifact file | +| 400 | Invalid path provided | +| 404 | Build not found or no file/artifacts | + +## Download a single artifact file from specific tag or branch + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/23538) in GitLab 11.5. + +Download a single artifact file for a specific job of the latest successful +pipeline for the given reference name from within the job's artifacts archive. +The file is extracted from the archive and streamed to the client. + +```plaintext +GET /projects/:id/jobs/artifacts/:ref_name/raw/*artifact_path?job=name +``` + +Parameters: + +| Attribute | Type | Required | Description | +|-----------------|----------------|----------|------------------------------------------------------------------------------------------------------------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | +| `ref_name` | string | yes | Branch or tag name in repository. HEAD or SHA references are not supported. | +| `artifact_path` | string | yes | Path to a file inside the artifacts archive. | +| `job` | string | yes | The name of the job. | + +Example request: + +```shell +curl --location --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/jobs/artifacts/master/raw/some/release/file.pdf?job=pdf" +``` + +Possible response status codes: + +| Status | Description | +|-----------|--------------------------------------| +| 200 | Sends a single artifact file | +| 400 | Invalid path provided | +| 404 | Build not found or no file/artifacts | + +## Keep artifacts + +Prevents artifacts from being deleted when expiration is set. + +```plaintext +POST /projects/:id/jobs/:job_id/artifacts/keep +``` + +Parameters + +| Attribute | Type | Required | Description | +|-----------|----------------|----------|------------------------------------------------------------------------------------------------------------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | +| `job_id` | integer | yes | ID of a job. | + +Example request: + +```shell +curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/jobs/1/artifacts/keep" +``` + +Example response: + +```json +{ + "commit": { + "author_email": "admin@example.com", + "author_name": "Administrator", + "created_at": "2015-12-24T16:51:14.000+01:00", + "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd", + "message": "Test the CI integration.", + "short_id": "0ff3ae19", + "title": "Test the CI integration." + }, + "coverage": null, + "allow_failure": false, + "download_url": null, + "id": 42, + "name": "rubocop", + "ref": "master", + "artifacts": [], + "runner": null, + "stage": "test", + "created_at": "2016-01-11T10:13:33.506Z", + "started_at": "2016-01-11T10:13:33.506Z", + "finished_at": "2016-01-11T10:15:10.506Z", + "duration": 97.0, + "status": "failed", + "tag": false, + "web_url": "https://example.com/foo/bar/-/jobs/42", + "user": null +} +``` + +## Delete artifacts + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/25522) in GitLab 11.9. + +Delete artifacts of a job. + +```plaintext +DELETE /projects/:id/jobs/:job_id/artifacts +``` + +| Attribute | Type | Required | Description | +|-----------|----------------|----------|------------------------------------------------------------------------------------------------------------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | +| `job_id` | integer | yes | ID of a job. | + +Example request: + +```shell +curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/jobs/1/artifacts" +``` + +NOTE: **Note:** +At least Maintainer role is required to delete artifacts. + +If the artifacts were deleted successfully, a response with status `204 No Content` is returned. diff --git a/doc/api/jobs.md b/doc/api/jobs.md index ef192dae329..3b23c6a7917 100644 --- a/doc/api/jobs.md +++ b/doc/api/jobs.md @@ -428,198 +428,6 @@ Example of response } ``` -## Get job artifacts - -> **Notes**: -> -> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/2893) in GitLab 8.5. -> - The use of `CI_JOB_TOKEN` in the artifacts download API was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/2346) -> in [GitLab Premium](https://about.gitlab.com/pricing/) 9.5. - -Get the job's artifacts zipped archive of a project. - -```plaintext -GET /projects/:id/jobs/:job_id/artifacts -``` - -| Attribute | Type | Required | Description | -|-------------|----------------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------| -| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | -| `job_id` | integer | yes | ID of a job. | -| `job_token` **(PREMIUM)** | string | no | To be used with [triggers](../ci/triggers/README.md#when-a-pipeline-depends-on-the-artifacts-of-another-pipeline-premium) for multi-project pipelines. It should be invoked only inside `.gitlab-ci.yml`. Its value is always `$CI_JOB_TOKEN`. | - -Example request using the `PRIVATE-TOKEN` header: - -```shell -curl --output artifacts.zip --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/jobs/42/artifacts" -``` - -To use this in a [`script` definition](../ci/yaml/README.md#script) inside -`.gitlab-ci.yml` **(PREMIUM)**, you can use either: - -- The `JOB-TOKEN` header with the GitLab-provided `CI_JOB_TOKEN` variable. - For example, the following job will download the artifacts of the job with ID - `42`. Note that the command is wrapped into single quotes since it contains a - colon (`:`): - - ```yaml - artifact_download: - stage: test - script: - - 'curl --location --output artifacts.zip --header "JOB-TOKEN: $CI_JOB_TOKEN" "https://gitlab.example.com/api/v4/projects/1/jobs/42/artifacts"' - ``` - -- Or the `job_token` attribute with the GitLab-provided `CI_JOB_TOKEN` variable. - For example, the following job will download the artifacts of the job with ID `42`: - - ```yaml - artifact_download: - stage: test - script: - - 'curl --location --output artifacts.zip "https://gitlab.example.com/api/v4/projects/1/jobs/42/artifacts?job_token=$CI_JOB_TOKEN"' - ``` - -Possible response status codes: - -| Status | Description | -|-----------|---------------------------------| -| 200 | Serves the artifacts file. | -| 404 | Build not found or no artifacts.| - -## Download the artifacts archive - -> **Notes**: -> -> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/5347) in GitLab 8.10. -> - The use of `CI_JOB_TOKEN` in the artifacts download API was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/2346) -> in [GitLab Premium](https://about.gitlab.com/pricing/) 9.5. - -Download the artifacts zipped archive from the latest successful pipeline for -the given reference name and job, provided the job finished successfully. This -is the same as [getting the job's artifacts](#get-job-artifacts), but by -defining the job's name instead of its ID. - -```plaintext -GET /projects/:id/jobs/artifacts/:ref_name/download?job=name -``` - -Parameters - -| Attribute | Type | Required | Description | -|-------------|----------------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------| -| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | -| `ref_name` | string | yes | Branch or tag name in repository. HEAD or SHA references are not supported. | -| `job` | string | yes | The name of the job. | -| `job_token` **(PREMIUM)** | string | no | To be used with [triggers](../ci/triggers/README.md#when-a-pipeline-depends-on-the-artifacts-of-another-pipeline-premium) for multi-project pipelines. It should be invoked only inside `.gitlab-ci.yml`. Its value is always `$CI_JOB_TOKEN`. | - -Example request using the `PRIVATE-TOKEN` header: - -```shell -curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/jobs/artifacts/master/download?job=test" -``` - -To use this in a [`script` definition](../ci/yaml/README.md#script) inside -`.gitlab-ci.yml` **(PREMIUM)**, you can use either: - -- The `JOB-TOKEN` header with the GitLab-provided `CI_JOB_TOKEN` variable. - For example, the following job will download the artifacts of the `test` job - of the `master` branch. Note that the command is wrapped into single quotes - since it contains a colon (`:`): - - ```yaml - artifact_download: - stage: test - script: - - 'curl --location --output artifacts.zip --header "JOB-TOKEN: $CI_JOB_TOKEN" "https://gitlab.example.com/api/v4/projects/$CI_PROJECT_ID/jobs/artifacts/master/download?job=test"' - ``` - -- Or the `job_token` attribute with the GitLab-provided `CI_JOB_TOKEN` variable. - For example, the following job will download the artifacts of the `test` job - of the `master` branch: - - ```yaml - artifact_download: - stage: test - script: - - 'curl --location --output artifacts.zip "https://gitlab.example.com/api/v4/projects/$CI_PROJECT_ID/jobs/artifacts/master/download?job=test&job_token=$CI_JOB_TOKEN"' - ``` - -Possible response status codes: - -| Status | Description | -|-----------|---------------------------------| -| 200 | Serves the artifacts file. | -| 404 | Build not found or no artifacts.| - -## Download a single artifact file by job ID - -> Introduced in GitLab 10.0 - -Download a single artifact file from a job with a specified ID from within -the job's artifacts zipped archive. The file is extracted from the archive and -streamed to the client. - -```plaintext -GET /projects/:id/jobs/:job_id/artifacts/*artifact_path -``` - -Parameters - -| Attribute | Type | Required | Description | -|-----------------|----------------|----------|------------------------------------------------------------------------------------------------------------------| -| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | -| `job_id` | integer | yes | The unique job identifier. | -| `artifact_path` | string | yes | Path to a file inside the artifacts archive. | - -Example request: - -```shell -curl --location --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/jobs/5/artifacts/some/release/file.pdf" -``` - -Possible response status codes: - -| Status | Description | -|-----------|--------------------------------------| -| 200 | Sends a single artifact file | -| 400 | Invalid path provided | -| 404 | Build not found or no file/artifacts | - -## Download a single artifact file from specific tag or branch - -> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/23538) in GitLab 11.5. - -Download a single artifact file for a specific job of the latest successful -pipeline for the given reference name from within the job's artifacts archive. -The file is extracted from the archive and streamed to the client. - -```plaintext -GET /projects/:id/jobs/artifacts/:ref_name/raw/*artifact_path?job=name -``` - -Parameters: - -| Attribute | Type | Required | Description | -|-----------------|----------------|----------|------------------------------------------------------------------------------------------------------------------| -| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | -| `ref_name` | string | yes | Branch or tag name in repository. HEAD or SHA references are not supported. | -| `artifact_path` | string | yes | Path to a file inside the artifacts archive. | -| `job` | string | yes | The name of the job. | - -Example request: - -```shell -curl --location --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/jobs/artifacts/master/raw/some/release/file.pdf?job=pdf" -``` - -Possible response status codes: - -| Status | Description | -|-----------|--------------------------------------| -| 200 | Sends a single artifact file | -| 400 | Invalid path provided | -| 404 | Build not found or no file/artifacts | - ## Get a log file Get a log (trace) of a specific job of a project: @@ -796,86 +604,6 @@ Example of response } ``` -## Keep artifacts - -Prevents artifacts from being deleted when expiration is set. - -```plaintext -POST /projects/:id/jobs/:job_id/artifacts/keep -``` - -Parameters - -| Attribute | Type | Required | Description | -|-----------|----------------|----------|------------------------------------------------------------------------------------------------------------------| -| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | -| `job_id` | integer | yes | ID of a job. | - -Example request: - -```shell -curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/jobs/1/artifacts/keep" -``` - -Example response: - -```json -{ - "commit": { - "author_email": "admin@example.com", - "author_name": "Administrator", - "created_at": "2015-12-24T16:51:14.000+01:00", - "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd", - "message": "Test the CI integration.", - "short_id": "0ff3ae19", - "title": "Test the CI integration." - }, - "coverage": null, - "allow_failure": false, - "download_url": null, - "id": 42, - "name": "rubocop", - "ref": "master", - "artifacts": [], - "runner": null, - "stage": "test", - "created_at": "2016-01-11T10:13:33.506Z", - "started_at": "2016-01-11T10:13:33.506Z", - "finished_at": "2016-01-11T10:15:10.506Z", - "duration": 97.0, - "status": "failed", - "tag": false, - "web_url": "https://example.com/foo/bar/-/jobs/42", - "user": null -} -``` - -## Delete artifacts - -> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/25522) in GitLab 11.9. - -Delete artifacts of a job. - -```plaintext -DELETE /projects/:id/jobs/:job_id/artifacts -``` - -| Attribute | Type | Required | Description | -|-----------|----------------|----------|------------------------------------------------------------------------------------------------------------------| -| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | -| `job_id` | integer | yes | ID of a job. | - -Example request: - -```shell -curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/jobs/1/artifacts" -``` - -NOTE: **Note:** -At least Maintainer role is required to delete artifacts. - -If the artifacts were deleted successfully, a response with status `204 No Content` is returned. - ## Play a job Triggers a manual action to start a job. diff --git a/doc/ci/pipelines/job_artifacts.md b/doc/ci/pipelines/job_artifacts.md index a099dc371d2..3d034da58a6 100644 --- a/doc/ci/pipelines/job_artifacts.md +++ b/doc/ci/pipelines/job_artifacts.md @@ -17,7 +17,7 @@ Job artifacts are a list of files and directories created by a job once it finishes. This feature is [enabled by default](../../administration/job_artifacts.md) in all GitLab installations. -Job artifacts created by GitLab Runner are uploaded to GitLab and are downloadable as a single archive using the GitLab UI or the [GitLab API](../../api/jobs.md#get-job-artifacts). +Job artifacts created by GitLab Runner are uploaded to GitLab and are downloadable as a single archive using the GitLab UI or the [GitLab API](../../api/job_artifacts.md#get-job-artifacts). <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> For an overview, watch the video [GitLab CI Pipeline, Artifacts, and Environments](https://www.youtube.com/watch?v=PCKDICEe10s). @@ -439,7 +439,7 @@ To erase a job: ## Retrieve artifacts of private projects when using GitLab CI -In order to retrieve a job artifact of a different project, you might need to use a private token in order to [authenticate and download](../../api/jobs.md#get-job-artifacts) the artifacts. +In order to retrieve a job artifact of a different project, you might need to use a private token in order to [authenticate and download](../../api/job_artifacts.md#get-job-artifacts) the artifacts. <!-- ## Troubleshooting diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md index 8be43d77a0e..0d237a1f530 100644 --- a/doc/ci/quick_start/README.md +++ b/doc/ci/quick_start/README.md @@ -69,11 +69,15 @@ blog about it](https://about.gitlab.com/blog/2015/05/06/why-were-replacing-gitla ### Creating a simple `.gitlab-ci.yml` file NOTE: **Note:** -`.gitlab-ci.yml` is a [YAML](https://en.wikipedia.org/wiki/YAML) file -so you have to pay extra attention to indentation. Always use spaces, not tabs. +A GitLab team member has made an [unofficial visual pipeline editor](https://unofficial.gitlab.tools/visual-pipelines/). +There is a [plan to make it an official part of GitLab](https://gitlab.com/groups/gitlab-org/-/epics/4069) +in the future, but it's available for anyone who wants to try it at the above link. You need to create a file named `.gitlab-ci.yml` in the root directory of your -repository. Below is an example for a Ruby on Rails project. +repository. This is a [YAML](https://en.wikipedia.org/wiki/YAML) file +so you have to pay extra attention to indentation. Always use spaces, not tabs. + +Below is an example for a Ruby on Rails project: ```yaml image: "ruby:2.5" diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md index c88c55d8556..df0516e5fd2 100644 --- a/doc/ci/triggers/README.md +++ b/doc/ci/triggers/README.md @@ -97,7 +97,7 @@ This allows you to use that for multi-project pipelines and download artifacts from any project to which you have access as this follows the same principles with the [permission model](../../user/permissions.md#job-permissions). -Read more about the [jobs API](../../api/jobs.md#download-the-artifacts-archive). +Read more about the [jobs API](../../api/job_artifacts.md#download-the-artifacts-archive). ## Adding a new trigger diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 7108b76b5f8..1d18d2e7c28 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -1365,7 +1365,7 @@ check the value of `$CI_COMMIT_BEFORE_SHA`. It has a value of `0000000000000000000000000000000000000000`: - In branches with no commits. -- Tag pipelines and scheduled pipelines. You should define rules very +- In tag pipelines and scheduled pipelines. You should define rules very narrowly if you don't want to skip these. To skip pipelines on all empty branches, but also tags and schedules: diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 5cc034c0e04..42a49d4afc4 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -14999,6 +14999,9 @@ msgstr "" msgid "Metrics|Check out the CI/CD documentation on deploying to an environment" msgstr "" +msgid "Metrics|Copy YAML" +msgstr "" + msgid "Metrics|Create custom dashboard %{fileName}" msgstr "" @@ -15017,6 +15020,12 @@ msgstr "" msgid "Metrics|Current" msgstr "" +msgid "Metrics|Define and preview panel" +msgstr "" + +msgid "Metrics|Define panel YAML to preview panel." +msgstr "" + msgid "Metrics|Delete metric" msgstr "" @@ -15085,6 +15094,15 @@ msgstr "" msgid "Metrics|Open repository" msgstr "" +msgid "Metrics|Panel YAML" +msgstr "" + +msgid "Metrics|Panel YAML copied" +msgstr "" + +msgid "Metrics|Preview panel" +msgstr "" + msgid "Metrics|PromQL query is valid" msgstr "" diff --git a/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js b/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js new file mode 100644 index 00000000000..cf7a1673a10 --- /dev/null +++ b/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js @@ -0,0 +1,146 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlCard, GlForm, GlFormTextarea, GlAlert } from '@gitlab/ui'; +import { createStore } from '~/monitoring/stores'; +import DashboardPanel from '~/monitoring/components/dashboard_panel.vue'; +import * as types from '~/monitoring/stores/mutation_types'; +import { metricsDashboardResponse } from '../fixture_data'; + +import DashboardPanelBuilder from '~/monitoring/components/dashboard_panel_builder.vue'; + +const mockPanel = metricsDashboardResponse.dashboard.panel_groups[0].panels[0]; + +describe('dashboard invalid url parameters', () => { + let store; + let wrapper; + let mockShowToast; + + const createComponent = (props = {}, options = {}) => { + wrapper = shallowMount(DashboardPanelBuilder, { + propsData: { ...props }, + store, + stubs: { + GlCard, + }, + mocks: { + $toast: { + show: mockShowToast, + }, + }, + options, + }); + }; + + const findForm = () => wrapper.find(GlForm); + const findTxtArea = () => findForm().find(GlFormTextarea); + const findSubmitBtn = () => findForm().find('[type="submit"]'); + const findClipboardCopyBtn = () => wrapper.find({ ref: 'clipboardCopyBtn' }); + const findPanel = () => wrapper.find(DashboardPanel); + + beforeEach(() => { + mockShowToast = jest.fn(); + store = createStore(); + createComponent(); + jest.spyOn(store, 'dispatch').mockResolvedValue(); + }); + + afterEach(() => {}); + + it('is mounted', () => { + expect(wrapper.exists()).toBe(true); + }); + + it('displays an empty dashboard panel', () => { + expect(findPanel().exists()).toBe(true); + expect(findPanel().props('graphData')).toBe(null); + }); + + it('does not fetch initial data by default', () => { + expect(store.dispatch).not.toHaveBeenCalled(); + }); + + describe('yml form', () => { + it('form exists and can be submitted', () => { + expect(findForm().exists()).toBe(true); + expect(findSubmitBtn().exists()).toBe(true); + expect(findSubmitBtn().is('[disabled]')).toBe(false); + }); + + it('form has a text area with a default value', () => { + expect(findTxtArea().exists()).toBe(true); + + const value = findTxtArea().attributes('value'); + + // Panel definition should contain a title and a type + expect(value).toContain('title:'); + expect(value).toContain('type:'); + }); + + it('"copy to clipboard" button works', () => { + findClipboardCopyBtn().vm.$emit('click'); + const clipboardText = findClipboardCopyBtn().attributes('data-clipboard-text'); + + expect(clipboardText).toContain('title:'); + expect(clipboardText).toContain('type:'); + + expect(mockShowToast).toHaveBeenCalledTimes(1); + }); + + it('on submit fetches a panel preview', () => { + findForm().vm.$emit('submit', new Event('submit')); + + return wrapper.vm.$nextTick().then(() => { + expect(store.dispatch).toHaveBeenCalledWith( + 'monitoringDashboard/fetchPanelPreview', + expect.stringContaining('title:'), + ); + }); + }); + + describe('when form is submitted', () => { + beforeEach(() => { + store.commit(`monitoringDashboard/${types.REQUEST_PANEL_PREVIEW}`, 'mock yml content'); + return wrapper.vm.$nextTick(); + }); + + it('submit button is disabled', () => { + expect(findSubmitBtn().is('[disabled]')).toBe(true); + }); + }); + }); + + describe('when there is an error', () => { + const mockError = 'an error ocurred!'; + + beforeEach(() => { + store.commit(`monitoringDashboard/${types.RECEIVE_PANEL_PREVIEW_FAILURE}`, mockError); + return wrapper.vm.$nextTick(); + }); + + it('displays an alert', () => { + expect(wrapper.find(GlAlert).exists()).toBe(true); + expect(wrapper.find(GlAlert).text()).toBe(mockError); + }); + + it('displays an empty dashboard panel', () => { + expect(findPanel().props('graphData')).toBe(null); + }); + }); + + describe('when panel data is available', () => { + beforeEach(() => { + store.commit(`monitoringDashboard/${types.RECEIVE_PANEL_PREVIEW_SUCCESS}`, mockPanel); + return wrapper.vm.$nextTick(); + }); + + it('displays no alert', () => { + expect(wrapper.find(GlAlert).exists()).toBe(false); + }); + + it('displays panel with data', () => { + const { title, type } = wrapper.find(DashboardPanel).props('graphData'); + + expect(title).toBe(mockPanel.title); + expect(type).toBe(mockPanel.type); + }); + }); +}); diff --git a/spec/frontend/monitoring/pages/panel_new_page_spec.js b/spec/frontend/monitoring/pages/panel_new_page_spec.js index 665040a0bfd..83365b754d9 100644 --- a/spec/frontend/monitoring/pages/panel_new_page_spec.js +++ b/spec/frontend/monitoring/pages/panel_new_page_spec.js @@ -1,6 +1,9 @@ import { shallowMount } from '@vue/test-utils'; import { GlButton } from '@gitlab/ui'; -import { DASHBOARD_PAGE } from '~/monitoring/router/constants'; +import { DASHBOARD_PAGE, PANEL_NEW_PAGE } from '~/monitoring/router/constants'; +import { createStore } from '~/monitoring/stores'; +import DashboardPanelBuilder from '~/monitoring/components/dashboard_panel_builder.vue'; + import PanelNewPage from '~/monitoring/pages/panel_new_page.vue'; const dashboard = 'dashboard.yml'; @@ -15,26 +18,37 @@ const GlButtonStub = { }; describe('monitoring/pages/panel_new_page', () => { + let store; let wrapper; let $route; + let $router; - const mountComponent = (propsData = {}, routeParams = { dashboard }) => { - $route = { - params: routeParams, + const mountComponent = (propsData = {}, route) => { + $route = route ?? { name: PANEL_NEW_PAGE, params: { dashboard } }; + $router = { + push: jest.fn(), }; wrapper = shallowMount(PanelNewPage, { propsData, + store, stubs: { GlButton: GlButtonStub, }, mocks: { + $router, $route, }, }); }; const findBackButton = () => wrapper.find(GlButtonStub); + const findPanelBuilder = () => wrapper.find(DashboardPanelBuilder); + + beforeEach(() => { + store = createStore(); + mountComponent(); + }); afterEach(() => { wrapper.destroy(); @@ -42,18 +56,43 @@ describe('monitoring/pages/panel_new_page', () => { describe('back to dashboard button', () => { it('is rendered', () => { - mountComponent(); expect(findBackButton().exists()).toBe(true); expect(findBackButton().props('icon')).toBe('go-back'); }); it('links back to the dashboard', () => { - const dashboardLocation = { + expect(findBackButton().props('to')).toEqual({ name: DASHBOARD_PAGE, params: { dashboard }, + }); + }); + + it('links back to the dashboard while preserving query params', () => { + $route = { + name: PANEL_NEW_PAGE, + params: { dashboard }, + query: { another: 'param' }, }; - expect(findBackButton().props('to')).toEqual(dashboardLocation); + mountComponent({}, $route); + + expect(findBackButton().props('to')).toEqual({ + name: DASHBOARD_PAGE, + params: { dashboard }, + query: { another: 'param' }, + }); + }); + }); + + describe('dashboard panel builder', () => { + it('is rendered', () => { + expect(findPanelBuilder().exists()).toBe(true); + }); + }); + + describe('page routing', () => { + it('route is not updated by default', () => { + expect($router.push).not.toHaveBeenCalled(); }); }); }); diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js index a6d6accbe23..4b7b733720c 100644 --- a/spec/frontend/monitoring/store/actions_spec.js +++ b/spec/frontend/monitoring/store/actions_spec.js @@ -9,6 +9,7 @@ import { defaultTimeRange } from '~/vue_shared/constants'; import * as getters from '~/monitoring/stores/getters'; import { ENVIRONMENT_AVAILABLE_STATE } from '~/monitoring/constants'; import { backoffMockImplementation } from 'jest/helpers/backoff_helper'; +import * as requests from '~/monitoring/requests'; import { createStore } from '~/monitoring/stores'; import * as types from '~/monitoring/stores/mutation_types'; @@ -31,6 +32,7 @@ import { duplicateSystemDashboard, updateVariablesAndFetchData, fetchVariableMetricLabelValues, + fetchPanelPreview, } from '~/monitoring/stores/actions'; import { gqClient, @@ -1154,4 +1156,56 @@ describe('Monitoring store actions', () => { ); }); }); + + describe('fetchPanelPreview', () => { + const mockYmlContent = 'mock yml content'; + + it('should not commit or dispatch if payload is empty', () => { + testAction(fetchPanelPreview, '', state, [], []); + }); + + it('should store the yml content and panel in the store and fetch corresponding metrics', () => { + const mockPanel = { + title: 'title', + type: 'area-chart', + }; + + // TODO Use a axios mock instead of spy when backend is implemented + // https://gitlab.com/gitlab-org/gitlab/-/issues/228758 + jest.spyOn(requests, 'getPanelJson').mockResolvedValue(mockPanel); + + testAction( + fetchPanelPreview, + 'mock yml content', + state, + [ + { type: types.REQUEST_PANEL_PREVIEW, payload: mockYmlContent }, + { type: types.RECEIVE_PANEL_PREVIEW_SUCCESS, payload: mockPanel }, + ], + [ + { + type: 'fetchPanelPreviewMetrics', + }, + ], + ); + }); + + it('should commit a failure when backend fails', () => { + const mockError = 'error'; + // TODO Use a axios mock instead of spy when backend is implemented + // https://gitlab.com/gitlab-org/gitlab/-/issues/228758 + jest.spyOn(requests, 'getPanelJson').mockRejectedValue(mockError); + + testAction( + fetchPanelPreview, + mockYmlContent, + state, + [ + { type: types.REQUEST_PANEL_PREVIEW, payload: mockYmlContent }, + { type: types.RECEIVE_PANEL_PREVIEW_FAILURE, payload: mockError }, + ], + [], + ); + }); + }); }); diff --git a/spec/frontend/monitoring/store/mutations_spec.js b/spec/frontend/monitoring/store/mutations_spec.js index 14b38d79aa2..8ffd23641a4 100644 --- a/spec/frontend/monitoring/store/mutations_spec.js +++ b/spec/frontend/monitoring/store/mutations_spec.js @@ -488,4 +488,42 @@ describe('Monitoring mutations', () => { }); }); }); + + describe('REQUEST_PANEL_PREVIEW', () => { + it('saves yml content and resets other preview data', () => { + const mockYmlContent = 'mock yml content'; + mutations[types.REQUEST_PANEL_PREVIEW](stateCopy, mockYmlContent); + + expect(stateCopy.panelPreviewIsLoading).toBe(true); + expect(stateCopy.panelPreviewYml).toBe(mockYmlContent); + expect(stateCopy.panelPreviewGraphData).toBe(null); + expect(stateCopy.panelPreviewError).toBe(null); + }); + }); + + describe('RECEIVE_PANEL_PREVIEW_SUCCESS', () => { + it('saves graph data', () => { + mutations[types.RECEIVE_PANEL_PREVIEW_SUCCESS](stateCopy, { + title: 'My Title', + type: 'area-chart', + }); + + expect(stateCopy.panelPreviewIsLoading).toBe(false); + expect(stateCopy.panelPreviewGraphData).toMatchObject({ + title: 'My Title', + type: 'area-chart', + }); + expect(stateCopy.panelPreviewError).toBe(null); + }); + }); + + describe('RECEIVE_PANEL_PREVIEW_FAILURE', () => { + it('saves graph data', () => { + mutations[types.RECEIVE_PANEL_PREVIEW_FAILURE](stateCopy, 'Error!'); + + expect(stateCopy.panelPreviewIsLoading).toBe(false); + expect(stateCopy.panelPreviewGraphData).toBe(null); + expect(stateCopy.panelPreviewError).toBe('Error!'); + }); + }); }); diff --git a/spec/frontend/notes/components/discussion_actions_spec.js b/spec/frontend/notes/components/discussion_actions_spec.js index 44dc148933c..3e1e43d0c6a 100644 --- a/spec/frontend/notes/components/discussion_actions_spec.js +++ b/spec/frontend/notes/components/discussion_actions_spec.js @@ -21,7 +21,7 @@ const createUnallowedNote = () => describe('DiscussionActions', () => { let wrapper; - const createComponentFactory = (shallow = true) => props => { + const createComponentFactory = (shallow = true) => (props, options) => { const store = createStore(); const mountFn = shallow ? shallowMount : mount; @@ -35,6 +35,11 @@ describe('DiscussionActions', () => { shouldShowJumpToNextDiscussion: true, ...props, }, + provide: { + glFeatures: { + hideJumpToNextUnresolvedInThreads: options?.hideJumpToNextUnresolvedInThreads, + }, + }, }); }; @@ -96,6 +101,13 @@ describe('DiscussionActions', () => { }); }); + it('does not render jump to next discussion button if feature flag is enabled', () => { + const createComponent = createComponentFactory(); + createComponent({}, { hideJumpToNextUnresolvedInThreads: true }); + + expect(wrapper.find(JumpToNextDiscussionButton).exists()).toBe(false); + }); + describe('events handling', () => { const createComponent = createComponentFactory(false); diff --git a/spec/services/admin/propagate_integration_service_spec.rb b/spec/services/admin/propagate_integration_service_spec.rb index 843b78a41e9..2e879cf06d1 100644 --- a/spec/services/admin/propagate_integration_service_spec.rb +++ b/spec/services/admin/propagate_integration_service_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' RSpec.describe Admin::PropagateIntegrationService do describe '.propagate' do - let(:excluded_attributes) { %w[id project_id inherit_from_id instance created_at updated_at title description] } + let(:excluded_attributes) { %w[id project_id inherit_from_id instance created_at updated_at default] } let!(:project) { create(:project) } let!(:instance_integration) do JiraService.create!( diff --git a/spec/services/projects/propagate_service_template_spec.rb b/spec/services/projects/propagate_service_template_spec.rb index 266bf2cc213..df69e5a29fb 100644 --- a/spec/services/projects/propagate_service_template_spec.rb +++ b/spec/services/projects/propagate_service_template_spec.rb @@ -20,7 +20,7 @@ RSpec.describe Projects::PropagateServiceTemplate do end let!(:project) { create(:project) } - let(:excluded_attributes) { %w[id project_id template created_at updated_at title description] } + let(:excluded_attributes) { %w[id project_id template created_at updated_at default] } it 'creates services for projects' do expect(project.pushover_service).to be_nil @@ -120,7 +120,7 @@ RSpec.describe Projects::PropagateServiceTemplate do describe 'external tracker' do it 'updates the project external tracker' do - service_template.update!(category: 'issue_tracker', default: false) + service_template.update!(category: 'issue_tracker') expect { described_class.propagate(service_template) } .to change { project.reload.has_external_issue_tracker }.to(true) |