Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
984357420a
commit
591b0e86e3
|
@ -1 +1 @@
|
|||
d2e978f8e8f47a49c3bcfbd470b2f790e52c5ee2
|
||||
ab2f2386ab69575cd0a58f7279be707a17d7a6c8
|
||||
|
|
|
@ -20,6 +20,7 @@ const Api = {
|
|||
projectPath: '/api/:version/projects/:id',
|
||||
forkedProjectsPath: '/api/:version/projects/:id/forks',
|
||||
projectLabelsPath: '/:namespace_path/:project_path/-/labels',
|
||||
projectFileSchemaPath: '/:namespace_path/:project_path/-/schema/:ref/:filename',
|
||||
projectUsersPath: '/api/:version/projects/:id/users',
|
||||
projectMergeRequestsPath: '/api/:version/projects/:id/merge_requests',
|
||||
projectMergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid',
|
||||
|
|
|
@ -125,10 +125,12 @@ export default {
|
|||
params: { id: filename },
|
||||
query: $route.query,
|
||||
}"
|
||||
class="card cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new"
|
||||
class="card gl-cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new"
|
||||
>
|
||||
<div class="card-body p-0 d-flex-center overflow-hidden position-relative">
|
||||
<div v-if="icon.name" data-testid="designEvent" class="design-event position-absolute">
|
||||
<div
|
||||
class="card-body gl-p-0 gl-display-flex gl-align-items-center gl-justify-content-center gl-overflow-hidden gl-relative"
|
||||
>
|
||||
<div v-if="icon.name" data-testid="designEvent" class="design-event gl-absolute">
|
||||
<span :title="icon.tooltip" :aria-label="icon.tooltip">
|
||||
<gl-icon :name="icon.name" :size="18" :class="icon.classes" />
|
||||
</span>
|
||||
|
@ -145,25 +147,28 @@ export default {
|
|||
v-show="showImage"
|
||||
:src="imageLink"
|
||||
:alt="filename"
|
||||
class="block mx-auto mw-100 mh-100 design-img"
|
||||
class="gl-display-block gl-mx-auto gl-max-w-full mh-100 design-img"
|
||||
data-qa-selector="design_image"
|
||||
@load="onImageLoad"
|
||||
@error="onImageError"
|
||||
/>
|
||||
</gl-intersection-observer>
|
||||
</div>
|
||||
<div class="card-footer d-flex w-100">
|
||||
<div class="d-flex flex-column str-truncated-100">
|
||||
<span class="bold str-truncated-100" data-qa-selector="design_file_name">{{
|
||||
<div class="card-footer gl-display-flex gl-w-full">
|
||||
<div class="gl-display-flex gl-flex-direction-column str-truncated-100">
|
||||
<span class="gl-font-weight-bold str-truncated-100" data-qa-selector="design_file_name">{{
|
||||
filename
|
||||
}}</span>
|
||||
<span v-if="updatedAt" class="str-truncated-100">
|
||||
{{ __('Updated') }} <timeago :time="updatedAt" tooltip-placement="bottom" />
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="notesCount" class="ml-auto d-flex align-items-center text-secondary">
|
||||
<gl-icon name="comments" class="ml-1" />
|
||||
<span :aria-label="notesLabel" class="ml-1">
|
||||
<div
|
||||
v-if="notesCount"
|
||||
class="gl-ml-auto gl-display-flex gl-align-items-center gl-text-gray-500"
|
||||
>
|
||||
<gl-icon name="comments" class="gl-ml-2" />
|
||||
<span :aria-label="notesLabel" class="gl-ml-2">
|
||||
{{ notesCount }}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
@ -14,7 +14,7 @@ import Editor from '../lib/editor';
|
|||
import FileTemplatesBar from './file_templates/bar.vue';
|
||||
import { __ } from '~/locale';
|
||||
import { extractMarkdownImagesFromEntries } from '../stores/utils';
|
||||
import { getPathParent, readFileAsDataURL } from '../utils';
|
||||
import { getPathParent, readFileAsDataURL, registerSchema } from '../utils';
|
||||
import { getRulesWithTraversal } from '../lib/editorconfig/parser';
|
||||
import mapRulesToMonaco from '../lib/editorconfig/rules_mapper';
|
||||
|
||||
|
@ -56,6 +56,7 @@ export default {
|
|||
'isEditModeActive',
|
||||
'isCommitModeActive',
|
||||
'currentBranch',
|
||||
'getJsonSchemaForPath',
|
||||
]),
|
||||
...mapGetters('fileTemplates', ['showFileTemplatesBar']),
|
||||
shouldHideEditor() {
|
||||
|
@ -197,6 +198,8 @@ export default {
|
|||
|
||||
this.editor.clearEditor();
|
||||
|
||||
this.registerSchemaForFile();
|
||||
|
||||
Promise.all([this.fetchFileData(), this.fetchEditorconfigRules()])
|
||||
.then(() => {
|
||||
this.createEditorInstance();
|
||||
|
@ -330,6 +333,10 @@ export default {
|
|||
// do nothing if no image is found in the clipboard
|
||||
return Promise.resolve();
|
||||
},
|
||||
registerSchemaForFile() {
|
||||
const schema = this.getJsonSchemaForPath(this.file.path);
|
||||
registerSchema(schema);
|
||||
},
|
||||
},
|
||||
viewerTypes,
|
||||
FILE_VIEW_MODE_EDITOR,
|
||||
|
|
|
@ -7,10 +7,9 @@ import ModelManager from './common/model_manager';
|
|||
import { editorOptions, defaultEditorOptions, defaultDiffEditorOptions } from './editor_options';
|
||||
import { themes } from './themes';
|
||||
import languages from './languages';
|
||||
import schemas from './schemas';
|
||||
import keymap from './keymap.json';
|
||||
import { clearDomElement } from '~/editor/utils';
|
||||
import { registerLanguages, registerSchemas } from '../utils';
|
||||
import { registerLanguages } from '../utils';
|
||||
|
||||
function setupThemes() {
|
||||
themes.forEach(theme => {
|
||||
|
@ -46,10 +45,6 @@ export default class Editor {
|
|||
setupThemes();
|
||||
registerLanguages(...languages);
|
||||
|
||||
if (gon.features?.schemaLinting) {
|
||||
registerSchemas(...schemas);
|
||||
}
|
||||
|
||||
this.debouncedUpdate = debounce(() => {
|
||||
this.updateDimensions();
|
||||
}, 200);
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
import json from './json';
|
||||
import yaml from './yaml';
|
||||
|
||||
export default [json, yaml];
|
|
@ -1,8 +0,0 @@
|
|||
export default {
|
||||
language: 'json',
|
||||
options: {
|
||||
validate: true,
|
||||
enableSchemaRequest: true,
|
||||
schemas: [],
|
||||
},
|
||||
};
|
|
@ -1,4 +0,0 @@
|
|||
export default {
|
||||
uri: 'https://json.schemastore.org/gitlab-ci',
|
||||
fileMatch: ['*.gitlab-ci.yml'],
|
||||
};
|
|
@ -1,12 +0,0 @@
|
|||
import gitlabCi from './gitlab_ci';
|
||||
|
||||
export default {
|
||||
language: 'yaml',
|
||||
options: {
|
||||
validate: true,
|
||||
enableSchemaRequest: true,
|
||||
hover: true,
|
||||
completion: true,
|
||||
schemas: [gitlabCi],
|
||||
},
|
||||
};
|
|
@ -6,6 +6,7 @@ import {
|
|||
PERMISSION_CREATE_MR,
|
||||
PERMISSION_PUSH_CODE,
|
||||
} from '../constants';
|
||||
import Api from '~/api';
|
||||
|
||||
export const activeFile = state => state.openFiles.find(file => file.active) || null;
|
||||
|
||||
|
@ -177,3 +178,18 @@ export const getAvailableFileName = (state, getters) => path => {
|
|||
|
||||
export const getUrlForPath = state => path =>
|
||||
`/project/${state.currentProjectId}/tree/${state.currentBranchId}/-/${path}/`;
|
||||
|
||||
export const getJsonSchemaForPath = (state, getters) => path => {
|
||||
const [namespace, ...project] = state.currentProjectId.split('/');
|
||||
return {
|
||||
uri:
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
location.origin +
|
||||
Api.buildUrl(Api.projectFileSchemaPath)
|
||||
.replace(':namespace_path', namespace)
|
||||
.replace(':project_path', project.join('/'))
|
||||
.replace(':ref', getters.currentBranch?.commit.id || state.currentBranchId)
|
||||
.replace(':filename', path),
|
||||
fileMatch: [`*${path}`],
|
||||
};
|
||||
};
|
||||
|
|
|
@ -75,17 +75,17 @@ export function registerLanguages(def, ...defs) {
|
|||
languages.setLanguageConfiguration(languageId, def.conf);
|
||||
}
|
||||
|
||||
export function registerSchemas({ language, options }, ...schemas) {
|
||||
schemas.forEach(schema => registerSchemas(schema));
|
||||
|
||||
const defaults = {
|
||||
json: languages.json.jsonDefaults,
|
||||
yaml: languages.yaml.yamlDefaults,
|
||||
};
|
||||
|
||||
if (defaults[language]) {
|
||||
defaults[language].setDiagnosticsOptions(options);
|
||||
}
|
||||
export function registerSchema(schema) {
|
||||
const defaults = [languages.json.jsonDefaults, languages.yaml.yamlDefaults];
|
||||
defaults.forEach(d =>
|
||||
d.setDiagnosticsOptions({
|
||||
validate: true,
|
||||
enableSchemaRequest: true,
|
||||
hover: true,
|
||||
completion: true,
|
||||
schemas: [schema],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export const otherSide = side => (side === SIDE_RIGHT ? SIDE_LEFT : SIDE_RIGHT);
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
fragment IncidentFields on Issue {
|
||||
severity
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
#import "ee_else_ce/incidents/graphql/fragments/incident_fields.fragment.graphql"
|
||||
|
||||
query getIncidents(
|
||||
$projectPath: ID!
|
||||
$issueTypes: [IssueType!]
|
||||
|
@ -39,8 +41,7 @@ query getIncidents(
|
|||
webUrl
|
||||
}
|
||||
}
|
||||
statusPagePublishedIncident
|
||||
severity
|
||||
...IncidentFields
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
|
|
|
@ -7,7 +7,7 @@ import MilestoneSelect from './milestone_select';
|
|||
import issueStatusSelect from './issue_status_select';
|
||||
import subscriptionSelect from './subscription_select';
|
||||
import LabelsSelect from './labels_select';
|
||||
import issueableEventHub from './issuables_list/eventhub';
|
||||
import issueableEventHub from './issues_list/eventhub';
|
||||
|
||||
const HIDDEN_CLASS = 'hidden';
|
||||
const DISABLED_CONTENT_CLASS = 'disabled-content';
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
} from '~/jira_import/utils/jira_import_utils';
|
||||
|
||||
export default {
|
||||
name: 'IssuableListRoot',
|
||||
name: 'JiraIssuesList',
|
||||
components: {
|
||||
GlAlert,
|
||||
GlLabel,
|
|
@ -2,10 +2,10 @@ import Vue from 'vue';
|
|||
import VueApollo from 'vue-apollo';
|
||||
import createDefaultClient from '~/lib/graphql';
|
||||
import { parseBoolean, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||
import IssuableListRootApp from './components/issuable_list_root_app.vue';
|
||||
import JiraIssuesListRoot from './components/jira_issues_list_root.vue';
|
||||
import IssuablesListApp from './components/issuables_list_app.vue';
|
||||
|
||||
function mountIssuableListRootApp() {
|
||||
function mountJiraIssuesListApp() {
|
||||
const el = document.querySelector('.js-projects-issues-root');
|
||||
|
||||
if (!el) {
|
||||
|
@ -23,7 +23,7 @@ function mountIssuableListRootApp() {
|
|||
el,
|
||||
apolloProvider,
|
||||
render(createComponent) {
|
||||
return createComponent(IssuableListRootApp, {
|
||||
return createComponent(JiraIssuesListRoot, {
|
||||
props: {
|
||||
canEdit: parseBoolean(el.dataset.canEdit),
|
||||
isJiraConfigured: parseBoolean(el.dataset.isJiraConfigured),
|
||||
|
@ -62,6 +62,6 @@ function mountIssuablesListApp() {
|
|||
}
|
||||
|
||||
export default function initIssuablesList() {
|
||||
mountIssuableListRootApp();
|
||||
mountJiraIssuesListApp();
|
||||
mountIssuablesListApp();
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import { last } from 'lodash';
|
||||
import { JIRA_IMPORT_SUCCESS_ALERT_HIDE_MAP_KEY } from '~/issuables_list/constants';
|
||||
import { JIRA_IMPORT_SUCCESS_ALERT_HIDE_MAP_KEY } from '~/issues_list/constants';
|
||||
|
||||
export const IMPORT_STATE = {
|
||||
FAILED: 'failed',
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
|
||||
import initIssuablesList from '~/issuables_list';
|
||||
import initIssuablesList from '~/issues_list';
|
||||
import projectSelect from '~/project_select';
|
||||
import initFilteredSearch from '~/pages/search/init_filtered_search';
|
||||
import issuableInitBulkUpdateSidebar from '~/issuable_init_bulk_update_sidebar';
|
||||
|
|
|
@ -6,6 +6,33 @@ import GpgBadges from '~/gpg_badges';
|
|||
import '~/sourcegraph/load';
|
||||
import PipelineTourSuccessModal from '~/blob/pipeline_tour_success_modal.vue';
|
||||
|
||||
const createGitlabCiYmlVisualization = (containerId = '#js-blob-toggle-graph-preview') => {
|
||||
const el = document.querySelector(containerId);
|
||||
const { filename, blobData } = el?.dataset;
|
||||
|
||||
const nameRegexp = /\.gitlab-ci.yml/;
|
||||
|
||||
if (!el || !nameRegexp.test(filename)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
el,
|
||||
components: {
|
||||
GitlabCiYamlVisualization: () =>
|
||||
import('~/pipelines/components/pipeline_graph/gitlab_ci_yaml_visualization.vue'),
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement('gitlabCiYamlVisualization', {
|
||||
props: {
|
||||
blobData,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
new BlobViewer(); // eslint-disable-line no-new
|
||||
initBlob();
|
||||
|
@ -63,4 +90,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (gon?.features?.gitlabCiYmlPreview) {
|
||||
createGitlabCiYmlVisualization();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import initIssuablesList from '~/issuables_list';
|
||||
import initIssuablesList from '~/issues_list';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initIssuablesList();
|
||||
|
|
|
@ -7,7 +7,7 @@ import UsersSelect from '~/users_select';
|
|||
import initFilteredSearch from '~/pages/search/init_filtered_search';
|
||||
import { FILTERED_SEARCH } from '~/pages/constants';
|
||||
import { ISSUABLE_INDEX } from '~/pages/projects/constants';
|
||||
import initIssuablesList from '~/issuables_list';
|
||||
import initIssuablesList from '~/issues_list';
|
||||
import initManualOrdering from '~/manual_ordering';
|
||||
import { showLearnGitLabIssuesPopover } from '~/onboarding_issues';
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import FilteredSearchServiceDesk from './filtered_search';
|
||||
import initIssuablesList from '~/issuables_list';
|
||||
import initIssuablesList from '~/issues_list';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const supportBotData = JSON.parse(
|
||||
|
|
|
@ -5,6 +5,8 @@ import axios from '~/lib/utils/axios_utils';
|
|||
import PerformanceBarService from './services/performance_bar_service';
|
||||
import PerformanceBarStore from './stores/performance_bar_store';
|
||||
|
||||
import initPerformanceBarLog from './performance_bar_log';
|
||||
|
||||
const initPerformanceBar = el => {
|
||||
const performanceBarData = el.dataset;
|
||||
|
||||
|
@ -128,4 +130,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
}
|
||||
});
|
||||
|
||||
initPerformanceBarLog();
|
||||
|
||||
export default initPerformanceBar;
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
/* eslint-disable no-console */
|
||||
import { getCLS, getFID, getLCP } from 'web-vitals';
|
||||
|
||||
const initVitalsLog = () => {
|
||||
const reportVital = data => {
|
||||
console.log(`${String.fromCodePoint(0x1f4c8)} ${data.name} : `, data);
|
||||
};
|
||||
|
||||
console.log(
|
||||
`${String.fromCodePoint(
|
||||
0x1f4d1,
|
||||
)} To get the final web vital numbers reported you maybe need to switch away and back to the tab`,
|
||||
);
|
||||
getCLS(reportVital);
|
||||
getFID(reportVital);
|
||||
getLCP(reportVital);
|
||||
};
|
||||
|
||||
const initPerformanceBarLog = () => {
|
||||
console.log(
|
||||
`%c ${String.fromCodePoint(0x1f98a)} GitLab performance bar`,
|
||||
'width:100%;background-color: #292961; color: #FFFFFF; font-size:24px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto; padding: 10px;display:block;padding-right: 100px;',
|
||||
);
|
||||
|
||||
initVitalsLog();
|
||||
};
|
||||
|
||||
export default initPerformanceBarLog;
|
|
@ -0,0 +1,76 @@
|
|||
<script>
|
||||
import { GlTab, GlTabs } from '@gitlab/ui';
|
||||
import jsYaml from 'js-yaml';
|
||||
import PipelineGraph from './pipeline_graph.vue';
|
||||
import { preparePipelineGraphData } from '../../utils';
|
||||
|
||||
export default {
|
||||
FILE_CONTENT_SELECTOR: '#blob-content',
|
||||
EMPTY_FILE_SELECTOR: '.nothing-here-block',
|
||||
|
||||
components: {
|
||||
GlTab,
|
||||
GlTabs,
|
||||
PipelineGraph,
|
||||
},
|
||||
props: {
|
||||
blobData: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedTabIndex: 0,
|
||||
pipelineData: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isVisualizationTab() {
|
||||
return this.selectedTabIndex === 1;
|
||||
},
|
||||
},
|
||||
async created() {
|
||||
if (this.blobData) {
|
||||
// The blobData in this case represents the gitlab-ci.yml data
|
||||
const json = await jsYaml.load(this.blobData);
|
||||
this.pipelineData = preparePipelineGraphData(json);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// This is used because the blob page still uses haml, and we can't make
|
||||
// our haml hide the unused section so we resort to a standard query here.
|
||||
toggleFileContent({ isFileTab }) {
|
||||
const el = document.querySelector(this.$options.FILE_CONTENT_SELECTOR);
|
||||
const emptySection = document.querySelector(this.$options.EMPTY_FILE_SELECTOR);
|
||||
|
||||
const elementToHide = el || emptySection;
|
||||
|
||||
if (!elementToHide) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Checking for the current style display prevents user
|
||||
// from toggling visiblity on and off when clicking on the tab
|
||||
if (!isFileTab && elementToHide.style.display !== 'none') {
|
||||
elementToHide.style.display = 'none';
|
||||
}
|
||||
|
||||
if (isFileTab && elementToHide.style.display === 'none') {
|
||||
elementToHide.style.display = 'block';
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<gl-tabs v-model="selectedTabIndex">
|
||||
<gl-tab :title="__('File')" @click="toggleFileContent({ isFileTab: true })" />
|
||||
<gl-tab :title="__('Visualization')" @click="toggleFileContent({ isFileTab: false })" />
|
||||
</gl-tabs>
|
||||
</div>
|
||||
<pipeline-graph v-if="isVisualizationTab" :pipeline-data="pipelineData" />
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,24 @@
|
|||
<script>
|
||||
import tooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
tooltipOnTruncate,
|
||||
},
|
||||
props: {
|
||||
jobName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<tooltip-on-truncate :title="jobName" truncate-target="child" placement="top">
|
||||
<div
|
||||
class="gl-bg-white gl-text-center gl-text-truncate gl-rounded-pill gl-inset-border-1-green-600 gl-mb-3 gl-px-5 gl-py-2 pipeline-job-pill "
|
||||
>
|
||||
{{ jobName }}
|
||||
</div>
|
||||
</tooltip-on-truncate>
|
||||
</template>
|
|
@ -0,0 +1,57 @@
|
|||
<script>
|
||||
import { isEmpty } from 'lodash';
|
||||
import { GlAlert } from '@gitlab/ui';
|
||||
import JobPill from './job_pill.vue';
|
||||
import StagePill from './stage_pill.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlAlert,
|
||||
JobPill,
|
||||
StagePill,
|
||||
},
|
||||
props: {
|
||||
pipelineData: {
|
||||
required: true,
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isPipelineDataEmpty() {
|
||||
return isEmpty(this.pipelineData);
|
||||
},
|
||||
emptyClass() {
|
||||
return !this.isPipelineDataEmpty ? 'gl-py-7' : '';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="gl-display-flex gl-bg-gray-50 gl-px-4 gl-overflow-auto" :class="emptyClass">
|
||||
<gl-alert v-if="isPipelineDataEmpty" variant="tip" :dismissible="false">
|
||||
{{ __('No content to show') }}
|
||||
</gl-alert>
|
||||
<template v-else>
|
||||
<div
|
||||
v-for="(stage, index) in pipelineData.stages"
|
||||
:key="`${stage.name}-${index}`"
|
||||
class="gl-flex-direction-column"
|
||||
>
|
||||
<div
|
||||
class="gl-display-flex gl-align-items-center gl-bg-white gl-w-full gl-px-8 gl-py-4 gl-mb-5"
|
||||
:class="{
|
||||
'stage-left-rounded': index === 0,
|
||||
'stage-right-rounded': index === pipelineData.stages.length - 1,
|
||||
}"
|
||||
>
|
||||
<stage-pill :stage-name="stage.name" :is-empty="stage.groups.length === 0" />
|
||||
</div>
|
||||
<div
|
||||
class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-w-full gl-px-8"
|
||||
>
|
||||
<job-pill v-for="group in stage.groups" :key="group.name" :job-name="group.name" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,35 @@
|
|||
<script>
|
||||
import tooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
tooltipOnTruncate,
|
||||
},
|
||||
props: {
|
||||
stageName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
isEmpty: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
emptyClass() {
|
||||
return this.isEmpty ? 'gl-bg-gray-200' : 'gl-bg-gray-600';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<tooltip-on-truncate :title="stageName" truncate-target="child" placement="top">
|
||||
<div
|
||||
class="gl-px-5 gl-py-2 gl-text-white gl-text-center gl-text-truncate gl-rounded-pill pipeline-stage-pill"
|
||||
:class="emptyClass"
|
||||
>
|
||||
{{ stageName }}
|
||||
</div>
|
||||
</tooltip-on-truncate>
|
||||
</template>
|
|
@ -4,3 +4,46 @@ import { SUPPORTED_FILTER_PARAMETERS } from './constants';
|
|||
export const validateParams = params => {
|
||||
return pickBy(params, (val, key) => SUPPORTED_FILTER_PARAMETERS.includes(key) && val);
|
||||
};
|
||||
|
||||
/**
|
||||
* This function takes a json payload that comes from a yml
|
||||
* file converted to json through `jsyaml` library. Because we
|
||||
* naively convert the entire yaml to json, some keys (like `includes`)
|
||||
* are irrelevant to rendering the graph and must be removed. We also
|
||||
* restructure the data to have the structure from an API response for the
|
||||
* pipeline data.
|
||||
* @param {Object} jsonData
|
||||
* @returns {Array} - Array of stages containing all jobs
|
||||
*/
|
||||
export const preparePipelineGraphData = jsonData => {
|
||||
const jsonKeys = Object.keys(jsonData);
|
||||
const jobNames = jsonKeys.filter(job => jsonData[job]?.stage);
|
||||
|
||||
// We merge both the stages from the "stages" key in the yaml and the stage associated
|
||||
// with each job to show the user both the stages they explicitly defined, and those
|
||||
// that they added under jobs. We also remove duplicates.
|
||||
const jobStages = jobNames.map(job => jsonData[job].stage);
|
||||
const userDefinedStages = jsonData?.stages ?? [];
|
||||
|
||||
// The order is important here. We always show the stages in order they were
|
||||
// defined in the `stages` key first, and then stages that are under the jobs.
|
||||
const stages = Array.from(new Set([...userDefinedStages, ...jobStages]));
|
||||
|
||||
const arrayOfJobsByStage = stages.map(val => {
|
||||
return jobNames.filter(job => {
|
||||
return jsonData[job].stage === val;
|
||||
});
|
||||
});
|
||||
|
||||
const pipelineData = stages.map((stage, index) => {
|
||||
const stageJobs = arrayOfJobsByStage[index];
|
||||
return {
|
||||
name: stage,
|
||||
groups: stageJobs.map(job => {
|
||||
return { name: job, jobs: [{ ...jsonData[job] }] };
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
return { stages: pipelineData };
|
||||
};
|
||||
|
|
|
@ -1081,3 +1081,19 @@ button.mini-pipeline-graph-dropdown-toggle {
|
|||
.progress-bar.bg-primary {
|
||||
background-color: $blue-500 !important;
|
||||
}
|
||||
|
||||
.pipeline-stage-pill {
|
||||
width: 10rem;
|
||||
}
|
||||
|
||||
.pipeline-job-pill {
|
||||
width: 8rem;
|
||||
}
|
||||
|
||||
.stage-left-rounded {
|
||||
border-radius: 2rem 0 0 2rem;
|
||||
}
|
||||
|
||||
.stage-right-rounded {
|
||||
border-radius: 0 2rem 2rem 0;
|
||||
}
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Admin::InstanceStatisticsController < Admin::ApplicationController
|
||||
include Analytics::UniqueVisitsHelper
|
||||
|
||||
before_action :check_feature_flag
|
||||
|
||||
track_unique_visits :index, target_id: 'i_analytics_instance_statistics'
|
||||
|
||||
def index
|
||||
end
|
||||
|
||||
|
|
|
@ -100,7 +100,7 @@ module NotesActions
|
|||
# the finder. Here, we select between returning all notes since then, or a
|
||||
# page's worth of notes.
|
||||
def gather_notes
|
||||
if Feature.enabled?(:paginated_notes, project)
|
||||
if Feature.enabled?(:paginated_notes, noteable.try(:resource_parent))
|
||||
gather_some_notes
|
||||
else
|
||||
gather_all_notes
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Profiles::KeysController < Profiles::ApplicationController
|
||||
skip_before_action :authenticate_user!, only: [:get_keys]
|
||||
|
||||
def index
|
||||
@keys = current_user.keys.order_id_desc
|
||||
@key = Key.new
|
||||
|
@ -31,6 +33,25 @@ class Profiles::KeysController < Profiles::ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
# Get all keys of a user(params[:username]) in a text format
|
||||
# Helpful for sysadmins to put in respective servers
|
||||
def get_keys
|
||||
if params[:username].present?
|
||||
begin
|
||||
user = UserFinder.new(params[:username]).find_by_username
|
||||
if user.present?
|
||||
render plain: user.all_ssh_keys.join("\n")
|
||||
else
|
||||
render_404
|
||||
end
|
||||
rescue => e
|
||||
render html: e.message
|
||||
end
|
||||
else
|
||||
render_404
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def key_params
|
||||
|
|
|
@ -35,6 +35,7 @@ class Projects::BlobController < Projects::ApplicationController
|
|||
before_action only: :show do
|
||||
push_frontend_feature_flag(:code_navigation, @project, default_enabled: true)
|
||||
push_frontend_feature_flag(:suggest_pipeline) if experiment_enabled?(:suggest_pipeline)
|
||||
push_frontend_feature_flag(:gitlab_ci_yml_preview, @project, default_enabled: false)
|
||||
end
|
||||
|
||||
track_redis_hll_event :create, :update, name: 'g_edit_by_sfe', feature: :track_editor_edit_actions, feature_default_enabled: true
|
||||
|
|
|
@ -37,12 +37,6 @@ class UsersController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
# Get all keys of a user(params[:username]) in a text format
|
||||
# Helpful for sysadmins to put in respective servers
|
||||
def ssh_keys
|
||||
render plain: user.all_ssh_keys.join("\n")
|
||||
end
|
||||
|
||||
def activity
|
||||
respond_to do |format|
|
||||
format.html { render 'show' }
|
||||
|
|
|
@ -214,7 +214,7 @@ class Snippet < ApplicationRecord
|
|||
def blobs
|
||||
return [] unless repository_exists?
|
||||
|
||||
repository.ls_files(repository.root_ref).map { |file| Blob.lazy(repository, repository.root_ref, file) }
|
||||
repository.ls_files(default_branch).map { |file| Blob.lazy(repository, default_branch, file) }
|
||||
end
|
||||
|
||||
def hook_attrs
|
||||
|
@ -309,6 +309,11 @@ class Snippet < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
override :default_branch
|
||||
def default_branch
|
||||
super || 'master'
|
||||
end
|
||||
|
||||
def repository_storage
|
||||
snippet_repository&.shard_name || self.class.pick_repository_storage
|
||||
end
|
||||
|
@ -336,17 +341,17 @@ class Snippet < ApplicationRecord
|
|||
def file_name_on_repo
|
||||
return if repository.empty?
|
||||
|
||||
list_files(repository.root_ref).first
|
||||
list_files(default_branch).first
|
||||
end
|
||||
|
||||
def list_files(ref = nil)
|
||||
return [] if repository.empty?
|
||||
|
||||
repository.ls_files(ref)
|
||||
repository.ls_files(ref || default_branch)
|
||||
end
|
||||
|
||||
def multiple_files?
|
||||
list_files(repository.root_ref).size > 1
|
||||
list_files.size > 1
|
||||
end
|
||||
|
||||
class << self
|
||||
|
|
|
@ -93,7 +93,7 @@ class SnippetRepository < ApplicationRecord
|
|||
end
|
||||
|
||||
def get_last_empty_file_index
|
||||
repository.ls_files(nil).inject(0) do |max, file|
|
||||
repository.ls_files(snippet.default_branch).inject(0) do |max, file|
|
||||
idx = file[EMPTY_FILE_PATTERN, 1].to_i
|
||||
[idx, max].max
|
||||
end
|
||||
|
|
|
@ -25,7 +25,7 @@ class SnippetStatistics < ApplicationRecord
|
|||
|
||||
def update_file_count
|
||||
count = if snippet.repository_exists?
|
||||
repository.ls_files(repository.root_ref).size
|
||||
repository.ls_files(snippet.default_branch).size
|
||||
else
|
||||
0
|
||||
end
|
||||
|
|
|
@ -5,27 +5,34 @@ module Terraform
|
|||
include UsageStatistics
|
||||
include FileStoreMounter
|
||||
|
||||
DEFAULT = '{"version":1}'.freeze
|
||||
HEX_REGEXP = %r{\A\h+\z}.freeze
|
||||
UUID_LENGTH = 32
|
||||
|
||||
belongs_to :project
|
||||
belongs_to :locked_by_user, class_name: 'User'
|
||||
|
||||
has_many :versions, class_name: 'Terraform::StateVersion', foreign_key: :terraform_state_id
|
||||
has_one :latest_version, -> { ordered_by_version_desc }, class_name: 'Terraform::StateVersion', foreign_key: :terraform_state_id
|
||||
|
||||
scope :versioning_not_enabled, -> { where(versioning_enabled: false) }
|
||||
|
||||
validates :project_id, presence: true
|
||||
validates :uuid, presence: true, uniqueness: true, length: { is: UUID_LENGTH },
|
||||
format: { with: HEX_REGEXP, message: 'only allows hex characters' }
|
||||
|
||||
default_value_for(:uuid, allows_nil: false) { SecureRandom.hex(UUID_LENGTH / 2) }
|
||||
default_value_for(:versioning_enabled, true)
|
||||
|
||||
mount_file_store_uploader StateUploader
|
||||
|
||||
default_value_for(:file) { CarrierWaveStringFile.new(DEFAULT) }
|
||||
|
||||
def file_store
|
||||
super || StateUploader.default_store
|
||||
end
|
||||
|
||||
def latest_file
|
||||
versioning_enabled ? latest_version&.file : file
|
||||
end
|
||||
|
||||
def local?
|
||||
file_store == ObjectStorage::Store::LOCAL
|
||||
end
|
||||
|
@ -33,6 +40,17 @@ module Terraform
|
|||
def locked?
|
||||
self.lock_xid.present?
|
||||
end
|
||||
|
||||
def update_file!(data, version:)
|
||||
if versioning_enabled?
|
||||
new_version = versions.build(version: version)
|
||||
new_version.assign_attributes(created_by_user: locked_by_user, file: data)
|
||||
new_version.save!
|
||||
else
|
||||
self.file = data
|
||||
save!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Terraform
|
||||
class StateVersion < ApplicationRecord
|
||||
include FileStoreMounter
|
||||
|
||||
belongs_to :terraform_state, class_name: 'Terraform::State', optional: false
|
||||
belongs_to :created_by_user, class_name: 'User', optional: true
|
||||
|
||||
scope :ordered_by_version_desc, -> { order(version: :desc) }
|
||||
|
||||
default_value_for(:file_store) { VersionedStateUploader.default_store }
|
||||
|
||||
mount_file_store_uploader VersionedStateUploader
|
||||
|
||||
delegate :project_id, :uuid, to: :terraform_state, allow_nil: true
|
||||
end
|
||||
end
|
|
@ -46,7 +46,7 @@ module AlertManagement
|
|||
def issue_summary_markdown
|
||||
<<~MARKDOWN.chomp
|
||||
#{metadata_list}
|
||||
#{alert_details}#{metric_embed_for_alert}
|
||||
#{metric_embed_for_alert}
|
||||
MARKDOWN
|
||||
end
|
||||
|
||||
|
@ -65,23 +65,6 @@ module AlertManagement
|
|||
metadata.join(MARKDOWN_LINE_BREAK)
|
||||
end
|
||||
|
||||
def alert_details
|
||||
if details.present?
|
||||
<<~MARKDOWN.chomp
|
||||
|
||||
#### Alert Details
|
||||
|
||||
#{details_list}
|
||||
MARKDOWN
|
||||
end
|
||||
end
|
||||
|
||||
def details_list
|
||||
details
|
||||
.map { |label, value| list_item(label, value) }
|
||||
.join(MARKDOWN_LINE_BREAK)
|
||||
end
|
||||
|
||||
def metric_embed_for_alert
|
||||
"\n[](#{metrics_dashboard_url})" if metrics_dashboard_url
|
||||
end
|
||||
|
|
|
@ -51,18 +51,10 @@ module Projects
|
|||
def issue_summary_markdown
|
||||
<<~MARKDOWN.chomp
|
||||
#{metadata_list}
|
||||
#{alert_details}#{metric_embed_for_alert}
|
||||
#{metric_embed_for_alert}
|
||||
MARKDOWN
|
||||
end
|
||||
|
||||
def details_list
|
||||
strong_memoize(:details_list) do
|
||||
details
|
||||
.map { |label, value| list_item(label, value) }
|
||||
.join(MARKDOWN_LINE_BREAK)
|
||||
end
|
||||
end
|
||||
|
||||
def metric_embed_for_alert
|
||||
"\n[](#{metrics_dashboard_url})" if metrics_dashboard_url
|
||||
end
|
||||
|
@ -111,17 +103,6 @@ module Projects
|
|||
Gitlab::Utils::InlineHash.merge_keys(payload)
|
||||
end
|
||||
|
||||
def alert_details
|
||||
if details.present?
|
||||
<<~MARKDOWN.chomp
|
||||
|
||||
#### Alert Details
|
||||
|
||||
#{details_list}
|
||||
MARKDOWN
|
||||
end
|
||||
end
|
||||
|
||||
def list_item(key, value)
|
||||
"**#{key}:** #{value}".strip
|
||||
end
|
||||
|
|
|
@ -21,7 +21,7 @@ module Issues
|
|||
user = current_user
|
||||
issue.run_after_commit do
|
||||
NewIssueWorker.perform_async(issue.id, user.id)
|
||||
IssuePlacementWorker.perform_async(issue.id)
|
||||
IssuePlacementWorker.perform_async(nil, issue.project_id)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -82,7 +82,7 @@ module Snippets
|
|||
|
||||
def create_commit
|
||||
commit_attrs = {
|
||||
branch_name: 'master',
|
||||
branch_name: @snippet.default_branch,
|
||||
message: 'Initial commit'
|
||||
}
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ module Snippets
|
|||
def check_branch_name_default!
|
||||
branches = repository.branch_names
|
||||
|
||||
return if branches.first == Gitlab::Checks::SnippetCheck::DEFAULT_BRANCH
|
||||
return if branches.first == snippet.default_branch
|
||||
|
||||
raise RepositoryValidationError, _('Repository has an invalid default branch name.')
|
||||
end
|
||||
|
@ -51,7 +51,7 @@ module Snippets
|
|||
end
|
||||
|
||||
def check_file_count!
|
||||
file_count = repository.ls_files(nil).size
|
||||
file_count = repository.ls_files(snippet.default_branch).size
|
||||
limit = Snippet.max_file_limit(current_user)
|
||||
|
||||
if file_count > limit
|
||||
|
|
|
@ -93,7 +93,7 @@ module Snippets
|
|||
raise UpdateError unless snippet.snippet_repository
|
||||
|
||||
commit_attrs = {
|
||||
branch_name: 'master',
|
||||
branch_name: snippet.default_branch,
|
||||
message: 'Update snippet'
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Terraform
|
||||
class VersionedStateUploader < StateUploader
|
||||
def filename
|
||||
"#{model.version}.tfstate"
|
||||
end
|
||||
|
||||
def store_dir
|
||||
Gitlab::HashedPath.new(model.uuid, root_hash: project_id)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,6 +1,10 @@
|
|||
- simple_viewer = blob.simple_viewer
|
||||
- rich_viewer = blob.rich_viewer
|
||||
- rich_viewer_active = rich_viewer && params[:viewer] != 'simple'
|
||||
- blob_data = defined?(@blob) ? @blob.data : {}
|
||||
- filename = defined?(@blob) ? @blob.name : ''
|
||||
|
||||
#js-blob-toggle-graph-preview{ data: { blob_data: blob_data, filename: filename } }
|
||||
|
||||
= render 'projects/blob/viewer', viewer: simple_viewer, hidden: rich_viewer_active
|
||||
|
||||
|
|
|
@ -4,8 +4,10 @@
|
|||
- state_human_name, state_icon_name = state_name_with_icon(@merge_request)
|
||||
|
||||
- if @merge_request.closed_without_fork?
|
||||
.alert.alert-danger
|
||||
The source project of this merge request has been removed.
|
||||
.gl-alert.gl-alert-danger.gl-mb-5
|
||||
= sprite_icon('error', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
|
||||
.gl-alert-body
|
||||
The source project of this merge request has been removed.
|
||||
|
||||
.detail-page-header.border-bottom-0.pt-0.pb-0
|
||||
.detail-page-header-body
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
.file-content.code.js-syntax-highlight
|
||||
#blob-content.file-content.code.js-syntax-highlight
|
||||
.line-numbers
|
||||
- if blob.data.present?
|
||||
- link_icon = sprite_icon('link', size: 12)
|
||||
|
|
|
@ -14,7 +14,7 @@ class IssuePlacementWorker
|
|||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def perform(issue_id, project_id = nil)
|
||||
issue = Issue.id_in(issue_id).first
|
||||
issue = find_issue(issue_id, project_id)
|
||||
return unless issue
|
||||
|
||||
# Move the oldest 100 unpositioned items to the end.
|
||||
|
@ -31,10 +31,19 @@ class IssuePlacementWorker
|
|||
|
||||
Issue.move_nulls_to_end(to_place)
|
||||
Issues::BaseService.new(nil).rebalance_if_needed(to_place.max_by(&:relative_position))
|
||||
IssuePlacementWorker.perform_async(leftover.id) if leftover.present?
|
||||
IssuePlacementWorker.perform_async(nil, leftover.project_id) if leftover.present?
|
||||
rescue RelativePositioning::NoSpaceLeft => e
|
||||
Gitlab::ErrorTracking.log_exception(e, issue_id: issue_id)
|
||||
IssueRebalancingWorker.perform_async(nil, issue.project_id)
|
||||
Gitlab::ErrorTracking.log_exception(e, issue_id: issue_id, project_id: project_id)
|
||||
IssueRebalancingWorker.perform_async(nil, project_id.presence || issue.project_id)
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
def find_issue(issue_id, project_id)
|
||||
return Issue.id_in(issue_id).first if issue_id
|
||||
|
||||
project = Project.id_in(project_id).first
|
||||
return unless project
|
||||
|
||||
project.issues.first
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add versioning support to Terraform state backend
|
||||
merge_request: 35211
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Replace bootstrap alerts in app/views/projects/merge_requests/_mr_title.html.haml
|
||||
merge_request: 41399
|
||||
author: Gilang Gumilar
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add instance statistics visits to usage data
|
||||
merge_request: 42211
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Make SSH keys publicly accessible
|
||||
merge_request: 42288
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Pass project ID to issue placement worker
|
||||
merge_request: 42091
|
||||
author:
|
||||
type: performance
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: allow project bot account to clone through http
|
||||
merge_request: 40635
|
||||
author: Philippe Vienne @PhilippeVienne
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix incident list by restricting query on FOSS
|
||||
merge_request: 42301
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
name: gitlab_ci_yml_preview
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40880
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/244905
|
||||
group: group::ci
|
||||
type: development
|
||||
default_enabled: false
|
|
@ -55,7 +55,7 @@ end
|
|||
|
||||
constraints(::Constraints::UserUrlConstrainer.new) do
|
||||
# Get all keys of user
|
||||
get ':username.keys', controller: :users, action: :ssh_keys, constraints: { username: Gitlab::PathRegex.root_namespace_route_regex }
|
||||
get ':username.keys' => 'profiles/keys#get_keys', constraints: { username: Gitlab::PathRegex.root_namespace_route_regex }
|
||||
|
||||
scope(path: ':username',
|
||||
as: :user,
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateTerraformStateVersions < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
create_table :terraform_state_versions, if_not_exists: true do |t|
|
||||
t.references :terraform_state, index: false, null: false, foreign_key: { on_delete: :cascade }
|
||||
t.references :created_by_user, foreign_key: false
|
||||
t.timestamps_with_timezone null: false
|
||||
t.integer :version, null: false
|
||||
t.integer :file_store, limit: 2, null: false
|
||||
t.text :file, null: false
|
||||
|
||||
t.index [:terraform_state_id, :version], unique: true, name: 'index_terraform_state_versions_on_state_id_and_version'
|
||||
end
|
||||
|
||||
add_text_limit :terraform_state_versions, :file, 255
|
||||
end
|
||||
|
||||
def down
|
||||
drop_table :terraform_state_versions
|
||||
end
|
||||
end
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddVersioningEnabledToTerraformStates < ActiveRecord::Migration[6.0]
|
||||
DOWNTIME = false
|
||||
|
||||
def change
|
||||
add_column :terraform_states, :versioning_enabled, :boolean, null: false, default: false
|
||||
end
|
||||
end
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddUsersForeignKeyToTerraformStateVersions < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_foreign_key :terraform_state_versions, :users, column: :created_by_user_id, on_delete: :nullify
|
||||
end
|
||||
|
||||
def down
|
||||
with_lock_retries do
|
||||
remove_foreign_key_if_exists :terraform_state_versions, :users, column: :created_by_user_id
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
354524319f4c426328c7485619e248d00df323842873eaf7a2b3fbd2ad93048f
|
|
@ -0,0 +1 @@
|
|||
1f698671f226289fa1eabbb988b94ecd6486038f4922076bb981e44ee2356b25
|
|
@ -0,0 +1 @@
|
|||
d0ede6c4a28988494b0e18c073e56c1d985de73c443cc6b6d99e0b34a7f37642
|
|
@ -15994,6 +15994,27 @@ CREATE SEQUENCE public.term_agreements_id_seq
|
|||
|
||||
ALTER SEQUENCE public.term_agreements_id_seq OWNED BY public.term_agreements.id;
|
||||
|
||||
CREATE TABLE public.terraform_state_versions (
|
||||
id bigint NOT NULL,
|
||||
terraform_state_id bigint NOT NULL,
|
||||
created_by_user_id bigint,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
version integer NOT NULL,
|
||||
file_store smallint NOT NULL,
|
||||
file text NOT NULL,
|
||||
CONSTRAINT check_0824bb7bbd CHECK ((char_length(file) <= 255))
|
||||
);
|
||||
|
||||
CREATE SEQUENCE public.terraform_state_versions_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
ALTER SEQUENCE public.terraform_state_versions_id_seq OWNED BY public.terraform_state_versions.id;
|
||||
|
||||
CREATE TABLE public.terraform_states (
|
||||
id bigint NOT NULL,
|
||||
project_id bigint NOT NULL,
|
||||
|
@ -16011,6 +16032,7 @@ CREATE TABLE public.terraform_states (
|
|||
verification_retry_count smallint,
|
||||
verification_checksum bytea,
|
||||
verification_failure text,
|
||||
versioning_enabled boolean DEFAULT false NOT NULL,
|
||||
CONSTRAINT check_21a47163ea CHECK ((char_length(verification_failure) <= 255))
|
||||
);
|
||||
|
||||
|
@ -17563,6 +17585,8 @@ ALTER TABLE ONLY public.tags ALTER COLUMN id SET DEFAULT nextval('public.tags_id
|
|||
|
||||
ALTER TABLE ONLY public.term_agreements ALTER COLUMN id SET DEFAULT nextval('public.term_agreements_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY public.terraform_state_versions ALTER COLUMN id SET DEFAULT nextval('public.terraform_state_versions_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY public.terraform_states ALTER COLUMN id SET DEFAULT nextval('public.terraform_states_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY public.timelogs ALTER COLUMN id SET DEFAULT nextval('public.timelogs_id_seq'::regclass);
|
||||
|
@ -18871,6 +18895,9 @@ ALTER TABLE ONLY public.tags
|
|||
ALTER TABLE ONLY public.term_agreements
|
||||
ADD CONSTRAINT term_agreements_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY public.terraform_state_versions
|
||||
ADD CONSTRAINT terraform_state_versions_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY public.terraform_states
|
||||
ADD CONSTRAINT terraform_states_pkey PRIMARY KEY (id);
|
||||
|
||||
|
@ -21115,6 +21142,10 @@ CREATE INDEX index_term_agreements_on_term_id ON public.term_agreements USING bt
|
|||
|
||||
CREATE INDEX index_term_agreements_on_user_id ON public.term_agreements USING btree (user_id);
|
||||
|
||||
CREATE INDEX index_terraform_state_versions_on_created_by_user_id ON public.terraform_state_versions USING btree (created_by_user_id);
|
||||
|
||||
CREATE UNIQUE INDEX index_terraform_state_versions_on_state_id_and_version ON public.terraform_state_versions USING btree (terraform_state_id, version);
|
||||
|
||||
CREATE INDEX index_terraform_states_on_file_store ON public.terraform_states USING btree (file_store);
|
||||
|
||||
CREATE INDEX index_terraform_states_on_locked_by_user_id ON public.terraform_states USING btree (locked_by_user_id);
|
||||
|
@ -21913,6 +21944,9 @@ ALTER TABLE ONLY public.geo_event_log
|
|||
ALTER TABLE ONLY public.projects
|
||||
ADD CONSTRAINT fk_6e5c14658a FOREIGN KEY (pool_repository_id) REFERENCES public.pool_repositories(id) ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE ONLY public.terraform_state_versions
|
||||
ADD CONSTRAINT fk_6e81384d7f FOREIGN KEY (created_by_user_id) REFERENCES public.users(id) ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE ONLY public.protected_branch_push_access_levels
|
||||
ADD CONSTRAINT fk_7111b68cdb FOREIGN KEY (group_id) REFERENCES public.namespaces(id) ON DELETE CASCADE;
|
||||
|
||||
|
@ -22336,6 +22370,9 @@ ALTER TABLE ONLY public.events
|
|||
ALTER TABLE ONLY public.ip_restrictions
|
||||
ADD CONSTRAINT fk_rails_04a93778d5 FOREIGN KEY (group_id) REFERENCES public.namespaces(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY public.terraform_state_versions
|
||||
ADD CONSTRAINT fk_rails_04f176e239 FOREIGN KEY (terraform_state_id) REFERENCES public.terraform_states(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY public.ci_build_report_results
|
||||
ADD CONSTRAINT fk_rails_056d298d48 FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
|
@ -9407,6 +9407,16 @@ type MergeRequest implements CurrentUserTodos & Noteable {
|
|||
"""
|
||||
allowCollaboration: Boolean
|
||||
|
||||
"""
|
||||
Number of approvals left
|
||||
"""
|
||||
approvalsLeft: Int
|
||||
|
||||
"""
|
||||
Number of approvals required
|
||||
"""
|
||||
approvalsRequired: Int
|
||||
|
||||
"""
|
||||
Indicates if the merge request has all the required approvals. Returns true if no required approvals are configured.
|
||||
"""
|
||||
|
|
|
@ -26108,6 +26108,34 @@
|
|||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "approvalsLeft",
|
||||
"description": "Number of approvals left",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "approvalsRequired",
|
||||
"description": "Number of approvals required",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "approved",
|
||||
"description": "Indicates if the merge request has all the required approvals. Returns true if no required approvals are configured.",
|
||||
|
|
|
@ -1416,6 +1416,8 @@ Autogenerated return type of MarkAsSpamSnippet.
|
|||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `allowCollaboration` | Boolean | Indicates if members of the target project can push to the fork |
|
||||
| `approvalsLeft` | Int | Number of approvals left |
|
||||
| `approvalsRequired` | Int | Number of approvals required |
|
||||
| `approved` | Boolean! | Indicates if the merge request has all the required approvals. Returns true if no required approvals are configured. |
|
||||
| `author` | User | User who created this merge request |
|
||||
| `autoMergeEnabled` | Boolean! | Indicates if auto merge is enabled for the merge request |
|
||||
|
|
|
@ -14,11 +14,9 @@ Git repositories become larger over time. When large files are added to a Git re
|
|||
- Git repository storage limits [can be reached](#storage-limits).
|
||||
|
||||
Rewriting a repository can remove unwanted history to make the repository smaller.
|
||||
[`git filter-repo`](https://github.com/newren/git-filter-repo) is a tool for quickly rewriting Git
|
||||
repository history, and is recommended over both:
|
||||
|
||||
- [`git filter-branch`](https://git-scm.com/docs/git-filter-branch).
|
||||
- [BFG](https://rtyley.github.io/bfg-repo-cleaner/).
|
||||
We **recommend [`git filter-repo`](https://github.com/newren/git-filter-repo/blob/main/README.md)**
|
||||
over [`git filter-branch`](https://git-scm.com/docs/git-filter-branch) and
|
||||
[BFG](https://rtyley.github.io/bfg-repo-cleaner/).
|
||||
|
||||
DANGER: **Danger:**
|
||||
Rewriting repository history is a destructive operation. Make sure to backup your repository before
|
||||
|
|
|
@ -35,10 +35,10 @@ module API
|
|||
route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
|
||||
get do
|
||||
remote_state_handler.find_with_lock do |state|
|
||||
no_content! unless state.file.exists?
|
||||
no_content! unless state.latest_file && state.latest_file.exists?
|
||||
|
||||
env['api.format'] = :binary # this bypasses json serialization
|
||||
body state.file.read
|
||||
body state.latest_file.read
|
||||
status :ok
|
||||
end
|
||||
end
|
||||
|
@ -52,8 +52,7 @@ module API
|
|||
no_content! if data.empty?
|
||||
|
||||
remote_state_handler.handle_with_lock do |state|
|
||||
state.file = CarrierWaveStringFile.new(data)
|
||||
state.save!
|
||||
state.update_file!(CarrierWaveStringFile.new(data), version: params[:serial])
|
||||
status :ok
|
||||
end
|
||||
end
|
||||
|
|
|
@ -50,7 +50,7 @@ module Gitlab
|
|||
build_access_token_check(login, password) ||
|
||||
lfs_token_check(login, password, project) ||
|
||||
oauth_access_token_check(login, password) ||
|
||||
personal_access_token_check(password) ||
|
||||
personal_access_token_check(password, project) ||
|
||||
deploy_token_check(login, password, project) ||
|
||||
user_with_password_for_git(login, password) ||
|
||||
Gitlab::Auth::Result.new
|
||||
|
@ -189,12 +189,18 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
def personal_access_token_check(password)
|
||||
def personal_access_token_check(password, project)
|
||||
return unless password.present?
|
||||
|
||||
token = PersonalAccessTokensFinder.new(state: 'active').find_by_token(password)
|
||||
|
||||
if token && valid_scoped_token?(token, all_available_scopes) && token.user.can?(:log_in)
|
||||
return unless token
|
||||
|
||||
return if project && token.user.project_bot? && !project.bots.include?(token.user)
|
||||
|
||||
return unless valid_scoped_token?(token, all_available_scopes)
|
||||
|
||||
if token.user.project_bot? || token.user.can?(:log_in)
|
||||
Gitlab::Auth::Result.new(token.user, nil, :personal_access_token, abilities_for_scopes(token.scopes))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
module Gitlab
|
||||
module Checks
|
||||
class SnippetCheck < BaseChecker
|
||||
DEFAULT_BRANCH = 'master'.freeze
|
||||
ERROR_MESSAGES = {
|
||||
create_delete_branch: 'You can not create or delete branches.'
|
||||
}.freeze
|
||||
|
@ -11,17 +10,18 @@ module Gitlab
|
|||
ATTRIBUTES = %i[oldrev newrev ref branch_name tag_name logger].freeze
|
||||
attr_reader(*ATTRIBUTES)
|
||||
|
||||
def initialize(change, logger:)
|
||||
def initialize(change, default_branch:, logger:)
|
||||
@oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref)
|
||||
@branch_name = Gitlab::Git.branch_name(@ref)
|
||||
@tag_name = Gitlab::Git.tag_name(@ref)
|
||||
|
||||
@default_branch = default_branch
|
||||
@logger = logger
|
||||
@logger.append_message("Running checks for ref: #{@branch_name || @tag_name}")
|
||||
end
|
||||
|
||||
def validate!
|
||||
if creation? || deletion?
|
||||
if !@default_branch || creation? || deletion?
|
||||
raise GitAccess::ForbiddenError, ERROR_MESSAGES[:create_delete_branch]
|
||||
end
|
||||
|
||||
|
@ -31,7 +31,7 @@ module Gitlab
|
|||
private
|
||||
|
||||
def creation?
|
||||
@branch_name != DEFAULT_BRANCH && super
|
||||
@branch_name != @default_branch && super
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -119,7 +119,7 @@ module Gitlab
|
|||
|
||||
override :check_single_change_access
|
||||
def check_single_change_access(change, _skip_lfs_integrity_check: false)
|
||||
Checks::SnippetCheck.new(change, logger: logger).validate!
|
||||
Checks::SnippetCheck.new(change, default_branch: snippet.default_branch, logger: logger).validate!
|
||||
Checks::PushFileCountCheck.new(change, repository: repository, limit: Snippet.max_file_limit(user), logger: logger).validate!
|
||||
rescue Checks::TimedLogger::TimeoutError
|
||||
raise TimeoutError, logger.full_message
|
||||
|
|
|
@ -77,6 +77,10 @@
|
|||
category: analytics
|
||||
redis_slot: analytics
|
||||
aggregation: weekly
|
||||
- name: i_analytics_instance_statistics
|
||||
category: analytics
|
||||
redis_slot: analytics
|
||||
aggregation: weekly
|
||||
- name: g_edit_by_web_ide
|
||||
category: ide_edit
|
||||
redis_slot: edit
|
||||
|
|
|
@ -17002,6 +17002,9 @@ msgstr ""
|
|||
msgid "No containers available"
|
||||
msgstr ""
|
||||
|
||||
msgid "No content to show"
|
||||
msgstr ""
|
||||
|
||||
msgid "No contributions"
|
||||
msgstr ""
|
||||
|
||||
|
@ -28083,6 +28086,9 @@ msgstr ""
|
|||
msgid "VisualReviewApp|Steps 1 and 2 (and sometimes 3) are performed once by the developer before requesting feedback. Steps 3 (if necessary), 4 is performed by the reviewer each time they perform a review."
|
||||
msgstr ""
|
||||
|
||||
msgid "Visualization"
|
||||
msgstr ""
|
||||
|
||||
msgid "Vulnerabilities"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -153,6 +153,7 @@
|
|||
"vue-virtual-scroll-list": "^1.4.4",
|
||||
"vuedraggable": "^2.23.0",
|
||||
"vuex": "^3.5.1",
|
||||
"web-vitals": "^0.2.4",
|
||||
"webpack": "^4.42.0",
|
||||
"webpack-bundle-analyzer": "^3.6.0",
|
||||
"webpack-cli": "^3.3.11",
|
||||
|
|
|
@ -5,7 +5,7 @@ module QA
|
|||
module Project
|
||||
module Issue
|
||||
class Index < Page::Base
|
||||
view 'app/assets/javascripts/issuables_list/components/issuable.vue' do
|
||||
view 'app/assets/javascripts/issues_list/components/issuable.vue' do
|
||||
element :issue_container
|
||||
element :issue_link
|
||||
end
|
||||
|
|
|
@ -18,7 +18,6 @@ RSpec.describe Admin::CohortsController do
|
|||
|
||||
describe 'GET #index' do
|
||||
it_behaves_like 'tracking unique visits', :index do
|
||||
let(:request_params) { {} }
|
||||
let(:target_id) { 'i_analytics_cohorts' }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -18,7 +18,6 @@ RSpec.describe Admin::DevOpsReportController do
|
|||
end
|
||||
|
||||
it_behaves_like 'tracking unique visits', :show do
|
||||
let(:request_params) { {} }
|
||||
let(:target_id) { 'i_analytics_dev_ops_score' }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Admin::InstanceStatisticsController do
|
||||
let(:admin) { create(:user, :admin) }
|
||||
|
||||
before do
|
||||
sign_in(admin)
|
||||
end
|
||||
|
||||
describe 'GET #show' do
|
||||
it_behaves_like 'tracking unique visits', :index do
|
||||
let(:target_id) { 'i_analytics_instance_statistics' }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -20,4 +20,108 @@ RSpec.describe Profiles::KeysController do
|
|||
expect(Key.last.expires_at).to be_like_time(expires_at)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#get_keys" do
|
||||
describe "non existent user" do
|
||||
it "does not generally work" do
|
||||
get :get_keys, params: { username: 'not-existent' }
|
||||
|
||||
expect(response).not_to be_successful
|
||||
end
|
||||
end
|
||||
|
||||
describe "user with no keys" do
|
||||
it "does generally work" do
|
||||
get :get_keys, params: { username: user.username }
|
||||
|
||||
expect(response).to be_successful
|
||||
end
|
||||
|
||||
it "renders all keys separated with a new line" do
|
||||
get :get_keys, params: { username: user.username }
|
||||
|
||||
expect(response.body).to eq("")
|
||||
end
|
||||
it "responds with text/plain content type" do
|
||||
get :get_keys, params: { username: user.username }
|
||||
expect(response.content_type).to eq("text/plain")
|
||||
end
|
||||
end
|
||||
|
||||
describe "user with keys" do
|
||||
let!(:key) { create(:key, user: user) }
|
||||
let!(:another_key) { create(:another_key, user: user) }
|
||||
let!(:deploy_key) { create(:deploy_key, user: user) }
|
||||
|
||||
describe "while signed in" do
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it "does generally work" do
|
||||
get :get_keys, params: { username: user.username }
|
||||
|
||||
expect(response).to be_successful
|
||||
end
|
||||
|
||||
it "renders all non deploy keys separated with a new line" do
|
||||
get :get_keys, params: { username: user.username }
|
||||
|
||||
expect(response.body).not_to eq('')
|
||||
expect(response.body).to eq(user.all_ssh_keys.join("\n"))
|
||||
|
||||
expect(response.body).to include(key.key.sub(' dummy@gitlab.com', ''))
|
||||
expect(response.body).to include(another_key.key.sub(' dummy@gitlab.com', ''))
|
||||
|
||||
expect(response.body).not_to include(deploy_key.key)
|
||||
end
|
||||
|
||||
it "does not render the comment of the key" do
|
||||
get :get_keys, params: { username: user.username }
|
||||
expect(response.body).not_to match(/dummy@gitlab.com/)
|
||||
end
|
||||
|
||||
it "responds with text/plain content type" do
|
||||
get :get_keys, params: { username: user.username }
|
||||
|
||||
expect(response.content_type).to eq("text/plain")
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when logged out' do
|
||||
before do
|
||||
sign_out(user)
|
||||
end
|
||||
|
||||
it "still does generally work" do
|
||||
get :get_keys, params: { username: user.username }
|
||||
|
||||
expect(response).to be_successful
|
||||
end
|
||||
|
||||
it "renders all non deploy keys separated with a new line" do
|
||||
get :get_keys, params: { username: user.username }
|
||||
|
||||
expect(response.body).not_to eq('')
|
||||
expect(response.body).to eq(user.all_ssh_keys.join("\n"))
|
||||
|
||||
expect(response.body).to include(key.key.sub(' dummy@gitlab.com', ''))
|
||||
expect(response.body).to include(another_key.key.sub(' dummy@gitlab.com', ''))
|
||||
|
||||
expect(response.body).not_to include(deploy_key.key)
|
||||
end
|
||||
|
||||
it "does not render the comment of the key" do
|
||||
get :get_keys, params: { username: user.username }
|
||||
expect(response.body).not_to match(/dummy@gitlab.com/)
|
||||
end
|
||||
|
||||
it "responds with text/plain content type" do
|
||||
get :get_keys, params: { username: user.username }
|
||||
|
||||
expect(response.content_type).to eq("text/plain")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -114,71 +114,6 @@ RSpec.describe UsersController do
|
|||
end
|
||||
end
|
||||
|
||||
describe "#ssh_keys" do
|
||||
describe "non existent user" do
|
||||
it "does not generally work" do
|
||||
get :ssh_keys, params: { username: 'not-existent' }
|
||||
|
||||
expect(response).not_to be_successful
|
||||
end
|
||||
end
|
||||
|
||||
describe "user with no keys" do
|
||||
it "does generally work" do
|
||||
get :ssh_keys, params: { username: user.username }
|
||||
|
||||
expect(response).to be_successful
|
||||
end
|
||||
|
||||
it "renders all keys separated with a new line" do
|
||||
get :ssh_keys, params: { username: user.username }
|
||||
|
||||
expect(response.body).to eq("")
|
||||
end
|
||||
|
||||
it "responds with text/plain content type" do
|
||||
get :ssh_keys, params: { username: user.username }
|
||||
expect(response.content_type).to eq("text/plain")
|
||||
end
|
||||
end
|
||||
|
||||
describe "user with keys" do
|
||||
let!(:key) { create(:key, user: user) }
|
||||
let!(:another_key) { create(:another_key, user: user) }
|
||||
let!(:deploy_key) { create(:deploy_key, user: user) }
|
||||
|
||||
it "does generally work" do
|
||||
get :ssh_keys, params: { username: user.username }
|
||||
|
||||
expect(response).to be_successful
|
||||
end
|
||||
|
||||
it "renders all non deploy keys separated with a new line" do
|
||||
get :ssh_keys, params: { username: user.username }
|
||||
|
||||
expect(response.body).not_to eq('')
|
||||
expect(response.body).to eq(user.all_ssh_keys.join("\n"))
|
||||
|
||||
expect(response.body).to include(key.key.sub(' dummy@gitlab.com', ''))
|
||||
expect(response.body).to include(another_key.key.sub(' dummy@gitlab.com', ''))
|
||||
|
||||
expect(response.body).not_to include(deploy_key.key)
|
||||
end
|
||||
|
||||
it "does not render the comment of the key" do
|
||||
get :ssh_keys, params: { username: user.username }
|
||||
|
||||
expect(response.body).not_to match(/dummy@gitlab.com/)
|
||||
end
|
||||
|
||||
it "responds with text/plain content type" do
|
||||
get :ssh_keys, params: { username: user.username }
|
||||
|
||||
expect(response.content_type).to eq("text/plain")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET #calendar' do
|
||||
context 'for user' do
|
||||
let(:project) { create(:project) }
|
||||
|
|
|
@ -7,6 +7,7 @@ FactoryBot.define do
|
|||
sequence(:name) { |n| "state-#{n}" }
|
||||
|
||||
trait :with_file do
|
||||
versioning_enabled { false }
|
||||
file { fixture_file_upload('spec/fixtures/terraform/terraform.tfstate', 'application/json') }
|
||||
end
|
||||
|
||||
|
@ -25,5 +26,14 @@ FactoryBot.define do
|
|||
with_file
|
||||
verification_failure { 'Could not calculate the checksum' }
|
||||
end
|
||||
|
||||
trait :with_version do
|
||||
after(:create) do |state|
|
||||
create(:terraform_state_version, :with_file, terraform_state: state)
|
||||
end
|
||||
end
|
||||
|
||||
# Remove with https://gitlab.com/gitlab-org/gitlab/-/issues/235108
|
||||
factory :legacy_terraform_state, parent: :terraform_state, traits: [:with_file]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :terraform_state_version, class: 'Terraform::StateVersion' do
|
||||
terraform_state factory: :terraform_state
|
||||
created_by_user factory: :user
|
||||
|
||||
sequence(:version)
|
||||
file { fixture_file_upload('spec/fixtures/terraform/terraform.tfstate', 'application/json') }
|
||||
end
|
||||
end
|
|
@ -10,11 +10,11 @@ exports[`Design management list item component when item appears in view after i
|
|||
|
||||
exports[`Design management list item component with notes renders item with multiple comments 1`] = `
|
||||
<router-link-stub
|
||||
class="card cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new"
|
||||
class="card gl-cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new"
|
||||
to="[object Object]"
|
||||
>
|
||||
<div
|
||||
class="card-body p-0 d-flex-center overflow-hidden position-relative"
|
||||
class="card-body gl-p-0 gl-display-flex gl-align-items-center gl-justify-content-center gl-overflow-hidden gl-relative"
|
||||
>
|
||||
<!---->
|
||||
|
||||
|
@ -23,7 +23,7 @@ exports[`Design management list item component with notes renders item with mult
|
|||
|
||||
<img
|
||||
alt="test"
|
||||
class="block mx-auto mw-100 mh-100 design-img"
|
||||
class="gl-display-block gl-mx-auto gl-max-w-full mh-100 design-img"
|
||||
data-qa-selector="design_image"
|
||||
src=""
|
||||
/>
|
||||
|
@ -31,13 +31,13 @@ exports[`Design management list item component with notes renders item with mult
|
|||
</div>
|
||||
|
||||
<div
|
||||
class="card-footer d-flex w-100"
|
||||
class="card-footer gl-display-flex gl-w-full"
|
||||
>
|
||||
<div
|
||||
class="d-flex flex-column str-truncated-100"
|
||||
class="gl-display-flex gl-flex-direction-column str-truncated-100"
|
||||
>
|
||||
<span
|
||||
class="bold str-truncated-100"
|
||||
class="gl-font-weight-bold str-truncated-100"
|
||||
data-qa-selector="design_file_name"
|
||||
>
|
||||
test
|
||||
|
@ -57,17 +57,17 @@ exports[`Design management list item component with notes renders item with mult
|
|||
</div>
|
||||
|
||||
<div
|
||||
class="ml-auto d-flex align-items-center text-secondary"
|
||||
class="gl-ml-auto gl-display-flex gl-align-items-center gl-text-gray-500"
|
||||
>
|
||||
<gl-icon-stub
|
||||
class="ml-1"
|
||||
class="gl-ml-2"
|
||||
name="comments"
|
||||
size="16"
|
||||
/>
|
||||
|
||||
<span
|
||||
aria-label="2 comments"
|
||||
class="ml-1"
|
||||
class="gl-ml-2"
|
||||
>
|
||||
|
||||
2
|
||||
|
@ -80,11 +80,11 @@ exports[`Design management list item component with notes renders item with mult
|
|||
|
||||
exports[`Design management list item component with notes renders item with single comment 1`] = `
|
||||
<router-link-stub
|
||||
class="card cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new"
|
||||
class="card gl-cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new"
|
||||
to="[object Object]"
|
||||
>
|
||||
<div
|
||||
class="card-body p-0 d-flex-center overflow-hidden position-relative"
|
||||
class="card-body gl-p-0 gl-display-flex gl-align-items-center gl-justify-content-center gl-overflow-hidden gl-relative"
|
||||
>
|
||||
<!---->
|
||||
|
||||
|
@ -93,7 +93,7 @@ exports[`Design management list item component with notes renders item with sing
|
|||
|
||||
<img
|
||||
alt="test"
|
||||
class="block mx-auto mw-100 mh-100 design-img"
|
||||
class="gl-display-block gl-mx-auto gl-max-w-full mh-100 design-img"
|
||||
data-qa-selector="design_image"
|
||||
src=""
|
||||
/>
|
||||
|
@ -101,13 +101,13 @@ exports[`Design management list item component with notes renders item with sing
|
|||
</div>
|
||||
|
||||
<div
|
||||
class="card-footer d-flex w-100"
|
||||
class="card-footer gl-display-flex gl-w-full"
|
||||
>
|
||||
<div
|
||||
class="d-flex flex-column str-truncated-100"
|
||||
class="gl-display-flex gl-flex-direction-column str-truncated-100"
|
||||
>
|
||||
<span
|
||||
class="bold str-truncated-100"
|
||||
class="gl-font-weight-bold str-truncated-100"
|
||||
data-qa-selector="design_file_name"
|
||||
>
|
||||
test
|
||||
|
@ -127,17 +127,17 @@ exports[`Design management list item component with notes renders item with sing
|
|||
</div>
|
||||
|
||||
<div
|
||||
class="ml-auto d-flex align-items-center text-secondary"
|
||||
class="gl-ml-auto gl-display-flex gl-align-items-center gl-text-gray-500"
|
||||
>
|
||||
<gl-icon-stub
|
||||
class="ml-1"
|
||||
class="gl-ml-2"
|
||||
name="comments"
|
||||
size="16"
|
||||
/>
|
||||
|
||||
<span
|
||||
aria-label="1 comment"
|
||||
class="ml-1"
|
||||
class="gl-ml-2"
|
||||
>
|
||||
|
||||
1
|
||||
|
|
|
@ -202,28 +202,6 @@ describe('Multi-file editor library', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('schemas', () => {
|
||||
let originalGon;
|
||||
|
||||
beforeEach(() => {
|
||||
originalGon = window.gon;
|
||||
window.gon = { features: { schemaLinting: true } };
|
||||
|
||||
delete Editor.editorInstance;
|
||||
instance = Editor.create();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.gon = originalGon;
|
||||
});
|
||||
|
||||
it('registers custom schemas defined with Monaco', () => {
|
||||
expect(monacoLanguages.yaml.yamlDefaults.diagnosticsOptions).toMatchObject({
|
||||
schemas: [{ fileMatch: ['*.gitlab-ci.yml'] }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('replaceSelectedText', () => {
|
||||
let model;
|
||||
let editor;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { TEST_HOST } from 'helpers/test_constants';
|
||||
import * as getters from '~/ide/stores/getters';
|
||||
import { createStore } from '~/ide/stores';
|
||||
import { file } from '../helpers';
|
||||
|
@ -493,4 +494,37 @@ describe('IDE store getters', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getJsonSchemaForPath', () => {
|
||||
beforeEach(() => {
|
||||
localState.currentProjectId = 'path/to/some/project';
|
||||
localState.currentBranchId = 'master';
|
||||
});
|
||||
|
||||
it('returns a json schema uri and match config for a json/yaml file that can be loaded by monaco', () => {
|
||||
expect(localStore.getters.getJsonSchemaForPath('.gitlab-ci.yml')).toEqual({
|
||||
fileMatch: ['*.gitlab-ci.yml'],
|
||||
uri: `${TEST_HOST}/path/to/some/project/-/schema/master/.gitlab-ci.yml`,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a path containing sha if branch details are present in state', () => {
|
||||
localState.projects['path/to/some/project'] = {
|
||||
name: 'project',
|
||||
branches: {
|
||||
master: {
|
||||
name: 'master',
|
||||
commit: {
|
||||
id: 'abcdef123456',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(localStore.getters.getJsonSchemaForPath('.gitlab-ci.yml')).toEqual({
|
||||
fileMatch: ['*.gitlab-ci.yml'],
|
||||
uri: `${TEST_HOST}/path/to/some/project/-/schema/abcdef123456/.gitlab-ci.yml`,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,7 +2,7 @@ import { languages } from 'monaco-editor';
|
|||
import {
|
||||
isTextFile,
|
||||
registerLanguages,
|
||||
registerSchemas,
|
||||
registerSchema,
|
||||
trimPathComponents,
|
||||
insertFinalNewline,
|
||||
trimTrailingWhitespace,
|
||||
|
@ -159,55 +159,37 @@ describe('WebIDE utils', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('registerSchemas', () => {
|
||||
let options;
|
||||
describe('registerSchema', () => {
|
||||
let schema;
|
||||
|
||||
beforeEach(() => {
|
||||
options = {
|
||||
validate: true,
|
||||
enableSchemaRequest: true,
|
||||
hover: true,
|
||||
completion: true,
|
||||
schemas: [
|
||||
{
|
||||
uri: 'http://myserver/foo-schema.json',
|
||||
fileMatch: ['*'],
|
||||
schema: {
|
||||
id: 'http://myserver/foo-schema.json',
|
||||
type: 'object',
|
||||
properties: {
|
||||
p1: { enum: ['v1', 'v2'] },
|
||||
p2: { $ref: 'http://myserver/bar-schema.json' },
|
||||
},
|
||||
},
|
||||
schema = {
|
||||
uri: 'http://myserver/foo-schema.json',
|
||||
fileMatch: ['*'],
|
||||
schema: {
|
||||
id: 'http://myserver/foo-schema.json',
|
||||
type: 'object',
|
||||
properties: {
|
||||
p1: { enum: ['v1', 'v2'] },
|
||||
p2: { $ref: 'http://myserver/bar-schema.json' },
|
||||
},
|
||||
{
|
||||
uri: 'http://myserver/bar-schema.json',
|
||||
schema: {
|
||||
id: 'http://myserver/bar-schema.json',
|
||||
type: 'object',
|
||||
properties: { q1: { enum: ['x1', 'x2'] } },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
jest.spyOn(languages.json.jsonDefaults, 'setDiagnosticsOptions');
|
||||
jest.spyOn(languages.yaml.yamlDefaults, 'setDiagnosticsOptions');
|
||||
});
|
||||
|
||||
it.each`
|
||||
language | defaultsObj
|
||||
${'json'} | ${languages.json.jsonDefaults}
|
||||
${'yaml'} | ${languages.yaml.yamlDefaults}
|
||||
`(
|
||||
'registers the given schemas with monaco for lang: $language',
|
||||
({ language, defaultsObj }) => {
|
||||
registerSchemas({ language, options });
|
||||
it('registers the given schemas with monaco for both json and yaml languages', () => {
|
||||
registerSchema(schema);
|
||||
|
||||
expect(defaultsObj.setDiagnosticsOptions).toHaveBeenCalledWith(options);
|
||||
},
|
||||
);
|
||||
expect(languages.json.jsonDefaults.setDiagnosticsOptions).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ schemas: [schema] }),
|
||||
);
|
||||
expect(languages.yaml.yamlDefaults.setDiagnosticsOptions).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ schemas: [schema] }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('trimTrailingWhitespace', () => {
|
||||
|
|
|
@ -5,7 +5,7 @@ import { trimText } from 'helpers/text_helper';
|
|||
import initUserPopovers from '~/user_popovers';
|
||||
import { formatDate } from '~/lib/utils/datetime_utility';
|
||||
import { mergeUrlParams } from '~/lib/utils/url_utility';
|
||||
import Issuable from '~/issuables_list/components/issuable.vue';
|
||||
import Issuable from '~/issues_list/components/issuable.vue';
|
||||
import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
|
||||
import { simpleIssue, testAssignees, testLabels } from '../issuable_list_test_data';
|
||||
import { isScopedLabel } from '~/lib/utils/common_utils';
|
|
@ -9,14 +9,14 @@ import {
|
|||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { TEST_HOST } from 'helpers/test_constants';
|
||||
import { deprecatedCreateFlash as flash } from '~/flash';
|
||||
import IssuablesListApp from '~/issuables_list/components/issuables_list_app.vue';
|
||||
import Issuable from '~/issuables_list/components/issuable.vue';
|
||||
import IssuablesListApp from '~/issues_list/components/issuables_list_app.vue';
|
||||
import Issuable from '~/issues_list/components/issuable.vue';
|
||||
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
|
||||
import issueablesEventBus from '~/issuables_list/eventhub';
|
||||
import { PAGE_SIZE, PAGE_SIZE_MANUAL, RELATIVE_POSITION } from '~/issuables_list/constants';
|
||||
import issueablesEventBus from '~/issues_list/eventhub';
|
||||
import { PAGE_SIZE, PAGE_SIZE_MANUAL, RELATIVE_POSITION } from '~/issues_list/constants';
|
||||
|
||||
jest.mock('~/flash');
|
||||
jest.mock('~/issuables_list/eventhub');
|
||||
jest.mock('~/issues_list/eventhub');
|
||||
jest.mock('~/lib/utils/common_utils', () => ({
|
||||
...jest.requireActual('~/lib/utils/common_utils'),
|
||||
scrollToElement: () => {},
|
|
@ -1,9 +1,9 @@
|
|||
import { GlAlert, GlLabel } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import Vue from 'vue';
|
||||
import IssuableListRootApp from '~/issuables_list/components/issuable_list_root_app.vue';
|
||||
import JiraIssuesListRoot from '~/issues_list/components/jira_issues_list_root.vue';
|
||||
|
||||
describe('IssuableListRootApp', () => {
|
||||
describe('JiraIssuesListRoot', () => {
|
||||
const issuesPath = 'gitlab-org/gitlab-test/-/issues';
|
||||
const label = {
|
||||
color: '#333',
|
||||
|
@ -19,7 +19,7 @@ describe('IssuableListRootApp', () => {
|
|||
shouldShowFinishedAlert = false,
|
||||
shouldShowInProgressAlert = false,
|
||||
} = {}) =>
|
||||
shallowMount(IssuableListRootApp, {
|
||||
shallowMount(JiraIssuesListRoot, {
|
||||
propsData: {
|
||||
canEdit: true,
|
||||
isJiraConfigured: true,
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue