Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-03-28 09:07:06 +00:00
parent c222aa0890
commit b9e3013993
56 changed files with 432 additions and 220 deletions

View File

@ -1 +1 @@
36aaf4e475fdcc4ae89f14772662fa89125d7716 e302aa4a8caf07caad38c236d610fea49a41aa2f

Binary file not shown.

Before

Width:  |  Height:  |  Size: 596 B

View File

@ -1,13 +1,5 @@
<script> <script>
import { import { GlDropdown, GlDropdownItem, GlModalDirective, GlTooltip } from '@gitlab/ui';
GlButton,
GlDropdown,
GlDropdownItem,
GlModalDirective,
GlTooltipDirective,
GlDropdownDivider,
GlDropdownSectionHeader,
} from '@gitlab/ui';
import { INSTALL_AGENT_MODAL_ID, CLUSTERS_ACTIONS } from '../constants'; import { INSTALL_AGENT_MODAL_ID, CLUSTERS_ACTIONS } from '../constants';
@ -15,84 +7,91 @@ export default {
i18n: CLUSTERS_ACTIONS, i18n: CLUSTERS_ACTIONS,
INSTALL_AGENT_MODAL_ID, INSTALL_AGENT_MODAL_ID,
components: { components: {
GlButton,
GlDropdown, GlDropdown,
GlDropdownItem, GlDropdownItem,
GlDropdownDivider, GlTooltip,
GlDropdownSectionHeader,
}, },
directives: { directives: {
GlModalDirective, GlModalDirective,
GlTooltip: GlTooltipDirective,
}, },
inject: [ inject: [
'newClusterPath', 'newClusterPath',
'addClusterPath', 'addClusterPath',
'newClusterDocsPath',
'canAddCluster', 'canAddCluster',
'displayClusterAgents', 'displayClusterAgents',
'certificateBasedClustersEnabled', 'certificateBasedClustersEnabled',
], ],
computed: { computed: {
tooltip() {
const { connectWithAgent, connectExistingCluster, dropdownDisabledHint } = this.$options.i18n;
if (!this.canAddCluster) {
return dropdownDisabledHint;
} else if (this.displayClusterAgents) {
return connectWithAgent;
}
return connectExistingCluster;
},
shouldTriggerModal() { shouldTriggerModal() {
return this.canAddCluster && this.displayClusterAgents; return this.canAddCluster && this.displayClusterAgents;
}, },
defaultActionText() {
const { connectCluster, connectWithAgent, connectClusterDeprecated } = this.$options.i18n;
if (!this.displayClusterAgents) {
return connectClusterDeprecated;
} else if (!this.certificateBasedClustersEnabled) {
return connectCluster;
}
return connectWithAgent;
},
defaultActionUrl() {
if (this.displayClusterAgents) {
return null;
}
return this.addClusterPath;
},
}, },
}; };
</script> </script>
<template> <template>
<div class="nav-controls gl-ml-auto"> <div class="nav-controls gl-ml-auto">
<gl-tooltip
v-if="!canAddCluster"
:target="() => $refs.dropdown.$el"
:title="$options.i18n.dropdownDisabledHint"
/>
<gl-dropdown <gl-dropdown
v-if="certificateBasedClustersEnabled"
ref="dropdown" ref="dropdown"
v-gl-modal-directive="shouldTriggerModal && $options.INSTALL_AGENT_MODAL_ID" v-gl-modal-directive="shouldTriggerModal && $options.INSTALL_AGENT_MODAL_ID"
v-gl-tooltip="tooltip"
category="primary" category="primary"
variant="confirm" variant="confirm"
:text="$options.i18n.actionsButton" :text="defaultActionText"
:disabled="!canAddCluster" :disabled="!canAddCluster"
:split="displayClusterAgents" :split-href="defaultActionUrl"
split
right right
> >
<template v-if="displayClusterAgents"> <gl-dropdown-item
<gl-dropdown-section-header>{{ $options.i18n.agent }}</gl-dropdown-section-header> v-if="displayClusterAgents"
<gl-dropdown-item :href="newClusterDocsPath"
v-gl-modal-directive="$options.INSTALL_AGENT_MODAL_ID" data-testid="create-cluster-link"
data-testid="connect-new-agent-link" @click.stop
> >
{{ $options.i18n.connectWithAgent }} {{ $options.i18n.createCluster }}
</gl-dropdown-item>
<template v-if="displayClusterAgents && certificateBasedClustersEnabled">
<gl-dropdown-item :href="newClusterPath" data-testid="new-cluster-link" @click.stop>
{{ $options.i18n.createClusterCertificate }}
</gl-dropdown-item>
<gl-dropdown-item :href="addClusterPath" data-testid="connect-cluster-link" @click.stop>
{{ $options.i18n.connectClusterCertificate }}
</gl-dropdown-item> </gl-dropdown-item>
<gl-dropdown-divider />
<gl-dropdown-section-header>{{ $options.i18n.certificate }}</gl-dropdown-section-header>
</template> </template>
<gl-dropdown-item :href="newClusterPath" data-testid="new-cluster-link" @click.stop> <gl-dropdown-item
{{ $options.i18n.createNewCluster }} v-if="certificateBasedClustersEnabled && !displayClusterAgents"
</gl-dropdown-item> :href="newClusterPath"
<gl-dropdown-item :href="addClusterPath" data-testid="connect-cluster-link" @click.stop> data-testid="new-cluster-link"
{{ $options.i18n.connectExistingCluster }} @click.stop
>
{{ $options.i18n.createClusterDeprecated }}
</gl-dropdown-item> </gl-dropdown-item>
</gl-dropdown> </gl-dropdown>
<gl-button
v-else
v-gl-modal-directive="$options.INSTALL_AGENT_MODAL_ID"
v-gl-tooltip="tooltip"
:disabled="!canAddCluster"
category="primary"
variant="confirm"
>
{{ $options.i18n.connectWithAgent }}
</gl-button>
</div> </div>
</template> </template>

View File

