Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
dad16033c2
commit
0b789f95a3
58 changed files with 589 additions and 178 deletions
|
@ -1 +1 @@
|
|||
1f98d5a94c880e3e556ae3ace095f83e44f002fb
|
||||
86aa7ee82a5dd241fd7d4b33435da0a7ecad12b0
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
<script>
|
||||
import { GlListbox } from '@gitlab/ui';
|
||||
import { s__ } from '~/locale';
|
||||
import { setUrlParams, visitUrl } from '~/lib/utils/url_utility';
|
||||
|
||||
export default {
|
||||
name: 'BackgroundMigrationsDatabaseListbox',
|
||||
i18n: {
|
||||
database: s__('BackgroundMigrations|Database'),
|
||||
},
|
||||
components: {
|
||||
GlListbox,
|
||||
},
|
||||
props: {
|
||||
databases: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
selectedDatabase: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selected: this.selectedDatabase,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
selectDatabase(database) {
|
||||
visitUrl(setUrlParams({ database }));
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gl-display-flex gl-align-items-center" data-testid="database-listbox">
|
||||
<label id="label" class="gl-font-weight-bold gl-mr-4 gl-mb-0">{{
|
||||
$options.i18n.database
|
||||
}}</label>
|
||||
<gl-listbox
|
||||
v-model="selected"
|
||||
:items="databases"
|
||||
right
|
||||
:toggle-text="selectedDatabase"
|
||||
aria-labelledby="label"
|
||||
@select="selectDatabase"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
38
app/assets/javascripts/admin/background_migrations/index.js
Normal file
38
app/assets/javascripts/admin/background_migrations/index.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
import Vue from 'vue';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import Translate from '~/vue_shared/translate';
|
||||
import BackgroundMigrationsDatabaseListbox from './components/database_listbox.vue';
|
||||
|
||||
Vue.use(Translate);
|
||||
|
||||
export const initBackgroundMigrationsApp = () => {
|
||||
const el = document.getElementById('js-database-listbox');
|
||||
|
||||
if (!el) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { selectedDatabase } = el.dataset;
|
||||
let { databases } = el.dataset;
|
||||
|
||||
try {
|
||||
databases = JSON.parse(databases).map((database) => ({
|
||||
value: database,
|
||||
text: database,
|
||||
}));
|
||||
} catch (e) {
|
||||
Sentry.captureException(e);
|
||||
}
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
render(createElement) {
|
||||
return createElement(BackgroundMigrationsDatabaseListbox, {
|
||||
props: {
|
||||
databases,
|
||||
selectedDatabase,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
|
@ -72,7 +72,7 @@ export default {
|
|||
async applySelectedLanguage(language) {
|
||||
this.selectedLanguage = language;
|
||||
|
||||
await codeBlockLanguageLoader.loadLanguages([language.syntax]);
|
||||
await codeBlockLanguageLoader.loadLanguage(language.syntax);
|
||||
|
||||
this.tiptapEditor.commands.setCodeBlock({ language: this.selectedLanguage.syntax });
|
||||
},
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
<script>
|
||||
import { NodeViewWrapper, NodeViewContent } from '@tiptap/vue-2';
|
||||
import { __ } from '~/locale';
|
||||
import codeBlockLanguageLoader from '../../services/code_block_language_loader';
|
||||
|
||||
export default {
|
||||
name: 'FrontMatter',
|
||||
name: 'CodeBlock',
|
||||
components: {
|
||||
NodeViewWrapper,
|
||||
NodeViewContent,
|
||||
|
@ -13,6 +14,16 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
updateAttributes: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
const lang = codeBlockLanguageLoader.findLanguageBySyntax(this.node.attrs.language);
|
||||
await codeBlockLanguageLoader.loadLanguage(lang.syntax);
|
||||
|
||||
this.updateAttributes({ language: this.node.attrs.language });
|
||||
},
|
||||
i18n: {
|
||||
frontmatter: __('frontmatter'),
|
||||
|
@ -22,6 +33,7 @@ export default {
|
|||
<template>
|
||||
<node-view-wrapper class="content-editor-code-block gl-relative code highlight" as="pre">
|
||||
<span
|
||||
v-if="node.attrs.isFrontmatter"
|
||||
data-testid="frontmatter-label"
|
||||
class="gl-absolute gl-top-0 gl-right-3"
|
||||
contenteditable="false"
|
|
@ -1,6 +1,8 @@
|
|||
import { CodeBlockLowlight } from '@tiptap/extension-code-block-lowlight';
|
||||
import { textblockTypeInputRule } from '@tiptap/core';
|
||||
import codeBlockLanguageLoader from '../services/code_block_language_loader';
|
||||
import { VueNodeViewRenderer } from '@tiptap/vue-2';
|
||||
import languageLoader from '../services/code_block_language_loader';
|
||||
import CodeBlockWrapper from '../components/wrappers/code_block.vue';
|
||||
|
||||
const extractLanguage = (element) => element.getAttribute('lang');
|
||||
export const backtickInputRegex = /^```([a-z]+)?[\s\n]$/;
|
||||
|
@ -9,14 +11,6 @@ export const tildeInputRegex = /^~~~([a-z]+)?[\s\n]$/;
|
|||
export default CodeBlockLowlight.extend({
|
||||
isolating: true,
|
||||
exitOnArrowDown: false,
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
languageLoader: codeBlockLanguageLoader,
|
||||
};
|
||||
},
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
language: {
|
||||
|
@ -30,7 +24,6 @@ export default CodeBlockLowlight.extend({
|
|||
};
|
||||
},
|
||||
addInputRules() {
|
||||
const { languageLoader } = this.options;
|
||||
const getAttributes = (match) => languageLoader?.loadLanguageFromInputRule(match) || {};
|
||||
|
||||
return [
|
||||
|
@ -65,4 +58,8 @@ export default CodeBlockLowlight.extend({
|
|||
['code', {}, 0],
|
||||
];
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return new VueNodeViewRenderer(CodeBlockWrapper);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -14,6 +14,9 @@ export default CodeBlockHighlight.extend({
|
|||
return element.dataset.diagram;
|
||||
},
|
||||
},
|
||||
isDiagram: {
|
||||
default: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
|
|
|
@ -1,10 +1,18 @@
|
|||
import { VueNodeViewRenderer } from '@tiptap/vue-2';
|
||||
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
|
||||
import FrontmatterWrapper from '../components/wrappers/frontmatter.vue';
|
||||
import CodeBlockHighlight from './code_block_highlight';
|
||||
|
||||
export default CodeBlockHighlight.extend({
|
||||
name: 'frontmatter',
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
isFrontmatter: {
|
||||
default: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
|
@ -24,9 +32,6 @@ export default CodeBlockHighlight.extend({
|
|||
},
|
||||
};
|
||||
},
|
||||
addNodeView() {
|
||||
return new VueNodeViewRenderer(FrontmatterWrapper);
|
||||
},
|
||||
|
||||
addInputRules() {
|
||||
return [];
|
||||
|
|
|
@ -40,25 +40,21 @@ const codeBlockLanguageLoader = {
|
|||
loadLanguageFromInputRule(match) {
|
||||
const { syntax } = this.findLanguageBySyntax(match[1]);
|
||||
|
||||
this.loadLanguages([syntax]);
|
||||
this.loadLanguage(syntax);
|
||||
|
||||
return { language: syntax };
|
||||
},
|
||||
|
||||
loadLanguages(languageList = []) {
|
||||
const loaders = languageList
|
||||
.filter(
|
||||
(languageName) => !this.isLanguageLoaded(languageName) && languageName in languageLoader,
|
||||
)
|
||||
.map((languageName) => {
|
||||
return languageLoader[languageName]()
|
||||
.then(({ default: language }) => {
|
||||
this.lowlight.registerLanguage(languageName, language);
|
||||
})
|
||||
.catch(() => false);
|
||||
});
|
||||
async loadLanguage(languageName) {
|
||||
if (this.isLanguageLoaded(languageName)) return false;
|
||||
|
||||
return Promise.all(loaders);
|
||||
try {
|
||||
const { default: language } = await languageLoader[languageName]();
|
||||
this.lowlight.registerLanguage(languageName, language);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -3,12 +3,11 @@ import { LOADING_CONTENT_EVENT, LOADING_SUCCESS_EVENT, LOADING_ERROR_EVENT } fro
|
|||
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
export class ContentEditor {
|
||||
constructor({ tiptapEditor, serializer, deserializer, assetResolver, eventHub, languageLoader }) {
|
||||
constructor({ tiptapEditor, serializer, deserializer, assetResolver, eventHub }) {
|
||||
this._tiptapEditor = tiptapEditor;
|
||||
this._serializer = serializer;
|
||||
this._deserializer = deserializer;
|
||||
this._eventHub = eventHub;
|
||||
this._languageLoader = languageLoader;
|
||||
this._assetResolver = assetResolver;
|
||||
}
|
||||
|
||||
|
@ -49,7 +48,7 @@ export class ContentEditor {
|
|||
}
|
||||
|
||||
async setSerializedContent(serializedContent) {
|
||||
const { _tiptapEditor: editor, _eventHub: eventHub, _languageLoader: languageLoader } = this;
|
||||
const { _tiptapEditor: editor, _eventHub: eventHub } = this;
|
||||
const { doc, tr } = editor.state;
|
||||
const selection = TextSelection.create(doc, 0, doc.content.size);
|
||||
|
||||
|
@ -58,12 +57,8 @@ export class ContentEditor {
|
|||
const result = await this.deserialize(serializedContent);
|
||||
|
||||
if (Object.keys(result).length !== 0) {
|
||||
const { document, languages } = result;
|
||||
|
||||
await languageLoader.loadLanguages(languages);
|
||||
|
||||
tr.setSelection(selection)
|
||||
.replaceSelectionWith(document, false)
|
||||
.replaceSelectionWith(result.document, false)
|
||||
.setMeta('preventUpdate', true);
|
||||
editor.view.dispatch(tr);
|
||||
}
|
||||
|
|
|
@ -62,7 +62,6 @@ import createGlApiMarkdownDeserializer from './gl_api_markdown_deserializer';
|
|||
import createRemarkMarkdownDeserializer from './remark_markdown_deserializer';
|
||||
import createAssetResolver from './asset_resolver';
|
||||
import trackInputRulesAndShortcuts from './track_input_rules_and_shortcuts';
|
||||
import languageLoader from './code_block_language_loader';
|
||||
|
||||
const createTiptapEditor = ({ extensions = [], ...options } = {}) =>
|
||||
new Editor({
|
||||
|
@ -96,7 +95,7 @@ export const createContentEditor = ({
|
|||
BulletList,
|
||||
Code,
|
||||
ColorChip,
|
||||
CodeBlockHighlight.configure({ lowlight, languageLoader }),
|
||||
CodeBlockHighlight.configure({ lowlight }),
|
||||
DescriptionItem,
|
||||
DescriptionList,
|
||||
Details,
|
||||
|
@ -160,7 +159,6 @@ export const createContentEditor = ({
|
|||
serializer,
|
||||
eventHub,
|
||||
deserializer,
|
||||
languageLoader,
|
||||
assetResolver,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -18,7 +18,6 @@ export default ({ render }) => {
|
|||
return {
|
||||
deserialize: async ({ schema, content }) => {
|
||||
const html = await render(content);
|
||||
const languages = [];
|
||||
|
||||
if (!html) return {};
|
||||
|
||||
|
@ -28,11 +27,7 @@ export default ({ render }) => {
|
|||
// append original source as a comment that nodes can access
|
||||
body.append(document.createComment(content));
|
||||
|
||||
body.querySelectorAll('pre').forEach((preElement) => {
|
||||
languages.push(preElement.getAttribute('lang'));
|
||||
});
|
||||
|
||||
return { document: ProseMirrorDOMParser.fromSchema(schema).parse(body), languages };
|
||||
return { document: ProseMirrorDOMParser.fromSchema(schema).parse(body) };
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -35,10 +35,10 @@ const factorySpecs = {
|
|||
pre: {
|
||||
block: 'codeBlock',
|
||||
skipChildren: true,
|
||||
getContent: ({ hastNodeText }) => hastNodeText,
|
||||
getContent: ({ hastNodeText }) => hastNodeText.replace(/\n$/, ''),
|
||||
getAttrs: (hastNode) => {
|
||||
const languageClass = hastNode.children[0]?.properties.className?.[0];
|
||||
const language = isString(languageClass) ? languageClass.replace('language-', '') : '';
|
||||
const language = isString(languageClass) ? languageClass.replace('language-', '') : null;
|
||||
|
||||
return { language };
|
||||
},
|
||||
|
@ -81,7 +81,7 @@ export default () => {
|
|||
}),
|
||||
});
|
||||
|
||||
return { document, languages: [] };
|
||||
return { document };
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -170,23 +170,6 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"cobertura": {
|
||||
"description": "Path for file(s) that should be parsed as Cobertura XML coverage report",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Path to a single XML file"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"description": "A list of paths to XML files that will automatically be merged into one report",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"minItems": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
"coverage_report": {
|
||||
"type": "object",
|
||||
"description": "Used to collect coverage reports from the job.",
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
import { initBackgroundMigrationsApp } from '~/admin/background_migrations';
|
||||
|
||||
initBackgroundMigrationsApp();
|
|
@ -54,7 +54,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
|
|||
end
|
||||
|
||||
def service_usage_data
|
||||
@service_ping_data_present = Rails.cache.exist?('usage_data')
|
||||
@service_ping_data_present = prerecorded_service_ping_data.present?
|
||||
end
|
||||
|
||||
def update
|
||||
|
@ -64,7 +64,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
|
|||
def usage_data
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
usage_data_json = Gitlab::Json.pretty_generate(Gitlab::Usage::ServicePingReport.for(output: :all_metrics_values, cached: true))
|
||||
usage_data_json = Gitlab::Json.pretty_generate(service_ping_data)
|
||||
|
||||
render html: Gitlab::Highlight.highlight('payload.json', usage_data_json, language: 'json')
|
||||
end
|
||||
|
@ -72,7 +72,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
|
|||
format.json do
|
||||
Gitlab::UsageDataCounters::ServiceUsageDataCounter.count(:download_payload_click)
|
||||
|
||||
render json: Gitlab::Usage::ServicePingReport.for(output: :all_metrics_values, cached: true).to_json
|
||||
render json: service_ping_data.to_json
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -307,6 +307,14 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
|
|||
def valid_setting_panels
|
||||
VALID_SETTING_PANELS
|
||||
end
|
||||
|
||||
def service_ping_data
|
||||
prerecorded_service_ping_data || Gitlab::Usage::ServicePingReport.for(output: :all_metrics_values)
|
||||
end
|
||||
|
||||
def prerecorded_service_ping_data
|
||||
Rails.cache.fetch(Gitlab::Usage::ServicePingReport::CACHE_KEY) || ::RawUsageData.for_current_reporting_cycle.first&.payload
|
||||
end
|
||||
end
|
||||
|
||||
Admin::ApplicationSettingsController.prepend_mod_with('Admin::ApplicationSettingsController')
|
||||
|
|
|
@ -53,9 +53,9 @@ class Admin::BackgroundMigrationsController < Admin::ApplicationController
|
|||
end
|
||||
|
||||
def base_model
|
||||
database = params[:database] || Gitlab::Database::MAIN_DATABASE_NAME
|
||||
@selected_database = params[:database] || Gitlab::Database::MAIN_DATABASE_NAME
|
||||
|
||||
Gitlab::Database.database_base_models[database]
|
||||
Gitlab::Database.database_base_models[@selected_database]
|
||||
end
|
||||
|
||||
def batched_migration_class
|
||||
|
|
|
@ -21,6 +21,7 @@ module Types
|
|||
field :status, Types::ContainerRepositoryStatusEnum, null: true, description: 'Status of the container repository.'
|
||||
field :tags_count, GraphQL::Types::Int, null: false, description: 'Number of tags associated with this image.'
|
||||
field :updated_at, Types::TimeType, null: false, description: 'Timestamp when the container repository was updated.'
|
||||
field :last_cleanup_deleted_tags_count, GraphQL::Types::Int, null: true, description: 'Number of deleted tags from the last cleanup.'
|
||||
|
||||
def can_delete
|
||||
Ability.allowed?(current_user, :update_container_image, object)
|
||||
|
|
|
@ -1,9 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RawUsageData < ApplicationRecord
|
||||
REPORTING_CADENCE = 7.days.freeze
|
||||
|
||||
validates :payload, presence: true
|
||||
validates :recorded_at, presence: true, uniqueness: true
|
||||
|
||||
scope :for_current_reporting_cycle, -> do
|
||||
where('created_at >= ?', REPORTING_CADENCE.ago.beginning_of_day)
|
||||
.order(created_at: :desc)
|
||||
end
|
||||
|
||||
def update_version_metadata!(usage_data_id:)
|
||||
self.update_columns(sent_at: Time.current, version_usage_data_id_value: usage_data_id)
|
||||
end
|
||||
|
|
|
@ -12,14 +12,14 @@
|
|||
= gl_badge_tag migration.status_name.to_s.humanize, { size: :sm, variant: batched_migration_status_badge_variant(migration) }
|
||||
%td{ role: 'cell', data: { label: _('Action') } }
|
||||
- if migration.active?
|
||||
= button_to pause_admin_background_migration_path(migration),
|
||||
= button_to pause_admin_background_migration_path(migration, database: params[:database]),
|
||||
class: 'gl-button btn btn-icon has-tooltip', title: _('Pause'), 'aria-label' => _('Pause') do
|
||||
= sprite_icon('pause', css_class: 'gl-button-icon gl-icon')
|
||||
- elsif migration.paused?
|
||||
= button_to resume_admin_background_migration_path(migration),
|
||||
= button_to resume_admin_background_migration_path(migration, database: params[:database]),
|
||||
class: 'gl-button btn btn-icon has-tooltip', title: _('Resume'), 'aria-label' => _('Resume') do
|
||||
= sprite_icon('play', css_class: 'gl-button-icon gl-icon')
|
||||
- elsif migration.failed?
|
||||
= button_to retry_admin_background_migration_path(migration),
|
||||
= button_to retry_admin_background_migration_path(migration, database: params[:database]),
|
||||
class: 'gl-button btn btn-icon has-tooltip', title: _('Retry'), 'aria-label' => _('Retry') do
|
||||
= sprite_icon('retry', css_class: 'gl-button-icon gl-icon')
|
||||
|
|
|
@ -1,13 +1,25 @@
|
|||
- page_title _('Background Migrations')
|
||||
- page_title s_('BackgroundMigrations|Background Migrations')
|
||||
|
||||
.gl-display-flex.gl-sm-flex-direction-column.gl-sm-align-items-flex-end.gl-pb-5.gl-border-b-1.gl-border-b-solid.gl-border-b-gray-100
|
||||
.gl-flex-grow-1.gl-mr-7
|
||||
%h3= s_('BackgroundMigrations|Background Migrations')
|
||||
%p.light.gl-mb-0
|
||||
- learnmore_link = help_page_path('development/database/batched_background_migrations')
|
||||
- learnmore_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: learnmore_link }
|
||||
= html_escape(s_('BackgroundMigrations|Background migrations are used to perform data migrations whenever a migration exceeds the time limits in our guidelines. %{linkStart}Learn more%{linkEnd}')) % { linkStart: learnmore_link_start, linkEnd: '</a>'.html_safe }
|
||||
|
||||
- if @databases.size > 1
|
||||
.gl-display-flex.gl-align-items-center.gl-flex-grow-0.gl-flex-basis-0.gl-sm-mt-0.gl-mt-5
|
||||
#js-database-listbox{ data: { databases: @databases, selected_database: @selected_database } }
|
||||
|
||||
= gl_tabs_nav do
|
||||
= gl_tab_link_to admin_background_migrations_path, item_active: @current_tab == 'queued' do
|
||||
= gl_tab_link_to admin_background_migrations_path({ tab: nil, database: params[:database] }), item_active: @current_tab == 'queued' do
|
||||
= _('Queued')
|
||||
= gl_tab_counter_badge limited_counter_with_delimiter(@relations_by_tab['queued'])
|
||||
= gl_tab_link_to admin_background_migrations_path(tab: 'failed'), item_active: @current_tab == 'failed' do
|
||||
= gl_tab_link_to admin_background_migrations_path({ tab: 'failed', database: params[:database] }), item_active: @current_tab == 'failed' do
|
||||
= _('Failed')
|
||||
= gl_tab_counter_badge limited_counter_with_delimiter(@relations_by_tab['failed'])
|
||||
= gl_tab_link_to admin_background_migrations_path(tab: 'finished'), item_active: @current_tab == 'finished' do
|
||||
= gl_tab_link_to admin_background_migrations_path({ tab: 'finished', database: params[:database] }), item_active: @current_tab == 'finished' do
|
||||
= _('Finished')
|
||||
= gl_tab_counter_badge limited_counter_with_delimiter(@relations_by_tab['finished'])
|
||||
|
||||
|
|
|
@ -9833,6 +9833,7 @@ A container repository.
|
|||
| <a id="containerrepositoryexpirationpolicycleanupstatus"></a>`expirationPolicyCleanupStatus` | [`ContainerRepositoryCleanupStatus`](#containerrepositorycleanupstatus) | Tags cleanup status for the container repository. |
|
||||
| <a id="containerrepositoryexpirationpolicystartedat"></a>`expirationPolicyStartedAt` | [`Time`](#time) | Timestamp when the cleanup done by the expiration policy was started on the container repository. |
|
||||
| <a id="containerrepositoryid"></a>`id` | [`ID!`](#id) | ID of the container repository. |
|
||||
| <a id="containerrepositorylastcleanupdeletedtagscount"></a>`lastCleanupDeletedTagsCount` | [`Int`](#int) | Number of deleted tags from the last cleanup. |
|
||||
| <a id="containerrepositorylocation"></a>`location` | [`String!`](#string) | URL of the container repository. |
|
||||
| <a id="containerrepositorymigrationstate"></a>`migrationState` | [`String!`](#string) | Migration state of the container repository. |
|
||||
| <a id="containerrepositoryname"></a>`name` | [`String!`](#string) | Name of the container repository. |
|
||||
|
@ -9855,6 +9856,7 @@ Details of a container repository.
|
|||
| <a id="containerrepositorydetailsexpirationpolicycleanupstatus"></a>`expirationPolicyCleanupStatus` | [`ContainerRepositoryCleanupStatus`](#containerrepositorycleanupstatus) | Tags cleanup status for the container repository. |
|
||||
| <a id="containerrepositorydetailsexpirationpolicystartedat"></a>`expirationPolicyStartedAt` | [`Time`](#time) | Timestamp when the cleanup done by the expiration policy was started on the container repository. |
|
||||
| <a id="containerrepositorydetailsid"></a>`id` | [`ID!`](#id) | ID of the container repository. |
|
||||
| <a id="containerrepositorydetailslastcleanupdeletedtagscount"></a>`lastCleanupDeletedTagsCount` | [`Int`](#int) | Number of deleted tags from the last cleanup. |
|
||||
| <a id="containerrepositorydetailslocation"></a>`location` | [`String!`](#string) | URL of the container repository. |
|
||||
| <a id="containerrepositorydetailsmigrationstate"></a>`migrationState` | [`String!`](#string) | Migration state of the container repository. |
|
||||
| <a id="containerrepositorydetailsname"></a>`name` | [`String!`](#string) | Name of the container repository. |
|
||||
|
|
|
@ -80,33 +80,16 @@ GitLab can display the results of one or more reports in:
|
|||
- The [security dashboard](../../user/application_security/security_dashboard/index.md).
|
||||
- The [Project Vulnerability report](../../user/application_security/vulnerability_report/index.md).
|
||||
|
||||
## `artifacts:reports:cobertura` (DEPRECATED)
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3708) in GitLab 12.9.
|
||||
> - [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/78132) in GitLab 14.9.
|
||||
|
||||
WARNING:
|
||||
This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/78132)
|
||||
in GitLab 14.9 and planned for removal in GitLab 15.0. The alternative `artifacts:reports:coverage_report`
|
||||
is available GitLab 14.10.
|
||||
|
||||
The `cobertura` report collects [Cobertura coverage XML files](../../user/project/merge_requests/test_coverage_visualization.md).
|
||||
The collected Cobertura coverage reports upload to GitLab as an artifact.
|
||||
|
||||
GitLab can display the results of one or more reports in the merge request
|
||||
[diff annotations](../../user/project/merge_requests/test_coverage_visualization.md).
|
||||
|
||||
Cobertura was originally developed for Java, but there are many third-party ports for other languages such as
|
||||
JavaScript, Python, and Ruby.
|
||||
|
||||
## `artifacts:reports:coverage_report`
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/344533) in GitLab 14.10.
|
||||
|
||||
Use `coverage_report` to collect coverage report in Cobertura format, similar to `artifacts:reports:cobertura`.
|
||||
Use `coverage_report` to collect coverage report in Cobertura format.
|
||||
|
||||
NOTE:
|
||||
`artifacts:reports:coverage_report` cannot be used at the same time with `artifacts:reports:cobertura`.
|
||||
The `cobertura` report collects [Cobertura coverage XML files](../../user/project/merge_requests/test_coverage_visualization.md).
|
||||
|
||||
Cobertura was originally developed for Java, but there are many third-party ports for other languages such as
|
||||
JavaScript, Python, and Ruby.
|
||||
|
||||
```yaml
|
||||
artifacts:
|
||||
|
|
|
@ -698,7 +698,7 @@ Properties of customer critical merge requests:
|
|||
- It is required that the reviewer(s) and maintainer(s) involved with a customer critical merge request are engaged as soon as this decision is made.
|
||||
- It is required to prioritize work for those involved on a customer critical merge request so that they have the time available necessary to focus on it.
|
||||
- It is required to adhere to GitLab [values](https://about.gitlab.com/handbook/values/) and processes when working on customer critical merge requests, taking particular note of family and friends first/work second, definition of done, iteration, and release when it's ready.
|
||||
- Customer critical merge requests are required to not reduce security, introduce data-loss risk, reduce availability, nor break existing functionality per the process for [prioritizing technical decisions](https://about.gitlab.com/handbook/engineering/principles/#prioritizing-technical-decisions).
|
||||
- Customer critical merge requests are required to not reduce security, introduce data-loss risk, reduce availability, nor break existing functionality per the process for [prioritizing technical decisions](https://about.gitlab.com/handbook/engineering/development/principles/#prioritizing-technical-decisions).
|
||||
- On customer critical requests, it is _recommended_ that those involved _consider_ coordinating synchronously (Zoom, Slack) in addition to asynchronously (merge requests comments) if they believe this may reduce the elapsed time to merge even though this _may_ sacrifice [efficiency](https://about.gitlab.com/company/culture/all-remote/asynchronous/#evaluating-efficiency.md).
|
||||
- After a customer critical merge request is merged, a retrospective must be completed with the intention of reducing the frequency of future customer critical merge requests.
|
||||
|
||||
|
|
|
@ -66,7 +66,7 @@ continue to apply. However, there are a few things that deserve special emphasis
|
|||
Danger is a powerful tool and flexible tool, but not always the most appropriate
|
||||
way to solve a given problem or workflow.
|
||||
|
||||
First, be aware of the GitLab [commitment to dogfooding](https://about.gitlab.com/handbook/engineering/principles/#dogfooding).
|
||||
First, be aware of the GitLab [commitment to dogfooding](https://about.gitlab.com/handbook/engineering/development/principles/#dogfooding).
|
||||
The code we write for Danger is GitLab-specific, and it **may not** be most
|
||||
appropriate place to implement functionality that addresses a need we encounter.
|
||||
Our users, customers, and even our own satellite projects, such as [Gitaly](https://gitlab.com/gitlab-org/gitaly),
|
||||
|
|
|
@ -539,7 +539,7 @@ When writing about licenses:
|
|||
|
||||
- Do not use variations such as **cloud license**, **offline license**, or **legacy license**.
|
||||
- Do not use interchangeably with **subscription**:
|
||||
- A license grants users access to the subscription they purchased, and contains information such as the number of seats and subscription dates.
|
||||
- A license grants users access to the subscription they purchased, and contains information such as the number of seats they purchased and subscription dates.
|
||||
- A subscription is the subscription tier that the user purchases.
|
||||
|
||||
Use:
|
||||
|
|
|
@ -9,7 +9,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
Anti-patterns may seem like good approaches at first, but it has been shown that they bring more ills than benefits. These should
|
||||
generally be avoided.
|
||||
|
||||
Throughout the GitLab codebase, there may be historic uses of these anti-patterns. Please [use discretion](https://about.gitlab.com/handbook/engineering/principles/#balance-refactoring-and-velocity)
|
||||
Throughout the GitLab codebase, there may be historic uses of these anti-patterns. Please [use discretion](https://about.gitlab.com/handbook/engineering/development/principles/#balance-refactoring-and-velocity)
|
||||
when figuring out whether or not to refactor, when touching code that uses one of these legacy patterns.
|
||||
|
||||
NOTE:
|
||||
|
|
|
@ -98,6 +98,20 @@ virtual machine:
|
|||
fips-mode-setup --disable
|
||||
```
|
||||
|
||||
#### Detect FIPS enablement in code
|
||||
|
||||
You can query `GitLab::FIPS` in Ruby code to determine if the instance is FIPS-enabled:
|
||||
|
||||
```ruby
|
||||
def default_min_key_size(name)
|
||||
if Gitlab::FIPS.enabled?
|
||||
Gitlab::SSHPublicKey.supported_sizes(name).select(&:positive?).min || -1
|
||||
else
|
||||
0
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Set up a FIPS-enabled cluster
|
||||
|
||||
You can use the [GitLab Environment Toolkit](https://gitlab.com/gitlab-org/gitlab-environment-toolkit) to spin
|
||||
|
|
|
@ -12,7 +12,7 @@ which itself includes files under
|
|||
[`.gitlab/ci/`](https://gitlab.com/gitlab-org/gitlab/-/tree/master/.gitlab/ci)
|
||||
for easier maintenance.
|
||||
|
||||
We're striving to [dogfood](https://about.gitlab.com/handbook/engineering/principles/#dogfooding)
|
||||
We're striving to [dogfood](https://about.gitlab.com/handbook/engineering/development/principles/#dogfooding)
|
||||
GitLab [CI/CD features and best-practices](../ci/yaml/index.md)
|
||||
as much as possible.
|
||||
|
||||
|
|
|
@ -272,4 +272,4 @@ and merged back independently.
|
|||
- **Give yourself enough time to fix problems ahead of a milestone release.** GitLab moves fast.
|
||||
As a Ruby upgrade requires many MRs to be sent and reviewed, make sure all changes are merged at least a week
|
||||
before the 22nd. This gives us extra time to act if something breaks. If in doubt, it is better to
|
||||
postpone the upgrade to the following month, as we [prioritize availability over velocity](https://about.gitlab.com/handbook/engineering/principles/#prioritizing-technical-decisions).
|
||||
postpone the upgrade to the following month, as we [prioritize availability over velocity](https://about.gitlab.com/handbook/engineering/development/principles/#prioritizing-technical-decisions).
|
||||
|
|
|
@ -289,6 +289,8 @@ Enabled by default in GitLab 13.7 and later.
|
|||
|
||||
To increment the values, the related feature `usage_data_<event_name>` must be enabled.
|
||||
|
||||
Feature flags are required for this API and they can't be removed, they can be set to `default_enabled: true`.
|
||||
|
||||
```plaintext
|
||||
POST /usage_data/increment_counter
|
||||
```
|
||||
|
|
|
@ -214,7 +214,7 @@ apply more than one:
|
|||
```shell
|
||||
omnibus_gitconfig['system'] = {
|
||||
# Set the http.postBuffer size, in bytes
|
||||
"http" => ["postBuffer => 524288000"]
|
||||
"http" => ["postBuffer = 524288000"]
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ between pipeline completion and the visualization loading on the page.
|
|||
|
||||
For the coverage analysis to work, you have to provide a properly formatted
|
||||
[Cobertura XML](https://cobertura.github.io/cobertura/) report to
|
||||
[`artifacts:reports:cobertura`](../../../ci/yaml/artifacts_reports.md#artifactsreportscobertura-deprecated).
|
||||
[`artifacts:reports:coverage_report`](../../../ci/yaml/artifacts_reports.md#artifactsreportscoverage_report).
|
||||
This format was originally developed for Java, but most coverage analysis frameworks
|
||||
for other languages have plugins to add support for it, like:
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ module Gitlab
|
|||
ALLOWED_KEYS =
|
||||
%i[junit codequality sast secret_detection dependency_scanning container_scanning
|
||||
dast performance browser_performance load_performance license_scanning metrics lsif
|
||||
dotenv cobertura terraform accessibility
|
||||
dotenv terraform accessibility
|
||||
requirements coverage_fuzzing api_fuzzing cluster_image_scanning
|
||||
coverage_report].freeze
|
||||
|
||||
|
@ -45,13 +45,10 @@ module Gitlab
|
|||
validates :metrics, array_of_strings_or_string: true
|
||||
validates :lsif, array_of_strings_or_string: true
|
||||
validates :dotenv, array_of_strings_or_string: true
|
||||
validates :cobertura, array_of_strings_or_string: true
|
||||
validates :terraform, array_of_strings_or_string: true
|
||||
validates :accessibility, array_of_strings_or_string: true
|
||||
validates :requirements, array_of_strings_or_string: true
|
||||
end
|
||||
|
||||
validates :config, mutually_exclusive_keys: [:coverage_report, :cobertura]
|
||||
end
|
||||
|
||||
def value
|
||||
|
|
|
@ -84,7 +84,9 @@ test_artifacts:
|
|||
artifacts:
|
||||
reports:
|
||||
junit: "./artifacts/results.xml"
|
||||
cobertura: "./artifacts/cobertura.xml"
|
||||
coverage_report:
|
||||
coverage_format: cobertura
|
||||
path: "./artifacts/cobertura.xml"
|
||||
paths:
|
||||
- "./artifacts"
|
||||
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
module Gitlab
|
||||
module Usage
|
||||
class ServicePingReport
|
||||
CACHE_KEY = 'usage_data'
|
||||
|
||||
class << self
|
||||
def for(output:, cached: false)
|
||||
case output.to_sym
|
||||
|
@ -26,7 +28,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def all_metrics_values(cached)
|
||||
Rails.cache.fetch('usage_data', force: !cached, expires_in: 2.weeks) do
|
||||
Rails.cache.fetch(CACHE_KEY, force: !cached, expires_in: 2.weeks) do
|
||||
Gitlab::UsageData.data
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5545,6 +5545,15 @@ msgstr ""
|
|||
msgid "Background color"
|
||||
msgstr ""
|
||||
|
||||
msgid "BackgroundMigrations|Background Migrations"
|
||||
msgstr ""
|
||||
|
||||
msgid "BackgroundMigrations|Background migrations are used to perform data migrations whenever a migration exceeds the time limits in our guidelines. %{linkStart}Learn more%{linkEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "BackgroundMigrations|Database"
|
||||
msgstr ""
|
||||
|
||||
msgid "Badges"
|
||||
msgstr ""
|
||||
|
||||
|
@ -11327,9 +11336,18 @@ msgstr ""
|
|||
msgid "DastProfiles|New site profile"
|
||||
msgstr ""
|
||||
|
||||
msgid "DastProfiles|No scanner profile selected"
|
||||
msgstr ""
|
||||
|
||||
msgid "DastProfiles|No scanner profile selected."
|
||||
msgstr ""
|
||||
|
||||
msgid "DastProfiles|No scanner profiles created yet"
|
||||
msgstr ""
|
||||
|
||||
msgid "DastProfiles|No site profile selected"
|
||||
msgstr ""
|
||||
|
||||
msgid "DastProfiles|No site profiles created yet"
|
||||
msgstr ""
|
||||
|
||||
|
@ -11378,9 +11396,27 @@ msgstr ""
|
|||
msgid "DastProfiles|Scanner name"
|
||||
msgstr ""
|
||||
|
||||
msgid "DastProfiles|Scanner profile"
|
||||
msgstr ""
|
||||
|
||||
msgid "DastProfiles|Scanner profiles define the configuration details of a security scanner. %{linkStart}Learn more%{linkEnd}."
|
||||
msgstr ""
|
||||
|
||||
msgid "DastProfiles|Select a scanner profile to run a DAST scan"
|
||||
msgstr ""
|
||||
|
||||
msgid "DastProfiles|Select a site profile to run a DAST scan"
|
||||
msgstr ""
|
||||
|
||||
msgid "DastProfiles|Select branch"
|
||||
msgstr ""
|
||||
|
||||
msgid "DastProfiles|Select scanner profile"
|
||||
msgstr ""
|
||||
|
||||
msgid "DastProfiles|Select site profile"
|
||||
msgstr ""
|
||||
|
||||
msgid "DastProfiles|Show debug messages"
|
||||
msgstr ""
|
||||
|
||||
|
@ -11393,6 +11429,9 @@ msgstr ""
|
|||
msgid "DastProfiles|Site name"
|
||||
msgstr ""
|
||||
|
||||
msgid "DastProfiles|Site profiles define the attributes and configuration details of your deployed application, website, or API. %{linkStart}Learn more%{linkEnd}."
|
||||
msgstr ""
|
||||
|
||||
msgid "DastProfiles|Site type"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -29,7 +29,8 @@ module QA
|
|||
it(
|
||||
'is determined based on forward:pipeline_variables condition',
|
||||
:aggregate_failures,
|
||||
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/360745'
|
||||
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/360745',
|
||||
quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/361400', type: :investigating }
|
||||
) do
|
||||
# Is inheritable when true
|
||||
expect(child1_pipeline).to have_variable(key: key, value: value),
|
||||
|
|
|
@ -66,6 +66,26 @@ RSpec.describe Admin::ApplicationSettingsController, :do_not_mock_admin_mode_set
|
|||
sign_in(admin)
|
||||
end
|
||||
|
||||
context 'when there are recent ServicePing reports' do
|
||||
it 'attempts to use prerecorded data' do
|
||||
create(:raw_usage_data)
|
||||
|
||||
expect(Gitlab::Usage::ServicePingReport).not_to receive(:for)
|
||||
|
||||
get :usage_data, format: :json
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are NO recent ServicePing reports' do
|
||||
it 'calculates data on the fly' do
|
||||
allow(Gitlab::Usage::ServicePingReport).to receive(:for).and_call_original
|
||||
|
||||
get :usage_data, format: :json
|
||||
|
||||
expect(Gitlab::Usage::ServicePingReport).to have_received(:for)
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns HTML data' do
|
||||
get :usage_data, format: :html
|
||||
|
||||
|
@ -368,4 +388,37 @@ RSpec.describe Admin::ApplicationSettingsController, :do_not_mock_admin_mode_set
|
|||
expect(response).to redirect_to("https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf")
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET #service_usage_data' do
|
||||
before do
|
||||
stub_usage_data_connections
|
||||
stub_database_flavor_check
|
||||
sign_in(admin)
|
||||
end
|
||||
|
||||
it 'assigns truthy value if there are recent ServicePing reports in database' do
|
||||
create(:raw_usage_data)
|
||||
|
||||
get :service_usage_data, format: :html
|
||||
|
||||
expect(assigns(:service_ping_data_present)).to be_truthy
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
|
||||
it 'assigns truthy value if there are recent ServicePing reports in cache', :use_clean_rails_memory_store_caching do
|
||||
Rails.cache.write('usage_data', true)
|
||||
|
||||
get :service_usage_data, format: :html
|
||||
|
||||
expect(assigns(:service_ping_data_present)).to be_truthy
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
|
||||
it 'assigns falsey value if there are NO recent ServicePing reports' do
|
||||
get :service_usage_data, format: :html
|
||||
|
||||
expect(assigns(:service_ping_data_present)).to be_falsey
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -77,6 +77,17 @@ RSpec.describe "Admin > Admin sees background migrations" do
|
|||
end
|
||||
end
|
||||
|
||||
it 'can fire an action with a database param' do
|
||||
visit admin_background_migrations_path(database: 'main')
|
||||
|
||||
within '#content-body' do
|
||||
tab = find_link 'Failed'
|
||||
tab.click
|
||||
|
||||
expect(page).to have_selector("[method='post'][action='/admin/background_migrations/#{failed_migration.id}/retry?database=main']")
|
||||
end
|
||||
end
|
||||
|
||||
it 'can view and retry them' do
|
||||
visit admin_background_migrations_path
|
||||
|
||||
|
@ -120,4 +131,83 @@ RSpec.describe "Admin > Admin sees background migrations" do
|
|||
expect(page).to have_content(finished_migration.status_name.to_s)
|
||||
end
|
||||
end
|
||||
|
||||
it 'can change tabs and retain database param' do
|
||||
visit admin_background_migrations_path(database: 'ci')
|
||||
|
||||
within '#content-body' do
|
||||
tab = find_link 'Finished'
|
||||
expect(tab[:class]).not_to include('gl-tab-nav-item-active')
|
||||
|
||||
tab.click
|
||||
|
||||
expect(page).to have_current_path(admin_background_migrations_path(tab: 'finished', database: 'ci'))
|
||||
expect(tab[:class]).to include('gl-tab-nav-item-active')
|
||||
end
|
||||
end
|
||||
|
||||
it 'can view documentation from Learn more link' do
|
||||
visit admin_background_migrations_path
|
||||
|
||||
within '#content-body' do
|
||||
expect(page).to have_link('Learn more', href: help_page_path('development/database/batched_background_migrations'))
|
||||
end
|
||||
end
|
||||
|
||||
describe 'selected database toggle', :js do
|
||||
context 'when multi database is not enabled' do
|
||||
before do
|
||||
allow(Gitlab::Database).to receive(:db_config_names).and_return(['main'])
|
||||
end
|
||||
|
||||
it 'does not render the database listbox' do
|
||||
visit admin_background_migrations_path
|
||||
|
||||
expect(page).not_to have_selector('[data-testid="database-listbox"]')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when multi database is enabled' do
|
||||
before do
|
||||
allow(Gitlab::Database).to receive(:db_config_names).and_return(%w[main ci])
|
||||
end
|
||||
|
||||
it 'does render the database listbox' do
|
||||
visit admin_background_migrations_path
|
||||
|
||||
expect(page).to have_selector('[data-testid="database-listbox"]')
|
||||
end
|
||||
|
||||
it 'defaults to main when no parameter is passed' do
|
||||
visit admin_background_migrations_path
|
||||
|
||||
listbox = page.find('[data-testid="database-listbox"]')
|
||||
|
||||
expect(listbox).to have_text('main')
|
||||
end
|
||||
|
||||
it 'shows correct database when a parameter is passed' do
|
||||
visit admin_background_migrations_path(database: 'ci')
|
||||
|
||||
listbox = page.find('[data-testid="database-listbox"]')
|
||||
|
||||
expect(listbox).to have_text('ci')
|
||||
end
|
||||
|
||||
it 'updates the path to correct database when clicking on listbox option' do
|
||||
visit admin_background_migrations_path
|
||||
|
||||
listbox = page.find('[data-testid="database-listbox"]')
|
||||
expect(listbox).to have_text('main')
|
||||
|
||||
listbox.find('button').click
|
||||
listbox.find('li', text: 'ci').click
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_current_path(admin_background_migrations_path(database: 'ci'))
|
||||
listbox = page.find('[data-testid="database-listbox"]')
|
||||
expect(listbox).to have_text('ci')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -834,10 +834,9 @@ RSpec.describe 'Admin updates settings' do
|
|||
stub_database_flavor_check
|
||||
end
|
||||
|
||||
context 'when service data cached', :clean_gitlab_redis_cache do
|
||||
context 'when service data cached', :use_clean_rails_memory_store_caching do
|
||||
before do
|
||||
allow(Rails.cache).to receive(:exist?).with('usage_data').and_return(true)
|
||||
|
||||
visit usage_data_admin_application_settings_path
|
||||
visit service_usage_data_admin_application_settings_path
|
||||
end
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"type": "object",
|
||||
"required": ["id", "name", "path", "location", "createdAt", "updatedAt", "tagsCount", "canDelete", "expirationPolicyCleanupStatus", "project"],
|
||||
"required": ["id", "name", "path", "location", "createdAt", "updatedAt", "tagsCount", "canDelete", "expirationPolicyCleanupStatus", "project", "lastCleanupDeletedTagsCount"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
|
@ -38,6 +38,9 @@
|
|||
},
|
||||
"project": {
|
||||
"type": "object"
|
||||
},
|
||||
"lastCleanupDeletedTagsCount": {
|
||||
"type": ["string", "null"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
import { GlListbox } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import BackgroundMigrationsDatabaseListbox from '~/admin/background_migrations/components/database_listbox.vue';
|
||||
import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
|
||||
import { MOCK_DATABASES, MOCK_SELECTED_DATABASE } from '../mock_data';
|
||||
|
||||
jest.mock('~/lib/utils/url_utility', () => ({
|
||||
visitUrl: jest.fn(),
|
||||
setUrlParams: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('BackgroundMigrationsDatabaseListbox', () => {
|
||||
let wrapper;
|
||||
|
||||
const defaultProps = {
|
||||
databases: MOCK_DATABASES,
|
||||
selectedDatabase: MOCK_SELECTED_DATABASE,
|
||||
};
|
||||
|
||||
const createComponent = (props = {}) => {
|
||||
wrapper = shallowMount(BackgroundMigrationsDatabaseListbox, {
|
||||
propsData: {
|
||||
...defaultProps,
|
||||
...props,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
const findGlListbox = () => wrapper.findComponent(GlListbox);
|
||||
|
||||
describe('template always', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('renders GlListbox', () => {
|
||||
expect(findGlListbox().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('actions', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('selecting a listbox item fires visitUrl with the database param', () => {
|
||||
findGlListbox().vm.$emit('select', MOCK_DATABASES[1].value);
|
||||
|
||||
expect(setUrlParams).toHaveBeenCalledWith({ database: MOCK_DATABASES[1].value });
|
||||
expect(visitUrl).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
6
spec/frontend/admin/background_migrations/mock_data.js
Normal file
6
spec/frontend/admin/background_migrations/mock_data.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
export const MOCK_DATABASES = [
|
||||
{ value: 'main', text: 'main' },
|
||||
{ value: 'ci', text: 'ci' },
|
||||
];
|
||||
|
||||
export const MOCK_SELECTED_DATABASE = 'main';
|
|
@ -124,7 +124,7 @@ describe('content_editor/components/bubble_menus/code_block', () => {
|
|||
|
||||
describe('when dropdown item is clicked', () => {
|
||||
beforeEach(async () => {
|
||||
jest.spyOn(codeBlockLanguageLoader, 'loadLanguages').mockResolvedValue();
|
||||
jest.spyOn(codeBlockLanguageLoader, 'loadLanguage').mockResolvedValue();
|
||||
|
||||
findDropdownItems().at(1).vm.$emit('click');
|
||||
|
||||
|
@ -132,7 +132,7 @@ describe('content_editor/components/bubble_menus/code_block', () => {
|
|||
});
|
||||
|
||||
it('loads language', () => {
|
||||
expect(codeBlockLanguageLoader.loadLanguages).toHaveBeenCalledWith(['java']);
|
||||
expect(codeBlockLanguageLoader.loadLanguage).toHaveBeenCalledWith('java');
|
||||
});
|
||||
|
||||
it('sets code block', () => {
|
||||
|
|
|
@ -1,20 +1,33 @@
|
|||
import { nextTick } from 'vue';
|
||||
import { NodeViewWrapper, NodeViewContent } from '@tiptap/vue-2';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import FrontmatterWrapper from '~/content_editor/components/wrappers/frontmatter.vue';
|
||||
import CodeBlockWrapper from '~/content_editor/components/wrappers/code_block.vue';
|
||||
import codeBlockLanguageLoader from '~/content_editor/services/code_block_language_loader';
|
||||
|
||||
describe('content/components/wrappers/frontmatter', () => {
|
||||
jest.mock('~/content_editor/services/code_block_language_loader');
|
||||
|
||||
describe('content/components/wrappers/code_block', () => {
|
||||
const language = 'yaml';
|
||||
let wrapper;
|
||||
let updateAttributesFn;
|
||||
|
||||
const createWrapper = async (nodeAttrs = { language: 'yaml' }) => {
|
||||
wrapper = shallowMount(FrontmatterWrapper, {
|
||||
const createWrapper = async (nodeAttrs = { language }) => {
|
||||
updateAttributesFn = jest.fn();
|
||||
|
||||
wrapper = shallowMount(CodeBlockWrapper, {
|
||||
propsData: {
|
||||
node: {
|
||||
attrs: nodeAttrs,
|
||||
},
|
||||
updateAttributes: updateAttributesFn,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
codeBlockLanguageLoader.findLanguageBySyntax.mockReturnValue({ syntax: language });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
@ -38,11 +51,21 @@ describe('content/components/wrappers/frontmatter', () => {
|
|||
});
|
||||
|
||||
it('renders label indicating that code block is frontmatter', () => {
|
||||
createWrapper();
|
||||
createWrapper({ isFrontmatter: true, language });
|
||||
|
||||
const label = wrapper.find('[data-testid="frontmatter-label"]');
|
||||
|
||||
expect(label.text()).toEqual('frontmatter:yaml');
|
||||
expect(label.classes()).toEqual(['gl-absolute', 'gl-top-0', 'gl-right-3']);
|
||||
});
|
||||
|
||||
it('loads code block’s syntax highlight language', async () => {
|
||||
createWrapper();
|
||||
|
||||
expect(codeBlockLanguageLoader.loadLanguage).toHaveBeenCalledWith(language);
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(updateAttributesFn).toHaveBeenCalledWith({ language });
|
||||
});
|
||||
});
|
|
@ -1,4 +1,5 @@
|
|||
import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
|
||||
import languageLoader from '~/content_editor/services/code_block_language_loader';
|
||||
import { createTestEditor, createDocBuilder, triggerNodeInputRule } from '../test_utils';
|
||||
|
||||
const CODE_BLOCK_HTML = `<pre class="code highlight js-syntax-highlight language-javascript" lang="javascript" v-pre="true">
|
||||
|
@ -9,20 +10,20 @@ const CODE_BLOCK_HTML = `<pre class="code highlight js-syntax-highlight language
|
|||
</code>
|
||||
</pre>`;
|
||||
|
||||
jest.mock('~/content_editor/services/code_block_language_loader');
|
||||
|
||||
describe('content_editor/extensions/code_block_highlight', () => {
|
||||
let parsedCodeBlockHtmlFixture;
|
||||
let tiptapEditor;
|
||||
let doc;
|
||||
let codeBlock;
|
||||
let languageLoader;
|
||||
|
||||
const parseHTML = (html) => new DOMParser().parseFromString(html, 'text/html');
|
||||
const preElement = () => parsedCodeBlockHtmlFixture.querySelector('pre');
|
||||
|
||||
beforeEach(() => {
|
||||
languageLoader = { loadLanguages: jest.fn() };
|
||||
tiptapEditor = createTestEditor({
|
||||
extensions: [CodeBlockHighlight.configure({ languageLoader })],
|
||||
extensions: [CodeBlockHighlight],
|
||||
});
|
||||
|
||||
({
|
||||
|
@ -70,6 +71,8 @@ describe('content_editor/extensions/code_block_highlight', () => {
|
|||
const language = 'javascript';
|
||||
|
||||
beforeEach(() => {
|
||||
languageLoader.loadLanguageFromInputRule.mockReturnValueOnce({ language });
|
||||
|
||||
triggerNodeInputRule({
|
||||
tiptapEditor,
|
||||
inputRuleText: `${inputRule}${language} `,
|
||||
|
@ -83,7 +86,9 @@ describe('content_editor/extensions/code_block_highlight', () => {
|
|||
});
|
||||
|
||||
it('loads language when language loader is available', () => {
|
||||
expect(languageLoader.loadLanguages).toHaveBeenCalledWith([language]);
|
||||
expect(languageLoader.loadLanguageFromInputRule).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([`${inputRule}${language} `, language]),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
16
spec/frontend/content_editor/extensions/diagram_spec.js
Normal file
16
spec/frontend/content_editor/extensions/diagram_spec.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
import Diagram from '~/content_editor/extensions/diagram';
|
||||
import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
|
||||
|
||||
describe('content_editor/extensions/diagram', () => {
|
||||
it('inherits from code block highlight extension', () => {
|
||||
expect(Diagram.parent).toBe(CodeBlockHighlight);
|
||||
});
|
||||
|
||||
it('sets isDiagram attribute to true by default', () => {
|
||||
expect(Diagram.config.addAttributes()).toEqual(
|
||||
expect.objectContaining({
|
||||
isDiagram: { default: true },
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
|
@ -22,6 +22,10 @@ describe('content_editor/extensions/frontmatter', () => {
|
|||
}));
|
||||
});
|
||||
|
||||
it('inherits from code block highlight extension', () => {
|
||||
expect(Frontmatter.parent).toBe(CodeBlockHighlight);
|
||||
});
|
||||
|
||||
it('does not insert a frontmatter block when executing code block input rule', () => {
|
||||
const expectedDoc = doc(codeBlock({ language: 'plaintext' }, ''));
|
||||
const inputRuleText = '``` ';
|
||||
|
@ -31,6 +35,14 @@ describe('content_editor/extensions/frontmatter', () => {
|
|||
expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
|
||||
});
|
||||
|
||||
it('sets isFrontmatter attribute to true by default', () => {
|
||||
expect(Frontmatter.config.addAttributes()).toEqual(
|
||||
expect.objectContaining({
|
||||
isFrontmatter: { default: true },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it.each`
|
||||
command | result | resultDesc
|
||||
${'toggleCodeBlock'} | ${() => doc(codeBlock(''))} | ${'code block element'}
|
||||
|
|
|
@ -53,23 +53,19 @@ describe('content_editor/services/code_block_language_loader', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('loadLanguages', () => {
|
||||
describe('loadLanguage', () => {
|
||||
it('loads highlight.js language packages identified by a list of languages', async () => {
|
||||
const languages = ['javascript', 'ruby'];
|
||||
const language = 'javascript';
|
||||
|
||||
await languageLoader.loadLanguages(languages);
|
||||
await languageLoader.loadLanguage(language);
|
||||
|
||||
languages.forEach((language) => {
|
||||
expect(lowlight.registerLanguage).toHaveBeenCalledWith(language, expect.any(Function));
|
||||
});
|
||||
expect(lowlight.registerLanguage).toHaveBeenCalledWith(language, expect.any(Function));
|
||||
});
|
||||
|
||||
describe('when language is already registered', () => {
|
||||
it('does not load the language again', async () => {
|
||||
const languages = ['javascript'];
|
||||
|
||||
await languageLoader.loadLanguages(languages);
|
||||
await languageLoader.loadLanguages(languages);
|
||||
await languageLoader.loadLanguage('javascript');
|
||||
await languageLoader.loadLanguage('javascript');
|
||||
|
||||
expect(lowlight.registerLanguage).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
@ -94,7 +90,7 @@ describe('content_editor/services/code_block_language_loader', () => {
|
|||
|
||||
expect(languageLoader.isLanguageLoaded(language)).toBe(false);
|
||||
|
||||
await languageLoader.loadLanguages([language]);
|
||||
await languageLoader.loadLanguage(language);
|
||||
|
||||
expect(languageLoader.isLanguageLoaded(language)).toBe(true);
|
||||
});
|
||||
|
|
|
@ -11,7 +11,6 @@ describe('content_editor/services/content_editor', () => {
|
|||
let contentEditor;
|
||||
let serializer;
|
||||
let deserializer;
|
||||
let languageLoader;
|
||||
let eventHub;
|
||||
let doc;
|
||||
let p;
|
||||
|
@ -28,14 +27,12 @@ describe('content_editor/services/content_editor', () => {
|
|||
|
||||
serializer = { serialize: jest.fn() };
|
||||
deserializer = { deserialize: jest.fn() };
|
||||
languageLoader = { loadLanguages: jest.fn() };
|
||||
eventHub = eventHubFactory();
|
||||
contentEditor = new ContentEditor({
|
||||
tiptapEditor,
|
||||
serializer,
|
||||
deserializer,
|
||||
eventHub,
|
||||
languageLoader,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -77,12 +74,6 @@ describe('content_editor/services/content_editor', () => {
|
|||
|
||||
expect(contentEditor.tiptapEditor.state.doc.toJSON()).toEqual(document.toJSON());
|
||||
});
|
||||
|
||||
it('passes deserialized DOM document to language loader', async () => {
|
||||
await contentEditor.setSerializedContent(testMarkdown);
|
||||
|
||||
expect(languageLoader.loadLanguages).toHaveBeenCalledWith(languages);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when setSerializedContent fails', () => {
|
||||
|
|
|
@ -46,10 +46,6 @@ describe('content_editor/services/gl_api_markdown_deserializer', () => {
|
|||
|
||||
expect(result.document.toJSON()).toEqual(document.toJSON());
|
||||
});
|
||||
|
||||
it('returns languages of code blocks found in the document', () => {
|
||||
expect(result.languages).toEqual(['javascript']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the render function returns an empty value', () => {
|
||||
|
|
|
@ -273,7 +273,7 @@ two
|
|||
markdown: `
|
||||
const fn = () => 'GitLab';
|
||||
`,
|
||||
doc: doc(codeBlock({ language: '' }, "const fn = () => 'GitLab';\n")),
|
||||
doc: doc(codeBlock({ language: null }, "const fn = () => 'GitLab';")),
|
||||
},
|
||||
{
|
||||
markdown: `
|
||||
|
@ -281,14 +281,14 @@ two
|
|||
const fn = () => 'GitLab';
|
||||
\`\`\`\
|
||||
`,
|
||||
doc: doc(codeBlock({ language: 'javascript' }, " const fn = () => 'GitLab';\n")),
|
||||
doc: doc(codeBlock({ language: 'javascript' }, " const fn = () => 'GitLab';")),
|
||||
},
|
||||
{
|
||||
markdown: `
|
||||
\`\`\`
|
||||
\`\`\`\
|
||||
`,
|
||||
doc: doc(codeBlock({ language: '' }, '')),
|
||||
doc: doc(codeBlock({ language: null }, '')),
|
||||
},
|
||||
{
|
||||
markdown: `
|
||||
|
@ -298,7 +298,7 @@ two
|
|||
|
||||
\`\`\`\
|
||||
`,
|
||||
doc: doc(codeBlock({ language: 'javascript' }, " const fn = () => 'GitLab';\n\n\n")),
|
||||
doc: doc(codeBlock({ language: 'javascript' }, " const fn = () => 'GitLab';\n\n")),
|
||||
},
|
||||
])('deserializes %s correctly', async ({ markdown, doc: expectedDoc }) => {
|
||||
const { schema } = tiptapEditor;
|
||||
|
|
|
@ -97,7 +97,10 @@
|
|||
"expire_in": "1 week",
|
||||
"reports": {
|
||||
"junit": "result.xml",
|
||||
"cobertura": "cobertura-coverage.xml",
|
||||
"coverage_report": {
|
||||
"coverage_format": "cobertura",
|
||||
"path": "cobertura-coverage.xml"
|
||||
},
|
||||
"codequality": "codequality.json",
|
||||
"sast": "sast.json",
|
||||
"dependency_scanning": "scan.json",
|
||||
|
@ -147,7 +150,10 @@
|
|||
"artifacts": {
|
||||
"reports": {
|
||||
"junit": ["result.xml"],
|
||||
"cobertura": ["cobertura-coverage.xml"],
|
||||
"coverage_report": {
|
||||
"coverage_format": "cobertura",
|
||||
"path": "cobertura-coverage.xml"
|
||||
},
|
||||
"codequality": ["codequality.json"],
|
||||
"sast": ["sast.json"],
|
||||
"dependency_scanning": ["scan.json"],
|
||||
|
|
|
@ -5,7 +5,7 @@ require 'spec_helper'
|
|||
RSpec.describe GitlabSchema.types['ContainerRepositoryDetails'] do
|
||||
fields = %i[id name path location created_at updated_at expiration_policy_started_at
|
||||
status tags_count can_delete expiration_policy_cleanup_status tags size
|
||||
project migration_state]
|
||||
project migration_state last_cleanup_deleted_tags_count]
|
||||
|
||||
it { expect(described_class.graphql_name).to eq('ContainerRepositoryDetails') }
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ require 'spec_helper'
|
|||
RSpec.describe GitlabSchema.types['ContainerRepository'] do
|
||||
fields = %i[id name path location created_at updated_at expiration_policy_started_at
|
||||
status tags_count can_delete expiration_policy_cleanup_status project
|
||||
migration_state]
|
||||
migration_state last_cleanup_deleted_tags_count]
|
||||
|
||||
it { expect(described_class.graphql_name).to eq('ContainerRepository') }
|
||||
|
||||
|
|
|
@ -45,7 +45,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Reports do
|
|||
:load_performance | 'load-performance.json'
|
||||
:lsif | 'lsif.json'
|
||||
:dotenv | 'build.dotenv'
|
||||
:cobertura | 'cobertura-coverage.xml'
|
||||
:terraform | 'tfplan.json'
|
||||
:accessibility | 'gl-accessibility.json'
|
||||
end
|
||||
|
@ -89,18 +88,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Reports do
|
|||
expect(entry.value).to eq({ coverage_report: coverage_report, dast: ['gl-dast-report.json'] })
|
||||
end
|
||||
end
|
||||
|
||||
context 'and a direct coverage report format is specified' do
|
||||
let(:config) { { coverage_report: coverage_report, cobertura: 'cobertura-coverage.xml' } }
|
||||
|
||||
it 'is not valid' do
|
||||
expect(entry).not_to be_valid
|
||||
end
|
||||
|
||||
it 'reports error' do
|
||||
expect(entry.errors).to include /please use only one the following keys: coverage_report, cobertura/
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -3,6 +3,31 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe RawUsageData do
|
||||
context 'scopes' do
|
||||
describe '.for_current_reporting_cycle' do
|
||||
subject(:recent_service_ping_reports) { described_class.for_current_reporting_cycle }
|
||||
|
||||
before_all do
|
||||
create(:raw_usage_data, created_at: (described_class::REPORTING_CADENCE + 1.day).ago)
|
||||
end
|
||||
|
||||
it 'returns nil where no records match filter criteria' do
|
||||
expect(recent_service_ping_reports).to be_empty
|
||||
end
|
||||
|
||||
context 'with records matching filtering criteria' do
|
||||
let_it_be(:fresh_record) { create(:raw_usage_data) }
|
||||
let_it_be(:record_at_edge_of_time_range) do
|
||||
create(:raw_usage_data, created_at: described_class::REPORTING_CADENCE.ago)
|
||||
end
|
||||
|
||||
it 'return records within reporting cycle time range ordered by creation time' do
|
||||
expect(recent_service_ping_reports).to eq [fresh_record, record_at_edge_of_time_range]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'validations' do
|
||||
it { is_expected.to validate_presence_of(:payload) }
|
||||
it { is_expected.to validate_presence_of(:recorded_at) }
|
||||
|
|
Loading…
Reference in a new issue