Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
e94d662e5a
commit
ed7568cc80
90 changed files with 1256 additions and 587 deletions
89
.gitpod.yml
Normal file
89
.gitpod.yml
Normal file
|
@ -0,0 +1,89 @@
|
|||
image: registry.gitlab.com/gitlab-org/gitlab-development-kit/gitpod-workspace:gitpod-workspace-image
|
||||
|
||||
tasks:
|
||||
|
||||
- name: GDK
|
||||
command: gp sync-await gdk-copied && cd /workspace/gitlab-development-kit && gdk help
|
||||
|
||||
- init: |
|
||||
echo "$(date) – Copying GDK" | tee -a /workspace/startup.log
|
||||
mv $HOME/.rvm-workspace /workspace/.rvm
|
||||
cp -r $HOME/gitlab-development-kit /workspace/
|
||||
(
|
||||
set -e
|
||||
cd /workspace/gitlab-development-kit
|
||||
[[ ! -L /workspace/gitlab-development-kit/gitlab ]] && ln -fs /workspace/gitlab /workspace/gitlab-development-kit/gitlab
|
||||
# make webpack static, prevents that GitLab tries to connect to localhost webpack from browser outside the workspace
|
||||
echo "webpack:" >> gdk.yml
|
||||
echo " static: true" >> gdk.yml
|
||||
# reconfigure GDK
|
||||
echo "$(date) – Reconfiguring GDK" | tee -a /workspace/startup.log
|
||||
gdk reconfigure
|
||||
# run DB migrations
|
||||
echo "$(date) – Running DB migrations" | tee -a /workspace/startup.log
|
||||
make gitlab-db-migrate
|
||||
cd -
|
||||
# stop GDK
|
||||
echo "$(date) – Stopping GDK" | tee -a /workspace/startup.log
|
||||
gdk stop
|
||||
echo "$(date) – GDK stopped" | tee -a /workspace/startup.log
|
||||
)
|
||||
command: |
|
||||
(
|
||||
set -e
|
||||
gp sync-done gdk-copied
|
||||
SECONDS=0
|
||||
cd /workspace/gitlab-development-kit
|
||||
# update GDK
|
||||
if [ "$GITLAB_UPDATE_GDK" == true ]; then
|
||||
echo "$(date) – Updating GDK" | tee -a /workspace/startup.log
|
||||
gdk update
|
||||
fi
|
||||
# start GDK
|
||||
echo "$(date) – Starting GDK" | tee -a /workspace/startup.log
|
||||
export RAILS_HOSTS=$(gp url 3000 | sed -e 's+^http[s]*://++')
|
||||
gdk start
|
||||
# Run DB migrations
|
||||
if [ "$GITLAB_RUN_DB_MIGRATIONS" == true ]; then
|
||||
make gitlab-db-migrate
|
||||
fi
|
||||
# Fix DB key
|
||||
if [ "$GITLAB_FIX_DB_KEY" = true ]; then
|
||||
echo "$(date) – Fixing DB key" | tee -a /workspace/startup.log
|
||||
cd gitlab
|
||||
# see https://gitlab.com/gitlab-org/gitlab-foss/-/issues/56403#note_132515069
|
||||
printf 'ApplicationSetting.last.update_column(:runners_registration_token_encrypted, nil)\nexit\n' | bundle exec rails c
|
||||
cd -
|
||||
fi
|
||||
# Waiting for GitLab ...
|
||||
gp await-port 3000
|
||||
printf "Waiting for GitLab at $(gp url 3000) ..."
|
||||
until $(curl -sNL $(gp url 3000) | grep -q "GitLab"); do printf '.'; sleep 5; done && echo ""
|
||||
# Give Gitpod a few more seconds to set up everything ...
|
||||
sleep 5
|
||||
printf "$(date) – GitLab is up (took ~%.1f minutes)\n" "$((10*$SECONDS/60))e-1" | tee -a /workspace/startup.log
|
||||
gp preview $(gp url 3000) || true
|
||||
)
|
||||
|
||||
ports:
|
||||
- port: 3000 # rails-web
|
||||
onOpen: ignore
|
||||
- port: 3010 # gitlab-pages
|
||||
onOpen: ignore
|
||||
- port: 3808 # webpack
|
||||
onOpen: ignore
|
||||
- port: 5000 # auto_devops
|
||||
onOpen: ignore
|
||||
- port: 5778 # jaeger
|
||||
onOpen: ignore
|
||||
- port: 9000 # object_store / minio
|
||||
onOpen: ignore
|
||||
|
||||
vscode:
|
||||
extensions:
|
||||
- rebornix.ruby@0.27.0:QyGBeRyslOfdRgOPRGm6PQ==
|
||||
- wingrunr21.vscode-ruby@0.27.0:beIqQUhLRuJ5Vao4B2Lyng==
|
||||
- karunamurti.haml@1.1.0:twCwOYt3/Ttfb3+iwblPDA==
|
||||
- octref.vetur@0.25.0:UofirBhedyhdx/jCnPeJDg==
|
||||
- dbaeumer.vscode-eslint@2.1.3:1NRvj3UKNTNwmYjptmUmIw==
|
||||
- GitLab.gitlab-workflow@3.3.0:50q1byIi4M01G9qrTCCAYQ==
|
11
.theia/settings.json
Normal file
11
.theia/settings.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"ruby.codeCompletion": "rcodetools",
|
||||
"ruby.format": "standard",
|
||||
"ruby.intellisense": "rubyLocate",
|
||||
"ruby.useBundler": true,
|
||||
"ruby.useLanguageServer": true,
|
||||
"ruby.lint": {
|
||||
"rubocop": true,
|
||||
"useBundler": true
|
||||
}
|
||||
}
|
|
@ -1 +1 @@
|
|||
05cd75fb57f06f29978e6cc0da3f7bc35d85859f
|
||||
3bdd23173595a931aac476ad0c07c702c30f4391
|
||||
|
|
|
@ -14,7 +14,6 @@ export default function initGFMInput($els) {
|
|||
milestones: enableGFM,
|
||||
mergeRequests: enableGFM,
|
||||
labels: enableGFM,
|
||||
vulnerabilities: enableGFM,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ import { escape, template } from 'lodash';
|
|||
import SidebarMediator from '~/sidebar/sidebar_mediator';
|
||||
import glRegexp from './lib/utils/regexp';
|
||||
import AjaxCache from './lib/utils/ajax_cache';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { spriteIcon } from './lib/utils/common_utils';
|
||||
import * as Emoji from '~/emoji';
|
||||
|
||||
|
@ -53,7 +52,6 @@ export const defaultAutocompleteConfig = {
|
|||
milestones: true,
|
||||
labels: true,
|
||||
snippets: true,
|
||||
vulnerabilities: true,
|
||||
};
|
||||
|
||||
class GfmAutoComplete {
|
||||
|
@ -61,7 +59,6 @@ class GfmAutoComplete {
|
|||
this.dataSources = dataSources;
|
||||
this.cachedData = {};
|
||||
this.isLoadingData = {};
|
||||
this.previousQuery = '';
|
||||
}
|
||||
|
||||
setup(input, enableMap = defaultAutocompleteConfig) {
|
||||
|
@ -557,7 +554,7 @@ class GfmAutoComplete {
|
|||
}
|
||||
|
||||
getDefaultCallbacks() {
|
||||
const self = this;
|
||||
const fetchData = this.fetchData.bind(this);
|
||||
|
||||
return {
|
||||
sorter(query, items, searchKey) {
|
||||
|
@ -570,15 +567,7 @@ class GfmAutoComplete {
|
|||
},
|
||||
filter(query, data, searchKey) {
|
||||
if (GfmAutoComplete.isLoading(data)) {
|
||||
self.fetchData(this.$inputor, this.at);
|
||||
return data;
|
||||
}
|
||||
if (
|
||||
GfmAutoComplete.typesWithBackendFiltering.includes(GfmAutoComplete.atTypeMap[this.at]) &&
|
||||
self.previousQuery !== query
|
||||
) {
|
||||
self.fetchData(this.$inputor, this.at, query);
|
||||
self.previousQuery = query;
|
||||
fetchData(this.$inputor, this.at);
|
||||
return data;
|
||||
}
|
||||
return $.fn.atwho.default.callbacks.filter(query, data, searchKey);
|
||||
|
@ -626,22 +615,13 @@ class GfmAutoComplete {
|
|||
};
|
||||
}
|
||||
|
||||
fetchData($input, at, search) {
|
||||
fetchData($input, at) {
|
||||
if (this.isLoadingData[at]) return;
|
||||
|
||||
this.isLoadingData[at] = true;
|
||||
const dataSource = this.dataSources[GfmAutoComplete.atTypeMap[at]];
|
||||
|
||||
if (GfmAutoComplete.typesWithBackendFiltering.includes(GfmAutoComplete.atTypeMap[at])) {
|
||||
axios
|
||||
.get(dataSource, { params: { search } })
|
||||
.then(({ data }) => {
|
||||
this.loadData($input, at, data);
|
||||
})
|
||||
.catch(() => {
|
||||
this.isLoadingData[at] = false;
|
||||
});
|
||||
} else if (this.cachedData[at]) {
|
||||
if (this.cachedData[at]) {
|
||||
this.loadData($input, at, this.cachedData[at]);
|
||||
} else if (GfmAutoComplete.atTypeMap[at] === 'emojis') {
|
||||
this.loadEmojiData($input, at).catch(() => {});
|
||||
|
@ -727,8 +707,7 @@ class GfmAutoComplete {
|
|||
// https://github.com/ichord/At.js
|
||||
const atSymbolsWithBar = Object.keys(controllers)
|
||||
.join('|')
|
||||
.replace(/[$]/, '\\$&')
|
||||
.replace(/[+]/, '\\+');
|
||||
.replace(/[$]/, '\\$&');
|
||||
const atSymbolsWithoutBar = Object.keys(controllers).join('');
|
||||
const targetSubtext = subtext.split(GfmAutoComplete.regexSubtext).pop();
|
||||
const resultantFlag = flag.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&');
|
||||
|
@ -759,12 +738,9 @@ GfmAutoComplete.atTypeMap = {
|
|||
'~': 'labels',
|
||||
'%': 'milestones',
|
||||
'/': 'commands',
|
||||
'+': 'vulnerabilities',
|
||||
$: 'snippets',
|
||||
};
|
||||
|
||||
GfmAutoComplete.typesWithBackendFiltering = ['vulnerabilities'];
|
||||
|
||||
function findEmoji(name) {
|
||||
return Emoji.searchEmoji(name, { match: 'contains', raw: true }).sort((a, b) => {
|
||||
if (a.index !== b.index) {
|
||||
|
|
|
@ -310,7 +310,7 @@ export default {
|
|||
category="primary"
|
||||
variant="success"
|
||||
:href="newIncidentPath"
|
||||
@click="redirecting = true"
|
||||
@click="navigateToCreateNewIncident"
|
||||
>
|
||||
{{ $options.i18n.createIncidentBtnLabel }}
|
||||
</gl-button>
|
||||
|
|
|
@ -34,6 +34,13 @@ export const INCIDENT_STATUS_TABS = [
|
|||
},
|
||||
];
|
||||
|
||||
export const DEFAULT_PAGE_SIZE = 20;
|
||||
export const TH_CREATED_AT_TEST_ID = { 'data-testid': 'incident-management-created-at-sort' };
|
||||
export const TH_SEVERITY_TEST_ID = { 'data-testid': 'incident-management-severity-sort' };
|
||||
export const TH_INCIDENT_SLA_TEST_ID = { 'data-testid': 'incident-management-sla' };
|
||||
export const TH_PUBLISHED_TEST_ID = { 'data-testid': 'incident-management-published-sort' };
|
||||
export const INCIDENT_DETAILS_PATH = 'incident';
|
||||
|
||||
/**
|
||||
* Tracks snowplow event when user clicks create new incident
|
||||
*/
|
||||
|
@ -43,16 +50,17 @@ export const trackIncidentCreateNewOptions = {
|
|||
};
|
||||
|
||||
/**
|
||||
* Tracks snowplow event when user views incident list
|
||||
* Tracks snowplow event when user views incidents list
|
||||
*/
|
||||
export const trackIncidentListViewsOptions = {
|
||||
category: 'Incident Management',
|
||||
action: 'view_incidents_list',
|
||||
};
|
||||
|
||||
export const DEFAULT_PAGE_SIZE = 20;
|
||||
export const TH_CREATED_AT_TEST_ID = { 'data-testid': 'incident-management-created-at-sort' };
|
||||
export const TH_SEVERITY_TEST_ID = { 'data-testid': 'incident-management-severity-sort' };
|
||||
export const TH_INCIDENT_SLA_TEST_ID = { 'data-testid': 'incident-management-sla' };
|
||||
export const TH_PUBLISHED_TEST_ID = { 'data-testid': 'incident-management-published-sort' };
|
||||
export const INCIDENT_DETAILS_PATH = 'incident';
|
||||
/**
|
||||
* Tracks snowplow event when user views incident details
|
||||
*/
|
||||
export const trackIncidentDetailsViewsOptions = {
|
||||
category: 'Incident Management',
|
||||
action: 'view_incident_details',
|
||||
};
|
||||
|
|
|
@ -5,8 +5,10 @@ import HighlightBar from './highlight_bar.vue';
|
|||
import createFlash from '~/flash';
|
||||
import { s__ } from '~/locale';
|
||||
import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
|
||||
import Tracking from '~/tracking';
|
||||
|
||||
import getAlert from './graphql/queries/get_alert.graphql';
|
||||
import { trackIncidentDetailsViewsOptions } from '~/incidents/constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -46,6 +48,15 @@ export default {
|
|||
return this.$apollo.queries.alert.loading;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.trackPageViews();
|
||||
},
|
||||
methods: {
|
||||
trackPageViews() {
|
||||
const { category, action } = trackIncidentDetailsViewsOptions;
|
||||
Tracking.event(category, action);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
@ -13,6 +13,9 @@ export default () => {
|
|||
modules: {
|
||||
detail: createDetailModule(el.dataset),
|
||||
},
|
||||
featureFlags: {
|
||||
graphqlIndividualReleasePage: Boolean(gon.features?.graphqlIndividualReleasePage),
|
||||
},
|
||||
});
|
||||
|
||||
return new Vue({
|
||||
|
|
|
@ -3,7 +3,13 @@ import api from '~/api';
|
|||
import { deprecatedCreateFlash as createFlash } from '~/flash';
|
||||
import { s__ } from '~/locale';
|
||||
import { redirectTo } from '~/lib/utils/url_utility';
|
||||
import { releaseToApiJson, apiJsonToRelease } from '~/releases/util';
|
||||
import {
|
||||
releaseToApiJson,
|
||||
apiJsonToRelease,
|
||||
gqClient,
|
||||
convertOneReleaseGraphQLResponse,
|
||||
} from '~/releases/util';
|
||||
import oneReleaseQuery from '~/releases/queries/one_release.query.graphql';
|
||||
|
||||
export const initializeRelease = ({ commit, dispatch, getters }) => {
|
||||
if (getters.isExistingRelease) {
|
||||
|
@ -18,9 +24,29 @@ export const initializeRelease = ({ commit, dispatch, getters }) => {
|
|||
return Promise.resolve();
|
||||
};
|
||||
|
||||
export const fetchRelease = ({ commit, state }) => {
|
||||
export const fetchRelease = ({ commit, state, rootState }) => {
|
||||
commit(types.REQUEST_RELEASE);
|
||||
|
||||
if (rootState.featureFlags?.graphqlIndividualReleasePage) {
|
||||
return gqClient
|
||||
.query({
|
||||
query: oneReleaseQuery,
|
||||
variables: {
|
||||
fullPath: state.projectPath,
|
||||
tagName: state.tagName,
|
||||
},
|
||||
})
|
||||
.then(response => {
|
||||
const { data: release } = convertOneReleaseGraphQLResponse(response);
|
||||
|
||||
commit(types.RECEIVE_RELEASE_SUCCESS, release);
|
||||
})
|
||||
.catch(error => {
|
||||
commit(types.RECEIVE_RELEASE_ERROR, error);
|
||||
createFlash(s__('Release|Something went wrong while getting the release details'));
|
||||
});
|
||||
}
|
||||
|
||||
return api
|
||||
.release(state.projectId, state.tagName)
|
||||
.then(({ data }) => {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
export default ({
|
||||
projectId,
|
||||
projectPath,
|
||||
markdownDocsPath,
|
||||
markdownPreviewPath,
|
||||
updateReleaseApiDocsPath,
|
||||
|
@ -12,6 +13,7 @@ export default ({
|
|||
defaultBranch = null,
|
||||
}) => ({
|
||||
projectId,
|
||||
projectPath,
|
||||
markdownDocsPath,
|
||||
markdownPreviewPath,
|
||||
updateReleaseApiDocsPath,
|
||||
|
|
|
@ -16,6 +16,5 @@ export default (initGFM = true) => {
|
|||
milestones: initGFM,
|
||||
labels: initGFM,
|
||||
snippets: initGFM,
|
||||
vulnerabilities: initGFM,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -61,45 +61,46 @@ export default {
|
|||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="container gl-flex-grow-1 gl-display-flex gl-flex-direction-column">
|
||||
<div
|
||||
v-if="savedContentMeta"
|
||||
class="gl-fixed gl-left-0 gl-right-0 gl-border-b-solid gl-border-b-1 gl-border-b-gray-100"
|
||||
>
|
||||
<div>
|
||||
<div class="gl-border-b-solid gl-border-b-1 gl-border-b-gray-100">
|
||||
<div class="container gl-py-4">
|
||||
<gl-button
|
||||
v-if="appData.returnUrl"
|
||||
ref="returnToSiteButton"
|
||||
class="gl-mr-5"
|
||||
:href="appData.returnUrl"
|
||||
>{{ $options.returnToSiteBtnText }}</gl-button
|
||||
>
|
||||
<strong>
|
||||
{{ updatedFileDescription }}
|
||||
</strong>
|
||||
<div class="gl-display-flex">
|
||||
<gl-button
|
||||
v-if="appData.returnUrl"
|
||||
ref="returnToSiteButton"
|
||||
class="gl-mr-5 gl-align-self-start"
|
||||
:href="appData.returnUrl"
|
||||
>{{ $options.returnToSiteBtnText }}</gl-button
|
||||
>
|
||||
<strong class="gl-mt-2">
|
||||
{{ updatedFileDescription }}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<gl-empty-state
|
||||
class="gl-my-9"
|
||||
:title="savedContentMeta ? $options.title : $options.submittingTitle"
|
||||
:primary-button-text="savedContentMeta && $options.primaryButtonText"
|
||||
:primary-button-link="savedContentMeta && savedContentMeta.mergeRequest.url"
|
||||
:svg-path="mergeRequestsIllustrationPath"
|
||||
:svg-height="146"
|
||||
>
|
||||
<template #description>
|
||||
<div v-if="savedContentMeta">
|
||||
<p>{{ $options.mergeRequestInstructionsHeading }}</p>
|
||||
<p>{{ $options.addTitleInstruction }}</p>
|
||||
<p>{{ $options.addDescriptionInstruction }}</p>
|
||||
<p>{{ $options.assignMergeRequestInstruction }}</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p>{{ $options.submittingNotePrimary }}</p>
|
||||
<p>{{ $options.submittingNoteSecondary }}</p>
|
||||
<gl-loading-icon size="xl" />
|
||||
</div>
|
||||
</template>
|
||||
</gl-empty-state>
|
||||
<div class="container">
|
||||
<gl-empty-state
|
||||
class="gl-my-7"
|
||||
:title="savedContentMeta ? $options.title : $options.submittingTitle"
|
||||
:primary-button-text="savedContentMeta && $options.primaryButtonText"
|
||||
:primary-button-link="savedContentMeta && savedContentMeta.mergeRequest.url"
|
||||
:svg-path="mergeRequestsIllustrationPath"
|
||||
:svg-height="146"
|
||||
>
|
||||
<template #description>
|
||||
<div v-if="savedContentMeta">
|
||||
<p>{{ $options.mergeRequestInstructionsHeading }}</p>
|
||||
<p>{{ $options.addTitleInstruction }}</p>
|
||||
<p>{{ $options.addDescriptionInstruction }}</p>
|
||||
<p>{{ $options.assignMergeRequestInstruction }}</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p>{{ $options.submittingNotePrimary }}</p>
|
||||
<p>{{ $options.submittingNoteSecondary }}</p>
|
||||
<gl-loading-icon size="xl" />
|
||||
</div>
|
||||
</template>
|
||||
</gl-empty-state>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -15,7 +15,7 @@ const markPrefix = `${marker}-${Date.now()}`;
|
|||
|
||||
const reHelpers = {
|
||||
template: `.| |\\t|\\n(?!(\\n|${markPrefix}))`,
|
||||
openTag: '<(?!iframe)[a-zA-Z]+.*?>',
|
||||
openTag: '<(?!figure|iframe)[a-zA-Z]+.*?>',
|
||||
closeTag: '</.+>',
|
||||
};
|
||||
const reTemplated = new RegExp(`(^${wrapPrefix}(${reHelpers.template})+?${wrapPostfix}$)`, 'gm');
|
||||
|
|
|
@ -178,7 +178,6 @@ export default {
|
|||
milestones: this.enableAutocomplete,
|
||||
labels: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete,
|
||||
snippets: this.enableAutocomplete,
|
||||
vulnerabilities: this.enableAutocomplete,
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
|
|
@ -2,9 +2,14 @@ import { __ } from '~/locale';
|
|||
|
||||
export const CUSTOM_EVENTS = {
|
||||
openAddImageModal: 'gl_openAddImageModal',
|
||||
openInsertVideoModal: 'gl_openInsertVideoModal',
|
||||
};
|
||||
|
||||
export const ALLOWED_VIDEO_ORIGINS = ['https://www.youtube.com'];
|
||||
export const YOUTUBE_URL = 'https://www.youtube.com';
|
||||
|
||||
export const YOUTUBE_EMBED_URL = `${YOUTUBE_URL}/embed`;
|
||||
|
||||
export const ALLOWED_VIDEO_ORIGINS = [YOUTUBE_URL];
|
||||
|
||||
/* eslint-disable @gitlab/require-i18n-strings */
|
||||
export const TOOLBAR_ITEM_CONFIGS = [
|
||||
|
@ -25,6 +30,7 @@ export const TOOLBAR_ITEM_CONFIGS = [
|
|||
{ icon: 'dash', command: 'HR', tooltip: __('Add a line') },
|
||||
{ icon: 'table', event: 'openPopupAddTable', classes: 'tui-table', tooltip: __('Add a table') },
|
||||
{ icon: 'doc-image', event: CUSTOM_EVENTS.openAddImageModal, tooltip: __('Insert an image') },
|
||||
{ icon: 'live-preview', event: CUSTOM_EVENTS.openInsertVideoModal, tooltip: __('Insert video') },
|
||||
{ isDivider: true },
|
||||
{ icon: 'code', command: 'Code', tooltip: __('Insert inline code') },
|
||||
{ icon: 'doc-code', command: 'CodeBlock', tooltip: __('Insert a code block') },
|
||||
|
@ -42,3 +48,10 @@ export const EDITOR_PREVIEW_STYLE = 'horizontal';
|
|||
export const IMAGE_TABS = { UPLOAD_TAB: 0, URL_TAB: 1 };
|
||||
|
||||
export const MAX_FILE_SIZE = 2097152; // 2Mb
|
||||
|
||||
export const VIDEO_ATTRIBUTES = {
|
||||
width: '560',
|
||||
height: '315',
|
||||
frameBorder: '0',
|
||||
allow: 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture',
|
||||
};
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
<script>
|
||||
import { GlModal, GlFormGroup, GlFormInput, GlSprintf } from '@gitlab/ui';
|
||||
import { isSafeURL } from '~/lib/utils/url_utility';
|
||||
import { __ } from '~/locale';
|
||||
import { YOUTUBE_URL, YOUTUBE_EMBED_URL } from '../constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlModal,
|
||||
GlFormGroup,
|
||||
GlFormInput,
|
||||
GlSprintf,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
url: null,
|
||||
urlError: null,
|
||||
description: __(
|
||||
'If the YouTube URL is https://www.youtube.com/watch?v=0t1DgySidms then the video ID is %{id}',
|
||||
),
|
||||
};
|
||||
},
|
||||
modalTitle: __('Insert a video'),
|
||||
okTitle: __('Insert video'),
|
||||
label: __('YouTube URL or ID'),
|
||||
methods: {
|
||||
show() {
|
||||
this.urlError = null;
|
||||
this.url = null;
|
||||
|
||||
this.$refs.modal.show();
|
||||
},
|
||||
onPrimary(event) {
|
||||
this.submitURL(event);
|
||||
},
|
||||
submitURL(event) {
|
||||
const url = this.generateUrl();
|
||||
|
||||
if (!url) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit('insertVideo', url);
|
||||
},
|
||||
generateUrl() {
|
||||
let { url } = this;
|
||||
const reYouTubeId = /^[A-z0-9]*$/;
|
||||
const reYouTubeUrl = RegExp(`${YOUTUBE_URL}/(embed/|watch\\?v=)([A-z0-9]+)`);
|
||||
|
||||
if (reYouTubeId.test(url)) {
|
||||
url = `${YOUTUBE_EMBED_URL}/${url}`;
|
||||
} else if (reYouTubeUrl.test(url)) {
|
||||
url = `${YOUTUBE_EMBED_URL}/${reYouTubeUrl.exec(url)[2]}`;
|
||||
}
|
||||
|
||||
if (!isSafeURL(url) || !reYouTubeUrl.test(url)) {
|
||||
this.urlError = __('Please provide a valid YouTube URL or ID');
|
||||
this.$refs.urlInput.$el.focus();
|
||||
return null;
|
||||
}
|
||||
|
||||
return url;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<gl-modal
|
||||
ref="modal"
|
||||
size="sm"
|
||||
modal-id="insert-video-modal"
|
||||
:title="$options.modalTitle"
|
||||
:ok-title="$options.okTitle"
|
||||
@primary="onPrimary"
|
||||
>
|
||||
<gl-form-group
|
||||
:label="$options.label"
|
||||
label-for="video-modal-url-input"
|
||||
:state="!Boolean(urlError)"
|
||||
:invalid-feedback="urlError"
|
||||
>
|
||||
<gl-form-input id="video-modal-url-input" ref="urlInput" v-model="url" />
|
||||
<gl-sprintf slot="description" :message="description" class="text-gl-muted">
|
||||
<template #id>
|
||||
<strong>{{ __('0t1DgySidms') }}</strong>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</gl-form-group>
|
||||
</gl-modal>
|
||||
</template>
|
|
@ -3,6 +3,7 @@ import 'codemirror/lib/codemirror.css';
|
|||
import '@toast-ui/editor/dist/toastui-editor.css';
|
||||
|
||||
import AddImageModal from './modals/add_image/add_image_modal.vue';
|
||||
import InsertVideoModal from './modals/insert_video_modal.vue';
|
||||
import { EDITOR_TYPES, EDITOR_HEIGHT, EDITOR_PREVIEW_STYLE, CUSTOM_EVENTS } from './constants';
|
||||
|
||||
import {
|
||||
|
@ -12,6 +13,7 @@ import {
|
|||
removeCustomEventListener,
|
||||
addImage,
|
||||
getMarkdown,
|
||||
insertVideo,
|
||||
} from './services/editor_service';
|
||||
|
||||
export default {
|
||||
|
@ -21,6 +23,7 @@ export default {
|
|||
toast => toast.Editor,
|
||||
),
|
||||
AddImageModal,
|
||||
InsertVideoModal,
|
||||
},
|
||||
props: {
|
||||
content: {
|
||||
|
@ -63,6 +66,12 @@ export default {
|
|||
editorInstance() {
|
||||
return this.$refs.editor;
|
||||
},
|
||||
customEventListeners() {
|
||||
return [
|
||||
{ event: CUSTOM_EVENTS.openAddImageModal, listener: this.onOpenAddImageModal },
|
||||
{ event: CUSTOM_EVENTS.openInsertVideoModal, listener: this.onOpenInsertVideoModal },
|
||||
];
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.editorOptions = getEditorOptions(this.options);
|
||||
|
@ -72,16 +81,16 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
addListeners(editorApi) {
|
||||
addCustomEventListener(editorApi, CUSTOM_EVENTS.openAddImageModal, this.onOpenAddImageModal);
|
||||
this.customEventListeners.forEach(({ event, listener }) => {
|
||||
addCustomEventListener(editorApi, event, listener);
|
||||
});
|
||||
|
||||
editorApi.eventManager.listen('changeMode', this.onChangeMode);
|
||||
},
|
||||
removeListeners() {
|
||||
removeCustomEventListener(
|
||||
this.editorApi,
|
||||
CUSTOM_EVENTS.openAddImageModal,
|
||||
this.onOpenAddImageModal,
|
||||
);
|
||||
this.customEventListeners.forEach(({ event, listener }) => {
|
||||
removeCustomEventListener(this.editorApi, event, listener);
|
||||
});
|
||||
|
||||
this.editorApi.eventManager.removeEventHandler('changeMode', this.onChangeMode);
|
||||
},
|
||||
|
@ -111,6 +120,12 @@ export default {
|
|||
|
||||
addImage(this.editorInstance, image);
|
||||
},
|
||||
onOpenInsertVideoModal() {
|
||||
this.$refs.insertVideoModal.show();
|
||||
},
|
||||
onInsertVideo(url) {
|
||||
insertVideo(this.editorInstance, url);
|
||||
},
|
||||
onChangeMode(newMode) {
|
||||
this.$emit('modeChange', newMode);
|
||||
},
|
||||
|
@ -130,5 +145,6 @@ export default {
|
|||
@load="onLoad"
|
||||
/>
|
||||
<add-image-modal ref="addImageModal" :image-root="imageRoot" @addImage="onAddImage" />
|
||||
<insert-video-modal ref="insertVideoModal" @insertVideo="onInsertVideo" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -3,7 +3,7 @@ import { defaults } from 'lodash';
|
|||
import ToolbarItem from '../toolbar_item.vue';
|
||||
import buildHtmlToMarkdownRenderer from './build_html_to_markdown_renderer';
|
||||
import buildCustomHTMLRenderer from './build_custom_renderer';
|
||||
import { TOOLBAR_ITEM_CONFIGS } from '../constants';
|
||||
import { TOOLBAR_ITEM_CONFIGS, VIDEO_ATTRIBUTES } from '../constants';
|
||||
import sanitizeHTML from './sanitize_html';
|
||||
|
||||
const buildWrapper = propsData => {
|
||||
|
@ -17,6 +17,23 @@ const buildWrapper = propsData => {
|
|||
return instance.$el;
|
||||
};
|
||||
|
||||
const buildVideoIframe = src => {
|
||||
const wrapper = document.createElement('figure');
|
||||
const iframe = document.createElement('iframe');
|
||||
const videoAttributes = { ...VIDEO_ATTRIBUTES, src };
|
||||
const wrapperClasses = ['gl-relative', 'gl-h-0', 'video_container'];
|
||||
const iframeClasses = ['gl-absolute', 'gl-top-0', 'gl-left-0', 'gl-w-full', 'gl-h-full'];
|
||||
|
||||
wrapper.setAttribute('contenteditable', 'false');
|
||||
wrapper.classList.add(...wrapperClasses);
|
||||
iframe.classList.add(...iframeClasses);
|
||||
Object.assign(iframe, videoAttributes);
|
||||
|
||||
wrapper.appendChild(iframe);
|
||||
|
||||
return wrapper;
|
||||
};
|
||||
|
||||
export const generateToolbarItem = config => {
|
||||
const { icon, classes, event, command, tooltip, isDivider } = config;
|
||||
|
||||
|
@ -44,6 +61,16 @@ export const removeCustomEventListener = (editorApi, event, handler) =>
|
|||
|
||||
export const addImage = ({ editor }, image) => editor.exec('AddImage', image);
|
||||
|
||||
export const insertVideo = ({ editor }, url) => {
|
||||
const videoIframe = buildVideoIframe(url);
|
||||
|
||||
if (editor.isWysiwygMode()) {
|
||||
editor.getSquire().insertElement(videoIframe);
|
||||
} else {
|
||||
editor.insertText(videoIframe.outerHTML);
|
||||
}
|
||||
};
|
||||
|
||||
export const getMarkdown = editorInstance => editorInstance.invoke('getMarkdown');
|
||||
|
||||
/**
|
||||
|
|
|
@ -11,8 +11,6 @@
|
|||
@import './pages/editor';
|
||||
@import './pages/environment_logs';
|
||||
@import './pages/events';
|
||||
@import './pages/experience_level';
|
||||
@import './pages/experimental_separate_sign_up';
|
||||
@import './pages/groups';
|
||||
@import './pages/help';
|
||||
@import './pages/import';
|
||||
|
|
|
@ -44,3 +44,11 @@
|
|||
@include gl-line-height-20;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Styling below ensures that YouTube videos are displayed in the editor the same as they would in about.gitlab.com
|
||||
* https://gitlab.com/gitlab-com/www-gitlab-com/-/blob/master/source/stylesheets/_base.scss#L977
|
||||
*/
|
||||
.video_container {
|
||||
padding-bottom: 56.25%;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
@import 'mixins_and_variables_and_functions';
|
||||
|
||||
.signup-page {
|
||||
.page-wrap {
|
||||
background-color: var(--gray-10, $gray-10);
|
||||
}
|
||||
|
||||
.signup-box-container {
|
||||
max-width: 960px;
|
||||
}
|
||||
|
||||
.signup-box {
|
||||
background-color: var(--white, $white);
|
||||
box-shadow: 0 0 0 1px var(--border-color, $border-color);
|
||||
border-radius: $border-radius;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
&:active,
|
||||
&:focus {
|
||||
background-color: var(--white, $white);
|
||||
}
|
||||
}
|
||||
|
||||
.devise-errors {
|
||||
h2 {
|
||||
font-size: $gl-font-size;
|
||||
color: var(--red-700, $red-700);
|
||||
}
|
||||
}
|
||||
|
||||
.omniauth-divider {
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
border-bottom: 1px solid var(--gray-100, $gray-100);
|
||||
margin: $gl-padding-24 0;
|
||||
}
|
||||
|
||||
&::before {
|
||||
margin-right: $gl-padding;
|
||||
}
|
||||
|
||||
&::after {
|
||||
margin-left: $gl-padding;
|
||||
}
|
||||
}
|
||||
|
||||
.omniauth-btn {
|
||||
width: 48%;
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
img {
|
||||
width: $default-icon-size;
|
||||
height: $default-icon-size;
|
||||
}
|
||||
}
|
||||
|
||||
.decline-page {
|
||||
width: 350px;
|
||||
}
|
||||
}
|
||||
|
||||
.signup-page[data-page^='registrations:experience_levels'] {
|
||||
$card-shadow-color: rgba(var(--black, $black), 0.2);
|
||||
|
||||
.page-wrap {
|
||||
background-color: var(--white, $white);
|
||||
}
|
||||
|
||||
.card-deck {
|
||||
max-width: 828px;
|
||||
}
|
||||
|
||||
.card {
|
||||
transition: box-shadow 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: 0 $gl-spacing-scale-3 $gl-spacing-scale-5 $card-shadow-color;
|
||||
}
|
||||
|
||||
@media (min-width: $breakpoint-sm) {
|
||||
.card-deck .card {
|
||||
margin: 0 $gl-spacing-scale-3;
|
||||
}
|
||||
}
|
||||
|
||||
.stretched-link:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
.signup-page[data-page^='registrations:experience_levels'] {
|
||||
$card-shadow-color: rgba($black, 0.2);
|
||||
|
||||
.page-wrap {
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
.card-deck {
|
||||
max-width: 828px;
|
||||
}
|
||||
|
||||
.card {
|
||||
transition: box-shadow 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: 0 $gl-spacing-scale-3 $gl-spacing-scale-5 $card-shadow-color;
|
||||
}
|
||||
|
||||
@media (min-width: $breakpoint-sm) {
|
||||
.card-deck .card {
|
||||
margin: 0 $gl-spacing-scale-3;
|
||||
}
|
||||
}
|
||||
|
||||
.stretched-link:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
.signup-page {
|
||||
.page-wrap {
|
||||
background-color: $gray-light;
|
||||
}
|
||||
|
||||
.signup-box-container {
|
||||
max-width: 960px;
|
||||
}
|
||||
|
||||
.signup-box {
|
||||
background-color: $white;
|
||||
box-shadow: 0 0 0 1px $border-color;
|
||||
border-radius: $border-radius;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
&:active,
|
||||
&:focus {
|
||||
background-color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.devise-errors {
|
||||
h2 {
|
||||
font-size: $gl-font-size;
|
||||
color: $red-700;
|
||||
}
|
||||
}
|
||||
|
||||
.omniauth-divider {
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
border-bottom: 1px solid $gray-dark;
|
||||
margin: $gl-padding-24 0;
|
||||
}
|
||||
|
||||
&::before {
|
||||
margin-right: $gl-padding;
|
||||
}
|
||||
|
||||
&::after {
|
||||
margin-left: $gl-padding;
|
||||
}
|
||||
}
|
||||
|
||||
.omniauth-btn {
|
||||
width: 48%;
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
img {
|
||||
width: $default-icon-size;
|
||||
height: $default-icon-size;
|
||||
}
|
||||
}
|
||||
|
||||
.decline-page {
|
||||
width: 350px;
|
||||
}
|
||||
}
|
|
@ -150,7 +150,7 @@ module IssuableCollections
|
|||
common_attributes + [:project, project: :namespace]
|
||||
when 'MergeRequest'
|
||||
common_attributes + [
|
||||
:target_project, :latest_merge_request_diff, :approvals, :approved_by_users,
|
||||
:target_project, :latest_merge_request_diff, :approvals, :approved_by_users, :reviewers,
|
||||
source_project: :route, head_pipeline: :project, target_project: :namespace
|
||||
]
|
||||
end
|
||||
|
|
|
@ -105,7 +105,7 @@ module MembershipActions
|
|||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
def resend_invite
|
||||
member = membershipable.members.find(params[:id])
|
||||
member = membershipable_members.find(params[:id])
|
||||
|
||||
if member.invite?
|
||||
member.resend_invite
|
||||
|
@ -122,6 +122,10 @@ module MembershipActions
|
|||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def membershipable_members
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def root_params_key
|
||||
case membershipable
|
||||
when Namespace
|
||||
|
|
|
@ -71,6 +71,10 @@ class Groups::GroupMembersController < Groups::ApplicationController
|
|||
def filter_params
|
||||
params.permit(:two_factor, :search).merge(sort: @sort)
|
||||
end
|
||||
|
||||
def membershipable_members
|
||||
group.members
|
||||
end
|
||||
end
|
||||
|
||||
Groups::GroupMembersController.prepend_if_ee('EE::Groups::GroupMembersController')
|
||||
|
|
|
@ -39,7 +39,7 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController
|
|||
private
|
||||
|
||||
def autocomplete_service
|
||||
@autocomplete_service ||= ::Projects::AutocompleteService.new(@project, current_user, params)
|
||||
@autocomplete_service ||= ::Projects::AutocompleteService.new(@project, current_user)
|
||||
end
|
||||
|
||||
def target
|
||||
|
|
|
@ -5,11 +5,11 @@ class Projects::PipelinesController < Projects::ApplicationController
|
|||
include Analytics::UniqueVisitsHelper
|
||||
|
||||
before_action :whitelist_query_limiting, only: [:create, :retry]
|
||||
before_action :pipeline, except: [:index, :new, :create, :charts]
|
||||
before_action :pipeline, except: [:index, :new, :create, :charts, :config_variables]
|
||||
before_action :set_pipeline_path, only: [:show]
|
||||
before_action :authorize_read_pipeline!
|
||||
before_action :authorize_read_build!, only: [:index]
|
||||
before_action :authorize_create_pipeline!, only: [:new, :create]
|
||||
before_action :authorize_create_pipeline!, only: [:new, :create, :config_variables]
|
||||
before_action :authorize_update_pipeline!, only: [:retry, :cancel]
|
||||
before_action do
|
||||
push_frontend_feature_flag(:filter_pipelines_search, project, default_enabled: true)
|
||||
|
@ -209,6 +209,14 @@ class Projects::PipelinesController < Projects::ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def config_variables
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
render json: Ci::ListConfigVariablesService.new(@project).execute(params[:sha])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def serialize_pipelines
|
||||
|
|
|
@ -57,6 +57,10 @@ class Projects::ProjectMembersController < Projects::ApplicationController
|
|||
def filter_params
|
||||
params.permit(:search).merge(sort: @sort)
|
||||
end
|
||||
|
||||
def membershipable_members
|
||||
project.members
|
||||
end
|
||||
end
|
||||
|
||||
Projects::ProjectMembersController.prepend_if_ee('EE::Projects::ProjectMembersController')
|
||||
|
|
|
@ -9,6 +9,7 @@ class Projects::ReleasesController < Projects::ApplicationController
|
|||
push_frontend_feature_flag(:graphql_release_data, project, default_enabled: true)
|
||||
push_frontend_feature_flag(:graphql_milestone_stats, project, default_enabled: true)
|
||||
push_frontend_feature_flag(:graphql_releases_page, project, default_enabled: true)
|
||||
push_frontend_feature_flag(:graphql_individual_release_page, project)
|
||||
end
|
||||
before_action :authorize_update_release!, only: %i[edit update]
|
||||
before_action :authorize_create_release!, only: :new
|
||||
|
|
|
@ -397,7 +397,7 @@ class IssuableFinder
|
|||
elsif params.filter_by_any_assignee?
|
||||
items.assigned
|
||||
elsif params.assignee
|
||||
items.assigned_to(params.assignee)
|
||||
items_assigned_to(items, params.assignee)
|
||||
elsif params.assignee_id? || params.assignee_username? # assignee not found
|
||||
items.none
|
||||
else
|
||||
|
@ -405,6 +405,10 @@ class IssuableFinder
|
|||
end
|
||||
end
|
||||
|
||||
def items_assigned_to(items, user)
|
||||
items.assigned_to(user)
|
||||
end
|
||||
|
||||
def by_negated_assignee(items)
|
||||
# We want CE users to be able to say "Issues not assigned to either PersonA nor PersonB"
|
||||
if not_params.assignees.present?
|
||||
|
|
|
@ -145,6 +145,10 @@ class MergeRequestsFinder < IssuableFinder
|
|||
.execute(items)
|
||||
end
|
||||
# rubocop: enable CodeReuse/Finder
|
||||
|
||||
def items_assigned_to(items, user)
|
||||
MergeRequest.from_union([super, items.reviewer_assigned_to(user)])
|
||||
end
|
||||
end
|
||||
|
||||
MergeRequestsFinder.prepend_if_ee('EE::MergeRequestsFinder')
|
||||
|
|
|
@ -159,7 +159,6 @@ module NotesHelper
|
|||
members: autocomplete,
|
||||
issues: autocomplete,
|
||||
mergeRequests: autocomplete,
|
||||
vulnerabilities: autocomplete,
|
||||
epics: autocomplete,
|
||||
milestones: autocomplete,
|
||||
labels: autocomplete
|
||||
|
|
|
@ -29,6 +29,14 @@ module ReleasesHelper
|
|||
end
|
||||
end
|
||||
|
||||
def data_for_show_page
|
||||
{
|
||||
project_id: @project.id,
|
||||
project_path: @project.full_path,
|
||||
tag_name: @release.tag
|
||||
}
|
||||
end
|
||||
|
||||
def data_for_edit_release_page
|
||||
new_edit_pages_shared_data.merge(
|
||||
tag_name: @release.tag,
|
||||
|
@ -48,6 +56,7 @@ module ReleasesHelper
|
|||
def new_edit_pages_shared_data
|
||||
{
|
||||
project_id: @project.id,
|
||||
project_path: @project.full_path,
|
||||
markdown_preview_path: preview_markdown_path(@project),
|
||||
markdown_docs_path: help_page_path('user/markdown'),
|
||||
update_release_api_docs_path: help_page_path('api/releases/index.md', anchor: 'update-a-release'),
|
||||
|
|
|
@ -22,7 +22,11 @@ module Clusters
|
|||
validate :has_at_least_one_log_enabled?
|
||||
|
||||
def chart
|
||||
'stable/fluentd'
|
||||
'fluentd/fluentd'
|
||||
end
|
||||
|
||||
def repository
|
||||
'https://gitlab-org.gitlab.io/cluster-integration/helm-stable-archive'
|
||||
end
|
||||
|
||||
def install_command
|
||||
|
|
|
@ -46,7 +46,11 @@ module Clusters
|
|||
end
|
||||
|
||||
def chart
|
||||
'stable/nginx-ingress'
|
||||
"#{name}/nginx-ingress"
|
||||
end
|
||||
|
||||
def repository
|
||||
'https://gitlab-org.gitlab.io/cluster-integration/helm-stable-archive'
|
||||
end
|
||||
|
||||
def values
|
||||
|
@ -60,6 +64,7 @@ module Clusters
|
|||
def install_command
|
||||
Gitlab::Kubernetes::Helm::InstallCommand.new(
|
||||
name: name,
|
||||
repository: repository,
|
||||
version: VERSION,
|
||||
rbac: cluster.platform_kubernetes_rbac?,
|
||||
chart: chart,
|
||||
|
|
|
@ -51,7 +51,11 @@ module Clusters
|
|||
end
|
||||
|
||||
def chart
|
||||
'stable/prometheus'
|
||||
"#{name}/prometheus"
|
||||
end
|
||||
|
||||
def repository
|
||||
'https://gitlab-org.gitlab.io/cluster-integration/helm-stable-archive'
|
||||
end
|
||||
|
||||
def service_name
|
||||
|
@ -65,6 +69,7 @@ module Clusters
|
|||
def install_command
|
||||
Gitlab::Kubernetes::Helm::InstallCommand.new(
|
||||
name: name,
|
||||
repository: repository,
|
||||
version: VERSION,
|
||||
rbac: cluster.platform_kubernetes_rbac?,
|
||||
chart: chart,
|
||||
|
@ -76,6 +81,7 @@ module Clusters
|
|||
def patch_command(values)
|
||||
::Gitlab::Kubernetes::Helm::PatchCommand.new(
|
||||
name: name,
|
||||
repository: repository,
|
||||
version: version,
|
||||
rbac: cluster.platform_kubernetes_rbac?,
|
||||
chart: chart,
|
||||
|
|
|
@ -22,7 +22,7 @@ module Mentionable
|
|||
def self.default_pattern
|
||||
strong_memoize(:default_pattern) do
|
||||
issue_pattern = Issue.reference_pattern
|
||||
link_patterns = Regexp.union([Issue, Commit, MergeRequest, Epic, Vulnerability].map(&:link_reference_pattern).compact)
|
||||
link_patterns = Regexp.union([Issue, Commit, MergeRequest, Epic].map(&:link_reference_pattern).compact)
|
||||
reference_pattern(link_patterns, issue_pattern)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -302,6 +302,10 @@ class MergeRequest < ApplicationRecord
|
|||
includes(:metrics)
|
||||
end
|
||||
|
||||
scope :reviewer_assigned_to, ->(user) do
|
||||
where("EXISTS (SELECT TRUE FROM merge_request_reviewers WHERE user_id = ? AND merge_request_id = merge_requests.id)", user.id)
|
||||
end
|
||||
|
||||
after_save :keep_around_commit, unless: :importing?
|
||||
|
||||
alias_attribute :project, :target_project
|
||||
|
|
|
@ -1340,7 +1340,8 @@ class Project < ApplicationRecord
|
|||
end
|
||||
|
||||
def find_or_initialize_services
|
||||
available_services_names = Service.available_services_names - disabled_services
|
||||
available_services_names =
|
||||
Service.available_services_names + Service.project_specific_services_names - disabled_services
|
||||
|
||||
available_services_names.map do |service_name|
|
||||
find_or_initialize_service(service_name)
|
||||
|
@ -2514,6 +2515,10 @@ class Project < ApplicationRecord
|
|||
ci_config_path.presence || Ci::Pipeline::DEFAULT_CONFIG_PATH
|
||||
end
|
||||
|
||||
def ci_config_for(sha)
|
||||
repository.gitlab_ci_yml_for(sha, ci_config_path_or_default)
|
||||
end
|
||||
|
||||
def enabled_group_deploy_keys
|
||||
return GroupDeployKey.none unless group
|
||||
|
||||
|
|
|
@ -208,6 +208,10 @@ class Service < ApplicationRecord
|
|||
DEV_SERVICE_NAMES
|
||||
end
|
||||
|
||||
def self.project_specific_services_names
|
||||
[]
|
||||
end
|
||||
|
||||
def self.available_services_types
|
||||
available_services_names.map { |service_name| "#{service_name}_service".camelize }
|
||||
end
|
||||
|
|
|
@ -1,21 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Placeholder class for model that is implemented in EE
|
||||
# It reserves '+' as a reference prefix, but the table does not exist in FOSS
|
||||
class Vulnerability < ApplicationRecord
|
||||
include IgnorableColumns
|
||||
|
||||
def self.link_reference_pattern
|
||||
nil
|
||||
end
|
||||
|
||||
def self.reference_prefix
|
||||
'+'
|
||||
end
|
||||
|
||||
def self.reference_prefix_escaped
|
||||
'+'
|
||||
end
|
||||
end
|
||||
|
||||
Vulnerability.prepend_if_ee('EE::Vulnerability')
|
||||
|
|
13
app/services/ci/list_config_variables_service.rb
Normal file
13
app/services/ci/list_config_variables_service.rb
Normal file
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Ci
|
||||
class ListConfigVariablesService < ::BaseService
|
||||
def execute(sha)
|
||||
config = project.ci_config_for(sha)
|
||||
return {} unless config
|
||||
|
||||
result = Gitlab::Ci::YamlProcessor.new(config).execute
|
||||
result.valid? ? result.variables_with_data : {}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,5 +1,6 @@
|
|||
!!! 5
|
||||
%html.devise-layout-html.navless{ class: system_message_class }
|
||||
- add_page_specific_style 'page_bundles/experimental_separate_sign_up'
|
||||
= render "layouts/head"
|
||||
%body.ui-indigo.signup-page{ class: "#{client_class_list}", data: { page: body_data_page, qa_selector: 'signup_page' } }
|
||||
= render "layouts/header/logo_with_title"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
!!! 5
|
||||
%html.devise-layout-html.navless{ class: system_message_class }
|
||||
- add_page_specific_style 'page_bundles/experimental_separate_sign_up'
|
||||
= render "layouts/head"
|
||||
%body.ui-indigo.signup-page{ class: "#{client_class_list}", data: { page: body_data_page, qa_selector: 'signup_page' } }
|
||||
= render "layouts/header/logo_with_title"
|
||||
|
|
|
@ -2,4 +2,4 @@
|
|||
- page_title @release.name
|
||||
- page_description @release.description_html
|
||||
|
||||
#js-show-release-page{ data: { project_id: @project.id, tag_name: @release.tag } }
|
||||
#js-show-release-page{ data: data_for_show_page }
|
||||
|
|
5
changelogs/unreleased/216642-embed-youtube-video.yml
Normal file
5
changelogs/unreleased/216642-embed-youtube-video.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add the ability to insert a YouTube video
|
||||
merge_request: 44102
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Snowplow tracking of Incident details views
|
||||
merge_request: 45011
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Allow re-sending invite to minimal access user
|
||||
merge_request: 44936
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Class and markup cleanup to prevent SVG header bar overlap in Static Site Editor
|
||||
merge_request: 45334
|
||||
author:
|
||||
type: fixed
|
5
changelogs/unreleased/use_mirror_of_helm_stable_repo.yml
Normal file
5
changelogs/unreleased/use_mirror_of_helm_stable_repo.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: "GitLab-managed apps: Use GitLab's repo as replacement for the Helm stable repo"
|
||||
merge_request: 44875
|
||||
author:
|
||||
type: other
|
|
@ -179,6 +179,7 @@ module Gitlab
|
|||
config.assets.precompile << "page_bundles/environments.css"
|
||||
config.assets.precompile << "page_bundles/error_tracking_details.css"
|
||||
config.assets.precompile << "page_bundles/error_tracking_index.css"
|
||||
config.assets.precompile << "page_bundles/experimental_separate_sign_up.css"
|
||||
config.assets.precompile << "page_bundles/ide.css"
|
||||
config.assets.precompile << "page_bundles/issues_list.css"
|
||||
config.assets.precompile << "page_bundles/jira_connect.css"
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
name: graphql_individual_release_page
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44779
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/263522
|
||||
type: development
|
||||
group: group::release management
|
||||
default_enabled: false
|
6
config/initializers/rails_host_authorization_gitpod.rb
Normal file
6
config/initializers/rails_host_authorization_gitpod.rb
Normal file
|
@ -0,0 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
if Rails.env.development? && ENV['GITPOD_WORKSPACE_ID'].present?
|
||||
gitpod_host = URI(`gp url 3000`.strip).host
|
||||
Rails.application.config.hosts += [gitpod_host]
|
||||
end
|
|
@ -7,6 +7,7 @@ resources :pipelines, only: [:index, :new, :create, :show, :destroy] do
|
|||
scope '(*ref)', constraints: { ref: Gitlab::PathRegex.git_reference_regex } do
|
||||
get :latest, action: :show, defaults: { latest: true }
|
||||
end
|
||||
get :config_variables
|
||||
end
|
||||
|
||||
member do
|
||||
|
|
|
@ -425,7 +425,6 @@ GFM recognizes the following:
|
|||
| merge request | `!123` | `namespace/project!123` | `project!123` |
|
||||
| snippet | `$123` | `namespace/project$123` | `project$123` |
|
||||
| epic **(ULTIMATE)** | `&123` | `group1/subgroup&123` | |
|
||||
| vulnerability **(ULTIMATE)** | `+123` | `namespace/project+123` | `project+123` |
|
||||
| label by ID | `~123` | `namespace/project~123` | `project~123` |
|
||||
| one-word label by name | `~bug` | `namespace/project~bug` | `project~bug` |
|
||||
| multi-word label by name | `~"feature request"` | `namespace/project~"feature request"` | `project~"feature request"` |
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
---
|
||||
description: 'Understand and explore the user permission levels in GitLab, and what features each of them grants you access to.'
|
||||
stage: Manage
|
||||
group: Access
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
---
|
||||
|
||||
# Permissions
|
||||
|
@ -42,17 +44,17 @@ or an instance administrator, who receives all permissions. For more information
|
|||
|
||||
The following table depicts the various user permission levels in a project.
|
||||
|
||||
| Action | Guest | Reporter | Developer |Maintainer| Owner* |
|
||||
| Action | Guest | Reporter | Developer |Maintainer| Owner (*10*) |
|
||||
|---------------------------------------------------|---------|------------|-------------|----------|--------|
|
||||
| Download project | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
|
||||
| Leave comments | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| View allowed and denied licenses **(ULTIMATE)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
|
||||
| View allowed and denied licenses **(ULTIMATE)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
|
||||
| View License Compliance reports **(ULTIMATE)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
|
||||
| View Security reports **(ULTIMATE)** | ✓ (*3*) | ✓ | ✓ | ✓ | ✓ |
|
||||
| View Dependency list **(ULTIMATE)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
|
||||
| View License list **(ULTIMATE)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
|
||||
| View licenses in Dependency list **(ULTIMATE)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
|
||||
| View [Design Management](project/issues/design_management.md) pages | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| View [Design Management](project/issues/design_management.md) pages | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| View project code | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
|
||||
| Pull project code | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
|
||||
| View GitLab Pages protected by [access control](project/pages/introduction.md#gitlab-pages-access-control) | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
|
@ -63,10 +65,14 @@ The following table depicts the various user permission levels in a project.
|
|||
| Create new issue | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| See related issues | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| Create confidential issue | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| View confidential issues | (*2*) | ✓ | ✓ | ✓ | ✓ |
|
||||
| View [Releases](project/releases/index.md) | ✓ (*6*) | ✓ | ✓ | ✓ | ✓ |
|
||||
| View requirements **(ULTIMATE)** | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| View Insights **(ULTIMATE)** | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| View Issue analytics **(PREMIUM)** | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| View Merge Request analytics **(STARTER)** | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| View Value Stream analytics | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| Manage user-starred metrics dashboards (*7*) | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| View confidential issues | (*2*) | ✓ | ✓ | ✓ | ✓ |
|
||||
| Assign issues | | ✓ | ✓ | ✓ | ✓ |
|
||||
| Label issues | | ✓ | ✓ | ✓ | ✓ |
|
||||
| Set issue weight | | ✓ | ✓ | ✓ | ✓ |
|
||||
|
@ -79,14 +85,15 @@ The following table depicts the various user permission levels in a project.
|
|||
| See a container registry | | ✓ | ✓ | ✓ | ✓ |
|
||||
| See environments | | ✓ | ✓ | ✓ | ✓ |
|
||||
| See a list of merge requests | | ✓ | ✓ | ✓ | ✓ |
|
||||
| View project statistics | | | ✓ | ✓ | ✓ |
|
||||
| View CI/CD analytics | | ✓ | ✓ | ✓ | ✓ |
|
||||
| View Code Review analytics **(STARTER)** | | ✓ | ✓ | ✓ | ✓ |
|
||||
| View Repository analytics | | ✓ | ✓ | ✓ | ✓ |
|
||||
| View Error Tracking list | | ✓ | ✓ | ✓ | ✓ |
|
||||
| Create new merge request | | ✓ | ✓ | ✓ | ✓ |
|
||||
| View metrics dashboard annotations | | ✓ | ✓ | ✓ | ✓ |
|
||||
| Create/edit requirements **(ULTIMATE)** | | ✓ | ✓ | ✓ | ✓ |
|
||||
| Pull [packages](packages/index.md) | | ✓ | ✓ | ✓ | ✓ |
|
||||
| Publish [packages](packages/index.md) | | | ✓ | ✓ | ✓ |
|
||||
| Delete [packages](packages/index.md) | | | | ✓ | ✓ |
|
||||
| Create/edit/delete a Cleanup policy | | | ✓ | ✓ | ✓ |
|
||||
| Upload [Design Management](project/issues/design_management.md) files | | | ✓ | ✓ | ✓ |
|
||||
| Create/edit/delete [Releases](project/releases/index.md)| | | ✓ | ✓ | ✓ |
|
||||
|
@ -99,9 +106,12 @@ The following table depicts the various user permission levels in a project.
|
|||
| Lock merge request threads | | | ✓ | ✓ | ✓ |
|
||||
| Approve merge requests (*9*) | | | ✓ | ✓ | ✓ |
|
||||
| Manage/Accept merge requests | | | ✓ | ✓ | ✓ |
|
||||
| View project statistics | | | ✓ | ✓ | ✓ |
|
||||
| Create new environments | | | ✓ | ✓ | ✓ |
|
||||
| Stop environments | | | ✓ | ✓ | ✓ |
|
||||
| Enable Review Apps | | | ✓ | ✓ | ✓ |
|
||||
| View Pods logs | | | ✓ | ✓ | ✓ |
|
||||
| Read Terraform state | | | ✓ | ✓ | ✓ |
|
||||
| Add tags | | | ✓ | ✓ | ✓ |
|
||||
| Cancel and retry jobs | | | ✓ | ✓ | ✓ |
|
||||
| Create or update commit status | | | ✓ (*5*) | ✓ | ✓ |
|
||||
|
@ -123,6 +133,7 @@ The following table depicts the various user permission levels in a project.
|
|||
| Manage Feature Flags **(PREMIUM)** | | | ✓ | ✓ | ✓ |
|
||||
| Create/edit/delete metrics dashboard annotations | | | ✓ | ✓ | ✓ |
|
||||
| Run CI/CD pipeline against a protected branch | | | ✓ (*5*) | ✓ | ✓ |
|
||||
| Delete [packages](packages/index.md) | | | | ✓ | ✓ |
|
||||
| Request a CVE ID **(FREE ONLY)** | | | | ✓ | ✓ |
|
||||
| Use environment terminals | | | | ✓ | ✓ |
|
||||
| Run Web IDE's Interactive Web Terminals **(ULTIMATE ONLY)** | | | | ✓ | ✓ |
|
||||
|
@ -133,6 +144,7 @@ The following table depicts the various user permission levels in a project.
|
|||
| Enable/disable tag protections | | | | ✓ | ✓ |
|
||||
| Edit project settings | | | | ✓ | ✓ |
|
||||
| Edit project badges | | | | ✓ | ✓ |
|
||||
| Export project | | | | ✓ | ✓ |
|
||||
| Share (invite) projects with groups | | | | ✓ (*8*) | ✓ (*8*)|
|
||||
| Add deploy keys to project | | | | ✓ | ✓ |
|
||||
| Configure project hooks | | | | ✓ | ✓ |
|
||||
|
@ -144,8 +156,6 @@ The following table depicts the various user permission levels in a project.
|
|||
| Remove GitLab Pages | | | | ✓ | ✓ |
|
||||
| Manage clusters | | | | ✓ | ✓ |
|
||||
| Manage Project Operations | | | | ✓ | ✓ |
|
||||
| View Pods logs | | | ✓ | ✓ | ✓ |
|
||||
| Read Terraform state | | | ✓ | ✓ | ✓ |
|
||||
| Manage Terraform state | | | | ✓ | ✓ |
|
||||
| Manage license policy **(ULTIMATE)** | | | | ✓ | ✓ |
|
||||
| Edit comments (posted by any user) | | | | ✓ | ✓ |
|
||||
|
@ -160,22 +170,12 @@ The following table depicts the various user permission levels in a project.
|
|||
| Remove fork relationship | | | | | ✓ |
|
||||
| Delete project | | | | | ✓ |
|
||||
| Archive project | | | | | ✓ |
|
||||
| Export project | | | | ✓ | ✓ |
|
||||
| Delete issues | | | | | ✓ |
|
||||
| Delete pipelines | | | | | ✓ |
|
||||
| Delete merge request | | | | | ✓ |
|
||||
| Disable notification emails | | | | | ✓ |
|
||||
| Force push to protected branches (*4*) | | | | | |
|
||||
| Remove protected branches (*4*) | | | | | |
|
||||
| View CI\CD analytics | | ✓ | ✓ | ✓ | ✓ |
|
||||
| View Code Review analytics **(STARTER)** | | ✓ | ✓ | ✓ | ✓ |
|
||||
| View Insights **(ULTIMATE)** | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| View Issue analytics **(PREMIUM)** | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| View Merge Request analytics **(STARTER)** | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| View Repository analytics | | ✓ | ✓ | ✓ | ✓ |
|
||||
| View Value Stream analytics | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
|
||||
\* Owner permission is only available at the group or personal namespace level (and for instance admins) and is inherited by its projects.
|
||||
|
||||
1. Guest users are able to perform this action on public and internal projects, but not private projects. This doesn't apply to [external users](#external-users) where explicit access must be given even if the project is internal.
|
||||
1. Guest users can only view the confidential issues they created themselves.
|
||||
|
@ -187,6 +187,7 @@ The following table depicts the various user permission levels in a project.
|
|||
1. When [Share Group Lock](./group/index.md#share-with-group-lock) is enabled the project can't be shared with other groups. It does not affect group with group sharing.
|
||||
1. For information on eligible approvers for merge requests, see
|
||||
[Eligible approvers](project/merge_requests/merge_request_approvals.md#eligible-approvers).
|
||||
1. Owner permission is only available at the group or personal namespace level (and for instance admins) and is inherited by its projects.
|
||||
|
||||
## Project features permissions
|
||||
|
||||
|
|
|
@ -115,6 +115,17 @@ company and a new feature has been added to the company product.
|
|||
1. You edit the file right there and click **Submit changes**.
|
||||
1. A new merge request is automatically created and you assign it to your colleague for review.
|
||||
|
||||
## Videos
|
||||
|
||||
> - Support for embedding YouTube videos through the WYSIWYG editor [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216642) in GitLab 13.5.
|
||||
|
||||
You can embed YouTube videos on the WYSIWYG mode by clicking the video icon (**{live-preview}**).
|
||||
The following URL/ID formats are supported:
|
||||
|
||||
- YouTube watch URL (e.g. `https://www.youtube.com/watch?v=0t1DgySidms`)
|
||||
- YouTube embed URL (e.g. `https://www.youtube.com/embed/0t1DgySidms`)
|
||||
- YouTube video ID (e.g. `0t1DgySidms`)
|
||||
|
||||
## Limitations
|
||||
|
||||
- The Static Site Editor still cannot be quickly added to existing Middleman sites. Follow this [epic](https://gitlab.com/groups/gitlab-org/-/epics/2784) for updates.
|
||||
|
|
|
@ -119,7 +119,7 @@ module Banzai
|
|||
|
||||
# Yields the link's URL and inner HTML whenever the node is a valid <a> tag.
|
||||
def yield_valid_link(node)
|
||||
link = unescape_link(node.attr('href').to_s)
|
||||
link = CGI.unescape(node.attr('href').to_s)
|
||||
inner_html = node.inner_html
|
||||
|
||||
return unless link.force_encoding('UTF-8').valid_encoding?
|
||||
|
@ -127,10 +127,6 @@ module Banzai
|
|||
yield link, inner_html
|
||||
end
|
||||
|
||||
def unescape_link(href)
|
||||
CGI.unescape(href)
|
||||
end
|
||||
|
||||
def replace_text_when_pattern_matches(node, index, pattern)
|
||||
return unless node.text =~ pattern
|
||||
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Banzai
|
||||
module Filter
|
||||
# The actual filter is implemented in the EE mixin
|
||||
class VulnerabilityReferenceFilter < IssuableReferenceFilter
|
||||
self.reference_type = :vulnerability
|
||||
|
||||
def self.object_class
|
||||
Vulnerability
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def project
|
||||
context[:project]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Banzai::Filter::VulnerabilityReferenceFilter.prepend_if_ee('EE::Banzai::Filter::VulnerabilityReferenceFilter')
|
|
@ -1,16 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Banzai
|
||||
module ReferenceParser
|
||||
# The actual parser is implemented in the EE mixin
|
||||
class VulnerabilityParser < IssuableParser
|
||||
self.reference_type = :vulnerability
|
||||
|
||||
def records_for_nodes(_nodes)
|
||||
{}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Banzai::ReferenceParser::VulnerabilityParser.prepend_if_ee('::EE::Banzai::ReferenceParser::VulnerabilityParser')
|
|
@ -54,6 +54,10 @@ module Gitlab
|
|||
root.variables_value
|
||||
end
|
||||
|
||||
def variables_with_data
|
||||
root.variables_entry.value_with_data
|
||||
end
|
||||
|
||||
def stages
|
||||
root.stages_value
|
||||
end
|
||||
|
|
|
@ -10,16 +10,32 @@ module Gitlab
|
|||
class Variables < ::Gitlab::Config::Entry::Node
|
||||
include ::Gitlab::Config::Entry::Validatable
|
||||
|
||||
ALLOWED_VALUE_DATA = %i[value description].freeze
|
||||
|
||||
validations do
|
||||
validates :config, variables: true
|
||||
validates :config, variables: { allowed_value_data: ALLOWED_VALUE_DATA }
|
||||
end
|
||||
|
||||
def value
|
||||
Hash[@config.map { |key, value| [key.to_s, expand_value(value)[:value]] }]
|
||||
end
|
||||
|
||||
def self.default(**)
|
||||
{}
|
||||
end
|
||||
|
||||
def value
|
||||
Hash[@config.map { |key, value| [key.to_s, value.to_s] }]
|
||||
def value_with_data
|
||||
Hash[@config.map { |key, value| [key.to_s, expand_value(value)] }]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def expand_value(value)
|
||||
if value.is_a?(Hash)
|
||||
{ value: value[:value].to_s, description: value[:description] }
|
||||
else
|
||||
{ value: value.to_s, description: nil }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -99,6 +99,10 @@ module Gitlab
|
|||
@ci_config&.to_hash&.to_yaml
|
||||
end
|
||||
|
||||
def variables_with_data
|
||||
@ci_config.variables_with_data
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def variables
|
||||
|
|
|
@ -50,6 +50,12 @@ module Gitlab
|
|||
variables.values.flatten(1).all?(&method(:validate_alphanumeric))
|
||||
end
|
||||
|
||||
def validate_string_or_hash_value_variables(variables, allowed_value_data)
|
||||
variables.is_a?(Hash) &&
|
||||
variables.keys.all?(&method(:validate_alphanumeric)) &&
|
||||
variables.values.all? { |value| validate_string_or_hash_value_variable(value, allowed_value_data) }
|
||||
end
|
||||
|
||||
def validate_alphanumeric(value)
|
||||
validate_string(value) || validate_integer(value)
|
||||
end
|
||||
|
@ -62,6 +68,14 @@ module Gitlab
|
|||
value.is_a?(String) || value.is_a?(Symbol)
|
||||
end
|
||||
|
||||
def validate_string_or_hash_value_variable(value, allowed_value_data)
|
||||
if value.is_a?(Hash)
|
||||
(value.keys - allowed_value_data).empty? && value.values.all?(&method(:validate_alphanumeric))
|
||||
else
|
||||
validate_alphanumeric(value)
|
||||
end
|
||||
end
|
||||
|
||||
def validate_regexp(value)
|
||||
Gitlab::UntrustedRegexp::RubySyntax.valid?(value)
|
||||
end
|
||||
|
|
|
@ -274,6 +274,8 @@ module Gitlab
|
|||
def validate_each(record, attribute, value)
|
||||
if options[:array_values]
|
||||
validate_key_array_values(record, attribute, value)
|
||||
elsif options[:allowed_value_data]
|
||||
validate_key_hash_values(record, attribute, value, options[:allowed_value_data])
|
||||
else
|
||||
validate_key_values(record, attribute, value)
|
||||
end
|
||||
|
@ -290,6 +292,12 @@ module Gitlab
|
|||
record.errors.add(attribute, 'should be a hash of key value pairs, value can be an array')
|
||||
end
|
||||
end
|
||||
|
||||
def validate_key_hash_values(record, attribute, value, allowed_value_data)
|
||||
unless validate_string_or_hash_value_variables(value, allowed_value_data)
|
||||
record.errors.add(attribute, 'should be a hash of key value pairs, value can be a hash')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class ExpressionValidator < ActiveModel::EachValidator
|
||||
|
|
|
@ -4,7 +4,7 @@ module Gitlab
|
|||
# Extract possible GFM references from an arbitrary String for further processing.
|
||||
class ReferenceExtractor < Banzai::ReferenceExtractor
|
||||
REFERABLES = %i(user issue label milestone mentioned_user mentioned_group mentioned_project
|
||||
merge_request snippet commit commit_range directly_addressed_user epic iteration vulnerability).freeze
|
||||
merge_request snippet commit commit_range directly_addressed_user epic iteration).freeze
|
||||
attr_accessor :project, :current_user, :author
|
||||
# This counter is increased by a number of references filtered out by
|
||||
# banzai reference exctractor. Note that this counter is stateful and
|
||||
|
@ -38,7 +38,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
REFERABLES.each do |type|
|
||||
define_method(type.to_s.pluralize) do
|
||||
define_method("#{type}s") do
|
||||
@references[type] ||= references(type)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1051,6 +1051,9 @@ msgstr ""
|
|||
msgid "0 for unlimited, only effective with remote storage enabled."
|
||||
msgstr ""
|
||||
|
||||
msgid "0t1DgySidms"
|
||||
msgstr ""
|
||||
|
||||
msgid "1 %{type} addition"
|
||||
msgid_plural "%{count} %{type} additions"
|
||||
msgstr[0] ""
|
||||
|
@ -13549,6 +13552,9 @@ msgstr ""
|
|||
msgid "If enabled, access to projects will be validated on an external service using their classification label."
|
||||
msgstr ""
|
||||
|
||||
msgid "If the YouTube URL is https://www.youtube.com/watch?v=0t1DgySidms then the video ID is %{id}"
|
||||
msgstr ""
|
||||
|
||||
msgid "If the number of active users exceeds the user limit, you will be charged for the number of %{users_over_license_link} at your next license reconciliation."
|
||||
msgstr ""
|
||||
|
||||
|
@ -14021,6 +14027,9 @@ msgstr ""
|
|||
msgid "Insert a quote"
|
||||
msgstr ""
|
||||
|
||||
msgid "Insert a video"
|
||||
msgstr ""
|
||||
|
||||
msgid "Insert an image"
|
||||
msgstr ""
|
||||
|
||||
|
@ -14036,6 +14045,9 @@ msgstr ""
|
|||
msgid "Insert suggestion"
|
||||
msgstr ""
|
||||
|
||||
msgid "Insert video"
|
||||
msgstr ""
|
||||
|
||||
msgid "Insights"
|
||||
msgstr ""
|
||||
|
||||
|
@ -19546,6 +19558,9 @@ msgstr ""
|
|||
msgid "Please provide a valid URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "Please provide a valid YouTube URL or ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Please provide a valid email address."
|
||||
msgstr ""
|
||||
|
||||
|
@ -30300,6 +30315,9 @@ msgstr ""
|
|||
msgid "YouTube"
|
||||
msgstr ""
|
||||
|
||||
msgid "YouTube URL or ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Your %{host} account was signed in to from a new location"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -1148,4 +1148,84 @@ RSpec.describe Projects::PipelinesController do
|
|||
}
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET config_variables.json' do
|
||||
let(:result) { YAML.dump(ci_config) }
|
||||
|
||||
before do
|
||||
stub_gitlab_ci_yml_for_sha(sha, result)
|
||||
end
|
||||
|
||||
context 'when sending a valid sha' do
|
||||
let(:sha) { 'master' }
|
||||
let(:ci_config) do
|
||||
{
|
||||
variables: {
|
||||
KEY1: { value: 'val 1', description: 'description 1' }
|
||||
},
|
||||
test: {
|
||||
stage: 'test',
|
||||
script: 'echo'
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'returns variable list' do
|
||||
get_config_variables
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response['KEY1']).to eq({ 'value' => 'val 1', 'description' => 'description 1' })
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sending an invalid sha' do
|
||||
let(:sha) { 'invalid-sha' }
|
||||
let(:ci_config) { nil }
|
||||
|
||||
it 'returns empty json' do
|
||||
get_config_variables
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response).to eq({})
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sending an invalid config' do
|
||||
let(:sha) { 'master' }
|
||||
let(:ci_config) do
|
||||
{
|
||||
variables: {
|
||||
KEY1: { value: 'val 1', description: 'description 1' }
|
||||
},
|
||||
test: {
|
||||
stage: 'invalid',
|
||||
script: 'echo'
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'returns empty result' do
|
||||
get_config_variables
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response).to eq({})
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def stub_gitlab_ci_yml_for_sha(sha, result)
|
||||
allow_any_instance_of(Repository)
|
||||
.to receive(:gitlab_ci_yml_for)
|
||||
.with(sha, '.gitlab-ci.yml')
|
||||
.and_return(result)
|
||||
end
|
||||
|
||||
def get_config_variables
|
||||
get :config_variables, params: { namespace_id: project.namespace,
|
||||
project_id: project,
|
||||
sha: sha },
|
||||
format: :json
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -573,4 +573,19 @@ RSpec.describe Projects::ProjectMembersController do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST resend_invite' do
|
||||
let(:member) { create(:project_member, project: project) }
|
||||
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it 'is successful' do
|
||||
post :resend_invite, params: { namespace_id: project.namespace, project_id: project, id: member }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:found)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -52,20 +52,29 @@ RSpec.describe 'Dashboard Merge Requests' do
|
|||
end
|
||||
|
||||
context 'merge requests exist' do
|
||||
let_it_be(:author_user) { create(:user) }
|
||||
let(:label) { create(:label) }
|
||||
|
||||
let!(:assigned_merge_request) do
|
||||
create(:merge_request,
|
||||
assignees: [current_user],
|
||||
source_project: project,
|
||||
author: create(:user))
|
||||
author: author_user)
|
||||
end
|
||||
|
||||
let!(:review_requested_merge_request) do
|
||||
create(:merge_request,
|
||||
reviewers: [current_user],
|
||||
source_branch: 'review',
|
||||
source_project: project,
|
||||
author: author_user)
|
||||
end
|
||||
|
||||
let!(:assigned_merge_request_from_fork) do
|
||||
create(:merge_request,
|
||||
source_branch: 'markdown', assignees: [current_user],
|
||||
target_project: public_project, source_project: forked_project,
|
||||
author: create(:user))
|
||||
author: author_user)
|
||||
end
|
||||
|
||||
let!(:authored_merge_request) do
|
||||
|
@ -94,7 +103,7 @@ RSpec.describe 'Dashboard Merge Requests' do
|
|||
create(:merge_request,
|
||||
source_branch: 'fix',
|
||||
source_project: project,
|
||||
author: create(:user))
|
||||
author: author_user)
|
||||
end
|
||||
|
||||
before do
|
||||
|
@ -111,6 +120,10 @@ RSpec.describe 'Dashboard Merge Requests' do
|
|||
expect(page).not_to have_content(labeled_merge_request.title)
|
||||
end
|
||||
|
||||
it 'shows review requested merge requests' do
|
||||
expect(page).to have_content(review_requested_merge_request.title)
|
||||
end
|
||||
|
||||
it 'shows authored merge requests', :js do
|
||||
reset_filters
|
||||
input_filtered_search("author:=#{current_user.to_reference}")
|
||||
|
|
|
@ -5,6 +5,7 @@ require 'spec_helper'
|
|||
RSpec.describe 'User views Release', :js do
|
||||
let(:project) { create(:project, :repository) }
|
||||
let(:user) { create(:user) }
|
||||
let(:graphql_feature_flag) { true }
|
||||
|
||||
let(:release) do
|
||||
create(:release,
|
||||
|
@ -14,6 +15,8 @@ RSpec.describe 'User views Release', :js do
|
|||
end
|
||||
|
||||
before do
|
||||
stub_feature_flags(graphql_individual_release_page: graphql_feature_flag)
|
||||
|
||||
project.add_developer(user)
|
||||
|
||||
sign_in(user)
|
||||
|
@ -23,23 +26,35 @@ RSpec.describe 'User views Release', :js do
|
|||
|
||||
it_behaves_like 'page meta description', 'Lorem ipsum dolor sit amet'
|
||||
|
||||
it 'renders the breadcrumbs' do
|
||||
within('.breadcrumbs') do
|
||||
expect(page).to have_content("#{project.creator.name} #{project.name} Releases #{release.name}")
|
||||
shared_examples 'release page' do
|
||||
it 'renders the breadcrumbs' do
|
||||
within('.breadcrumbs') do
|
||||
expect(page).to have_content("#{project.creator.name} #{project.name} Releases #{release.name}")
|
||||
|
||||
expect(page).to have_link(project.creator.name, href: user_path(project.creator))
|
||||
expect(page).to have_link(project.name, href: project_path(project))
|
||||
expect(page).to have_link('Releases', href: project_releases_path(project))
|
||||
expect(page).to have_link(release.name, href: project_release_path(project, release))
|
||||
expect(page).to have_link(project.creator.name, href: user_path(project.creator))
|
||||
expect(page).to have_link(project.name, href: project_path(project))
|
||||
expect(page).to have_link('Releases', href: project_releases_path(project))
|
||||
expect(page).to have_link(release.name, href: project_release_path(project, release))
|
||||
end
|
||||
end
|
||||
|
||||
it 'renders the release details' do
|
||||
within('.release-block') do
|
||||
expect(page).to have_content(release.name)
|
||||
expect(page).to have_content(release.tag)
|
||||
expect(page).to have_content(release.commit.short_id)
|
||||
expect(page).to have_content('Lorem ipsum dolor sit amet')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'renders the release details' do
|
||||
within('.release-block') do
|
||||
expect(page).to have_content(release.name)
|
||||
expect(page).to have_content(release.tag)
|
||||
expect(page).to have_content(release.commit.short_id)
|
||||
expect(page).to have_content('Lorem ipsum dolor sit amet')
|
||||
end
|
||||
describe 'when the graphql_individual_release_page feature flag is enabled' do
|
||||
it_behaves_like 'release page'
|
||||
end
|
||||
|
||||
describe 'when the graphql_individual_release_page feature flag is disabled' do
|
||||
let(:graphql_feature_flag) { false }
|
||||
|
||||
it_behaves_like 'release page'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -333,6 +333,8 @@ RSpec.describe MergeRequestsFinder do
|
|||
end
|
||||
|
||||
context 'assignee filtering' do
|
||||
let_it_be(:user3) { create(:user) }
|
||||
|
||||
let(:issuables) { described_class.new(user, params).execute }
|
||||
|
||||
it_behaves_like 'assignee ID filter' do
|
||||
|
@ -351,7 +353,6 @@ RSpec.describe MergeRequestsFinder do
|
|||
merge_request3.assignees = [user2, user3]
|
||||
end
|
||||
|
||||
let_it_be(:user3) { create(:user) }
|
||||
let(:params) { { assignee_username: [user2.username, user3.username] } }
|
||||
let(:expected_issuables) { [merge_request3] }
|
||||
end
|
||||
|
@ -366,7 +367,6 @@ RSpec.describe MergeRequestsFinder do
|
|||
end
|
||||
|
||||
it_behaves_like 'no assignee filter' do
|
||||
let_it_be(:user3) { create(:user) }
|
||||
let(:expected_issuables) { [merge_request4, merge_request5] }
|
||||
end
|
||||
|
||||
|
@ -374,30 +374,54 @@ RSpec.describe MergeRequestsFinder do
|
|||
let(:expected_issuables) { [merge_request1, merge_request2, merge_request3] }
|
||||
end
|
||||
|
||||
context 'filtering by group milestone' do
|
||||
let(:group_milestone) { create(:milestone, group: group) }
|
||||
context 'with just reviewers' do
|
||||
it_behaves_like 'assignee username filter' do
|
||||
before do
|
||||
merge_request4.reviewers = [user3]
|
||||
merge_request4.assignees = []
|
||||
end
|
||||
|
||||
before do
|
||||
merge_request1.update!(milestone: group_milestone)
|
||||
merge_request2.update!(milestone: group_milestone)
|
||||
let(:params) { { assignee_username: [user3.username] } }
|
||||
let(:expected_issuables) { [merge_request4] }
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns merge requests assigned to that group milestone' do
|
||||
params = { milestone_title: group_milestone.title }
|
||||
context 'with an additional reviewer' do
|
||||
it_behaves_like 'assignee username filter' do
|
||||
before do
|
||||
merge_request3.assignees = [user3]
|
||||
merge_request4.reviewers = [user3]
|
||||
end
|
||||
|
||||
let(:params) { { assignee_username: [user3.username] } }
|
||||
let(:expected_issuables) { [merge_request3, merge_request4] }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'filtering by group milestone' do
|
||||
let(:group_milestone) { create(:milestone, group: group) }
|
||||
|
||||
before do
|
||||
merge_request1.update!(milestone: group_milestone)
|
||||
merge_request2.update!(milestone: group_milestone)
|
||||
end
|
||||
|
||||
it 'returns merge requests assigned to that group milestone' do
|
||||
params = { milestone_title: group_milestone.title }
|
||||
|
||||
merge_requests = described_class.new(user, params).execute
|
||||
|
||||
expect(merge_requests).to contain_exactly(merge_request1, merge_request2)
|
||||
end
|
||||
|
||||
context 'using NOT' do
|
||||
let(:params) { { not: { milestone_title: group_milestone.title } } }
|
||||
|
||||
it 'returns MRs not assigned to that group milestone' do
|
||||
merge_requests = described_class.new(user, params).execute
|
||||
|
||||
expect(merge_requests).to contain_exactly(merge_request1, merge_request2)
|
||||
end
|
||||
|
||||
context 'using NOT' do
|
||||
let(:params) { { not: { milestone_title: group_milestone.title } } }
|
||||
|
||||
it 'returns MRs not assigned to that group milestone' do
|
||||
merge_requests = described_class.new(user, params).execute
|
||||
|
||||
expect(merge_requests).to contain_exactly(merge_request3, merge_request4, merge_request5)
|
||||
end
|
||||
expect(merge_requests).to contain_exactly(merge_request3, merge_request4, merge_request5)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,228 +8,15 @@ import GfmAutoComplete, { membersBeforeSave } from 'ee_else_ce/gfm_auto_complete
|
|||
import { TEST_HOST } from 'helpers/test_constants';
|
||||
import { getJSONFixture } from 'helpers/fixtures';
|
||||
|
||||
import waitForPromises from 'jest/helpers/wait_for_promises';
|
||||
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import AjaxCache from '~/lib/utils/ajax_cache';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
|
||||
const labelsFixture = getJSONFixture('autocomplete_sources/labels.json');
|
||||
|
||||
describe('GfmAutoComplete', () => {
|
||||
const fetchDataMock = { fetchData: jest.fn() };
|
||||
let gfmAutoCompleteCallbacks = GfmAutoComplete.prototype.getDefaultCallbacks.call(fetchDataMock);
|
||||
const gfmAutoCompleteCallbacks = GfmAutoComplete.prototype.getDefaultCallbacks.call({
|
||||
fetchData: () => {},
|
||||
});
|
||||
|
||||
let atwhoInstance;
|
||||
let sorterValue;
|
||||
let filterValue;
|
||||
|
||||
describe('.typesWithBackendFiltering', () => {
|
||||
it('should contain vulnerabilities', () => {
|
||||
expect(GfmAutoComplete.typesWithBackendFiltering).toContain('vulnerabilities');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DefaultOptions.filter', () => {
|
||||
let items;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(fetchDataMock, 'fetchData');
|
||||
jest.spyOn($.fn.atwho.default.callbacks, 'filter').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
describe('assets loading', () => {
|
||||
beforeEach(() => {
|
||||
atwhoInstance = { setting: {}, $inputor: 'inputor', at: '+' };
|
||||
items = ['loading'];
|
||||
|
||||
filterValue = gfmAutoCompleteCallbacks.filter.call(atwhoInstance, '', items);
|
||||
});
|
||||
|
||||
it('should call the fetchData function without query', () => {
|
||||
expect(fetchDataMock.fetchData).toHaveBeenCalledWith('inputor', '+');
|
||||
});
|
||||
|
||||
it('should not call the default atwho filter', () => {
|
||||
expect($.fn.atwho.default.callbacks.filter).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return the passed unfiltered items', () => {
|
||||
expect(filterValue).toEqual(items);
|
||||
});
|
||||
});
|
||||
|
||||
describe('backend filtering', () => {
|
||||
beforeEach(() => {
|
||||
atwhoInstance = { setting: {}, $inputor: 'inputor', at: '+' };
|
||||
items = [];
|
||||
});
|
||||
|
||||
describe('when previous query is different from current one', () => {
|
||||
beforeEach(() => {
|
||||
gfmAutoCompleteCallbacks = GfmAutoComplete.prototype.getDefaultCallbacks.call({
|
||||
previousQuery: 'oldquery',
|
||||
...fetchDataMock,
|
||||
});
|
||||
filterValue = gfmAutoCompleteCallbacks.filter.call(atwhoInstance, 'newquery', items);
|
||||
});
|
||||
|
||||
it('should call the fetchData function with query', () => {
|
||||
expect(fetchDataMock.fetchData).toHaveBeenCalledWith('inputor', '+', 'newquery');
|
||||
});
|
||||
|
||||
it('should not call the default atwho filter', () => {
|
||||
expect($.fn.atwho.default.callbacks.filter).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return the passed unfiltered items', () => {
|
||||
expect(filterValue).toEqual(items);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when previous query is not different from current one', () => {
|
||||
beforeEach(() => {
|
||||
gfmAutoCompleteCallbacks = GfmAutoComplete.prototype.getDefaultCallbacks.call({
|
||||
previousQuery: 'oldquery',
|
||||
...fetchDataMock,
|
||||
});
|
||||
filterValue = gfmAutoCompleteCallbacks.filter.call(
|
||||
atwhoInstance,
|
||||
'oldquery',
|
||||
items,
|
||||
'searchKey',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not call the fetchData function', () => {
|
||||
expect(fetchDataMock.fetchData).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call the default atwho filter', () => {
|
||||
expect($.fn.atwho.default.callbacks.filter).toHaveBeenCalledWith(
|
||||
'oldquery',
|
||||
items,
|
||||
'searchKey',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchData', () => {
|
||||
const { fetchData } = GfmAutoComplete.prototype;
|
||||
let mock;
|
||||
|
||||
beforeEach(() => {
|
||||
mock = new MockAdapter(axios);
|
||||
jest.spyOn(axios, 'get');
|
||||
jest.spyOn(AjaxCache, 'retrieve');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
describe('already loading data', () => {
|
||||
beforeEach(() => {
|
||||
const context = {
|
||||
isLoadingData: { '+': true },
|
||||
dataSources: {},
|
||||
cachedData: {},
|
||||
};
|
||||
fetchData.call(context, {}, '+', '');
|
||||
});
|
||||
|
||||
it('should not call either axios nor AjaxCache', () => {
|
||||
expect(axios.get).not.toHaveBeenCalled();
|
||||
expect(AjaxCache.retrieve).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('backend filtering', () => {
|
||||
describe('data is not in cache', () => {
|
||||
let context;
|
||||
|
||||
beforeEach(() => {
|
||||
context = {
|
||||
isLoadingData: { '+': false },
|
||||
dataSources: { vulnerabilities: 'vulnerabilities_autocomplete_url' },
|
||||
cachedData: {},
|
||||
};
|
||||
});
|
||||
|
||||
it('should call axios with query', () => {
|
||||
fetchData.call(context, {}, '+', 'query');
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith('vulnerabilities_autocomplete_url', {
|
||||
params: { search: 'query' },
|
||||
});
|
||||
});
|
||||
|
||||
it.each([200, 500])('should set the loading state', async responseStatus => {
|
||||
mock.onGet('vulnerabilities_autocomplete_url').replyOnce(responseStatus);
|
||||
|
||||
fetchData.call(context, {}, '+', 'query');
|
||||
|
||||
expect(context.isLoadingData['+']).toBe(true);
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(context.isLoadingData['+']).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('data is in cache', () => {
|
||||
beforeEach(() => {
|
||||
const context = {
|
||||
isLoadingData: { '+': false },
|
||||
dataSources: { vulnerabilities: 'vulnerabilities_autocomplete_url' },
|
||||
cachedData: { '+': [{}] },
|
||||
};
|
||||
fetchData.call(context, {}, '+', 'query');
|
||||
});
|
||||
|
||||
it('should anyway call axios with query ignoring cache', () => {
|
||||
expect(axios.get).toHaveBeenCalledWith('vulnerabilities_autocomplete_url', {
|
||||
params: { search: 'query' },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('frontend filtering', () => {
|
||||
describe('data is not in cache', () => {
|
||||
beforeEach(() => {
|
||||
const context = {
|
||||
isLoadingData: { '#': false },
|
||||
dataSources: { issues: 'issues_autocomplete_url' },
|
||||
cachedData: {},
|
||||
};
|
||||
fetchData.call(context, {}, '#', 'query');
|
||||
});
|
||||
|
||||
it('should call AjaxCache', () => {
|
||||
expect(AjaxCache.retrieve).toHaveBeenCalledWith('issues_autocomplete_url', true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('data is in cache', () => {
|
||||
beforeEach(() => {
|
||||
const context = {
|
||||
isLoadingData: { '#': false },
|
||||
dataSources: { issues: 'issues_autocomplete_url' },
|
||||
cachedData: { '#': [{}] },
|
||||
loadData: () => {},
|
||||
};
|
||||
fetchData.call(context, {}, '#', 'query');
|
||||
});
|
||||
|
||||
it('should not call AjaxCache', () => {
|
||||
expect(AjaxCache.retrieve).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('DefaultOptions.sorter', () => {
|
||||
describe('assets loading', () => {
|
||||
|
@ -333,7 +120,7 @@ describe('GfmAutoComplete', () => {
|
|||
const defaultMatcher = (context, flag, subtext) =>
|
||||
gfmAutoCompleteCallbacks.matcher.call(context, flag, subtext);
|
||||
|
||||
const flagsUseDefaultMatcher = ['@', '#', '!', '~', '%', '$', '+'];
|
||||
const flagsUseDefaultMatcher = ['@', '#', '!', '~', '%', '$'];
|
||||
const otherFlags = ['/', ':'];
|
||||
const flags = flagsUseDefaultMatcher.concat(otherFlags);
|
||||
|
||||
|
@ -367,6 +154,7 @@ describe('GfmAutoComplete', () => {
|
|||
'я',
|
||||
'.',
|
||||
"'",
|
||||
'+',
|
||||
'-',
|
||||
'_',
|
||||
];
|
||||
|
|
|
@ -10,6 +10,8 @@ import {
|
|||
TH_CREATED_AT_TEST_ID,
|
||||
TH_SEVERITY_TEST_ID,
|
||||
TH_PUBLISHED_TEST_ID,
|
||||
trackIncidentCreateNewOptions,
|
||||
trackIncidentListViewsOptions,
|
||||
} from '~/incidents/constants';
|
||||
import mockIncidents from '../mocks/incidents.json';
|
||||
|
||||
|
@ -291,4 +293,25 @@ describe('Incidents List', () => {
|
|||
expect(columnHeader().attributes('aria-sort')).toBe(nextSort);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Snowplow tracking', () => {
|
||||
beforeEach(() => {
|
||||
mountComponent({
|
||||
data: { incidents: { list: mockIncidents }, incidentsCount: {} },
|
||||
loading: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should track incident list views', () => {
|
||||
const { category, action } = trackIncidentListViewsOptions;
|
||||
expect(Tracking.event).toHaveBeenCalledWith(category, action);
|
||||
});
|
||||
|
||||
it('should track incident creation events', async () => {
|
||||
findCreateIncidentBtn().vm.$emit('click');
|
||||
await wrapper.vm.$nextTick();
|
||||
const { category, action } = trackIncidentCreateNewOptions;
|
||||
expect(Tracking.event).toHaveBeenCalledWith(category, action);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,6 +6,8 @@ import { descriptionProps } from '../../mock_data';
|
|||
import DescriptionComponent from '~/issue_show/components/description.vue';
|
||||
import HighlightBar from '~/issue_show/components/incidents/highlight_bar.vue';
|
||||
import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
|
||||
import Tracking from '~/tracking';
|
||||
import { trackIncidentDetailsViewsOptions } from '~/incidents/constants';
|
||||
|
||||
const mockAlert = {
|
||||
__typename: 'AlertManagementAlert',
|
||||
|
@ -97,4 +99,16 @@ describe('Incident Tabs component', () => {
|
|||
expect(findDescriptionComponent().props()).toMatchObject(descriptionProps);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Snowplow tracking', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(Tracking, 'event');
|
||||
mountComponent();
|
||||
});
|
||||
|
||||
it('should track incident details views', () => {
|
||||
const { category, action } = trackIncidentDetailsViewsOptions;
|
||||
expect(Tracking.event).toHaveBeenCalledWith(category, action);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -34,6 +34,12 @@ describe('Release detail actions', () => {
|
|||
isExistingRelease: true,
|
||||
};
|
||||
|
||||
const rootState = {
|
||||
featureFlags: {
|
||||
graphqlIndividualReleasePage: false,
|
||||
},
|
||||
};
|
||||
|
||||
state = {
|
||||
...createState({
|
||||
projectId: '18',
|
||||
|
@ -44,6 +50,7 @@ describe('Release detail actions', () => {
|
|||
updateReleaseApiDocsPath: 'path/to/api/docs',
|
||||
}),
|
||||
...getters,
|
||||
...rootState,
|
||||
...updates,
|
||||
};
|
||||
};
|
||||
|
@ -154,7 +161,7 @@ describe('Release detail actions', () => {
|
|||
});
|
||||
|
||||
it(`shows a flash message`, () => {
|
||||
return actions.fetchRelease({ commit: jest.fn(), state }).then(() => {
|
||||
return actions.fetchRelease({ commit: jest.fn(), state, rootState: state }).then(() => {
|
||||
expect(createFlash).toHaveBeenCalledTimes(1);
|
||||
expect(createFlash).toHaveBeenCalledWith(
|
||||
'Something went wrong while getting the release details',
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
removeCustomEventListener,
|
||||
registerHTMLToMarkdownRenderer,
|
||||
addImage,
|
||||
insertVideo,
|
||||
getMarkdown,
|
||||
getEditorOptions,
|
||||
} from '~/vue_shared/components/rich_content_editor/services/editor_service';
|
||||
|
@ -19,11 +20,21 @@ describe('Editor Service', () => {
|
|||
let mockInstance;
|
||||
let event;
|
||||
let handler;
|
||||
const parseHtml = str => {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.innerHTML = str;
|
||||
return wrapper.firstChild;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockInstance = {
|
||||
eventManager: { addEventType: jest.fn(), removeEventHandler: jest.fn(), listen: jest.fn() },
|
||||
editor: { exec: jest.fn() },
|
||||
editor: {
|
||||
exec: jest.fn(),
|
||||
isWysiwygMode: jest.fn(),
|
||||
getSquire: jest.fn(),
|
||||
insertText: jest.fn(),
|
||||
},
|
||||
invoke: jest.fn(),
|
||||
toMarkOptions: {
|
||||
renderer: {
|
||||
|
@ -89,6 +100,38 @@ describe('Editor Service', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('insertVideo', () => {
|
||||
const mockUrl = 'some/url';
|
||||
const htmlString = `<figure contenteditable="false" class="gl-relative gl-h-0 video_container"><iframe class="gl-absolute gl-top-0 gl-left-0 gl-w-full gl-h-full" width="560" height="315" frameborder="0" src="some/url"></iframe></figure>`;
|
||||
const mockInsertElement = jest.fn();
|
||||
|
||||
beforeEach(() =>
|
||||
mockInstance.editor.getSquire.mockReturnValue({ insertElement: mockInsertElement }),
|
||||
);
|
||||
|
||||
describe('WYSIWYG mode', () => {
|
||||
it('calls the insertElement method on the squire instance with an iFrame element', () => {
|
||||
mockInstance.editor.isWysiwygMode.mockReturnValue(true);
|
||||
|
||||
insertVideo(mockInstance, mockUrl);
|
||||
|
||||
expect(mockInstance.editor.getSquire().insertElement).toHaveBeenCalledWith(
|
||||
parseHtml(htmlString),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Markdown mode', () => {
|
||||
it('calls the insertText method on the editor instance with the iFrame element HTML', () => {
|
||||
mockInstance.editor.isWysiwygMode.mockReturnValue(false);
|
||||
|
||||
insertVideo(mockInstance, mockUrl);
|
||||
|
||||
expect(mockInstance.editor.insertText).toHaveBeenCalledWith(htmlString);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMarkdown', () => {
|
||||
it('calls the invoke method on the instance', () => {
|
||||
getMarkdown(mockInstance);
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { GlModal } from '@gitlab/ui';
|
||||
import InsertVideoModal from '~/vue_shared/components/rich_content_editor/modals/insert_video_modal.vue';
|
||||
|
||||
describe('Insert Video Modal', () => {
|
||||
let wrapper;
|
||||
|
||||
const findModal = () => wrapper.find(GlModal);
|
||||
const findUrlInput = () => wrapper.find({ ref: 'urlInput' });
|
||||
|
||||
const triggerInsertVideo = url => {
|
||||
const preventDefault = jest.fn();
|
||||
findUrlInput().vm.$emit('input', url);
|
||||
findModal().vm.$emit('primary', { preventDefault });
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(InsertVideoModal);
|
||||
});
|
||||
|
||||
afterEach(() => wrapper.destroy());
|
||||
|
||||
describe('when content is loaded', () => {
|
||||
it('renders a modal component', () => {
|
||||
expect(findModal().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders an input to add a URL', () => {
|
||||
expect(findUrlInput().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('insert video', () => {
|
||||
it.each`
|
||||
url | emitted
|
||||
${'https://www.youtube.com/embed/someId'} | ${[['https://www.youtube.com/embed/someId']]}
|
||||
${'https://www.youtube.com/watch?v=1234'} | ${[['https://www.youtube.com/embed/1234']]}
|
||||
${'::youtube.com/invalid/url'} | ${undefined}
|
||||
`('formats the url correctly', ({ url, emitted }) => {
|
||||
triggerInsertVideo(url);
|
||||
expect(wrapper.emitted('insertVideo')).toEqual(emitted);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,6 +1,7 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
|
||||
import AddImageModal from '~/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal.vue';
|
||||
import InsertVideoModal from '~/vue_shared/components/rich_content_editor/modals/insert_video_modal.vue';
|
||||
import {
|
||||
EDITOR_TYPES,
|
||||
EDITOR_HEIGHT,
|
||||
|
@ -12,6 +13,7 @@ import {
|
|||
addCustomEventListener,
|
||||
removeCustomEventListener,
|
||||
addImage,
|
||||
insertVideo,
|
||||
registerHTMLToMarkdownRenderer,
|
||||
getEditorOptions,
|
||||
} from '~/vue_shared/components/rich_content_editor/services/editor_service';
|
||||
|
@ -21,6 +23,7 @@ jest.mock('~/vue_shared/components/rich_content_editor/services/editor_service',
|
|||
addCustomEventListener: jest.fn(),
|
||||
removeCustomEventListener: jest.fn(),
|
||||
addImage: jest.fn(),
|
||||
insertVideo: jest.fn(),
|
||||
registerHTMLToMarkdownRenderer: jest.fn(),
|
||||
getEditorOptions: jest.fn(),
|
||||
}));
|
||||
|
@ -32,6 +35,7 @@ describe('Rich Content Editor', () => {
|
|||
const imageRoot = 'path/to/root/';
|
||||
const findEditor = () => wrapper.find({ ref: 'editor' });
|
||||
const findAddImageModal = () => wrapper.find(AddImageModal);
|
||||
const findInsertVideoModal = () => wrapper.find(InsertVideoModal);
|
||||
|
||||
const buildWrapper = () => {
|
||||
wrapper = shallowMount(RichContentEditor, {
|
||||
|
@ -122,6 +126,14 @@ describe('Rich Content Editor', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('adds the CUSTOM_EVENTS.openInsertVideoModal custom event listener', () => {
|
||||
expect(addCustomEventListener).toHaveBeenCalledWith(
|
||||
wrapper.vm.editorApi,
|
||||
CUSTOM_EVENTS.openInsertVideoModal,
|
||||
wrapper.vm.onOpenInsertVideoModal,
|
||||
);
|
||||
});
|
||||
|
||||
it('registers HTML to markdown renderer', () => {
|
||||
expect(registerHTMLToMarkdownRenderer).toHaveBeenCalledWith(wrapper.vm.editorApi);
|
||||
});
|
||||
|
@ -141,6 +153,16 @@ describe('Rich Content Editor', () => {
|
|||
wrapper.vm.onOpenAddImageModal,
|
||||
);
|
||||
});
|
||||
|
||||
it('removes the CUSTOM_EVENTS.openInsertVideoModal custom event listener', () => {
|
||||
wrapper.vm.$destroy();
|
||||
|
||||
expect(removeCustomEventListener).toHaveBeenCalledWith(
|
||||
wrapper.vm.editorApi,
|
||||
CUSTOM_EVENTS.openInsertVideoModal,
|
||||
wrapper.vm.onOpenInsertVideoModal,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('add image modal', () => {
|
||||
|
@ -161,4 +183,23 @@ describe('Rich Content Editor', () => {
|
|||
expect(addImage).toHaveBeenCalledWith(mockInstance, mockImage);
|
||||
});
|
||||
});
|
||||
|
||||
describe('insert video modal', () => {
|
||||
beforeEach(() => {
|
||||
buildWrapper();
|
||||
});
|
||||
|
||||
it('renders an insertVideoModal component', () => {
|
||||
expect(findInsertVideoModal().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('calls the onInsertVideo method when the insertVideo event is emitted', () => {
|
||||
const mockUrl = 'https://www.youtube.com/embed/someId';
|
||||
const mockInstance = { exec: jest.fn() };
|
||||
wrapper.vm.$refs.editor = mockInstance;
|
||||
|
||||
findInsertVideoModal().vm.$emit('insertVideo', mockUrl);
|
||||
expect(insertVideo).toHaveBeenCalledWith(mockInstance, mockUrl);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -64,6 +64,7 @@ RSpec.describe ReleasesHelper do
|
|||
describe '#data_for_edit_release_page' do
|
||||
it 'has the needed data to display the "edit release" page' do
|
||||
keys = %i(project_id
|
||||
project_path
|
||||
tag_name
|
||||
markdown_preview_path
|
||||
markdown_docs_path
|
||||
|
@ -80,6 +81,7 @@ RSpec.describe ReleasesHelper do
|
|||
describe '#data_for_new_release_page' do
|
||||
it 'has the needed data to display the "new release" page' do
|
||||
keys = %i(project_id
|
||||
project_path
|
||||
releases_page_path
|
||||
markdown_preview_path
|
||||
markdown_docs_path
|
||||
|
@ -92,5 +94,15 @@ RSpec.describe ReleasesHelper do
|
|||
expect(helper.data_for_new_release_page.keys).to match_array(keys)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#data_for_show_page' do
|
||||
it 'has the needed data to display the individual "release" page' do
|
||||
keys = %i(project_id
|
||||
project_path
|
||||
tag_name)
|
||||
|
||||
expect(helper.data_for_show_page.keys).to match_array(keys)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,56 +3,109 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Ci::Config::Entry::Variables do
|
||||
let(:entry) { described_class.new(config) }
|
||||
subject { described_class.new(config) }
|
||||
|
||||
describe 'validations' do
|
||||
context 'when entry config value is correct' do
|
||||
let(:config) do
|
||||
{ 'VARIABLE_1' => 'value 1', 'VARIABLE_2' => 'value 2' }
|
||||
end
|
||||
|
||||
describe '#value' do
|
||||
it 'returns hash with key value strings' do
|
||||
expect(entry.value).to eq config
|
||||
end
|
||||
|
||||
context 'with numeric keys and values in the config' do
|
||||
let(:config) { { 10 => 20 } }
|
||||
|
||||
it 'converts numeric key and numeric value into strings' do
|
||||
expect(entry.value).to eq('10' => '20')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#errors' do
|
||||
it 'does not append errors' do
|
||||
expect(entry.errors).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe '#valid?' do
|
||||
it 'is valid' do
|
||||
expect(entry).to be_valid
|
||||
end
|
||||
shared_examples 'valid config' do
|
||||
describe '#value' do
|
||||
it 'returns hash with key value strings' do
|
||||
expect(subject.value).to eq result
|
||||
end
|
||||
end
|
||||
|
||||
context 'when entry value is not correct' do
|
||||
let(:config) { [:VAR, 'test'] }
|
||||
|
||||
describe '#errors' do
|
||||
it 'saves errors' do
|
||||
expect(entry.errors)
|
||||
.to include /should be a hash of key value pairs/
|
||||
end
|
||||
describe '#errors' do
|
||||
it 'does not append errors' do
|
||||
expect(subject.errors).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe '#valid?' do
|
||||
it 'is not valid' do
|
||||
expect(entry).not_to be_valid
|
||||
end
|
||||
describe '#valid?' do
|
||||
it 'is valid' do
|
||||
expect(subject).to be_valid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'invalid config' do
|
||||
describe '#valid?' do
|
||||
it 'is not valid' do
|
||||
expect(subject).not_to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
describe '#errors' do
|
||||
it 'saves errors' do
|
||||
expect(subject.errors)
|
||||
.to include /should be a hash of key value pairs/
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when entry config value has key-value pairs' do
|
||||
let(:config) do
|
||||
{ 'VARIABLE_1' => 'value 1', 'VARIABLE_2' => 'value 2' }
|
||||
end
|
||||
|
||||
let(:result) do
|
||||
{ 'VARIABLE_1' => 'value 1', 'VARIABLE_2' => 'value 2' }
|
||||
end
|
||||
|
||||
it_behaves_like 'valid config'
|
||||
end
|
||||
|
||||
context 'with numeric keys and values in the config' do
|
||||
let(:config) { { 10 => 20 } }
|
||||
let(:result) do
|
||||
{ '10' => '20' }
|
||||
end
|
||||
|
||||
it_behaves_like 'valid config'
|
||||
end
|
||||
|
||||
context 'when entry config value has key-value pair and hash' do
|
||||
let(:config) do
|
||||
{ 'VARIABLE_1' => { value: 'value 1', description: 'variable 1' },
|
||||
'VARIABLE_2' => 'value 2' }
|
||||
end
|
||||
|
||||
let(:result) do
|
||||
{ 'VARIABLE_1' => 'value 1', 'VARIABLE_2' => 'value 2' }
|
||||
end
|
||||
|
||||
it_behaves_like 'valid config'
|
||||
end
|
||||
|
||||
context 'when entry value is an array' do
|
||||
let(:config) { [:VAR, 'test'] }
|
||||
|
||||
it_behaves_like 'invalid config'
|
||||
end
|
||||
|
||||
context 'when entry value has hash with other key-pairs' do
|
||||
let(:config) do
|
||||
{ 'VARIABLE_1' => { value: 'value 1', hello: 'variable 1' },
|
||||
'VARIABLE_2' => 'value 2' }
|
||||
end
|
||||
|
||||
it_behaves_like 'invalid config'
|
||||
end
|
||||
|
||||
context 'when entry config value has hash with nil description' do
|
||||
let(:config) do
|
||||
{ 'VARIABLE_1' => { value: 'value 1', description: nil } }
|
||||
end
|
||||
|
||||
it_behaves_like 'invalid config'
|
||||
end
|
||||
|
||||
context 'when entry config value has hash without description' do
|
||||
let(:config) do
|
||||
{ 'VARIABLE_1' => { value: 'value 1' } }
|
||||
end
|
||||
|
||||
let(:result) do
|
||||
{ 'VARIABLE_1' => 'value 1' }
|
||||
end
|
||||
|
||||
it_behaves_like 'valid config'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2465,13 +2465,13 @@ module Gitlab
|
|||
context 'returns errors if variables is not a map' do
|
||||
let(:config) { YAML.dump({ variables: "test", rspec: { script: "test" } }) }
|
||||
|
||||
it_behaves_like 'returns errors', 'variables config should be a hash of key value pairs'
|
||||
it_behaves_like 'returns errors', 'variables config should be a hash of key value pairs, value can be a hash'
|
||||
end
|
||||
|
||||
context 'returns errors if variables is not a map of key-value strings' do
|
||||
let(:config) { YAML.dump({ variables: { test: false }, rspec: { script: "test" } }) }
|
||||
|
||||
it_behaves_like 'returns errors', 'variables config should be a hash of key value pairs'
|
||||
it_behaves_like 'returns errors', 'variables config should be a hash of key value pairs, value can be a hash'
|
||||
end
|
||||
|
||||
context 'returns errors if job when is not on_success, on_failure or always' do
|
||||
|
|
|
@ -296,7 +296,7 @@ RSpec.describe Gitlab::ReferenceExtractor do
|
|||
end
|
||||
|
||||
it 'returns all supported prefixes' do
|
||||
expect(prefixes.keys.uniq).to match_array(%w(@ # ~ % ! $ & + *iteration:))
|
||||
expect(prefixes.keys.uniq).to match_array(%w(@ # ~ % ! $ & *iteration:))
|
||||
end
|
||||
|
||||
it 'does not allow one prefix for multiple referables if not allowed specificly' do
|
||||
|
|
|
@ -25,7 +25,7 @@ RSpec.describe Clusters::Applications::Fluentd do
|
|||
|
||||
it 'is initialized with fluentd arguments' do
|
||||
expect(subject.name).to eq('fluentd')
|
||||
expect(subject.chart).to eq('stable/fluentd')
|
||||
expect(subject.chart).to eq('fluentd/fluentd')
|
||||
expect(subject.version).to eq('2.4.0')
|
||||
expect(subject).to be_rbac
|
||||
end
|
||||
|
|
|
@ -135,7 +135,7 @@ RSpec.describe Clusters::Applications::Ingress do
|
|||
|
||||
it 'is initialized with ingress arguments' do
|
||||
expect(subject.name).to eq('ingress')
|
||||
expect(subject.chart).to eq('stable/nginx-ingress')
|
||||
expect(subject.chart).to eq('ingress/nginx-ingress')
|
||||
expect(subject.version).to eq('1.40.2')
|
||||
expect(subject).to be_rbac
|
||||
expect(subject.files).to eq(ingress.files)
|
||||
|
|
|
@ -152,7 +152,7 @@ RSpec.describe Clusters::Applications::Prometheus do
|
|||
|
||||
it 'is initialized with 3 arguments' do
|
||||
expect(subject.name).to eq('prometheus')
|
||||
expect(subject.chart).to eq('stable/prometheus')
|
||||
expect(subject.chart).to eq('prometheus/prometheus')
|
||||
expect(subject.version).to eq('10.4.1')
|
||||
expect(subject).to be_rbac
|
||||
expect(subject.files).to eq(prometheus.files)
|
||||
|
@ -240,7 +240,7 @@ RSpec.describe Clusters::Applications::Prometheus do
|
|||
|
||||
it 'is initialized with 3 arguments' do
|
||||
expect(patch_command.name).to eq('prometheus')
|
||||
expect(patch_command.chart).to eq('stable/prometheus')
|
||||
expect(patch_command.chart).to eq('prometheus/prometheus')
|
||||
expect(patch_command.version).to eq('10.4.1')
|
||||
expect(patch_command.files).to eq(prometheus.files)
|
||||
end
|
||||
|
|
|
@ -5509,12 +5509,13 @@ RSpec.describe Project do
|
|||
describe '#find_or_initialize_services' do
|
||||
it 'returns only enabled services' do
|
||||
allow(Service).to receive(:available_services_names).and_return(%w[prometheus pushover teamcity])
|
||||
allow(Service).to receive(:project_specific_services_names).and_return(%w[asana])
|
||||
allow(subject).to receive(:disabled_services).and_return(%w[prometheus])
|
||||
|
||||
services = subject.find_or_initialize_services
|
||||
|
||||
expect(services.count).to eq(2)
|
||||
expect(services.map(&:title)).to eq(['JetBrains TeamCity CI', 'Pushover'])
|
||||
expect(services.count).to eq(3)
|
||||
expect(services.map(&:title)).to eq(['Asana', 'JetBrains TeamCity CI', 'Pushover'])
|
||||
end
|
||||
end
|
||||
|
||||
|
|
77
spec/services/ci/list_config_variables_service_spec.rb
Normal file
77
spec/services/ci/list_config_variables_service_spec.rb
Normal file
|
@ -0,0 +1,77 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Ci::ListConfigVariablesService do
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
let(:service) { described_class.new(project) }
|
||||
let(:result) { YAML.dump(ci_config) }
|
||||
|
||||
subject { service.execute(sha) }
|
||||
|
||||
before do
|
||||
stub_gitlab_ci_yml_for_sha(sha, result)
|
||||
end
|
||||
|
||||
context 'when sending a valid sha' do
|
||||
let(:sha) { 'master' }
|
||||
let(:ci_config) do
|
||||
{
|
||||
variables: {
|
||||
KEY1: { value: 'val 1', description: 'description 1' },
|
||||
KEY2: { value: 'val 2', description: '' },
|
||||
KEY3: { value: 'val 3' },
|
||||
KEY4: 'val 4'
|
||||
},
|
||||
test: {
|
||||
stage: 'test',
|
||||
script: 'echo'
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'returns variable list' do
|
||||
expect(subject['KEY1']).to eq({ value: 'val 1', description: 'description 1' })
|
||||
expect(subject['KEY2']).to eq({ value: 'val 2', description: '' })
|
||||
expect(subject['KEY3']).to eq({ value: 'val 3', description: nil })
|
||||
expect(subject['KEY4']).to eq({ value: 'val 4', description: nil })
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sending an invalid sha' do
|
||||
let(:sha) { 'invalid-sha' }
|
||||
let(:ci_config) { nil }
|
||||
|
||||
it 'returns empty json' do
|
||||
expect(subject).to eq({})
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sending an invalid config' do
|
||||
let(:sha) { 'master' }
|
||||
let(:ci_config) do
|
||||
{
|
||||
variables: {
|
||||
KEY1: { value: 'val 1', description: 'description 1' }
|
||||
},
|
||||
test: {
|
||||
stage: 'invalid',
|
||||
script: 'echo'
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'returns empty result' do
|
||||
expect(subject).to eq({})
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def stub_gitlab_ci_yml_for_sha(sha, result)
|
||||
allow_any_instance_of(Repository)
|
||||
.to receive(:gitlab_ci_yml_for)
|
||||
.with(sha, '.gitlab-ci.yml')
|
||||
.and_return(result)
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue