)9`dz4EfNm}Nqxk%(;`eA{nIG*2
zUO5B3LH+p{8~&gEu;);UExA5P@iZJ|3-5H;xjy&tN-K!5nPo+3D;11jsXcQ#{lJsB)~El66F`}F3w5*s6m4cz|uHLN_5bkMSh
zI$K@a)j?yaA3To11_Hw=%_TQ~h!<5)|K7{s+-k2e43p{NU>UL1HUAoD{Nn>yNnrOw
z-|}zC9hO2Pwcyr8T_9;3IKN6)Kh(>8mbSTS*I>xC@r1PKbzBwO`TO7bpoRNU9C2*M
z%TqYY=+1ufA}dC93HJF=%kkml0+&JO6|37?;eK+Z#_vdd)Rf{xkuwWL09itLp(r5*f`dc)B^{+<@|^*5S%@>f$N20!goU
zTpr8e@2EP~FH%g8pi#o8FiP~;1iN{zTg}%u?;n>#egNL(e72ZiZ8kK=@cqxYM#0^4
zXty@u`&O4q862XN?O?0B3{A-8of9k?>@*xn(C)0#aNpUT`qRrtZSjH^B_pjIu|_v$otcH?{#x_`Ga!({!~l}Z|`C-e8FSA{6&IH*WfZuj8eyO=-Z~P
zEJEj!dg>pv7BzJzS%;H8b0wm}51ewcZvReId64kP$0L#G^#<=tR49T%xXM&X4qGH`
z{W>p}>M3uejeXysU*W}ABUzVBb^H$AoEMNYwR?AZqUfN9w!SA(cU1M3bFl;4PLfGz
ze|UWWj9xZtS|Lgw@K9BkrVV+Vls)k!Ie1R~w1KNc|GV02}sOnQYa!
zW4Zc@DxKR|CtcBQbv7a;pAeEe
z*IH?qM}n=M`KrtRb|vqA#}6Go3{6+Qz4Pl1PnR2`?`Ic=Y%5Zw?hc2+Sb-MiS6wHY
z(_>sIO>M2*OK0%|9G%t;Uq|QYCJq#
zTB$!m88Z^^oZ3a99&mCuyNwV-0E&X;-JLwI_}Um__$h9XCu_Oc<|S6ypx{Q6yD^TJHPoXk6yoCFiX~MVjRMvwA
z?dK6CeF>0*y%>q_{)kXPN_6bYwcibTV`ul~ZQSb3eR$|6UJEUaZ)a(&2VC0sF*i5R
zdWBHm$6YF&*#Y$QAdf{+LiPKz1t{BIf^5P);I_c);SY-zeCkW75UOc02>BSySzzim
z#{unhC-P5az_Ba#yCQj#<_A*V3z+y8OFjdKl!uW{CkFk+g$~IR$BA&;(>|^FA8@lJ
zk=HlC_z>_d^Mz!2Z3*4ndY=Q#IEy6_HrY?qONLo(UpKR~8;0~ZDr!U)+#BG&lAHqy
zk41o60EF&LoSseXZA(!Z!t&w$C?ChzO*Lux)K`w11NBh0{El}s1%}vA!2OyV^X~Jn
z-Xo6c#taGIHK|?QU)Fb-7qiS9>?JRqH>f8#01yh2KB6Gqh%krHc@G6
zndr(n;7HnD?aQcUZ&9zo*E>%S`~5e?+-I9@^ePbTMW!#>RGLVh-aR|%h<6;ui&DSZ
zkh@P~+Z%0NjJB7cr1(o$q1k4*Z@trV3|@BcTq#@3C_WdVP4D<`61fL^Ts%galB}<1
z0B!($1AX2_wi4s_xvI?DJuwIPXhl1VxWznP)!R!Q_aXELEI&g@nirk!@<0P+#qpq?
zfuN)LIBr##cHJfC!ttF(|D7U)`A}ui-(MCMOH3>-`s4_Oma1=(!|n^UmP-WH*Ske+
z9w0&ughB^ekJpB!yX?K)-1Z$Iuh)iexIB89PWLHt6^PppJ^Rt7cM(m%H3vNz>@T(d
zE`EN<^X>WVK!RTKrkzYJ=|m`Pg>+a2hh@kdd{o^ar-FAH3CYu84{^juG&4$l56+3qHoDw
zTE)e_zD95aVP-td>zA!KX5Y>5cH;GZF4z#g9-HlT)zRjBJzV?swDgwjUc8~=^a?;N
z3wok`yt@;RWa`~+9@GKs*9OA=5vIkG<;>Pe(z`uM*aiL?eqUfA0;5O`&=ZWlP{{b%
zV0rlEaI7J(LtE29Fph=fgNZLEv8YpmXC1e?vdL%TumNg_9}+<&;d24@MMDYQj^;q!
zH?ZvRn_^RM4Q^MH*0yp{zh>N?iw!9O5k#7T?qJAD5-rh$*ZKtq8@s_}eFrb}w-f4E
zVLLnCUlUd{GAc~WSPvH`MfYzjFH(Gq`6)sgiNJ-T%vx(U10woqt>Yy9G1u_(2e3GZ
z5AcB1b_5q$y2Zh~F(N6*t?+Gn{4`=OAE>8)Q&1b$IQKDSaKzBtiDS%UM&ISu=C@wf
zi)HVX(;Oc&V}#)v{Mzfg@dxUDH9i>hWO1O@zKmx7vkPh+%*EU=x6Q`ojqzY2W?1})
z25pVo1K&@P`tiC!i0;O@F|R-E=Z%U0YVOA$Un!VYwwm*eB+zooEHo4m&Jv-GI6u&!
zJ{|O>Yj890eYab@%_Sj{p%4X5*9~khR&SD`B|;=-eh(D-B~jMF#Q9vol98TX49b#y
z={jd?-`)k=ZH{{%(=farr%R0Geh^LawnSOow^Qc@zpeTjSNwHkZ-$!-NYRY}-k%z8
zeOvocE79p1OhL34ui4jMhyV~U|4E1uRj-wjzu)lt$&%rh#DgPQKtH#e8Eg-{zow&u
z)hkDQ9`26O>KIQP)eD{6O#cZYGW}JN`*EQt!5zm&5n4)wo`8D>KYHV%NsZ%pxSv4Y
zpZV%NBIJRu(>7Av#mn0p1OlmTQ1`)8Cz4ak#PHa5LJy^!E>~0`3_;fmh<^{ciIg;c{*4mBjiSzgn}z
zo=a@;blzQbA@I^lzKx5PC?oa{fN$OjZ(1I|gsZB>t82%Gns}GTTJ$Sz_`2UdJaR<5
z!2%J3?q|)8l8m`DRWNY>?517tEtZ#q(b7L}
zLX=N{w1xPT1^v4V0uU17JK)4z;NRcB_#1crh0R|s{yv+({6hI(!St7ezo_%~FaDy=
zKf3t)Z2t1gKVtJ2sQ+^3KfL%yM*5f9{)e=Gj^|nr;eV>1{Bu(Ofa<@(P5#I647srz
z#K!_+5CR-lA9#%_h^Y!((EoutCL%g9Wf7mUAY|Y(XVhym$8nOgpbJ3JZ6
tuYp_$qDBABY4HCeSburb7owlAcA;?9;It6h$&n#IR#DKnTPY6>`(LZ2LWBSS
literal 0
HcmV?d00001
diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/group1/view_case.ts b/x-pack/test/functional_with_es_ssl/apps/cases/group1/view_case.ts
index 233157e57e518..8c9f7a4450f83 100644
--- a/x-pack/test/functional_with_es_ssl/apps/cases/group1/view_case.ts
+++ b/x-pack/test/functional_with_es_ssl/apps/cases/group1/view_case.ts
@@ -618,6 +618,67 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
await testSubjects.click('case-view-tab-title-alerts');
await testSubjects.existOrFail('case-view-tab-content-alerts');
});
+
+ it("shows the 'files' tab when clicked", async () => {
+ await testSubjects.click('case-view-tab-title-files');
+ await testSubjects.existOrFail('case-view-tab-content-files');
+ });
+ });
+
+ describe('Files', () => {
+ createOneCaseBeforeDeleteAllAfter(getPageObject, getService);
+
+ it('adds a file to the case', async () => {
+ // navigate to files tab
+ await testSubjects.click('case-view-tab-title-files');
+ await testSubjects.existOrFail('case-view-tab-content-files');
+
+ await cases.casesFilesTable.addFile(require.resolve('./elastic_logo.png'));
+
+ // make sure the uploaded file is displayed on the table
+ await find.byButtonText('elastic_logo.png');
+ });
+
+ it('search by file name', async () => {
+ await cases.casesFilesTable.searchByFileName('foobar');
+
+ await cases.casesFilesTable.emptyOrFail();
+
+ await cases.casesFilesTable.searchByFileName('elastic');
+
+ await find.byButtonText('elastic_logo.png');
+ });
+
+ it('displays the file preview correctly', async () => {
+ await cases.casesFilesTable.openFilePreview(0);
+
+ await testSubjects.existOrFail('cases-files-image-preview');
+ });
+
+ it('pressing escape key closes the file preview', async () => {
+ await testSubjects.existOrFail('cases-files-image-preview');
+
+ await browser.pressKeys(browser.keys.ESCAPE);
+
+ await testSubjects.missingOrFail('cases-files-image-preview');
+ });
+
+ it('files added to a case can be deleted', async () => {
+ await cases.casesFilesTable.deleteFile(0);
+
+ await cases.casesFilesTable.emptyOrFail();
+ });
+
+ describe('Files User Activity', () => {
+ it('file user action is displayed correctly', async () => {
+ await cases.casesFilesTable.addFile(require.resolve('./elastic_logo.png'));
+
+ await testSubjects.click('case-view-tab-title-activity');
+ await testSubjects.existOrFail('case-view-tab-content-activity');
+
+ await find.byButtonText('elastic_logo.png');
+ });
+ });
});
});
};
From b3f65f79e5017b70fe26e5aa1c2ee1085e68c138 Mon Sep 17 00:00:00 2001
From: Hannah Mudge
Date: Wed, 26 Apr 2023 12:19:46 -0600
Subject: [PATCH 31/73] [Controls] Fix sorting of numeric keyword fields
(#155207)
Closes https://github.com/elastic/kibana/issues/155073
## Summary
### Before
Previously, the options list suggestions were stored as a dictionary
(i.e. an object of key+value pairs) - while this worked for most fields,
unbeknownst to us, Javascript tries to sort numeric keys (regardless of
if they are of type `string` or `number`) based on their value.
This meant that, as part of the parsing process when using an options
list control for a numeric `keyword` field, the results returned by the
ES query were **always** sorted in ascending numeric order regardless of
the sorting method that was picked (note that this is especially obvious
once you "load more", which is what I did for the following
screenshots):
| | Ascending | Descending |
|--------------|-----------|------------|
| Alphabetical |
|
|
| Doc count |
|
|
### After
This PR converts the options list suggestions to be stored as an
**array** of key/value pairs in order to preserve the order returned
from Elasticsearch - now, you get the expected string-sorted ordering
when using numeric `keyword` fields in an options list control:
| | Ascending | Descending |
|--------------|-----------|------------|
| Alphabetical |
|
|
| Doc count |
|
|
Notice in the above that we are now using **string sorting** for the
numeric values when alphabetical sorting is selected, which means you
aren't getting the expected "numeric" sorting - so for example, when
sorted ascending, `"6" > "52"` because it is only comparing the first
character and `"6" > "5"`. This will be handled much better once
[numeric field support](https://github.com/elastic/kibana/issues/126795)
is added to options lists.
### Checklist
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)
### For maintainers
- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
---
.../controls/common/options_list/mocks.tsx | 14 +-
.../controls/common/options_list/types.ts | 4 +-
.../public/__stories__/controls.stories.tsx | 10 +-
.../components/options_list_popover.test.tsx | 12 +-
.../options_list_popover_suggestions.tsx | 24 +--
.../embeddable/options_list_embeddable.tsx | 2 +-
.../options_list/options_list.story.ts | 2 +-
...ions_list_cheap_suggestion_queries.test.ts | 142 +++++++++---------
.../options_list_cheap_suggestion_queries.ts | 33 ++--
..._list_expensive_suggestion_queries.test.ts | 123 +++++++--------
...tions_list_expensive_suggestion_queries.ts | 21 ++-
11 files changed, 196 insertions(+), 191 deletions(-)
diff --git a/src/plugins/controls/common/options_list/mocks.tsx b/src/plugins/controls/common/options_list/mocks.tsx
index 936a620ec288c..d0e2977a9b439 100644
--- a/src/plugins/controls/common/options_list/mocks.tsx
+++ b/src/plugins/controls/common/options_list/mocks.tsx
@@ -17,13 +17,13 @@ const mockOptionsListComponentState = {
searchString: { value: '', valid: true },
field: undefined,
totalCardinality: 0,
- availableOptions: {
- woof: { doc_count: 100 },
- bark: { doc_count: 75 },
- meow: { doc_count: 50 },
- quack: { doc_count: 25 },
- moo: { doc_count: 5 },
- },
+ availableOptions: [
+ { value: 'woof', docCount: 100 },
+ { value: 'bark', docCount: 75 },
+ { value: 'meow', docCount: 50 },
+ { value: 'quack', docCount: 25 },
+ { value: 'moo', docCount: 5 },
+ ],
invalidSelections: [],
allowExpensiveQueries: true,
popoverOpen: false,
diff --git a/src/plugins/controls/common/options_list/types.ts b/src/plugins/controls/common/options_list/types.ts
index 510dac280fe76..8437eb0382b6e 100644
--- a/src/plugins/controls/common/options_list/types.ts
+++ b/src/plugins/controls/common/options_list/types.ts
@@ -28,9 +28,7 @@ export interface OptionsListEmbeddableInput extends DataControlInput {
placeholder?: string;
}
-export interface OptionsListSuggestions {
- [key: string]: { doc_count: number };
-}
+export type OptionsListSuggestions = Array<{ value: string; docCount?: number }>;
/**
* The Options list response is returned from the serverside Options List route.
diff --git a/src/plugins/controls/public/__stories__/controls.stories.tsx b/src/plugins/controls/public/__stories__/controls.stories.tsx
index 4326ce056d118..a0ba30622e150 100644
--- a/src/plugins/controls/public/__stories__/controls.stories.tsx
+++ b/src/plugins/controls/public/__stories__/controls.stories.tsx
@@ -35,7 +35,11 @@ import { injectStorybookDataView } from '../services/data_views/data_views.story
import { replaceOptionsListMethod } from '../services/options_list/options_list.story';
import { populateStorybookControlFactories } from './storybook_control_factories';
import { replaceValueSuggestionMethod } from '../services/unified_search/unified_search.story';
-import { OptionsListResponse, OptionsListRequest } from '../../common/options_list/types';
+import {
+ OptionsListResponse,
+ OptionsListRequest,
+ OptionsListSuggestions,
+} from '../../common/options_list/types';
export default {
title: 'Controls',
@@ -56,9 +60,9 @@ const storybookStubOptionsListRequest = async (
r({
suggestions: getFlightSearchOptions(request.field.name, request.searchString).reduce(
(o, current, index) => {
- return { ...o, [current]: { doc_count: index } };
+ return [...o, { value: current, docCount: index }];
},
- {}
+ [] as OptionsListSuggestions
),
totalCardinality: 100,
}),
diff --git a/src/plugins/controls/public/options_list/components/options_list_popover.test.tsx b/src/plugins/controls/public/options_list/components/options_list_popover.test.tsx
index ffdb1045cad88..e2fa74dfbf2f1 100644
--- a/src/plugins/controls/public/options_list/components/options_list_popover.test.tsx
+++ b/src/plugins/controls/public/options_list/components/options_list_popover.test.tsx
@@ -70,7 +70,7 @@ describe('Options list popover', () => {
});
test('no available options', async () => {
- const popover = await mountComponent({ componentState: { availableOptions: {} } });
+ const popover = await mountComponent({ componentState: { availableOptions: [] } });
const availableOptionsDiv = findTestSubject(popover, 'optionsList-control-available-options');
const noOptionsDiv = findTestSubject(
availableOptionsDiv,
@@ -127,9 +127,7 @@ describe('Options list popover', () => {
selectedOptions: ['bark', 'woof'],
},
componentState: {
- availableOptions: {
- bark: { doc_count: 75 },
- },
+ availableOptions: [{ value: 'bark', docCount: 75 }],
validSelections: ['bark'],
invalidSelections: ['woof'],
},
@@ -154,9 +152,7 @@ describe('Options list popover', () => {
const popover = await mountComponent({
explicitInput: { selectedOptions: ['bark', 'woof', 'meow'] },
componentState: {
- availableOptions: {
- bark: { doc_count: 75 },
- },
+ availableOptions: [{ value: 'bark', docCount: 75 }],
validSelections: ['bark'],
invalidSelections: ['woof', 'meow'],
},
@@ -219,7 +215,7 @@ describe('Options list popover', () => {
test('if existsSelected = false and no suggestions, then "Exists" does not show up', async () => {
const popover = await mountComponent({
- componentState: { availableOptions: {} },
+ componentState: { availableOptions: [] },
explicitInput: { existsSelected: false },
});
const existsOption = findTestSubject(popover, 'optionsList-control-selection-exists');
diff --git a/src/plugins/controls/public/options_list/components/options_list_popover_suggestions.tsx b/src/plugins/controls/public/options_list/components/options_list_popover_suggestions.tsx
index 8d727bed55e20..e5c14e5ea70fc 100644
--- a/src/plugins/controls/public/options_list/components/options_list_popover_suggestions.tsx
+++ b/src/plugins/controls/public/options_list/components/options_list_popover_suggestions.tsx
@@ -48,7 +48,7 @@ export const OptionsListPopoverSuggestions = ({
const canLoadMoreSuggestions = useMemo(
() =>
totalCardinality
- ? Object.keys(availableOptions ?? {}).length <
+ ? (availableOptions ?? []).length <
Math.min(totalCardinality, MAX_OPTIONS_LIST_REQUEST_SIZE)
: false,
[availableOptions, totalCardinality]
@@ -61,7 +61,7 @@ export const OptionsListPopoverSuggestions = ({
[invalidSelections]
);
const suggestions = useMemo(() => {
- return showOnlySelected ? selectedOptions : Object.keys(availableOptions ?? {});
+ return showOnlySelected ? selectedOptions : availableOptions ?? [];
}, [availableOptions, selectedOptions, showOnlySelected]);
const existsSelectableOption = useMemo(() => {
@@ -79,19 +79,23 @@ export const OptionsListPopoverSuggestions = ({
const [selectableOptions, setSelectableOptions] = useState([]); // will be set in following useEffect
useEffect(() => {
/* This useEffect makes selectableOptions responsive to search, show only selected, and clear selections */
- const options: EuiSelectableOption[] = (suggestions ?? []).map((key) => {
+ const options: EuiSelectableOption[] = (suggestions ?? []).map((suggestion) => {
+ if (typeof suggestion === 'string') {
+ // this means that `showOnlySelected` is true, and doc count is not known when this is the case
+ suggestion = { value: suggestion };
+ }
return {
- key,
- label: key,
- checked: selectedOptionsSet?.has(key) ? 'on' : undefined,
- 'data-test-subj': `optionsList-control-selection-${key}`,
+ key: suggestion.value,
+ label: suggestion.value,
+ checked: selectedOptionsSet?.has(suggestion.value) ? 'on' : undefined,
+ 'data-test-subj': `optionsList-control-selection-${suggestion.value}`,
className:
- showOnlySelected && invalidSelectionsSet.has(key)
+ showOnlySelected && invalidSelectionsSet.has(suggestion.value)
? 'optionsList__selectionInvalid'
: 'optionsList__validSuggestion',
append:
- !showOnlySelected && availableOptions?.[key] ? (
-
+ !showOnlySelected && suggestion?.docCount ? (
+
) : undefined,
};
});
diff --git a/src/plugins/controls/public/options_list/embeddable/options_list_embeddable.tsx b/src/plugins/controls/public/options_list/embeddable/options_list_embeddable.tsx
index e2503b4e530e8..08d5f1150baf5 100644
--- a/src/plugins/controls/public/options_list/embeddable/options_list_embeddable.tsx
+++ b/src/plugins/controls/public/options_list/embeddable/options_list_embeddable.tsx
@@ -371,7 +371,7 @@ export class OptionsListEmbeddable extends Embeddable {
this.dispatch.updateQueryResults({
- availableOptions: {},
+ availableOptions: [],
});
this.dispatch.setLoading(false);
});
diff --git a/src/plugins/controls/public/services/options_list/options_list.story.ts b/src/plugins/controls/public/services/options_list/options_list.story.ts
index 6d3305f97b9aa..cf674887a0ba0 100644
--- a/src/plugins/controls/public/services/options_list/options_list.story.ts
+++ b/src/plugins/controls/public/services/options_list/options_list.story.ts
@@ -18,7 +18,7 @@ let optionsListRequestMethod = async (request: OptionsListRequest, abortSignal:
setTimeout(
() =>
r({
- suggestions: {},
+ suggestions: [],
totalCardinality: 100,
}),
120
diff --git a/src/plugins/controls/server/options_list/options_list_cheap_suggestion_queries.test.ts b/src/plugins/controls/server/options_list/options_list_cheap_suggestion_queries.test.ts
index 31783a1267aca..0476788791f69 100644
--- a/src/plugins/controls/server/options_list/options_list_cheap_suggestion_queries.test.ts
+++ b/src/plugins/controls/server/options_list/options_list_cheap_suggestion_queries.test.ts
@@ -388,17 +388,20 @@ describe('options list cheap queries', () => {
expect(
suggestionAggBuilder.parse(rawSearchResponseMock, optionsListRequestBodyMock).suggestions
).toMatchInlineSnapshot(`
- Object {
- "cool1": Object {
- "doc_count": 5,
+ Array [
+ Object {
+ "docCount": 5,
+ "value": "cool1",
},
- "cool2": Object {
- "doc_count": 15,
+ Object {
+ "docCount": 15,
+ "value": "cool2",
},
- "cool3": Object {
- "doc_count": 10,
+ Object {
+ "docCount": 10,
+ "value": "cool3",
},
- }
+ ]
`);
});
@@ -421,14 +424,16 @@ describe('options list cheap queries', () => {
expect(
suggestionAggBuilder.parse(rawSearchResponseMock, optionsListRequestBodyMock).suggestions
).toMatchInlineSnapshot(`
- Object {
- "false": Object {
- "doc_count": 55,
+ Array [
+ Object {
+ "docCount": 55,
+ "value": "false",
},
- "true": Object {
- "doc_count": 155,
+ Object {
+ "docCount": 155,
+ "value": "true",
},
- }
+ ]
`);
});
@@ -455,17 +460,20 @@ describe('options list cheap queries', () => {
expect(
suggestionAggBuilder.parse(rawSearchResponseMock, optionsListRequestBodyMock).suggestions
).toMatchInlineSnapshot(`
- Object {
- "cool1": Object {
- "doc_count": 5,
+ Array [
+ Object {
+ "docCount": 5,
+ "value": "cool1",
},
- "cool2": Object {
- "doc_count": 15,
+ Object {
+ "docCount": 15,
+ "value": "cool2",
},
- "cool3": Object {
- "doc_count": 10,
+ Object {
+ "docCount": 10,
+ "value": "cool3",
},
- }
+ ]
`);
});
@@ -490,17 +498,20 @@ describe('options list cheap queries', () => {
expect(
suggestionAggBuilder.parse(rawSearchResponseMock, optionsListRequestBodyMock).suggestions
).toMatchInlineSnapshot(`
- Object {
- "cool1": Object {
- "doc_count": 5,
+ Array [
+ Object {
+ "docCount": 5,
+ "value": "cool1",
},
- "cool2": Object {
- "doc_count": 15,
+ Object {
+ "docCount": 15,
+ "value": "cool2",
},
- "cool3": Object {
- "doc_count": 10,
+ Object {
+ "docCount": 10,
+ "value": "cool3",
},
- }
+ ]
`);
});
});
@@ -552,55 +563,50 @@ describe('options list cheap queries', () => {
rawSearchResponseMock,
optionsListRequestBodyMock
).suggestions;
- /** first, verify that the sorting worked as expected */
- expect(Object.keys(parsed)).toMatchInlineSnapshot(`
- Array [
- "52:ae76:5947:5e2a:551:fe6a:712a:c72",
- "111.52.174.2",
- "196.162.13.39",
- "f7a9:640b:b5a0:1219:8d75:ed94:3c3e:2e63",
- "23.216.241.120",
- "28c7:c9a4:42fd:16b0:4de5:e41e:28d9:9172",
- "21.35.91.62",
- "21.35.91.61",
- "203.88.33.151",
- "1ec:aa98:b0a6:d07c:590:18a0:8a33:2eb8",
- ]
- `);
- /** then, make sure the object is structured properly */
+
expect(parsed).toMatchInlineSnapshot(`
- Object {
- "111.52.174.2": Object {
- "doc_count": 11,
+ Array [
+ Object {
+ "docCount": 12,
+ "value": "52:ae76:5947:5e2a:551:fe6a:712a:c72",
},
- "196.162.13.39": Object {
- "doc_count": 10,
+ Object {
+ "docCount": 11,
+ "value": "111.52.174.2",
},
- "1ec:aa98:b0a6:d07c:590:18a0:8a33:2eb8": Object {
- "doc_count": 6,
+ Object {
+ "docCount": 10,
+ "value": "196.162.13.39",
},
- "203.88.33.151": Object {
- "doc_count": 7,
+ Object {
+ "docCount": 10,
+ "value": "f7a9:640b:b5a0:1219:8d75:ed94:3c3e:2e63",
},
- "21.35.91.61": Object {
- "doc_count": 8,
+ Object {
+ "docCount": 9,
+ "value": "23.216.241.120",
},
- "21.35.91.62": Object {
- "doc_count": 8,
+ Object {
+ "docCount": 9,
+ "value": "28c7:c9a4:42fd:16b0:4de5:e41e:28d9:9172",
},
- "23.216.241.120": Object {
- "doc_count": 9,
+ Object {
+ "docCount": 8,
+ "value": "21.35.91.62",
},
- "28c7:c9a4:42fd:16b0:4de5:e41e:28d9:9172": Object {
- "doc_count": 9,
+ Object {
+ "docCount": 8,
+ "value": "21.35.91.61",
},
- "52:ae76:5947:5e2a:551:fe6a:712a:c72": Object {
- "doc_count": 12,
+ Object {
+ "docCount": 7,
+ "value": "203.88.33.151",
},
- "f7a9:640b:b5a0:1219:8d75:ed94:3c3e:2e63": Object {
- "doc_count": 10,
+ Object {
+ "docCount": 6,
+ "value": "1ec:aa98:b0a6:d07c:590:18a0:8a33:2eb8",
},
- }
+ ]
`);
});
});
diff --git a/src/plugins/controls/server/options_list/options_list_cheap_suggestion_queries.ts b/src/plugins/controls/server/options_list/options_list_cheap_suggestion_queries.ts
index 3a302cf62d04b..3b69b2818b909 100644
--- a/src/plugins/controls/server/options_list/options_list_cheap_suggestion_queries.ts
+++ b/src/plugins/controls/server/options_list/options_list_cheap_suggestion_queries.ts
@@ -51,11 +51,11 @@ const cheapSuggestionAggSubtypes: { [key: string]: OptionsListSuggestionAggregat
},
}),
parse: (rawEsResult) => ({
- suggestions: get(rawEsResult, 'aggregations.suggestions.buckets').reduce(
- (suggestions: OptionsListSuggestions, suggestion: EsBucket) => {
- return { ...suggestions, [suggestion.key]: { doc_count: suggestion.doc_count } };
+ suggestions: get(rawEsResult, 'aggregations.suggestions.buckets')?.reduce(
+ (acc: OptionsListSuggestions, suggestion: EsBucket) => {
+ return [...acc, { value: suggestion.key, docCount: suggestion.doc_count }];
},
- {}
+ []
),
}),
},
@@ -75,13 +75,10 @@ const cheapSuggestionAggSubtypes: { [key: string]: OptionsListSuggestionAggregat
}),
parse: (rawEsResult) => ({
suggestions: get(rawEsResult, 'aggregations.suggestions.buckets')?.reduce(
- (suggestions: OptionsListSuggestions, suggestion: EsBucket & { key_as_string: string }) => {
- return {
- ...suggestions,
- [suggestion.key_as_string]: { doc_count: suggestion.doc_count },
- };
+ (acc: OptionsListSuggestions, suggestion: EsBucket & { key_as_string: string }) => {
+ return [...acc, { value: suggestion.key_as_string, docCount: suggestion.doc_count }];
},
- {}
+ []
),
}),
},
@@ -134,7 +131,7 @@ const cheapSuggestionAggSubtypes: { [key: string]: OptionsListSuggestionAggregat
if (!Boolean(rawEsResult.aggregations?.suggestions)) {
// if this is happens, that means there is an invalid search that snuck through to the server side code;
// so, might as well early return with no suggestions
- return { suggestions: {} };
+ return { suggestions: [] };
}
const buckets: EsBucket[] = [];
@@ -153,9 +150,9 @@ const cheapSuggestionAggSubtypes: { [key: string]: OptionsListSuggestionAggregat
return {
suggestions: sortedSuggestions
.slice(0, 10) // only return top 10 results
- .reduce((suggestions, suggestion: EsBucket) => {
- return { ...suggestions, [suggestion.key]: { doc_count: suggestion.doc_count } };
- }, {}),
+ .reduce((acc: OptionsListSuggestions, suggestion: EsBucket) => {
+ return [...acc, { value: suggestion.key, docCount: suggestion.doc_count }];
+ }, []),
};
},
},
@@ -190,11 +187,11 @@ const cheapSuggestionAggSubtypes: { [key: string]: OptionsListSuggestionAggregat
};
},
parse: (rawEsResult) => ({
- suggestions: get(rawEsResult, 'aggregations.nestedSuggestions.suggestions.buckets').reduce(
- (suggestions: OptionsListSuggestions, suggestion: EsBucket) => {
- return { ...suggestions, [suggestion.key]: { doc_count: suggestion.doc_count } };
+ suggestions: get(rawEsResult, 'aggregations.nestedSuggestions.suggestions.buckets')?.reduce(
+ (acc: OptionsListSuggestions, suggestion: EsBucket) => {
+ return [...acc, { value: suggestion.key, docCount: suggestion.doc_count }];
},
- {}
+ []
),
}),
},
diff --git a/src/plugins/controls/server/options_list/options_list_expensive_suggestion_queries.test.ts b/src/plugins/controls/server/options_list/options_list_expensive_suggestion_queries.test.ts
index 7026359e10ee4..5638cbc347366 100644
--- a/src/plugins/controls/server/options_list/options_list_expensive_suggestion_queries.test.ts
+++ b/src/plugins/controls/server/options_list/options_list_expensive_suggestion_queries.test.ts
@@ -466,17 +466,20 @@ describe('options list expensive queries', () => {
expect(suggestionAggBuilder.parse(rawSearchResponseMock, optionsListRequestBodyMock))
.toMatchInlineSnapshot(`
Object {
- "suggestions": Object {
- "cool1": Object {
- "doc_count": 5,
+ "suggestions": Array [
+ Object {
+ "docCount": 5,
+ "value": "cool1",
},
- "cool2": Object {
- "doc_count": 15,
+ Object {
+ "docCount": 15,
+ "value": "cool2",
},
- "cool3": Object {
- "doc_count": 10,
+ Object {
+ "docCount": 10,
+ "value": "cool3",
},
- },
+ ],
"totalCardinality": 3,
}
`);
@@ -503,14 +506,16 @@ describe('options list expensive queries', () => {
expect(suggestionAggBuilder.parse(rawSearchResponseMock, optionsListRequestBodyMock))
.toMatchInlineSnapshot(`
Object {
- "suggestions": Object {
- "false": Object {
- "doc_count": 55,
+ "suggestions": Array [
+ Object {
+ "docCount": 55,
+ "value": "false",
},
- "true": Object {
- "doc_count": 155,
+ Object {
+ "docCount": 155,
+ "value": "true",
},
- },
+ ],
"totalCardinality": 2,
}
`);
@@ -546,17 +551,20 @@ describe('options list expensive queries', () => {
expect(suggestionAggBuilder.parse(rawSearchResponseMock, optionsListRequestBodyMock))
.toMatchInlineSnapshot(`
Object {
- "suggestions": Object {
- "cool1": Object {
- "doc_count": 5,
+ "suggestions": Array [
+ Object {
+ "docCount": 5,
+ "value": "cool1",
},
- "cool2": Object {
- "doc_count": 15,
+ Object {
+ "docCount": 15,
+ "value": "cool2",
},
- "cool3": Object {
- "doc_count": 10,
+ Object {
+ "docCount": 10,
+ "value": "cool3",
},
- },
+ ],
"totalCardinality": 3,
}
`);
@@ -621,55 +629,50 @@ describe('options list expensive queries', () => {
rawSearchResponseMock,
optionsListRequestBodyMock
).suggestions;
- /** first, verify that the sorting worked as expected */
- expect(Object.keys(parsed)).toMatchInlineSnapshot(`
- Array [
- "52:ae76:5947:5e2a:551:fe6a:712a:c72",
- "111.52.174.2",
- "196.162.13.39",
- "f7a9:640b:b5a0:1219:8d75:ed94:3c3e:2e63",
- "23.216.241.120",
- "28c7:c9a4:42fd:16b0:4de5:e41e:28d9:9172",
- "21.35.91.62",
- "21.35.91.61",
- "203.88.33.151",
- "1ec:aa98:b0a6:d07c:590:18a0:8a33:2eb8",
- ]
- `);
- /** then, make sure the object is structured properly */
+
expect(parsed).toMatchInlineSnapshot(`
- Object {
- "111.52.174.2": Object {
- "doc_count": 11,
+ Array [
+ Object {
+ "docCount": 12,
+ "value": "52:ae76:5947:5e2a:551:fe6a:712a:c72",
},
- "196.162.13.39": Object {
- "doc_count": 10,
+ Object {
+ "docCount": 11,
+ "value": "111.52.174.2",
},
- "1ec:aa98:b0a6:d07c:590:18a0:8a33:2eb8": Object {
- "doc_count": 6,
+ Object {
+ "docCount": 10,
+ "value": "196.162.13.39",
},
- "203.88.33.151": Object {
- "doc_count": 7,
+ Object {
+ "docCount": 10,
+ "value": "f7a9:640b:b5a0:1219:8d75:ed94:3c3e:2e63",
},
- "21.35.91.61": Object {
- "doc_count": 8,
+ Object {
+ "docCount": 9,
+ "value": "23.216.241.120",
},
- "21.35.91.62": Object {
- "doc_count": 8,
+ Object {
+ "docCount": 9,
+ "value": "28c7:c9a4:42fd:16b0:4de5:e41e:28d9:9172",
},
- "23.216.241.120": Object {
- "doc_count": 9,
+ Object {
+ "docCount": 8,
+ "value": "21.35.91.62",
},
- "28c7:c9a4:42fd:16b0:4de5:e41e:28d9:9172": Object {
- "doc_count": 9,
+ Object {
+ "docCount": 8,
+ "value": "21.35.91.61",
},
- "52:ae76:5947:5e2a:551:fe6a:712a:c72": Object {
- "doc_count": 12,
+ Object {
+ "docCount": 7,
+ "value": "203.88.33.151",
},
- "f7a9:640b:b5a0:1219:8d75:ed94:3c3e:2e63": Object {
- "doc_count": 10,
+ Object {
+ "docCount": 6,
+ "value": "1ec:aa98:b0a6:d07c:590:18a0:8a33:2eb8",
},
- }
+ ]
`);
});
});
diff --git a/src/plugins/controls/server/options_list/options_list_expensive_suggestion_queries.ts b/src/plugins/controls/server/options_list/options_list_expensive_suggestion_queries.ts
index 63347f8d436d3..a1114191d1fa8 100644
--- a/src/plugins/controls/server/options_list/options_list_expensive_suggestion_queries.ts
+++ b/src/plugins/controls/server/options_list/options_list_expensive_suggestion_queries.ts
@@ -93,9 +93,9 @@ const expensiveSuggestionAggSubtypes: { [key: string]: OptionsListSuggestionAggr
const suggestions = get(rawEsResult, `${basePath}.suggestions.buckets`)?.reduce(
(acc: OptionsListSuggestions, suggestion: EsBucket) => {
- return { ...acc, [suggestion.key]: { doc_count: suggestion.doc_count } };
+ return [...acc, { value: suggestion.key, docCount: suggestion.doc_count }];
},
- {}
+ []
);
return {
suggestions,
@@ -120,14 +120,11 @@ const expensiveSuggestionAggSubtypes: { [key: string]: OptionsListSuggestionAggr
parse: (rawEsResult) => {
const suggestions = get(rawEsResult, 'aggregations.suggestions.buckets')?.reduce(
(acc: OptionsListSuggestions, suggestion: EsBucket & { key_as_string: string }) => {
- return {
- ...acc,
- [suggestion.key_as_string]: { doc_count: suggestion.doc_count },
- };
+ return [...acc, { value: suggestion.key_as_string, docCount: suggestion.doc_count }];
},
- {}
+ []
);
- return { suggestions, totalCardinality: Object.keys(suggestions).length }; // cardinality is only ever 0, 1, or 2 so safe to use length here
+ return { suggestions, totalCardinality: suggestions.length }; // cardinality is only ever 0, 1, or 2 so safe to use length here
},
},
@@ -185,7 +182,7 @@ const expensiveSuggestionAggSubtypes: { [key: string]: OptionsListSuggestionAggr
if (!Boolean(rawEsResult.aggregations?.suggestions)) {
// if this is happens, that means there is an invalid search that snuck through to the server side code;
// so, might as well early return with no suggestions
- return { suggestions: {}, totalCardinality: 0 };
+ return { suggestions: [], totalCardinality: 0 };
}
const buckets: EsBucket[] = [];
getIpBuckets(rawEsResult, buckets, 'ipv4'); // modifies buckets array directly, i.e. "by reference"
@@ -200,11 +197,11 @@ const expensiveSuggestionAggSubtypes: { [key: string]: OptionsListSuggestionAggr
(bucketA: EsBucket, bucketB: EsBucket) => bucketB.doc_count - bucketA.doc_count
);
- const suggestions: OptionsListSuggestions = sortedSuggestions
+ const suggestions = sortedSuggestions
.slice(0, request.size)
.reduce((acc: OptionsListSuggestions, suggestion: EsBucket) => {
- return { ...acc, [suggestion.key]: { doc_count: suggestion.doc_count } };
- }, {});
+ return [...acc, { value: suggestion.key, docCount: suggestion.doc_count }];
+ }, []);
const totalCardinality =
(get(rawEsResult, `aggregations.suggestions.buckets.ipv4.unique_terms.value`) ?? 0) +
(get(rawEsResult, `aggregations.suggestions.buckets.ipv6.unique_terms.value`) ?? 0);
From 6ec97802d97d54599546e58374866a389ec412ed Mon Sep 17 00:00:00 2001
From: Tiago Costa
Date: Wed, 26 Apr 2023 19:44:22 +0100
Subject: [PATCH 32/73] skip failing version bump suite (#155924)
---
x-pack/test/accessibility/apps/ingest_node_pipelines.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/x-pack/test/accessibility/apps/ingest_node_pipelines.ts b/x-pack/test/accessibility/apps/ingest_node_pipelines.ts
index 4bbd9cde06d2d..a2aa7e3c860fe 100644
--- a/x-pack/test/accessibility/apps/ingest_node_pipelines.ts
+++ b/x-pack/test/accessibility/apps/ingest_node_pipelines.ts
@@ -14,7 +14,8 @@ export default function ({ getService, getPageObjects }: any) {
const log = getService('log');
const a11y = getService('a11y'); /* this is the wrapping service around axe */
- describe('Ingest Pipelines Accessibility', async () => {
+ // FAILING VERSION BUMP: https://github.com/elastic/kibana/issues/155924
+ describe.skip('Ingest Pipelines Accessibility', async () => {
before(async () => {
await putSamplePipeline(esClient);
await common.navigateToApp('ingestPipelines');
From 5e713fb225f5cd2312878c515f61f7f01b4a5e49 Mon Sep 17 00:00:00 2001
From: Nicolas Chaulet
Date: Wed, 26 Apr 2023 14:45:25 -0400
Subject: [PATCH 33/73] [Fleet] Add unit test for fleet agent id verification
config flag (#155720)
---
.../elasticsearch/template/template.test.ts | 33 +++++++++++++++++--
1 file changed, 30 insertions(+), 3 deletions(-)
diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts
index 8ef565ccd320a..5668ebacd5258 100644
--- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts
+++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts
@@ -17,6 +17,10 @@ import { appContextService } from '../../..';
import type { RegistryDataStream } from '../../../../types';
import { processFields } from '../../fields/field';
import type { Field } from '../../fields/field';
+import {
+ FLEET_COMPONENT_TEMPLATES,
+ FLEET_GLOBALS_COMPONENT_TEMPLATE_NAME,
+} from '../../../../constants';
import {
generateMappings,
@@ -26,7 +30,9 @@ import {
updateCurrentWriteIndices,
} from './template';
-const FLEET_COMPONENT_TEMPLATES = ['.fleet_globals-1', '.fleet_agent_id_verification-1'];
+const FLEET_COMPONENT_TEMPLATES_NAMES = FLEET_COMPONENT_TEMPLATES.map(
+ (componentTemplate) => componentTemplate.name
+);
// Add our own serialiser to just do JSON.stringify
expect.addSnapshotSerializer({
@@ -69,7 +75,28 @@ describe('EPM template', () => {
});
expect(template.composed_of).toStrictEqual([
...composedOfTemplates,
- ...FLEET_COMPONENT_TEMPLATES,
+ ...FLEET_COMPONENT_TEMPLATES_NAMES,
+ ]);
+ });
+
+ it('does not create fleet agent id verification component template if agentIdVerification is disabled', () => {
+ appContextService.start(
+ createAppContextStartContractMock({
+ agentIdVerificationEnabled: false,
+ })
+ );
+ const composedOfTemplates = ['component1', 'component2'];
+
+ const template = getTemplate({
+ templateIndexPattern: 'name-*',
+ packageName: 'nginx',
+ composedOfTemplates,
+ templatePriority: 200,
+ mappings: { properties: [] },
+ });
+ expect(template.composed_of).toStrictEqual([
+ ...composedOfTemplates,
+ FLEET_GLOBALS_COMPONENT_TEMPLATE_NAME,
]);
});
@@ -83,7 +110,7 @@ describe('EPM template', () => {
templatePriority: 200,
mappings: { properties: [] },
});
- expect(template.composed_of).toStrictEqual(FLEET_COMPONENT_TEMPLATES);
+ expect(template.composed_of).toStrictEqual(FLEET_COMPONENT_TEMPLATES_NAMES);
});
it('adds hidden field correctly', () => {
From 8e37b3841701e0343f265e544920697f1f6a8f59 Mon Sep 17 00:00:00 2001
From: Clint Andrew Hall
Date: Wed, 26 Apr 2023 14:48:23 -0400
Subject: [PATCH 34/73] [serverless] Create the Serverless Plugin (#155582)
> Derived from https://github.com/elastic/kibana/pull/153274 for
production.
## Summary
This PR creates the `serverless` plugin for Kibana Serverless projects.
![image](https://user-images.githubusercontent.com/297604/233892935-b3713575-a2f7-4e82-a9dd-e8c11823683f.png)
It uses the methodology proven out in the proof-of-concept
(https://github.com/elastic/kibana/pull/153274) and prepares it for
production:
- Adds chrome style and related API to the `chrome` services.
- Creates the `serverless` plugin.
- Invokes the new chrome style API for all serverless projects.
- Alters `yarn` scripts to support all project types, and switching
between them.
- Creates the new "Project Switcher" component for use in the new chrome
header for Serverless.
- Creates a Storybook config for this and future components.
- Adds API endpoint to trigger project switching and `Watcher` restarts.
## Next steps
- [x] Creating a PR for enabling/disabling related plugins for
Serverless. (https://github.com/elastic/kibana/pull/155583)
- [ ] Creating product plugin PR based on
https://github.com/elastic/kibana/pull/153274.
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
---
.../steps/storybooks/build_and_upload.ts | 1 +
.github/CODEOWNERS | 4 +
.i18nrc.json | 1 +
config/serverless.es.yml | 1 +
config/serverless.oblt.yml | 1 +
config/serverless.security.yml | 1 +
config/serverless.yml | 2 +
docs/developer/plugin-list.asciidoc | 4 +
package.json | 5 +
.../src/chrome_service.test.ts | 2 +
.../src/chrome_service.tsx | 99 ++++++++----
.../src/ui/index.ts | 1 +
.../src/ui/project/header.tsx | 90 +++++++++++
.../src/ui/project/index.ts | 9 ++
.../src/ui/project/navigation.tsx | 76 +++++++++
.../tsconfig.json | 5 +-
.../src/chrome_service.mock.ts | 2 +
.../core/chrome/core-chrome-browser/index.ts | 27 ++--
.../core-chrome-browser/src/contracts.ts | 13 +-
.../chrome/core-chrome-browser/src/index.ts | 2 +-
.../chrome/core-chrome-browser/src/types.ts | 3 +
packages/kbn-optimizer/limits.yml | 1 +
.../serverless/project_switcher/README.mdx | 12 ++
packages/serverless/project_switcher/index.ts | 11 ++
.../project_switcher/jest.config.js | 13 ++
.../serverless/project_switcher/kibana.jsonc | 5 +
.../project_switcher/mocks/jest.mock.ts | 23 +++
.../project_switcher/mocks/storybook.mock.ts | 51 ++++++
.../serverless/project_switcher/package.json | 6 +
.../project_switcher/src/constants.ts | 24 +++
.../project_switcher/src/header_button.tsx | 31 ++++
.../serverless/project_switcher/src/index.ts | 12 ++
.../serverless/project_switcher/src/item.tsx | 33 ++++
.../project_switcher/src/loader.tsx | 21 +++
.../serverless/project_switcher/src/logo.tsx | 32 ++++
.../project_switcher/src/services.tsx | 63 ++++++++
.../src/switcher.component.tsx | 73 +++++++++
.../project_switcher/src/switcher.stories.tsx | 42 +++++
.../project_switcher/src/switcher.test.tsx | 151 ++++++++++++++++++
.../project_switcher/src/switcher.tsx | 21 +++
.../serverless/project_switcher/src/types.ts | 39 +++++
.../serverless/project_switcher/tsconfig.json | 23 +++
.../serverless/storybook/config/README.mdx | 5 +
.../serverless/storybook/config/constants.ts | 13 ++
packages/serverless/storybook/config/index.ts | 9 ++
.../serverless/storybook/config/kibana.jsonc | 6 +
packages/serverless/storybook/config/main.ts | 17 ++
.../serverless/storybook/config/manager.ts | 23 +++
.../serverless/storybook/config/package.json | 6 +
.../serverless/storybook/config/preview.ts | 22 +++
.../serverless/storybook/config/tsconfig.json | 19 +++
packages/serverless/types/README.mdx | 10 ++
packages/serverless/types/index.d.ts | 9 ++
packages/serverless/types/kibana.jsonc | 5 +
packages/serverless/types/package.json | 6 +
packages/serverless/types/tsconfig.json | 17 ++
src/cli/serve/serve.js | 50 +++++-
src/core/public/styles/rendering/_base.scss | 3 +
src/dev/storybook/aliases.ts | 1 +
.../test_suites/core_plugins/rendering.ts | 2 +
tsconfig.base.json | 8 +
x-pack/.i18nrc.json | 1 +
x-pack/plugins/security/public/config.ts | 1 +
.../nav_control/nav_control_service.tsx | 7 +-
x-pack/plugins/security/public/plugin.tsx | 1 +
x-pack/plugins/security/server/config.test.ts | 6 +
x-pack/plugins/security/server/config.ts | 2 +
x-pack/plugins/security/server/index.ts | 1 +
x-pack/plugins/serverless/README.mdx | 22 +++
x-pack/plugins/serverless/assets/diagram.png | Bin 0 -> 438787 bytes
x-pack/plugins/serverless/common/index.ts | 12 ++
x-pack/plugins/serverless/kibana.jsonc | 21 +++
x-pack/plugins/serverless/package.json | 11 ++
x-pack/plugins/serverless/public/config.ts | 15 ++
x-pack/plugins/serverless/public/index.ts | 15 ++
x-pack/plugins/serverless/public/plugin.tsx | 65 ++++++++
x-pack/plugins/serverless/public/types.ts | 12 ++
x-pack/plugins/serverless/server/config.ts | 58 +++++++
x-pack/plugins/serverless/server/index.ts | 16 ++
x-pack/plugins/serverless/server/plugin.ts | 92 +++++++++++
x-pack/plugins/serverless/server/types.ts | 12 ++
x-pack/plugins/serverless/tsconfig.json | 24 +++
yarn.lock | 16 ++
83 files changed, 1621 insertions(+), 56 deletions(-)
create mode 100644 packages/core/chrome/core-chrome-browser-internal/src/ui/project/header.tsx
create mode 100644 packages/core/chrome/core-chrome-browser-internal/src/ui/project/index.ts
create mode 100644 packages/core/chrome/core-chrome-browser-internal/src/ui/project/navigation.tsx
create mode 100644 packages/serverless/project_switcher/README.mdx
create mode 100644 packages/serverless/project_switcher/index.ts
create mode 100644 packages/serverless/project_switcher/jest.config.js
create mode 100644 packages/serverless/project_switcher/kibana.jsonc
create mode 100644 packages/serverless/project_switcher/mocks/jest.mock.ts
create mode 100644 packages/serverless/project_switcher/mocks/storybook.mock.ts
create mode 100644 packages/serverless/project_switcher/package.json
create mode 100644 packages/serverless/project_switcher/src/constants.ts
create mode 100644 packages/serverless/project_switcher/src/header_button.tsx
create mode 100644 packages/serverless/project_switcher/src/index.ts
create mode 100644 packages/serverless/project_switcher/src/item.tsx
create mode 100644 packages/serverless/project_switcher/src/loader.tsx
create mode 100644 packages/serverless/project_switcher/src/logo.tsx
create mode 100644 packages/serverless/project_switcher/src/services.tsx
create mode 100644 packages/serverless/project_switcher/src/switcher.component.tsx
create mode 100644 packages/serverless/project_switcher/src/switcher.stories.tsx
create mode 100644 packages/serverless/project_switcher/src/switcher.test.tsx
create mode 100644 packages/serverless/project_switcher/src/switcher.tsx
create mode 100644 packages/serverless/project_switcher/src/types.ts
create mode 100644 packages/serverless/project_switcher/tsconfig.json
create mode 100644 packages/serverless/storybook/config/README.mdx
create mode 100644 packages/serverless/storybook/config/constants.ts
create mode 100755 packages/serverless/storybook/config/index.ts
create mode 100644 packages/serverless/storybook/config/kibana.jsonc
create mode 100644 packages/serverless/storybook/config/main.ts
create mode 100644 packages/serverless/storybook/config/manager.ts
create mode 100644 packages/serverless/storybook/config/package.json
create mode 100644 packages/serverless/storybook/config/preview.ts
create mode 100644 packages/serverless/storybook/config/tsconfig.json
create mode 100644 packages/serverless/types/README.mdx
create mode 100644 packages/serverless/types/index.d.ts
create mode 100644 packages/serverless/types/kibana.jsonc
create mode 100644 packages/serverless/types/package.json
create mode 100644 packages/serverless/types/tsconfig.json
create mode 100755 x-pack/plugins/serverless/README.mdx
create mode 100644 x-pack/plugins/serverless/assets/diagram.png
create mode 100644 x-pack/plugins/serverless/common/index.ts
create mode 100644 x-pack/plugins/serverless/kibana.jsonc
create mode 100644 x-pack/plugins/serverless/package.json
create mode 100644 x-pack/plugins/serverless/public/config.ts
create mode 100644 x-pack/plugins/serverless/public/index.ts
create mode 100644 x-pack/plugins/serverless/public/plugin.tsx
create mode 100644 x-pack/plugins/serverless/public/types.ts
create mode 100644 x-pack/plugins/serverless/server/config.ts
create mode 100644 x-pack/plugins/serverless/server/index.ts
create mode 100644 x-pack/plugins/serverless/server/plugin.ts
create mode 100644 x-pack/plugins/serverless/server/types.ts
create mode 100644 x-pack/plugins/serverless/tsconfig.json
diff --git a/.buildkite/scripts/steps/storybooks/build_and_upload.ts b/.buildkite/scripts/steps/storybooks/build_and_upload.ts
index b16e75abdb8a1..3796076d0a3cd 100644
--- a/.buildkite/scripts/steps/storybooks/build_and_upload.ts
+++ b/.buildkite/scripts/steps/storybooks/build_and_upload.ts
@@ -43,6 +43,7 @@ const STORYBOOKS = [
'observability',
'presentation',
'security_solution',
+ 'serverless',
'shared_ux',
'triggers_actions_ui',
'ui_actions_enhanced',
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 93f49f1277ac7..d76ed31b2b2b1 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -572,6 +572,10 @@ packages/kbn-securitysolution-t-grid @elastic/security-solution-platform
packages/kbn-securitysolution-utils @elastic/security-solution-platform
packages/kbn-server-http-tools @elastic/kibana-core
packages/kbn-server-route-repository @elastic/apm-ui
+x-pack/plugins/serverless @elastic/appex-sharedux
+packages/serverless/project_switcher @elastic/appex-sharedux
+packages/serverless/storybook/config @elastic/appex-sharedux
+packages/serverless/types @elastic/appex-sharedux
test/plugin_functional/plugins/session_notifications @elastic/kibana-core
x-pack/plugins/session_view @elastic/sec-cloudnative-integrations
packages/kbn-set-map @elastic/kibana-operations
diff --git a/.i18nrc.json b/.i18nrc.json
index 31ab7a91e206c..c2bf494c68c06 100644
--- a/.i18nrc.json
+++ b/.i18nrc.json
@@ -83,6 +83,7 @@
"share": "src/plugins/share",
"sharedUXPackages": "packages/shared-ux",
"securitySolutionPackages": "x-pack/packages/security-solution",
+ "serverlessPackages": "packages/serverless",
"coloring": "packages/kbn-coloring/src",
"languageDocumentationPopover": "packages/kbn-language-documentation-popover/src",
"statusPage": "src/legacy/core_plugins/status_page",
diff --git a/config/serverless.es.yml b/config/serverless.es.yml
index e69de29bb2d1d..71b4f03446401 100644
--- a/config/serverless.es.yml
+++ b/config/serverless.es.yml
@@ -0,0 +1 @@
+xpack.serverless.plugin.developer.projectSwitcher.currentType: 'search'
diff --git a/config/serverless.oblt.yml b/config/serverless.oblt.yml
index ba76648238348..ddf1066edb882 100644
--- a/config/serverless.oblt.yml
+++ b/config/serverless.oblt.yml
@@ -1 +1,2 @@
xpack.infra.logs.app_target: discover
+xpack.serverless.plugin.developer.projectSwitcher.currentType: 'observability'
diff --git a/config/serverless.security.yml b/config/serverless.security.yml
index e69de29bb2d1d..efa3558e0e9d9 100644
--- a/config/serverless.security.yml
+++ b/config/serverless.security.yml
@@ -0,0 +1 @@
+xpack.serverless.plugin.developer.projectSwitcher.currentType: 'security'
diff --git a/config/serverless.yml b/config/serverless.yml
index e65b15f064328..ec24139422975 100644
--- a/config/serverless.yml
+++ b/config/serverless.yml
@@ -1,4 +1,6 @@
newsfeed.enabled: false
+xpack.security.showNavLinks: false
+xpack.serverless.plugin.enabled: true
xpack.fleet.enableExperimental: ['fleetServerStandalone']
xpack.fleet.internal.disableILMPolicies: true
diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc
index 6b534f45b4f2d..412a9e8b5569e 100644
--- a/docs/developer/plugin-list.asciidoc
+++ b/docs/developer/plugin-list.asciidoc
@@ -706,6 +706,10 @@ Kibana.
|Welcome to the Kibana Security Solution plugin! This README will go over getting started with development and testing.
+|{kib-repo}blob/{branch}/x-pack/plugins/serverless/README.mdx[serverless]
+|
+
+
|{kib-repo}blob/{branch}/x-pack/plugins/session_view/README.md[sessionView]
|Session View is meant to provide a visualization into what is going on in a particular Linux environment where the agent is running. It looks likes a terminal emulator; however, it is a tool for introspecting process activity and understanding user and service behaviour in your Linux servers and infrastructure. It is a time-ordered series of process executions displayed in a tree over time.
diff --git a/package.json b/package.json
index e6761869fa3c7..0751d8d84ea09 100644
--- a/package.json
+++ b/package.json
@@ -54,6 +54,7 @@
"lint:es": "node scripts/eslint",
"lint:style": "node scripts/stylelint",
"makelogs": "node scripts/makelogs",
+ "serverless": "node scripts/kibana --dev --serverless",
"serverless-es": "node scripts/kibana --dev --serverless=es",
"serverless-oblt": "node scripts/kibana --dev --serverless=oblt",
"serverless-security": "node scripts/kibana --dev --serverless=security",
@@ -573,6 +574,9 @@
"@kbn/securitysolution-utils": "link:packages/kbn-securitysolution-utils",
"@kbn/server-http-tools": "link:packages/kbn-server-http-tools",
"@kbn/server-route-repository": "link:packages/kbn-server-route-repository",
+ "@kbn/serverless": "link:x-pack/plugins/serverless",
+ "@kbn/serverless-project-switcher": "link:packages/serverless/project_switcher",
+ "@kbn/serverless-types": "link:packages/serverless/types",
"@kbn/session-notifications-plugin": "link:test/plugin_functional/plugins/session_notifications",
"@kbn/session-view-plugin": "link:x-pack/plugins/session_view",
"@kbn/set-map": "link:packages/kbn-set-map",
@@ -1112,6 +1116,7 @@
"@kbn/repo-source-classifier": "link:packages/kbn-repo-source-classifier",
"@kbn/repo-source-classifier-cli": "link:packages/kbn-repo-source-classifier-cli",
"@kbn/security-api-integration-helpers": "link:x-pack/test/security_api_integration/packages/helpers",
+ "@kbn/serverless-storybook-config": "link:packages/serverless/storybook/config",
"@kbn/some-dev-log": "link:packages/kbn-some-dev-log",
"@kbn/sort-package-json": "link:packages/kbn-sort-package-json",
"@kbn/spec-to-console": "link:packages/kbn-spec-to-console",
diff --git a/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.test.ts b/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.test.ts
index 37b1b9a2eab7d..0087c5d019f98 100644
--- a/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.test.ts
+++ b/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.test.ts
@@ -126,6 +126,7 @@ describe('start', () => {
Array [
Array [
"kbnBody",
+ "kbnBody--classicLayout",
"kbnBody--noHeaderBanner",
"kbnBody--chromeHidden",
"kbnVersion-1-2-3",
@@ -143,6 +144,7 @@ describe('start', () => {
Array [
Array [
"kbnBody",
+ "kbnBody--classicLayout",
"kbnBody--noHeaderBanner",
"kbnBody--chromeHidden",
"kbnVersion-8-0-0",
diff --git a/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.tsx b/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.tsx
index 4e0762aee8620..41362b3d80dcd 100644
--- a/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.tsx
+++ b/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.tsx
@@ -26,6 +26,7 @@ import type {
ChromeGlobalHelpExtensionMenuLink,
ChromeHelpExtension,
ChromeUserBanner,
+ ChromeStyle,
} from '@kbn/core-chrome-browser';
import type { CustomBrandingStart } from '@kbn/core-custom-branding-browser';
import { KIBANA_ASK_ELASTIC_LINK } from './constants';
@@ -33,7 +34,7 @@ import { DocTitleService } from './doc_title';
import { NavControlsService } from './nav_controls';
import { NavLinksService } from './nav_links';
import { RecentlyAccessedService } from './recently_accessed';
-import { Header } from './ui';
+import { Header, ProjectHeader } from './ui';
import type { InternalChromeStart } from './types';
const IS_LOCKED_KEY = 'core.chrome.isLocked';
@@ -119,6 +120,7 @@ export class ChromeService {
const customNavLink$ = new BehaviorSubject(undefined);
const helpSupportUrl$ = new BehaviorSubject(KIBANA_ASK_ELASTIC_LINK);
const isNavDrawerLocked$ = new BehaviorSubject(localStorage.getItem(IS_LOCKED_KEY) === 'true');
+ const chromeStyle$ = new BehaviorSubject('classic');
const getKbnVersionClass = () => {
// we assume that the version is valid and has the form 'X.X.X'
@@ -131,10 +133,11 @@ export class ChromeService {
};
const headerBanner$ = new BehaviorSubject(undefined);
- const bodyClasses$ = combineLatest([headerBanner$, this.isVisible$!]).pipe(
- map(([headerBanner, isVisible]) => {
+ const bodyClasses$ = combineLatest([headerBanner$, this.isVisible$!, chromeStyle$]).pipe(
+ map(([headerBanner, isVisible, chromeStyle]) => {
return [
'kbnBody',
+ chromeStyle === 'project' ? 'kbnBody--projectLayout' : 'kbnBody--classicLayout',
headerBanner ? 'kbnBody--hasHeaderBanner' : 'kbnBody--noHeaderBanner',
isVisible ? 'kbnBody--chromeVisible' : 'kbnBody--chromeHidden',
getKbnVersionClass(),
@@ -163,6 +166,10 @@ export class ChromeService {
const getIsNavDrawerLocked$ = isNavDrawerLocked$.pipe(takeUntil(this.stop$));
+ const setChromeStyle = (style: ChromeStyle) => {
+ chromeStyle$.next(style);
+ };
+
const isIE = () => {
const ua = window.navigator.userAgent;
const msie = ua.indexOf('MSIE '); // IE 10 or older
@@ -203,41 +210,65 @@ export class ChromeService {
});
}
+ const getHeaderComponent = () => {
+ const Component = ({ style$ }: { style$: typeof chromeStyle$ }) => {
+ if (style$.getValue() === 'project') {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+ };
+ return ;
+ };
+
return {
navControls,
navLinks,
recentlyAccessed,
docTitle,
-
- getHeaderComponent: () => (
-
- ),
+ getHeaderComponent,
getIsVisible$: () => this.isVisible$,
@@ -302,6 +333,8 @@ export class ChromeService {
},
getBodyClasses$: () => bodyClasses$.pipe(takeUntil(this.stop$)),
+ setChromeStyle,
+ getChromeStyle$: () => chromeStyle$.pipe(takeUntil(this.stop$)),
};
}
diff --git a/packages/core/chrome/core-chrome-browser-internal/src/ui/index.ts b/packages/core/chrome/core-chrome-browser-internal/src/ui/index.ts
index 5afd3e0f587bb..7a5ecadd26f23 100644
--- a/packages/core/chrome/core-chrome-browser-internal/src/ui/index.ts
+++ b/packages/core/chrome/core-chrome-browser-internal/src/ui/index.ts
@@ -7,5 +7,6 @@
*/
export { Header } from './header';
+export { ProjectHeader } from './project';
export { LoadingIndicator } from './loading_indicator';
export type { NavType } from './header';
diff --git a/packages/core/chrome/core-chrome-browser-internal/src/ui/project/header.tsx b/packages/core/chrome/core-chrome-browser-internal/src/ui/project/header.tsx
new file mode 100644
index 0000000000000..e85ae262c3bb7
--- /dev/null
+++ b/packages/core/chrome/core-chrome-browser-internal/src/ui/project/header.tsx
@@ -0,0 +1,90 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import React from 'react';
+import { Router } from 'react-router-dom';
+import { EuiHeader, EuiHeaderLogo, EuiHeaderSection, EuiHeaderSectionItem } from '@elastic/eui';
+import {
+ ChromeBreadcrumb,
+ ChromeGlobalHelpExtensionMenuLink,
+ ChromeHelpExtension,
+ ChromeNavControl,
+} from '@kbn/core-chrome-browser/src';
+import { Observable } from 'rxjs';
+import { MountPoint } from '@kbn/core-mount-utils-browser';
+import { InternalApplicationStart } from '@kbn/core-application-browser-internal';
+import { HeaderBreadcrumbs } from '../header/header_breadcrumbs';
+import { HeaderActionMenu } from '../header/header_action_menu';
+import { HeaderHelpMenu } from '../header/header_help_menu';
+import { HeaderNavControls } from '../header/header_nav_controls';
+import { ProjectNavigation } from './navigation';
+
+interface Props {
+ breadcrumbs$: Observable;
+ actionMenu$: Observable;
+ kibanaDocLink: string;
+ globalHelpExtensionMenuLinks$: Observable;
+ helpExtension$: Observable;
+ helpSupportUrl$: Observable;
+ kibanaVersion: string;
+ application: InternalApplicationStart;
+ navControlsRight$: Observable;
+}
+
+export const ProjectHeader = ({
+ application,
+ kibanaDocLink,
+ kibanaVersion,
+ ...observables
+}: Props) => {
+ const renderLogo = () => (
+ e.preventDefault()}
+ aria-label="Go to home page"
+ />
+ );
+
+ return (
+ <>
+
+
+ {renderLogo()}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/packages/core/chrome/core-chrome-browser-internal/src/ui/project/index.ts b/packages/core/chrome/core-chrome-browser-internal/src/ui/project/index.ts
new file mode 100644
index 0000000000000..af18e057731b0
--- /dev/null
+++ b/packages/core/chrome/core-chrome-browser-internal/src/ui/project/index.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export { ProjectHeader } from './header';
diff --git a/packages/core/chrome/core-chrome-browser-internal/src/ui/project/navigation.tsx b/packages/core/chrome/core-chrome-browser-internal/src/ui/project/navigation.tsx
new file mode 100644
index 0000000000000..20549325ec851
--- /dev/null
+++ b/packages/core/chrome/core-chrome-browser-internal/src/ui/project/navigation.tsx
@@ -0,0 +1,76 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import React, { useCallback } from 'react';
+import useLocalStorage from 'react-use/lib/useLocalStorage';
+import { css } from '@emotion/react';
+
+import { i18n } from '@kbn/i18n';
+import { EuiButtonIcon, EuiCollapsibleNav, EuiThemeProvider, useEuiTheme } from '@elastic/eui';
+
+const LOCAL_STORAGE_IS_OPEN_KEY = 'PROJECT_NAVIGATION_OPEN' as const;
+const SIZE_OPEN = 248;
+const SIZE_CLOSED = 40;
+
+const buttonCSS = css`
+ margin-left: -32px;
+ margin-top: 12px;
+ position: fixed;
+ z-index: 1000;
+`;
+
+const openAriaLabel = i18n.translate('core.ui.chrome.projectNav.collapsibleNavOpenAriaLabel', {
+ defaultMessage: 'Close navigation',
+});
+
+const closedAriaLabel = i18n.translate('core.ui.chrome.projectNav.collapsibleNavClosedAriaLabel', {
+ defaultMessage: 'Open navigation',
+});
+
+export const ProjectNavigation: React.FC = ({ children }) => {
+ const { euiTheme, colorMode } = useEuiTheme();
+
+ const [isOpen, setIsOpen] = useLocalStorage(LOCAL_STORAGE_IS_OPEN_KEY, true);
+
+ const toggleOpen = useCallback(() => {
+ setIsOpen(!isOpen);
+ }, [isOpen, setIsOpen]);
+
+ const collabsibleNavCSS = css`
+ border-inline-end-width: 1,
+ background: ${euiTheme.colors.darkestShade},
+ display: flex,
+ flex-direction: row,
+ `;
+
+ return (
+
+
+
+
+ }
+ >
+ {isOpen && children}
+
+
+ );
+};
diff --git a/packages/core/chrome/core-chrome-browser-internal/tsconfig.json b/packages/core/chrome/core-chrome-browser-internal/tsconfig.json
index 4d4d6cad3bc21..cd27209bef12c 100644
--- a/packages/core/chrome/core-chrome-browser-internal/tsconfig.json
+++ b/packages/core/chrome/core-chrome-browser-internal/tsconfig.json
@@ -5,7 +5,10 @@
"types": [
"jest",
"node",
- "react"
+ "react",
+ "@kbn/ambient-ui-types",
+ "@kbn/ambient-storybook-types",
+ "@emotion/react/types/css-prop"
]
},
"include": [
diff --git a/packages/core/chrome/core-chrome-browser-mocks/src/chrome_service.mock.ts b/packages/core/chrome/core-chrome-browser-mocks/src/chrome_service.mock.ts
index 2f5c4deb1f38d..c7c62c7811277 100644
--- a/packages/core/chrome/core-chrome-browser-mocks/src/chrome_service.mock.ts
+++ b/packages/core/chrome/core-chrome-browser-mocks/src/chrome_service.mock.ts
@@ -61,6 +61,8 @@ const createStartContractMock = () => {
setHeaderBanner: jest.fn(),
hasHeaderBanner$: jest.fn(),
getBodyClasses$: jest.fn(),
+ getChromeStyle$: jest.fn(),
+ setChromeStyle: jest.fn(),
};
startContract.navLinks.getAll.mockReturnValue([]);
startContract.getIsVisible$.mockReturnValue(new BehaviorSubject(false));
diff --git a/packages/core/chrome/core-chrome-browser/index.ts b/packages/core/chrome/core-chrome-browser/index.ts
index 3fbef34126a4a..1d2dca4c957bc 100644
--- a/packages/core/chrome/core-chrome-browser/index.ts
+++ b/packages/core/chrome/core-chrome-browser/index.ts
@@ -7,25 +7,26 @@
*/
export type {
- ChromeUserBanner,
+ ChromeBadge,
ChromeBreadcrumb,
+ ChromeBreadcrumbsAppendExtension,
+ ChromeDocTitle,
+ ChromeGlobalHelpExtensionMenuLink,
ChromeHelpExtension,
- ChromeHelpExtensionMenuLink,
ChromeHelpExtensionLinkBase,
+ ChromeHelpExtensionMenuCustomLink,
+ ChromeHelpExtensionMenuDiscussLink,
+ ChromeHelpExtensionMenuDocumentationLink,
+ ChromeHelpExtensionMenuGitHubLink,
+ ChromeHelpExtensionMenuLink,
ChromeHelpMenuActions,
- ChromeNavLink,
- ChromeBreadcrumbsAppendExtension,
- ChromeNavLinks,
ChromeNavControl,
ChromeNavControls,
- ChromeBadge,
- ChromeHelpExtensionMenuGitHubLink,
- ChromeHelpExtensionMenuDocumentationLink,
- ChromeHelpExtensionMenuDiscussLink,
- ChromeHelpExtensionMenuCustomLink,
- ChromeGlobalHelpExtensionMenuLink,
- ChromeDocTitle,
- ChromeStart,
+ ChromeNavLink,
+ ChromeNavLinks,
ChromeRecentlyAccessed,
ChromeRecentlyAccessedHistoryItem,
+ ChromeStart,
+ ChromeStyle,
+ ChromeUserBanner,
} from './src';
diff --git a/packages/core/chrome/core-chrome-browser/src/contracts.ts b/packages/core/chrome/core-chrome-browser/src/contracts.ts
index a81d9c3c6338f..3f6f756e2d2b1 100644
--- a/packages/core/chrome/core-chrome-browser/src/contracts.ts
+++ b/packages/core/chrome/core-chrome-browser/src/contracts.ts
@@ -13,7 +13,7 @@ import type { ChromeDocTitle } from './doc_title';
import type { ChromeNavControls } from './nav_controls';
import type { ChromeHelpExtension } from './help_extension';
import type { ChromeBreadcrumb, ChromeBreadcrumbsAppendExtension } from './breadcrumb';
-import type { ChromeBadge, ChromeUserBanner } from './types';
+import type { ChromeBadge, ChromeStyle, ChromeUserBanner } from './types';
import { ChromeGlobalHelpExtensionMenuLink } from './help_extension';
/**
@@ -150,4 +150,15 @@ export interface ChromeStart {
* Get an observable of the current header banner presence state.
*/
hasHeaderBanner$(): Observable;
+
+ /**
+ * Sets the style type of the chrome.
+ * @param style The style type to apply to the chrome.
+ */
+ setChromeStyle(style: ChromeStyle): void;
+
+ /**
+ * Get an observable of the current style type of the chrome.
+ */
+ getChromeStyle$(): Observable;
}
diff --git a/packages/core/chrome/core-chrome-browser/src/index.ts b/packages/core/chrome/core-chrome-browser/src/index.ts
index 716af097fded7..89ba12d616d0e 100644
--- a/packages/core/chrome/core-chrome-browser/src/index.ts
+++ b/packages/core/chrome/core-chrome-browser/src/index.ts
@@ -26,4 +26,4 @@ export type {
ChromeRecentlyAccessed,
ChromeRecentlyAccessedHistoryItem,
} from './recently_accessed';
-export type { ChromeBadge, ChromeUserBanner } from './types';
+export type { ChromeBadge, ChromeUserBanner, ChromeStyle } from './types';
diff --git a/packages/core/chrome/core-chrome-browser/src/types.ts b/packages/core/chrome/core-chrome-browser/src/types.ts
index 81b8c32a1a04c..d4374687ff828 100644
--- a/packages/core/chrome/core-chrome-browser/src/types.ts
+++ b/packages/core/chrome/core-chrome-browser/src/types.ts
@@ -20,3 +20,6 @@ export interface ChromeBadge {
export interface ChromeUserBanner {
content: MountPoint;
}
+
+/** @public */
+export type ChromeStyle = 'classic' | 'project';
diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml
index 56259881447db..af6396c11e063 100644
--- a/packages/kbn-optimizer/limits.yml
+++ b/packages/kbn-optimizer/limits.yml
@@ -115,6 +115,7 @@ pageLoadAssetSize:
searchprofiler: 67080
security: 65433
securitySolution: 66738
+ serverless: 16573
sessionView: 77750
share: 71239
snapshotRestore: 79032
diff --git a/packages/serverless/project_switcher/README.mdx b/packages/serverless/project_switcher/README.mdx
new file mode 100644
index 0000000000000..240988346458c
--- /dev/null
+++ b/packages/serverless/project_switcher/README.mdx
@@ -0,0 +1,12 @@
+---
+id: serverless/components/ProjectSwitcher
+slug: /serverless/components/project-switcher
+title: Project Switcher
+description: A popup which allows a developer to switch between project types on their dev server.
+tags: ['serverless', 'component']
+date: 2023-04-23
+---
+
+When working on Serverless instances of Kibana, developers likely want to switch between different project types to test changes. This Project Switcher is intended to be placed into the header bar by the Serverless plugin when the server is in development mode to allow "quick switching" between configurations.
+
+The connected component uses `http` to post a selection to a given API endpoint, intended to alter the YML configuration and trigger Watcher to restart the server. To that end, it will post its message to a given API endpoint and replace the content of `document.body`. The remainder of the process is left to the Serverless plugin.
diff --git a/packages/serverless/project_switcher/index.ts b/packages/serverless/project_switcher/index.ts
new file mode 100644
index 0000000000000..69148308099a5
--- /dev/null
+++ b/packages/serverless/project_switcher/index.ts
@@ -0,0 +1,11 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export type { ProjectSwitcherProps, KibanaDependencies } from './src';
+
+export { ProjectSwitcher, ProjectSwitcherKibanaProvider, ProjectSwitcherProvider } from './src';
diff --git a/packages/serverless/project_switcher/jest.config.js b/packages/serverless/project_switcher/jest.config.js
new file mode 100644
index 0000000000000..713bdaaedaca2
--- /dev/null
+++ b/packages/serverless/project_switcher/jest.config.js
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+module.exports = {
+ preset: '@kbn/test',
+ rootDir: '../../..',
+ roots: ['/packages/serverless/project_switcher'],
+};
diff --git a/packages/serverless/project_switcher/kibana.jsonc b/packages/serverless/project_switcher/kibana.jsonc
new file mode 100644
index 0000000000000..6e37bb95cafda
--- /dev/null
+++ b/packages/serverless/project_switcher/kibana.jsonc
@@ -0,0 +1,5 @@
+{
+ "type": "shared-common",
+ "id": "@kbn/serverless-project-switcher",
+ "owner": "@elastic/appex-sharedux"
+}
diff --git a/packages/serverless/project_switcher/mocks/jest.mock.ts b/packages/serverless/project_switcher/mocks/jest.mock.ts
new file mode 100644
index 0000000000000..935b89b63dd13
--- /dev/null
+++ b/packages/serverless/project_switcher/mocks/jest.mock.ts
@@ -0,0 +1,23 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { Services, KibanaDependencies } from '../src/types';
+
+export const getProjectSwitcherServicesMock: () => jest.Mocked = () => ({
+ setProjectType: jest.fn(),
+});
+
+export const getProjectSwitcherKibanaDependenciesMock: () => jest.Mocked