@ -252,12 +252,13 @@ export const CERTIFICATE_TAB = {
export const CLUSTERS_TABS = [ALL_TAB, AGENT_TAB, CERTIFICATE_TAB]; export const CLUSTERS_TABS = [ALL_TAB, AGENT_TAB, CERTIFICATE_TAB];
export const CLUSTERS_ACTIONS = { export const CLUSTERS_ACTIONS = {
actionsButton: s__('ClusterAgents|Actions'), connectCluster: s__('ClusterAgents|Connect a cluster'),
createNewCluster: s__('ClusterAgents|Create a new cluster'), connectWithAgent: s__('ClusterAgents|Connect a cluster (agent)'),
connectWithAgent: s__('ClusterAgents|Connect with an agent'), connectClusterDeprecated: s__('ClusterAgents|Connect a cluster (deprecated)'),
connectExistingCluster: s__('ClusterAgents|Connect with a certificate'), createClusterDeprecated: s__('ClusterAgents|Create a cluster (deprecated)'),
agent: s__('ClusterAgents|Agent'), createCluster: s__('ClusterAgents|Create a cluster'),
certificate: s__('ClusterAgents|Certificate'), createClusterCertificate: s__('ClusterAgents|Create a cluster (certificate - deprecated)'),
connectClusterCertificate: s__('ClusterAgents|Connect a cluster (certificate - deprecated)'),
dropdownDisabledHint: s__( dropdownDisabledHint: s__(
'ClusterAgents|Requires a Maintainer or greater role to perform these actions', 'ClusterAgents|Requires a Maintainer or greater role to perform these actions',
), ),

View File

@ -25,6 +25,7 @@ export default () => {
kasAddress, kasAddress,
newClusterPath, newClusterPath,
addClusterPath, addClusterPath,
newClusterDocsPath,
emptyStateHelpText, emptyStateHelpText,
clustersEmptyStateImage, clustersEmptyStateImage,
canAddCluster, canAddCluster,
@ -43,6 +44,7 @@ export default () => {
kasAddress, kasAddress,
newClusterPath, newClusterPath,
addClusterPath, addClusterPath,
newClusterDocsPath,
emptyStateHelpText, emptyStateHelpText,
clustersEmptyStateImage, clustersEmptyStateImage,
canAddCluster: parseBoolean(canAddCluster), canAddCluster: parseBoolean(canAddCluster),

View File

@ -0,0 +1,56 @@
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
import CodeBlockHighlight from './code_block_highlight';
export default CodeBlockHighlight.extend({
name: 'diagram',
isolating: true,
addAttributes() {
return {
language: {
default: null,
parseHTML: (element) => {
return element.dataset.diagram;
},
},
};
},
parseHTML() {
return [
{
priority: PARSE_HTML_PRIORITY_HIGHEST,
tag: '[data-diagram]',
getContent(element, schema) {
const source = atob(element.dataset.diagramSrc.replace('data:text/plain;base64,', ''));
const node = schema.node('paragraph', {}, [schema.text(source)]);
return node.content;
},
},
];
},
renderHTML({ HTMLAttributes: { language, ...HTMLAttributes } }) {
return [
'div',
[
'pre',
{
language,
class: `content-editor-code-block code highlight`,
...HTMLAttributes,
},
['code', {}, 0],
],
];
},
addCommands() {
return {};
},
addInputRules() {
return [];
},
});

View File

@ -15,6 +15,7 @@ import DescriptionItem from '../extensions/description_item';
import DescriptionList from '../extensions/description_list'; import DescriptionList from '../extensions/description_list';
import Details from '../extensions/details'; import Details from '../extensions/details';
import DetailsContent from '../extensions/details_content'; import DetailsContent from '../extensions/details_content';
import Diagram from '../extensions/diagram';
import Division from '../extensions/division'; import Division from '../extensions/division';
import Document from '../extensions/document'; import Document from '../extensions/document';
import Dropcursor from '../extensions/dropcursor'; import Dropcursor from '../extensions/dropcursor';
@ -100,6 +101,7 @@ export const createContentEditor = ({
Details, Details,
DetailsContent, DetailsContent,
Document, Document,
Diagram,
Division, Division,
Dropcursor, Dropcursor,
Emoji, Emoji,

View File

@ -13,6 +13,7 @@ import DescriptionList from '../extensions/description_list';
import Details from '../extensions/details'; import Details from '../extensions/details';
import DetailsContent from '../extensions/details_content'; import DetailsContent from '../extensions/details_content';
import Division from '../extensions/division'; import Division from '../extensions/division';
import Diagram from '../extensions/diagram';
import Emoji from '../extensions/emoji'; import Emoji from '../extensions/emoji';
import Figure from '../extensions/figure'; import Figure from '../extensions/figure';
import FigureCaption from '../extensions/figure_caption'; import FigureCaption from '../extensions/figure_caption';
@ -48,6 +49,7 @@ import Video from '../extensions/video';
import WordBreak from '../extensions/word_break'; import WordBreak from '../extensions/word_break';
import { import {
isPlainURL, isPlainURL,
renderCodeBlock,
renderHardBreak, renderHardBreak,
renderTable, renderTable,
renderTableCell, renderTableCell,
@ -130,13 +132,8 @@ const defaultSerializerConfig = {
} }
}, },
[BulletList.name]: defaultMarkdownSerializer.nodes.bullet_list, [BulletList.name]: defaultMarkdownSerializer.nodes.bullet_list,
[CodeBlockHighlight.name]: (state, node) => { [CodeBlockHighlight.name]: renderCodeBlock,
state.write(`\`\`\`${node.attrs.language || ''}\n`); [Diagram.name]: renderCodeBlock,
state.text(node.textContent, false);
state.ensureNewLine();
state.write('```');
state.closeBlock(node);
},
[Division.name]: (state, node) => { [Division.name]: (state, node) => {
if (node.attrs.className?.includes('js-markdown-code')) { if (node.attrs.className?.includes('js-markdown-code')) {
state.renderInline(node); state.renderInline(node);

View File

@ -341,3 +341,11 @@ export function renderImage(state, node) {
export function renderPlayable(state, node) { export function renderPlayable(state, node) {
renderImage(state, node); renderImage(state, node);
} }
export function renderCodeBlock(state, node) {
state.write(`\`\`\`${node.attrs.language || ''}\n`);
state.text(node.textContent, false);
state.ensureNewLine();
state.write('```');
state.closeBlock(node);
}

View File

@ -127,7 +127,7 @@ function deferredInitialisation() {
// In case the user started searching before we bootstrapped, let's pass the search along. // In case the user started searching before we bootstrapped, let's pass the search along.
const initialSearchValue = searchInputBox.value; const initialSearchValue = searchInputBox.value;
await initHeaderSearchApp(initialSearchValue); await initHeaderSearchApp(initialSearchValue);
searchInputBox.focus(); document.querySelector('#search').focus();
}) })
.catch(() => {}); .catch(() => {});
} else { } else {

View File

@ -192,10 +192,12 @@ export default {
@keydown.enter.prevent="onSearchBoxEnter" @keydown.enter.prevent="onSearchBoxEnter"
/> />
<gl-dropdown-item @click="selectNoMilestone()"> <gl-dropdown-item
<span :class="{ 'gl-pl-6': true, 'selected-item': selectedMilestones.length === 0 }"> :is-checked="selectedMilestones.length === 0"
{{ $options.translations.noMilestone }} is-check-item
</span> @click="selectNoMilestone()"
>
{{ $options.translations.noMilestone }}
</gl-dropdown-item> </gl-dropdown-item>
<gl-dropdown-divider /> <gl-dropdown-divider />
@ -241,9 +243,10 @@ export default {
v-for="(item, idx) in extraLinks" v-for="(item, idx) in extraLinks"
:key="idx" :key="idx"
:href="item.url" :href="item.url"
:is-check-item="true"
data-testid="milestone-combobox-extra-links" data-testid="milestone-combobox-extra-links"
> >
<span class="gl-pl-6">{{ item.text }}</span> {{ item.text }}
</gl-dropdown-item> </gl-dropdown-item>
</gl-dropdown> </gl-dropdown>
</template> </template>

View File

@ -77,10 +77,14 @@ export default {
</div> </div>
</template> </template>
<template v-else> <template v-else>
<gl-dropdown-item v-for="{ title } in items" :key="title" @click="$emit('selected', title)"> <gl-dropdown-item
<span class="gl-pl-6" :class="{ 'selected-item': isSelectedMilestone(title) }"> v-for="{ title } in items"
{{ title }} :key="title"
</span> :is-checked="isSelectedMilestone(title)"
is-check-item
@click="$emit('selected', title)"
>
{{ title }}
</gl-dropdown-item> </gl-dropdown-item>
<gl-dropdown-divider /> <gl-dropdown-divider />
</template> </template>

View File

@ -1,13 +1,4 @@
.milestone-combobox { .milestone-combobox {
.selected-item {
/* stylelint-disable-next-line function-url-quotes */
background: url(asset_path('checkmark.png')) no-repeat 0 2px;
}
.dropdown-item-space {
padding: 8px 12px;
}
.dropdown-menu.show { .dropdown-menu.show {
overflow: hidden; overflow: hidden;
} }

View File

@ -14,7 +14,7 @@ class Clusters::ClustersController < Clusters::BaseController
before_action :authorize_create_cluster!, only: [:new, :connect, :authorize_aws_role] before_action :authorize_create_cluster!, only: [:new, :connect, :authorize_aws_role]
before_action :authorize_update_cluster!, only: [:update] before_action :authorize_update_cluster!, only: [:update]
before_action :update_applications_status, only: [:cluster_status] before_action :update_applications_status, only: [:cluster_status]
before_action :ensure_feature_enabled!, except: :index before_action :ensure_feature_enabled!, except: [:index, :new_cluster_docs]
helper_method :token_in_session helper_method :token_in_session

View File

@ -19,6 +19,7 @@ module ClustersHelper
empty_state_help_text: clusterable.empty_state_help_text, empty_state_help_text: clusterable.empty_state_help_text,
new_cluster_path: clusterable.new_path, new_cluster_path: clusterable.new_path,
add_cluster_path: clusterable.connect_path, add_cluster_path: clusterable.connect_path,
new_cluster_docs_path: clusterable.new_cluster_docs_path,
can_add_cluster: clusterable.can_add_cluster?.to_s, can_add_cluster: clusterable.can_add_cluster?.to_s,
can_admin_cluster: clusterable.can_admin_cluster?.to_s, can_admin_cluster: clusterable.can_admin_cluster?.to_s,
display_cluster_agents: display_cluster_agents?(clusterable).to_s, display_cluster_agents: display_cluster_agents?(clusterable).to_s,

View File

@ -36,6 +36,10 @@ class ClusterablePresenter < Gitlab::View::Presenter::Delegated
polymorphic_path([clusterable, :clusters], action: :connect) polymorphic_path([clusterable, :clusters], action: :connect)
end end
def new_cluster_docs_path
polymorphic_path([clusterable, :clusters], action: :new_cluster_docs)
end
def authorize_aws_role_path def authorize_aws_role_path
polymorphic_path([clusterable, :clusters], action: :authorize_aws_role) polymorphic_path([clusterable, :clusters], action: :authorize_aws_role)
end end

View File

@ -43,6 +43,11 @@ class InstanceClusterablePresenter < ClusterablePresenter
connect_admin_clusters_path connect_admin_clusters_path
end end
override :new_cluster_docs_path
def new_cluster_docs_path
nil
end
override :create_user_clusters_path override :create_user_clusters_path
def create_user_clusters_path def create_user_clusters_path
create_user_admin_clusters_path create_user_admin_clusters_path

View File

@ -1,8 +1,14 @@
- is_connect_page = local_assigns.fetch(:is_connect_page, false)
- docs_mode = local_assigns.fetch(:docs_mode, false)
- title = is_connect_page ? s_('ClusterIntegration|Connect a Kubernetes cluster') : s_('ClusterIntegration|Create a Kubernetes cluster')
%h3 %h3
= s_('ClusterIntegration|Connect a Kubernetes cluster') = title
%p %p
= clusterable.sidebar_text = clusterable.sidebar_text
%p
= clusterable.learn_more_link
= render 'clusters/clusters/multiple_clusters_message' - if !docs_mode
%p
= clusterable.learn_more_link
= render 'clusters/clusters/multiple_clusters_message'

View File

@ -1,12 +1,15 @@
- provider = local_assigns.fetch(:provider) - provider = local_assigns.fetch(:provider)
- is_current_provider = provider == params[:provider] - is_current_provider = provider == params[:provider]
- logo_path = local_assigns.fetch(:logo_path) - logo_path = local_assigns.fetch(:logo_path)
- help_path = local_assigns.fetch(:help_path)
- label = local_assigns.fetch(:label) - label = local_assigns.fetch(:label)
- last = local_assigns.fetch(:last, false) - last = local_assigns.fetch(:last, false)
- classes = ["btn btn-confirm gl-button btn-confirm-secondary gl-flex-direction-column gl-w-half js-create-#{provider}-cluster-button"] - docs_mode = local_assigns.fetch(:docs_mode, false)
- conditional_classes = [('gl-mr-5' unless last), ('active' if is_current_provider)] - classes = ["btn btn-confirm gl-button btn-confirm-secondary gl-flex-direction-column gl-w-half"]
- conditional_classes = [("gl-mr-5" unless last), ("active" if is_current_provider && !docs_mode), ("js-create-#{provider}-cluster-button" if !docs_mode)]
- link = docs_mode ? help_path : clusterable.new_path(provider: provider)
= link_to clusterable.new_path(provider: provider), class: classes + conditional_classes do = link_to link, class: classes + conditional_classes do
.svg-content.gl-p-3= image_tag logo_path, alt: label, class: 'gl-w-64 gl-h-64' .svg-content.gl-p-3= image_tag logo_path, alt: label, class: "gl-w-64 gl-h-64"
%span %span
= label = label

View File

@ -1,11 +1,15 @@
- gke_label = s_('ClusterIntegration|Google GKE') - gke_label = s_('ClusterIntegration|Google GKE')
- eks_label = s_('ClusterIntegration|Amazon EKS') - eks_label = s_('ClusterIntegration|Amazon EKS')
- create_cluster_label = s_('ClusterIntegration|Where do you want to create a cluster?') - create_cluster_label = s_('ClusterIntegration|Where do you want to create a cluster?')
- eks_help_path = help_page_path('user/infrastructure/clusters/connect/new_eks_cluster')
- gke_help_path = help_page_path('user/infrastructure/clusters/connect/new_gke_cluster')
- docs_mode = local_assigns.fetch(:docs_mode, false)
.gl-p-5 .gl-p-5
%h4.gl-mb-5 %h4.gl-mb-5
= create_cluster_label = create_cluster_label
.gl-display-flex .gl-display-flex
= render partial: 'clusters/clusters/cloud_providers/cloud_provider_button', = render partial: 'clusters/clusters/cloud_providers/cloud_provider_button',
locals: { provider: 'aws', label: eks_label, logo_path: 'illustrations/logos/amazon_eks.svg' } locals: { provider: 'aws', label: eks_label, logo_path: 'illustrations/logos/amazon_eks.svg', help_path: eks_help_path, docs_mode: docs_mode }
= render partial: 'clusters/clusters/cloud_providers/cloud_provider_button', = render partial: 'clusters/clusters/cloud_providers/cloud_provider_button',
locals: { provider: 'gcp', label: gke_label, logo_path: 'illustrations/logos/google_gke.svg', last: true } locals: { provider: 'gcp', label: gke_label, logo_path: 'illustrations/logos/google_gke.svg', help_path: gke_help_path, docs_mode: docs_mode, last: true }

View File

@ -3,9 +3,9 @@
- breadcrumb_title _('Connect a cluster') - breadcrumb_title _('Connect a cluster')
- page_title _('Connect a Kubernetes Cluster') - page_title _('Connect a Kubernetes Cluster')
.row.gl-mt-3 .gl-md-display-flex.gl-mt-3
.col-md-3 .gl-w-quarter.gl-xs-w-full.gl-flex-shrink-0.gl-md-mr-5
= render 'sidebar' = render 'sidebar', is_connect_page: true
.col-md-9 .gl-w-full
#js-cluster-new{ data: js_cluster_new } #js-cluster-new{ data: js_cluster_new }
= render 'clusters/clusters/user/form' = render 'clusters/clusters/user/form'

View File

@ -6,10 +6,10 @@
= render_gcp_signup_offer = render_gcp_signup_offer
.row.gl-mt-3 .gl-md-display-flex.gl-mt-3
.col-md-3 .gl-w-quarter.gl-xs-w-full.gl-flex-shrink-0.gl-md-mr-5
= render 'sidebar' = render 'sidebar', is_connect_page: false
.col-md-9 .gl-w-full
= render 'clusters/clusters/cloud_providers/cloud_provider_selector' = render 'clusters/clusters/cloud_providers/cloud_provider_selector'
- if ['aws', 'gcp'].include?(provider) - if ['aws', 'gcp'].include?(provider)

View File

@ -0,0 +1,13 @@
- @content_class = 'limit-container-width' unless fluid_layout
- add_to_breadcrumbs _('Kubernetes Clusters'), clusterable.index_path
- breadcrumb_title _('Create a cluster')
- page_title _('Create a Kubernetes cluster')
- docs_mode = true
= render_gcp_signup_offer
.gl-md-display-flex.gl-mt-3
.gl-w-quarter.gl-xs-w-full.gl-flex-shrink-0.gl-md-mr-5
= render 'sidebar', docs_mode: docs_mode, is_connect_page: false
.gl-w-full
= render 'clusters/clusters/cloud_providers/cloud_provider_selector', docs_mode: docs_mode

View File

@ -40,7 +40,7 @@
- search_menu_item = top_nav_search_menu_item_attrs - search_menu_item = top_nav_search_menu_item_attrs
%li.nav-item.header-search-new.d-none.d-lg-block.m-auto %li.nav-item.header-search-new.d-none.d-lg-block.m-auto
- unless current_controller?(:search) - unless current_controller?(:search)
- if Feature.enabled?(:new_header_search) - if Feature.enabled?(:new_header_search, default_enabled: :yaml)
= render 'layouts/header_search' = render 'layouts/header_search'
- else - else
= render 'layouts/search' = render 'layouts/search'

View File

@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/339348
milestone: '14.3' milestone: '14.3'
type: development type: development
group: group::global search group: group::global search
default_enabled: false default_enabled: true

View File

@ -237,6 +237,7 @@ Rails.application.routes.draw do
resources :clusters, only: [:index, :new, :show, :update, :destroy] do resources :clusters, only: [:index, :new, :show, :update, :destroy] do
collection do collection do
get :connect get :connect
get :new_cluster_docs
post :create_user post :create_user
post :create_gcp post :create_gcp
post :create_aws post :create_aws

View File

@ -39,6 +39,9 @@ module Banzai
allowlist[:attributes][:all].delete('name') allowlist[:attributes][:all].delete('name')
allowlist[:attributes]['a'].push('name') allowlist[:attributes]['a'].push('name')
allowlist[:attributes]['img'].push('data-diagram')
allowlist[:attributes]['img'].push('data-diagram-src')
# Allow any protocol in `a` elements # Allow any protocol in `a` elements
# and then remove links with unsafe protocols # and then remove links with unsafe protocols
allowlist[:protocols].delete('a') allowlist[:protocols].delete('a')

View File

@ -27,6 +27,13 @@ module Banzai
# make sure the original non-proxied src carries over to the link # make sure the original non-proxied src carries over to the link
link['data-canonical-src'] = img['data-canonical-src'] if img['data-canonical-src'] link['data-canonical-src'] = img['data-canonical-src'] if img['data-canonical-src']
if img['data-diagram'] && img['data-diagram-src']
link['data-diagram'] = img['data-diagram']
link['data-diagram-src'] = img['data-diagram-src']
img.remove_attribute('data-diagram')
img.remove_attribute('data-diagram-src')
end
link.children = if link_replaces_image link.children = if link_replaces_image
img['alt'] || img['data-src'] || img['src'] img['alt'] || img['data-src'] || img['src']
else else

View File

@ -22,7 +22,14 @@ module Banzai
doc.xpath(xpath).each do |node| doc.xpath(xpath).each do |node|
diagram_type = node.parent['lang'] diagram_type = node.parent['lang']
img_tag = Nokogiri::HTML::DocumentFragment.parse(%(<img src="#{create_image_src(diagram_type, diagram_format, node.content)}"/>)) img_tag = Nokogiri::HTML::DocumentFragment.parse(%(<img src="#{create_image_src(diagram_type, diagram_format, node.content)}"/>))
node.parent.replace(img_tag) img_tag = img_tag.children.first
unless img_tag.nil?
img_tag.set_attribute('data-diagram', node.parent['lang'])
img_tag.set_attribute('data-diagram-src', "data:text/plain;base64,#{Base64.strict_encode64(node.content)}")
node.parent.replace(img_tag)
end
end end
doc doc

View File

@ -15,8 +15,14 @@ module Banzai
doc.xpath(lang_tag).each do |node| doc.xpath(lang_tag).each do |node|
img_tag = Nokogiri::HTML::DocumentFragment.parse( img_tag = Nokogiri::HTML::DocumentFragment.parse(
Asciidoctor::PlantUml::Processor.plantuml_content(node.content, {})) Asciidoctor::PlantUml::Processor.plantuml_content(node.content, {})).css('img').first
node.parent.replace(img_tag)
unless img_tag.nil?
img_tag.set_attribute('data-diagram', 'plantuml')
img_tag.set_attribute('data-diagram-src', "data:text/plain;base64,#{Base64.strict_encode64(node.content)}")
node.parent.replace(img_tag)
end
end end
doc doc

View File

@ -12,6 +12,7 @@ module Banzai
def self.filters def self.filters
@filters ||= FilterArray[ @filters ||= FilterArray[
Filter::PlantumlFilter, Filter::PlantumlFilter,
Filter::KrokiFilter,
# Must always be before the SanitizationFilter to prevent XSS attacks # Must always be before the SanitizationFilter to prevent XSS attacks
Filter::SpacedLinkFilter, Filter::SpacedLinkFilter,
Filter::SanitizationFilter, Filter::SanitizationFilter,
@ -19,7 +20,6 @@ module Banzai
Filter::SyntaxHighlightFilter, Filter::SyntaxHighlightFilter,
Filter::MathFilter, Filter::MathFilter,
Filter::ColorFilter, Filter::ColorFilter,
Filter::KrokiFilter,
Filter::MermaidFilter, Filter::MermaidFilter,
Filter::VideoLinkFilter, Filter::VideoLinkFilter,
Filter::AudioLinkFilter, Filter::AudioLinkFilter,

View File

@ -7822,9 +7822,6 @@ msgstr ""
msgid "ClusterAgents|Access tokens" msgid "ClusterAgents|Access tokens"
msgstr "" msgstr ""
msgid "ClusterAgents|Actions"
msgstr ""
msgid "ClusterAgents|Add an agent configuration file to %{linkStart}this repository%{linkEnd} and select it, or create a new one to register with GitLab:" msgid "ClusterAgents|Add an agent configuration file to %{linkStart}this repository%{linkEnd} and select it, or create a new one to register with GitLab:"
msgstr "" msgstr ""
@ -7879,18 +7876,24 @@ msgstr ""
msgid "ClusterAgents|Configuration" msgid "ClusterAgents|Configuration"
msgstr "" msgstr ""
msgid "ClusterAgents|Connect a cluster"
msgstr ""
msgid "ClusterAgents|Connect a cluster (agent)"
msgstr ""
msgid "ClusterAgents|Connect a cluster (certificate - deprecated)"
msgstr ""
msgid "ClusterAgents|Connect a cluster (deprecated)"
msgstr ""
msgid "ClusterAgents|Connect a cluster through an agent" msgid "ClusterAgents|Connect a cluster through an agent"
msgstr "" msgstr ""
msgid "ClusterAgents|Connect existing cluster" msgid "ClusterAgents|Connect existing cluster"
msgstr "" msgstr ""
msgid "ClusterAgents|Connect with a certificate"
msgstr ""
msgid "ClusterAgents|Connect with an agent"
msgstr ""
msgid "ClusterAgents|Connect with the GitLab Agent" msgid "ClusterAgents|Connect with the GitLab Agent"
msgstr "" msgstr ""
@ -7906,7 +7909,13 @@ msgstr ""
msgid "ClusterAgents|Copy token" msgid "ClusterAgents|Copy token"
msgstr "" msgstr ""
msgid "ClusterAgents|Create a new cluster" msgid "ClusterAgents|Create a cluster"
msgstr ""
msgid "ClusterAgents|Create a cluster (certificate - deprecated)"
msgstr ""
msgid "ClusterAgents|Create a cluster (deprecated)"
msgstr "" msgstr ""
msgid "ClusterAgents|Create agent access token" msgid "ClusterAgents|Create agent access token"
@ -8289,6 +8298,9 @@ msgstr ""
msgid "ClusterIntegration|Create Kubernetes cluster" msgid "ClusterIntegration|Create Kubernetes cluster"
msgstr "" msgstr ""
msgid "ClusterIntegration|Create a Kubernetes cluster"
msgstr ""
msgid "ClusterIntegration|Creating Kubernetes cluster" msgid "ClusterIntegration|Creating Kubernetes cluster"
msgstr "" msgstr ""

View File

@ -50,7 +50,7 @@ const createMainOutput = ({ outFile, cssKeys, type }) => ({
htmlPaths: [ htmlPaths: [
path.join(FIXTURES_ROOT, `startup_css/project-${type}.html`), path.join(FIXTURES_ROOT, `startup_css/project-${type}.html`),
path.join(FIXTURES_ROOT, `startup_css/project-${type}-signed-out.html`), path.join(FIXTURES_ROOT, `startup_css/project-${type}-signed-out.html`),
path.join(FIXTURES_ROOT, `startup_css/project-${type}-search-ff-on.html`), path.join(FIXTURES_ROOT, `startup_css/project-${type}-search-ff-off.html`),
], ],
cssKeys, cssKeys,
purgeOptions: { purgeOptions: {

View File

@ -15,8 +15,8 @@ RSpec.describe 'Instance-level AWS EKS Cluster', :js do
before do before do
visit admin_clusters_path visit admin_clusters_path
click_button 'Actions' click_button(class: 'dropdown-toggle-split')
click_link 'Create a new cluster' click_link 'Create a cluster (deprecated)'
end end
context 'when user creates a cluster on AWS EKS' do context 'when user creates a cluster on AWS EKS' do

View File

@ -25,7 +25,7 @@ RSpec.describe 'Cluster agent registration', :js do
end end
it 'allows the user to select an agent to install, and displays the resulting agent token' do it 'allows the user to select an agent to install, and displays the resulting agent token' do
click_button('Actions') click_button('Connect a cluster')
expect(page).to have_content('Register') expect(page).to have_content('Register')
click_button('Select an agent') click_button('Select an agent')

View File

@ -30,7 +30,7 @@ RSpec.describe 'When a user searches for Sentry errors', :js, :use_clean_rails_m
expect(results.count).to be(3) expect(results.count).to be(3)
end end
find('.gl-form-input').set('NotFound').native.send_keys(:return) find('.filtered-search-input-container .gl-form-input').set('NotFound').native.send_keys(:return)
page.within(find('.gl-table')) do page.within(find('.gl-table')) do
results = page.all('.table-row') results = page.all('.table-row')

View File

@ -20,8 +20,8 @@ RSpec.describe 'Group AWS EKS Cluster', :js do
before do before do
visit group_clusters_path(group) visit group_clusters_path(group)
click_button 'Actions' click_button(class: 'dropdown-toggle-split')
click_link 'Create a new cluster' click_link 'Create a cluster (deprecated)'
end end
context 'when user creates a cluster on AWS EKS' do context 'when user creates a cluster on AWS EKS' do

View File

@ -20,7 +20,7 @@ RSpec.describe 'AWS EKS Cluster', :js do
visit project_clusters_path(project) visit project_clusters_path(project)
click_button(class: 'dropdown-toggle-split') click_button(class: 'dropdown-toggle-split')
click_link 'Create a new cluster' click_link 'Create a cluster (certificate - deprecated)'
end end
context 'when user creates a cluster on AWS EKS' do context 'when user creates a cluster on AWS EKS' do

View File

@ -135,7 +135,7 @@ RSpec.describe 'Gcp Cluster', :js do
visit project_clusters_path(project) visit project_clusters_path(project)
click_button(class: 'dropdown-toggle-split') click_button(class: 'dropdown-toggle-split')
click_link 'Connect with a certificate' click_link 'Connect a cluster (certificate - deprecated)'
end end
it 'user sees the "Environment scope" field' do it 'user sees the "Environment scope" field' do
@ -220,6 +220,6 @@ RSpec.describe 'Gcp Cluster', :js do
def visit_create_cluster_page def visit_create_cluster_page
click_button(class: 'dropdown-toggle-split') click_button(class: 'dropdown-toggle-split')
click_link 'Create a new cluster' click_link 'Create a cluster (certificate - deprecated)'
end end
end end

View File

@ -222,11 +222,11 @@ RSpec.describe 'Clusters', :js do
visit project_clusters_path(project) visit project_clusters_path(project)
click_button(class: 'dropdown-toggle-split') click_button(class: 'dropdown-toggle-split')
click_link 'Create a new cluster' click_link 'Create a cluster (certificate - deprecated)'
end end
def visit_connect_cluster_page def visit_connect_cluster_page
click_button(class: 'dropdown-toggle-split') click_button(class: 'dropdown-toggle-split')
click_link 'Connect with a certificate' click_link 'Connect a cluster (certificate - deprecated)'
end end
end end

View File

@ -8,6 +8,8 @@ RSpec.describe 'User searches for projects', :js do
context 'when signed out' do context 'when signed out' do
context 'when block_anonymous_global_searches is disabled' do context 'when block_anonymous_global_searches is disabled' do
before do before do
allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(:search_rate_limit).and_return(1000)
allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(:search_rate_limit_unauthenticated).and_return(1000)
stub_feature_flags(block_anonymous_global_searches: false) stub_feature_flags(block_anonymous_global_searches: false)
end end

View File

@ -17,12 +17,15 @@ RSpec.describe 'User uses header search field', :js do
end end
before do before do
allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(:search_rate_limit).and_return(1000)
allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(:search_rate_limit_unauthenticated).and_return(1000)
sign_in(user) sign_in(user)
end end
shared_examples 'search field examples' do shared_examples 'search field examples' do
before do before do
visit(url) visit(url)
wait_for_all_requests
end end
it 'starts searching by pressing the enter key' do it 'starts searching by pressing the enter key' do
@ -37,7 +40,6 @@ RSpec.describe 'User uses header search field', :js do
before do before do
find('#search') find('#search')
find('body').native.send_keys('s') find('body').native.send_keys('s')
wait_for_all_requests wait_for_all_requests
end end
@ -49,6 +51,7 @@ RSpec.describe 'User uses header search field', :js do
context 'when clicking the search field' do context 'when clicking the search field' do
before do before do
page.find('#search').click page.find('#search').click
wait_for_all_requests
end end
it 'shows category search dropdown', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/250285' do it 'shows category search dropdown', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/250285' do
@ -59,7 +62,7 @@ RSpec.describe 'User uses header search field', :js do
let!(:issue) { create(:issue, project: project, author: user, assignees: [user]) } let!(:issue) { create(:issue, project: project, author: user, assignees: [user]) }
it 'shows assigned issues' do it 'shows assigned issues' do
find('.search-input-container .dropdown-menu').click_link('Issues assigned to me') find('[data-testid="header-search-dropdown-menu"]').click_link('Issues assigned to me')
expect(page).to have_selector('.issues-list .issue') expect(page).to have_selector('.issues-list .issue')
expect_tokens([assignee_token(user.name)]) expect_tokens([assignee_token(user.name)])
@ -67,7 +70,7 @@ RSpec.describe 'User uses header search field', :js do
end end
it 'shows created issues' do it 'shows created issues' do
find('.search-input-container .dropdown-menu').click_link("Issues I've created") find('[data-testid="header-search-dropdown-menu"]').click_link("Issues I've created")
expect(page).to have_selector('.issues-list .issue') expect(page).to have_selector('.issues-list .issue')
expect_tokens([author_token(user.name)]) expect_tokens([author_token(user.name)])
@ -79,7 +82,7 @@ RSpec.describe 'User uses header search field', :js do
let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignees: [user]) } let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignees: [user]) }
it 'shows assigned merge requests' do it 'shows assigned merge requests' do
find('.search-input-container .dropdown-menu').click_link('Merge requests assigned to me') find('[data-testid="header-search-dropdown-menu"]').click_link('Merge requests assigned to me')
expect(page).to have_selector('.mr-list .merge-request') expect(page).to have_selector('.mr-list .merge-request')
expect_tokens([assignee_token(user.name)]) expect_tokens([assignee_token(user.name)])
@ -87,7 +90,7 @@ RSpec.describe 'User uses header search field', :js do
end end
it 'shows created merge requests' do it 'shows created merge requests' do
find('.search-input-container .dropdown-menu').click_link("Merge requests I've created") find('[data-testid="header-search-dropdown-menu"]').click_link("Merge requests I've created")
expect(page).to have_selector('.mr-list .merge-request') expect(page).to have_selector('.mr-list .merge-request')
expect_tokens([author_token(user.name)]) expect_tokens([author_token(user.name)])
@ -150,10 +153,9 @@ RSpec.describe 'User uses header search field', :js do
it 'displays search options' do it 'displays search options' do
fill_in_search('test') fill_in_search('test')
expect(page).to have_selector(scoped_search_link('test', search_code: true))
expect(page).to have_selector(scoped_search_link('test')) expect(page).to have_selector(scoped_search_link('test', group_id: group.id, search_code: true))
expect(page).to have_selector(scoped_search_link('test', group_id: group.id)) expect(page).to have_selector(scoped_search_link('test', project_id: project.id, group_id: group.id, search_code: true))
expect(page).to have_selector(scoped_search_link('test', project_id: project.id, group_id: group.id))
end end
end end
@ -165,10 +167,9 @@ RSpec.describe 'User uses header search field', :js do
it 'displays search options' do it 'displays search options' do
fill_in_search('test') fill_in_search('test')
expect(page).to have_selector(scoped_search_link('test', search_code: true, repository_ref: 'master'))
expect(page).to have_selector(scoped_search_link('test')) expect(page).not_to have_selector(scoped_search_link('test', search_code: true, group_id: project.namespace_id, repository_ref: 'master'))
expect(page).not_to have_selector(scoped_search_link('test', group_id: project.namespace_id)) expect(page).to have_selector(scoped_search_link('test', search_code: true, project_id: project.id, repository_ref: 'master'))
expect(page).to have_selector(scoped_search_link('test', project_id: project.id))
end end
it 'displays a link to project merge requests' do it 'displays a link to project merge requests' do
@ -217,7 +218,6 @@ RSpec.describe 'User uses header search field', :js do
it 'displays search options' do it 'displays search options' do
fill_in_search('test') fill_in_search('test')
expect(page).to have_selector(scoped_search_link('test')) expect(page).to have_selector(scoped_search_link('test'))
expect(page).to have_selector(scoped_search_link('test', group_id: group.id)) expect(page).to have_selector(scoped_search_link('test', group_id: group.id))
expect(page).not_to have_selector(scoped_search_link('test', project_id: project.id)) expect(page).not_to have_selector(scoped_search_link('test', project_id: project.id))
@ -248,18 +248,20 @@ RSpec.describe 'User uses header search field', :js do
end end
end end
def scoped_search_link(term, project_id: nil, group_id: nil) def scoped_search_link(term, project_id: nil, group_id: nil, search_code: nil, repository_ref: nil)
# search_path will accept group_id and project_id but the order does not match # search_path will accept group_id and project_id but the order does not match
# what is expected in the href, so the variable must be built manually # what is expected in the href, so the variable must be built manually
href = search_path(search: term) href = search_path(search: term)
href.concat("&nav_source=navbar")
href.concat("&project_id=#{project_id}") if project_id href.concat("&project_id=#{project_id}") if project_id
href.concat("&group_id=#{group_id}") if group_id href.concat("&group_id=#{group_id}") if group_id
href.concat("&nav_source=navbar") href.concat("&search_code=true") if search_code
href.concat("&repository_ref=#{repository_ref}") if repository_ref
".dropdown a[href='#{href}']" "[data-testid='header-search-dropdown-menu'] a[href='#{href}']"
end end
def dashboard_search_options_popup_menu def dashboard_search_options_popup_menu
"div[data-testid='dashboard-search-options']" "[data-testid='header-search-dropdown-menu'] .header-search-dropdown-content"
end end
end end

View File

@ -377,6 +377,34 @@
</ol> </ol>
</details> </details>
- name: diagram_kroki_nomnoml
markdown: |-
```nomnoml
#stroke: #a86128
[<frame>Decorator pattern|
[<abstract>Component||+ operation()]
[Client] depends --> [Component]
[Decorator|- next: Component]
[Decorator] decorates -- [ConcreteComponent]
[Component] <:- [Decorator]
[Component] <:- [ConcreteComponent]
]
```
html: |-
<a class="no-attachment-icon" href="http://localhost:8000/nomnoml/svg/eNp1jbsOwjAMRfd-haUuIJQBBlRFVZb2L1CGkBqpgtpR6oEhH0_CW6hsts-9xwD1LJHPqKF2zX67ayqAQ3uKbkLTo-fohCMEJ4KRUoYFu2MuOS-m4ykwIUlKG-CAOT0yrdb2EewuY2YWBgxIwwxKmXx8dZ6h95ekgPAqGv4miuk-YnEVFfmIgr-Fzw6tVt-CZb7osdUNUAReJA==" target="_blank" rel="noopener noreferrer" data-diagram="nomnoml" data-diagram-src="data:text/plain;base64,ICAjc3Ryb2tlOiAjYTg2MTI4CiAgWzxmcmFtZT5EZWNvcmF0b3IgcGF0dGVybnwKICAgIFs8YWJzdHJhY3Q+Q29tcG9uZW50fHwrIG9wZXJhdGlvbigpXQogICAgW0NsaWVudF0gZGVwZW5kcyAtLT4gW0NvbXBvbmVudF0KICAgIFtEZWNvcmF0b3J8LSBuZXh0OiBDb21wb25lbnRdCiAgICBbRGVjb3JhdG9yXSBkZWNvcmF0ZXMgLS0gW0NvbmNyZXRlQ29tcG9uZW50XQogICAgW0NvbXBvbmVudF0gPDotIFtEZWNvcmF0b3JdCiAgICBbQ29tcG9uZW50XSA8Oi0gW0NvbmNyZXRlQ29tcG9uZW50XQogIF0K"><img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" class="lazy" data-src="http://localhost:8000/nomnoml/svg/eNp1jbsOwjAMRfd-haUuIJQBBlRFVZb2L1CGkBqpgtpR6oEhH0_CW6hsts-9xwD1LJHPqKF2zX67ayqAQ3uKbkLTo-fohCMEJ4KRUoYFu2MuOS-m4ykwIUlKG-CAOT0yrdb2EewuY2YWBgxIwwxKmXx8dZ6h95ekgPAqGv4miuk-YnEVFfmIgr-Fzw6tVt-CZb7osdUNUAReJA=="></a>
- name: diagram_plantuml
markdown: |-
```plantuml
Alice -> Bob: Authentication Request
Bob --> Alice: Authentication Response
Alice -> Bob: Another authentication Request
Alice <-- Bob: Another authentication Response
```
html: |-
<a class="no-attachment-icon" href="http://localhost:8080/png/U9nJK73CoKnELT2rKt3AJx9IS2mjoKZDAybCJYp9pCzJ24ejB4qjBk5I0Cagw09LWPLZKLTSa9zNdCe5L8bcO5u-K6MHGY8kWo7ARNHr2QY7MW00AeWxTG00" target="_blank" rel="noopener noreferrer" data-diagram="plantuml" data-diagram-src="data:text/plain;base64,ICBBbGljZSAtPiBCb2I6IEF1dGhlbnRpY2F0aW9uIFJlcXVlc3QKICBCb2IgLS0+IEFsaWNlOiBBdXRoZW50aWNhdGlvbiBSZXNwb25zZQoKICBBbGljZSAtPiBCb2I6IEFub3RoZXIgYXV0aGVudGljYXRpb24gUmVxdWVzdAogIEFsaWNlIDwtLSBCb2I6IEFub3RoZXIgYXV0aGVudGljYXRpb24gUmVzcG9uc2UK"><img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" class="lazy" data-src="http://localhost:8080/png/U9nJK73CoKnELT2rKt3AJx9IS2mjoKZDAybCJYp9pCzJ24ejB4qjBk5I0Cagw09LWPLZKLTSa9zNdCe5L8bcO5u-K6MHGY8kWo7ARNHr2QY7MW00AeWxTG00"></a>
- name: div - name: div
markdown: |- markdown: |-
<div>plain text</div> <div>plain text</div>

View File

@ -1,4 +1,4 @@
import { GlDropdown, GlDropdownItem, GlButton } from '@gitlab/ui'; import { GlDropdown, GlDropdownItem, GlTooltip } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ClustersActions from '~/clusters_list/components/clusters_actions.vue'; import ClustersActions from '~/clusters_list/components/clusters_actions.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
@ -7,12 +7,14 @@ import { INSTALL_AGENT_MODAL_ID, CLUSTERS_ACTIONS } from '~/clusters_list/consta
describe('ClustersActionsComponent', () => { describe('ClustersActionsComponent', () => {
let wrapper; let wrapper;
const newClusterPath = 'path/to/create/cluster'; const newClusterPath = 'path/to/add/cluster';
const addClusterPath = 'path/to/connect/existing/cluster'; const addClusterPath = 'path/to/connect/existing/cluster';
const newClusterDocsPath = 'path/to/create/new/cluster';
const defaultProvide = { const defaultProvide = {
newClusterPath, newClusterPath,
addClusterPath, addClusterPath,
newClusterDocsPath,
canAddCluster: true, canAddCluster: true,
displayClusterAgents: true, displayClusterAgents: true,
certificateBasedClustersEnabled: true, certificateBasedClustersEnabled: true,
@ -20,12 +22,13 @@ describe('ClustersActionsComponent', () => {
const findDropdown = () => wrapper.findComponent(GlDropdown); const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findTooltip = () => wrapper.findComponent(GlTooltip);
const findDropdownItemIds = () => const findDropdownItemIds = () =>
findDropdownItems().wrappers.map((x) => x.attributes('data-testid')); findDropdownItems().wrappers.map((x) => x.attributes('data-testid'));
const findDropdownItemTexts = () => findDropdownItems().wrappers.map((x) => x.text());
const findNewClusterLink = () => wrapper.findByTestId('new-cluster-link'); const findNewClusterLink = () => wrapper.findByTestId('new-cluster-link');
const findNewClusterDocsLink = () => wrapper.findByTestId('create-cluster-link');
const findConnectClusterLink = () => wrapper.findByTestId('connect-cluster-link'); const findConnectClusterLink = () => wrapper.findByTestId('connect-cluster-link');
const findConnectNewAgentLink = () => wrapper.findByTestId('connect-new-agent-link');
const findConnectWithAgentButton = () => wrapper.findComponent(GlButton);
const createWrapper = (provideData = {}) => { const createWrapper = (provideData = {}) => {
wrapper = shallowMountExtended(ClustersActions, { wrapper = shallowMountExtended(ClustersActions, {
@ -35,7 +38,6 @@ describe('ClustersActionsComponent', () => {
}, },
directives: { directives: {
GlModalDirective: createMockDirective(), GlModalDirective: createMockDirective(),
GlTooltip: createMockDirective(),
}, },
}); });
}; };
@ -49,12 +51,15 @@ describe('ClustersActionsComponent', () => {
}); });
describe('when the certificate based clusters are enabled', () => { describe('when the certificate based clusters are enabled', () => {
it('renders actions menu', () => { it('renders actions menu', () => {
expect(findDropdown().props('text')).toBe(CLUSTERS_ACTIONS.actionsButton); expect(findDropdown().exists()).toBe(true);
}); });
it('renders correct href attributes for the links', () => { it('shows split button in dropdown', () => {
expect(findNewClusterLink().attributes('href')).toBe(newClusterPath); expect(findDropdown().props('split')).toBe(true);
expect(findConnectClusterLink().attributes('href')).toBe(addClusterPath); });
it("doesn't show the tooltip", () => {
expect(findTooltip().exists()).toBe(false);
}); });
describe('when user cannot add clusters', () => { describe('when user cannot add clusters', () => {
@ -67,8 +72,7 @@ describe('ClustersActionsComponent', () => {
}); });
it('shows tooltip explaining why dropdown is disabled', () => { it('shows tooltip explaining why dropdown is disabled', () => {
const tooltip = getBinding(findDropdown().element, 'gl-tooltip'); expect(findTooltip().attributes('title')).toBe(CLUSTERS_ACTIONS.dropdownDisabledHint);
expect(tooltip.value).toBe(CLUSTERS_ACTIONS.dropdownDisabledHint);
}); });
it('does not bind split dropdown button', () => { it('does not bind split dropdown button', () => {
@ -79,33 +83,36 @@ describe('ClustersActionsComponent', () => {
}); });
describe('when on project level', () => { describe('when on project level', () => {
it(`displays default action as ${CLUSTERS_ACTIONS.connectWithAgent}`, () => {
expect(findDropdown().props('text')).toBe(CLUSTERS_ACTIONS.connectWithAgent);
});
it('renders correct modal id for the default action', () => {
const binding = getBinding(findDropdown().element, 'gl-modal-directive');
expect(binding.value).toBe(INSTALL_AGENT_MODAL_ID);
});
it('renders a dropdown with 3 actions items', () => { it('renders a dropdown with 3 actions items', () => {
expect(findDropdownItemIds()).toEqual([ expect(findDropdownItemIds()).toEqual([
'connect-new-agent-link', 'create-cluster-link',
'new-cluster-link', 'new-cluster-link',
'connect-cluster-link', 'connect-cluster-link',
]); ]);
}); });
it('renders correct modal id for the agent link', () => { it('renders correct texts for the dropdown items', () => {
const binding = getBinding(findConnectNewAgentLink().element, 'gl-modal-directive'); expect(findDropdownItemTexts()).toEqual([
CLUSTERS_ACTIONS.createCluster,
expect(binding.value).toBe(INSTALL_AGENT_MODAL_ID); CLUSTERS_ACTIONS.createClusterCertificate,
CLUSTERS_ACTIONS.connectClusterCertificate,
]);
}); });
it('shows tooltip', () => { it('renders correct href attributes for the links', () => {
const tooltip = getBinding(findDropdown().element, 'gl-tooltip'); expect(findNewClusterDocsLink().attributes('href')).toBe(newClusterDocsPath);
expect(tooltip.value).toBe(CLUSTERS_ACTIONS.connectWithAgent); expect(findNewClusterLink().attributes('href')).toBe(newClusterPath);
}); expect(findConnectClusterLink().attributes('href')).toBe(addClusterPath);
it('shows split button in dropdown', () => {
expect(findDropdown().props('split')).toBe(true);
});
it('binds split button with modal id', () => {
const binding = getBinding(findDropdown().element, 'gl-modal-directive');
expect(binding.value).toBe(INSTALL_AGENT_MODAL_ID);
}); });
}); });
@ -114,17 +121,20 @@ describe('ClustersActionsComponent', () => {
createWrapper({ displayClusterAgents: false }); createWrapper({ displayClusterAgents: false });
}); });
it('renders a dropdown with 2 actions items', () => { it(`displays default action as ${CLUSTERS_ACTIONS.connectClusterDeprecated}`, () => {
expect(findDropdownItemIds()).toEqual(['new-cluster-link', 'connect-cluster-link']); expect(findDropdown().props('text')).toBe(CLUSTERS_ACTIONS.connectClusterDeprecated);
}); });
it('shows tooltip', () => { it('renders a dropdown with 1 action item', () => {
const tooltip = getBinding(findDropdown().element, 'gl-tooltip'); expect(findDropdownItemIds()).toEqual(['new-cluster-link']);
expect(tooltip.value).toBe(CLUSTERS_ACTIONS.connectExistingCluster);
}); });
it('does not show split button in dropdown', () => { it('renders correct text for the dropdown item', () => {
expect(findDropdown().props('split')).toBe(false); expect(findDropdownItemTexts()).toEqual([CLUSTERS_ACTIONS.createClusterDeprecated]);
});
it('renders correct href attributes for the links', () => {
expect(findNewClusterLink().attributes('href')).toBe(newClusterPath);
}); });
it('does not bind dropdown button to modal', () => { it('does not bind dropdown button to modal', () => {
@ -140,17 +150,26 @@ describe('ClustersActionsComponent', () => {
createWrapper({ certificateBasedClustersEnabled: false }); createWrapper({ certificateBasedClustersEnabled: false });
}); });
it('it does not show the the dropdown', () => { it(`displays default action as ${CLUSTERS_ACTIONS.connectCluster}`, () => {
expect(findDropdown().exists()).toBe(false); expect(findDropdown().props('text')).toBe(CLUSTERS_ACTIONS.connectCluster);
}); });
it('shows the connect with agent button', () => { it('renders correct modal id for the default action', () => {
expect(findConnectWithAgentButton().props()).toMatchObject({ const binding = getBinding(findDropdown().element, 'gl-modal-directive');
disabled: !defaultProvide.canAddCluster,
category: 'primary', expect(binding.value).toBe(INSTALL_AGENT_MODAL_ID);
variant: 'confirm', });
});
expect(findConnectWithAgentButton().text()).toBe(CLUSTERS_ACTIONS.connectWithAgent); it('renders a dropdown with 1 action item', () => {
expect(findDropdownItemIds()).toEqual(['create-cluster-link']);
});
it('renders correct text for the dropdown item', () => {
expect(findDropdownItemTexts()).toEqual([CLUSTERS_ACTIONS.createCluster]);
});
it('renders correct href attributes for the links', () => {
expect(findNewClusterDocsLink().attributes('href')).toBe(newClusterDocsPath);
}); });
}); });
}); });

View File

@ -41,12 +41,12 @@ RSpec.describe 'Startup CSS fixtures', type: :controller do
expect(response).to be_successful expect(response).to be_successful
end end
# This Feature Flag is off by default # This Feature Flag is on by default
# This ensures that the correct css is generated # This ensures that the correct css is generated
# When the feature flag is off, the general startup will capture it # When the feature flag is on, the general startup will capture it
# This will be removed as part of https://gitlab.com/gitlab-org/gitlab/-/issues/339348 # This will be removed as part of https://gitlab.com/gitlab-org/gitlab/-/issues/339348
it "startup_css/project-#{type}-search-ff-on.html" do it "startup_css/project-#{type}-search-ff-off.html" do
stub_feature_flags(new_header_search: true) stub_feature_flags(new_header_search: false)
get :show, params: { get :show, params: {
namespace_id: project.namespace.to_param, namespace_id: project.namespace.to_param,

View File

@ -340,7 +340,9 @@ describe('Milestone combobox component', () => {
await nextTick(); await nextTick();
expect( expect(
findFirstProjectMilestonesDropdownItem().find('span').classes('selected-item'), findFirstProjectMilestonesDropdownItem()
.find('svg')
.classes('gl-new-dropdown-item-check-icon'),
).toBe(true); ).toBe(true);
selectFirstProjectMilestone(); selectFirstProjectMilestone();
@ -348,8 +350,8 @@ describe('Milestone combobox component', () => {
await nextTick(); await nextTick();
expect( expect(
findFirstProjectMilestonesDropdownItem().find('span').classes('selected-item'), findFirstProjectMilestonesDropdownItem().find('svg').classes('gl-visibility-hidden'),
).toBe(false); ).toBe(true);
}); });
describe('when a project milestones is selected', () => { describe('when a project milestones is selected', () => {
@ -464,17 +466,19 @@ describe('Milestone combobox component', () => {
await nextTick(); await nextTick();
expect(findFirstGroupMilestonesDropdownItem().find('span').classes('selected-item')).toBe( expect(
true, findFirstGroupMilestonesDropdownItem()
); .find('svg')
.classes('gl-new-dropdown-item-check-icon'),
).toBe(true);
selectFirstGroupMilestone(); selectFirstGroupMilestone();
await nextTick(); await nextTick();
expect(findFirstGroupMilestonesDropdownItem().find('span').classes('selected-item')).toBe( expect(
false, findFirstGroupMilestonesDropdownItem().find('svg').classes('gl-visibility-hidden'),
); ).toBe(true);
}); });
describe('when a group milestones is selected', () => { describe('when a group milestones is selected', () => {

View File

@ -74,6 +74,10 @@ RSpec.describe ClustersHelper do
expect(subject[:add_cluster_path]).to eq("#{project_path(project)}/-/clusters/connect") expect(subject[:add_cluster_path]).to eq("#{project_path(project)}/-/clusters/connect")
end end
it 'displays create cluster path' do
expect(subject[:new_cluster_docs_path]).to eq("#{project_path(project)}/-/clusters/new_cluster_docs")
end
it 'displays project default branch' do it 'displays project default branch' do
expect(subject[:default_branch_name]).to eq(project.default_branch) expect(subject[:default_branch_name]).to eq(project.default_branch)
end end

View File

@ -46,6 +46,16 @@ RSpec.describe Banzai::Filter::ImageLinkFilter do
expect(doc.at_css('img')['data-canonical-src']).to eq doc.at_css('a')['data-canonical-src'] expect(doc.at_css('img')['data-canonical-src']).to eq doc.at_css('a')['data-canonical-src']
end end
it 'moves the data-diagram* attributes' do
doc = filter(%q(<img class="plantuml" src="http://localhost:8080/png/U9npoazIqBLJ24uiIbImKl18pSd91m0rkGMq" data-diagram="plantuml" data-diagram-src="data:text/plain;base64,Qm9iIC0+IFNhcmEgOiBIZWxsbw==">), context)
expect(doc.at_css('a')['data-diagram']).to eq "plantuml"
expect(doc.at_css('a')['data-diagram-src']).to eq "data:text/plain;base64,Qm9iIC0+IFNhcmEgOiBIZWxsbw=="
expect(doc.at_css('a img')['data-diagram']).to be_nil
expect(doc.at_css('a img')['data-diagram-src']).to be_nil
end
it 'adds no-attachment icon class to the link' do it 'adds no-attachment icon class to the link' do
doc = filter(image(path), context) doc = filter(image(path), context)

View File

@ -9,7 +9,7 @@ RSpec.describe Banzai::Filter::KrokiFilter do
stub_application_setting(kroki_enabled: true, kroki_url: "http://localhost:8000") stub_application_setting(kroki_enabled: true, kroki_url: "http://localhost:8000")
doc = filter("<pre lang='nomnoml'><code>[Pirate|eyeCount: Int|raid();pillage()|\n [beard]--[parrot]\n [beard]-:>[foul mouth]\n]</code></pre>") doc = filter("<pre lang='nomnoml'><code>[Pirate|eyeCount: Int|raid();pillage()|\n [beard]--[parrot]\n [beard]-:>[foul mouth]\n]</code></pre>")
expect(doc.to_s).to eq '<img src="http://localhost:8000/nomnoml/svg/eNqLDsgsSixJrUmtTHXOL80rsVLwzCupKUrMTNHQtC7IzMlJTE_V0KzhUlCITkpNLEqJ1dWNLkgsKsoviUUSs7KLTssvzVHIzS8tyYjligUAMhEd0g==">' expect(doc.to_s).to eq '<img src="http://localhost:8000/nomnoml/svg/eNqLDsgsSixJrUmtTHXOL80rsVLwzCupKUrMTNHQtC7IzMlJTE_V0KzhUlCITkpNLEqJ1dWNLkgsKsoviUUSs7KLTssvzVHIzS8tyYjligUAMhEd0g==" data-diagram="nomnoml" data-diagram-src="data:text/plain;base64,W1BpcmF0ZXxleWVDb3VudDogSW50fHJhaWQoKTtwaWxsYWdlKCl8CiAgW2JlYXJkXS0tW3BhcnJvdF0KICBbYmVhcmRdLTo+W2ZvdWwgbW91dGhdCl0=">'
end end
it 'replaces nomnoml pre tag with img tag if both kroki and plantuml are enabled' do it 'replaces nomnoml pre tag with img tag if both kroki and plantuml are enabled' do
@ -19,7 +19,7 @@ RSpec.describe Banzai::Filter::KrokiFilter do
plantuml_url: "http://localhost:8080") plantuml_url: "http://localhost:8080")
doc = filter("<pre lang='nomnoml'><code>[Pirate|eyeCount: Int|raid();pillage()|\n [beard]--[parrot]\n [beard]-:>[foul mouth]\n]</code></pre>") doc = filter("<pre lang='nomnoml'><code>[Pirate|eyeCount: Int|raid();pillage()|\n [beard]--[parrot]\n [beard]-:>[foul mouth]\n]</code></pre>")
expect(doc.to_s).to eq '<img src="http://localhost:8000/nomnoml/svg/eNqLDsgsSixJrUmtTHXOL80rsVLwzCupKUrMTNHQtC7IzMlJTE_V0KzhUlCITkpNLEqJ1dWNLkgsKsoviUUSs7KLTssvzVHIzS8tyYjligUAMhEd0g==">' expect(doc.to_s).to eq '<img src="http://localhost:8000/nomnoml/svg/eNqLDsgsSixJrUmtTHXOL80rsVLwzCupKUrMTNHQtC7IzMlJTE_V0KzhUlCITkpNLEqJ1dWNLkgsKsoviUUSs7KLTssvzVHIzS8tyYjligUAMhEd0g==" data-diagram="nomnoml" data-diagram-src="data:text/plain;base64,W1BpcmF0ZXxleWVDb3VudDogSW50fHJhaWQoKTtwaWxsYWdlKCl8CiAgW2JlYXJkXS0tW3BhcnJvdF0KICBbYmVhcmRdLTo+W2ZvdWwgbW91dGhdCl0=">'
end end
it 'does not replace nomnoml pre tag with img tag if kroki is disabled' do it 'does not replace nomnoml pre tag with img tag if kroki is disabled' do

View File

@ -9,7 +9,7 @@ RSpec.describe Banzai::Filter::PlantumlFilter do
stub_application_setting(plantuml_enabled: true, plantuml_url: "http://localhost:8080") stub_application_setting(plantuml_enabled: true, plantuml_url: "http://localhost:8080")
input = '<pre lang="plantuml"><code>Bob -> Sara : Hello</code></pre>' input = '<pre lang="plantuml"><code>Bob -> Sara : Hello</code></pre>'
output = '<div class="imageblock"><div class="content"><img class="plantuml" src="http://localhost:8080/png/U9npoazIqBLJ24uiIbImKl18pSd91m0rkGMq"></div></div>' output = '<img class="plantuml" src="http://localhost:8080/png/U9npoazIqBLJ24uiIbImKl18pSd91m0rkGMq" data-diagram="plantuml" data-diagram-src="data:text/plain;base64,Qm9iIC0+IFNhcmEgOiBIZWxsbw==">'
doc = filter(input) doc = filter(input)
expect(doc.to_s).to eq output expect(doc.to_s).to eq output
@ -29,7 +29,7 @@ RSpec.describe Banzai::Filter::PlantumlFilter do
stub_application_setting(plantuml_enabled: true, plantuml_url: "invalid") stub_application_setting(plantuml_enabled: true, plantuml_url: "invalid")
input = '<pre lang="plantuml"><code>Bob -> Sara : Hello</code></pre>' input = '<pre lang="plantuml"><code>Bob -> Sara : Hello</code></pre>'
output = '<div class="listingblock"><div class="content"><pre class="plantuml plantuml-error"> Error: cannot connect to PlantUML server at "invalid"</pre></div></div>' output = '<pre lang="plantuml"><code>Bob -&gt; Sara : Hello</code></pre>'
doc = filter(input) doc = filter(input)
expect(doc.to_s).to eq output expect(doc.to_s).to eq output

View File

@ -49,6 +49,12 @@ RSpec.describe ProjectClusterablePresenter do
it { is_expected.to eq(connect_project_clusters_path(project)) } it { is_expected.to eq(connect_project_clusters_path(project)) }
end end
describe '#new_cluster_docs_path' do
subject { presenter.new_cluster_docs_path }
it { is_expected.to eq(new_cluster_docs_project_clusters_path(project)) }
end
describe '#authorize_aws_role_path' do describe '#authorize_aws_role_path' do
subject { presenter.authorize_aws_role_path } subject { presenter.authorize_aws_role_path }

View File

@ -329,11 +329,6 @@ RSpec.configure do |config|
stub_feature_flags(disable_anonymous_search: false) stub_feature_flags(disable_anonymous_search: false)
stub_feature_flags(disable_anonymous_project_search: false) stub_feature_flags(disable_anonymous_project_search: false)
# Disable the refactored top nav search until there is functionality
# Can be removed once all existing functionality has been replicated
# For more information check https://gitlab.com/gitlab-org/gitlab/-/issues/339348
stub_feature_flags(new_header_search: false)
allow(Gitlab::GitalyClient).to receive(:can_use_disk?).and_return(enable_rugged) allow(Gitlab::GitalyClient).to receive(:can_use_disk?).and_return(enable_rugged)
else else
unstub_all_feature_flags unstub_all_feature_flags

View File

@ -2,9 +2,12 @@
module SearchHelpers module SearchHelpers
def fill_in_search(text) def fill_in_search(text)
page.within('.search-input-wrap') do # Once the `new_header_search` feature flag has been removed
# We can remove the `.search-input-wrap` selector
# https://gitlab.com/gitlab-org/gitlab/-/issues/339348
page.within('.header-search-new') do
find('#search').click find('#search').click
fill_in('search', with: text) fill_in 'search', with: text
end end
wait_for_all_requests wait_for_all_requests

View File

@ -270,7 +270,7 @@ module MarkdownMatchers
set_default_markdown_messages set_default_markdown_messages
match do |actual| match do |actual|
expect(actual).to have_link(href: 'http://localhost:8000/nomnoml/svg/eNqLDsgsSixJrUmtTHXOL80rsVLwzCupKUrMTNHQtC7IzMlJTE_V0KzhUlCITkpNLEqJ1dWNLkgsKsoviUUSs7KLTssvzVHIzS8tyYjligUAMhEd0g==') expect(actual).to have_link(href: 'http://localhost:8000/nomnoml/svg/eNqLDsgsSixJrUmtTHXOL80rsVLwzCupKUrMTNHQtC7IzMlJTE_V0KzhUlCITkpNLEqJ1dWNLkgsKsoviUUSs7KLTssvzVHIzS8tyYjliuUCAE_tHdw=')
end end
end end
end end

View File

@ -64,6 +64,9 @@ RSpec.shared_context 'API::Markdown Golden Master shared context' do |markdown_y
let(:substitutions) { markdown_example.fetch(:substitutions, {}) } let(:substitutions) { markdown_example.fetch(:substitutions, {}) }
it "verifies conversion of GFM to HTML", :unlimited_max_formatted_output_length do it "verifies conversion of GFM to HTML", :unlimited_max_formatted_output_length do
stub_application_setting(plantuml_enabled: true, plantuml_url: 'http://localhost:8080')
stub_application_setting(kroki_enabled: true, kroki_url: 'http://localhost:8000')
pending pending_reason if pending_reason pending pending_reason if pending_reason
normalized_example_html = normalize_html(example_html, substitutions) normalized_example_html = normalize_html(example_html, substitutions)

View File

@ -11,6 +11,7 @@ RSpec.shared_examples 'search timeouts' do |scope|
end end
it 'renders timeout information' do it 'renders timeout information' do
# expect(page).to have_content('This endpoint has been requested too many times.')
expect(page).to have_content('Your search timed out') expect(page).to have_content('Your search timed out')
end end