Skip to content

Commit 9c4b3e0

Browse files
scottybollingerConstancecee-chenelasticmachine
authored
Add Kea.js support to Enterprise Search plugin (#72160) (#73372)
* Add Kea packages - kea and kea-waitfor * Add Kea declarations and types Hopefully TypeScript support coming soon from author * Add Kea to entry point * Add logic for overview * Update components to use Kea * Fix a couple of tests that weren’t getting complete coverage * Remove kea-waitfor Turns out we don’t need it * Remove unused declaration * Update x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.ts Co-authored-by: Constance <constancecchen@users.noreply.github.com> * Update x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.ts Co-authored-by: Constance <constancecchen@users.noreply.github.com> * Update x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/__mocks__/overview_logic.mock.ts Co-authored-by: Constance <constancecchen@users.noreply.github.com> * Update x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.test.ts Co-authored-by: Constance <constancecchen@users.noreply.github.com> * [Opinionated] Remove extra actions defs - they're already being defined in IOverviewActions, so no need to repeat them * DRY out a new reusable/generics IKeaLogic/Listeners interface - Multiple logic files can now do IKeaListeners<SomeLogicActions> and not have to declare their own IListenerParams! + bonus IKeaSelectors just for consistency * DRY out Kea reducers definitions to generics interface * [Refactor] Improve KeaReducers generic to actually type-check/check key names - Typescript will now throw an error if you put in a key name that isn't declared in your actions/values interface - default & new states now will be type checked!! 🎉 * [Refactor] Update selectors() and listeners() to also check types and keys * [Refactor] Move param defs to bottom of file instead of inline - so that inline definitions mostly focus on type checks, and more boilerplate defs are DRYed out - I played around with 2.1 obj definitions and got terrible results here :( * Update tests and remove selectors per code review * Remove last statsColumns instance * Remove last instance of hideOnboarding Co-authored-by: Constance <constancecchen@users.noreply.github.com> Co-authored-by: Constance Chen <constance.chen.3@gmail.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> Co-authored-by: Constance <constancecchen@users.noreply.github.com> Co-authored-by: Constance Chen <constance.chen.3@gmail.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
1 parent 7c8706b commit 9c4b3e0

20 files changed

Lines changed: 633 additions & 236 deletions

x-pack/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,7 @@
285285
"json-stable-stringify": "^1.0.1",
286286
"jsonwebtoken": "^8.5.1",
287287
"jsts": "^1.6.2",
288+
"kea": "^2.0.1",
288289
"lodash": "^4.17.15",
289290
"lz-string": "^1.4.4",
290291
"mapbox-gl": "^1.10.0",
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
declare module 'kea' {
8+
export function useValues(logic?: object): object;
9+
export function useActions(logic?: object): object;
10+
export function getContext(): { store: object };
11+
export function resetContext(context: object): object;
12+
export function kea(logic: object): object;
13+
}

x-pack/plugins/enterprise_search/public/applications/shared/types.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,59 @@ export interface IFlashMessagesProps {
1212
isWrapped?: boolean;
1313
children?: React.ReactNode;
1414
}
15+
16+
export interface IKeaLogic<IKeaValues, IKeaActions> {
17+
mount(): void;
18+
values: IKeaValues;
19+
actions: IKeaActions;
20+
}
21+
22+
/**
23+
* This reusable interface mostly saves us a few characters / allows us to skip
24+
* defining params inline. Unfortunately, the return values *do not work* as
25+
* expected (hence the voids). While I can tell selectors to use TKeaSelectors,
26+
* the return value is *not* properly type checked if it's not declared inline. :/
27+
*
28+
* Also note that if you switch to Kea 2.1's plain object notation -
29+
* `selectors: {}` vs. `selectors: () => ({})`
30+
* - type checking also stops working and type errors become significantly less
31+
* helpful - showing less specific error messages and highlighting. 👎
32+
*/
33+
export interface IKeaParams<IKeaValues, IKeaActions> {
34+
selectors?(params: { selectors: IKeaValues }): void;
35+
listeners?(params: { actions: IKeaActions; values: IKeaValues }): void;
36+
}
37+
38+
/**
39+
* This reducers() type checks that:
40+
*
41+
* 1. The value object keys are defined within IKeaValues
42+
* 2. The default state (array[0]) matches the type definition within IKeaValues
43+
* 3. The action object keys (array[1]) are defined within IKeaActions
44+
* 3. The new state returned by the action matches the type definition within IKeaValues
45+
*/
46+
export type TKeaReducers<IKeaValues, IKeaActions> = {
47+
[Value in keyof IKeaValues]?: [
48+
IKeaValues[Value],
49+
{
50+
[Action in keyof IKeaActions]?: (state: IKeaValues, payload: IKeaValues) => IKeaValues[Value];
51+
}
52+
];
53+
};
54+
55+
/**
56+
* This selectors() type checks that:
57+
*
58+
* 1. The object keys are defined within IKeaValues
59+
* 2. The selected values are defined within IKeaValues
60+
* 3. The returned value match the type definition within IKeaValues
61+
*
62+
* The unknown[] and any[] are unfortunately because I have no idea how to
63+
* assert for arbitrary type/values as an array
64+
*/
65+
export type TKeaSelectors<IKeaValues> = {
66+
[Value in keyof IKeaValues]?: [
67+
(selectors: IKeaValues) => unknown[],
68+
(...args: any[]) => IKeaValues[Value] // eslint-disable-line @typescript-eslint/no-explicit-any
69+
];
70+
};
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
export { setMockValues, mockLogicValues, mockLogicActions } from './overview_logic.mock';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { IOverviewValues } from '../overview_logic';
8+
import { IAccount, IOrganization, IUser } from '../../../types';
9+
10+
export const mockLogicValues = {
11+
accountsCount: 0,
12+
activityFeed: [],
13+
canCreateContentSources: false,
14+
canCreateInvitations: false,
15+
currentUser: {} as IUser,
16+
fpAccount: {} as IAccount,
17+
hasOrgSources: false,
18+
hasUsers: false,
19+
isFederatedAuth: true,
20+
isOldAccount: false,
21+
organization: {} as IOrganization,
22+
pendingInvitationsCount: 0,
23+
personalSourcesCount: 0,
24+
sourcesCount: 0,
25+
dataLoading: true,
26+
hasErrorConnecting: false,
27+
flashMessages: {},
28+
} as IOverviewValues;
29+
30+
export const mockLogicActions = {
31+
initializeOverview: jest.fn(() => ({})),
32+
};
33+
34+
jest.mock('kea', () => ({
35+
...(jest.requireActual('kea') as object),
36+
useActions: jest.fn(() => ({ ...mockLogicActions })),
37+
useValues: jest.fn(() => ({ ...mockLogicValues })),
38+
}));
39+
40+
import { useValues } from 'kea';
41+
42+
export const setMockValues = (values: object) => {
43+
(useValues as jest.Mock).mockImplementationOnce(() => ({
44+
...mockLogicValues,
45+
...values,
46+
}));
47+
};

x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_steps.test.tsx

Lines changed: 33 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
*/
66

77
import '../../../__mocks__/shallow_usecontext.mock';
8+
import './__mocks__/overview_logic.mock';
9+
import { setMockValues } from './__mocks__';
810

911
import React from 'react';
1012
import { shallow } from 'enzyme';
@@ -16,7 +18,6 @@ import { sendTelemetry } from '../../../shared/telemetry';
1618

1719
import { OnboardingSteps, OrgNameOnboarding } from './onboarding_steps';
1820
import { OnboardingCard } from './onboarding_card';
19-
import { defaultServerData } from './overview';
2021

2122
const account = {
2223
id: '1',
@@ -30,7 +31,8 @@ const account = {
3031
describe('OnboardingSteps', () => {
3132
describe('Shared Sources', () => {
3233
it('renders 0 sources state', () => {
33-
const wrapper = shallow(<OnboardingSteps {...defaultServerData} />);
34+
setMockValues({ canCreateContentSources: true });
35+
const wrapper = shallow(<OnboardingSteps />);
3436

3537
expect(wrapper.find(OnboardingCard)).toHaveLength(1);
3638
expect(wrapper.find(OnboardingCard).prop('actionPath')).toBe(ORG_SOURCES_PATH);
@@ -40,35 +42,32 @@ describe('OnboardingSteps', () => {
4042
});
4143

4244
it('renders completed sources state', () => {
43-
const wrapper = shallow(
44-
<OnboardingSteps {...defaultServerData} sourcesCount={2} hasOrgSources />
45-
);
45+
setMockValues({ sourcesCount: 2, hasOrgSources: true });
46+
const wrapper = shallow(<OnboardingSteps />);
4647

4748
expect(wrapper.find(OnboardingCard).prop('description')).toEqual(
4849
'You have added 2 shared sources. Happy searching.'
4950
);
5051
});
5152

5253
it('disables link when the user cannot create sources', () => {
53-
const wrapper = shallow(
54-
<OnboardingSteps {...defaultServerData} canCreateContentSources={false} />
55-
);
54+
setMockValues({ canCreateContentSources: false });
55+
const wrapper = shallow(<OnboardingSteps />);
5656

5757
expect(wrapper.find(OnboardingCard).prop('actionPath')).toBe(undefined);
5858
});
5959
});
6060

6161
describe('Users & Invitations', () => {
6262
it('renders 0 users when not on federated auth', () => {
63-
const wrapper = shallow(
64-
<OnboardingSteps
65-
{...defaultServerData}
66-
isFederatedAuth={false}
67-
fpAccount={account}
68-
accountsCount={0}
69-
hasUsers={false}
70-
/>
71-
);
63+
setMockValues({
64+
canCreateInvitations: true,
65+
isFederatedAuth: false,
66+
fpAccount: account,
67+
accountsCount: 0,
68+
hasUsers: false,
69+
});
70+
const wrapper = shallow(<OnboardingSteps />);
7271

7372
expect(wrapper.find(OnboardingCard)).toHaveLength(2);
7473
expect(wrapper.find(OnboardingCard).last().prop('actionPath')).toBe(USERS_PATH);
@@ -78,37 +77,29 @@ describe('OnboardingSteps', () => {
7877
});
7978

8079
it('renders completed users state', () => {
81-
const wrapper = shallow(
82-
<OnboardingSteps
83-
{...defaultServerData}
84-
isFederatedAuth={false}
85-
fpAccount={account}
86-
accountsCount={1}
87-
hasUsers
88-
/>
89-
);
80+
setMockValues({
81+
isFederatedAuth: false,
82+
fpAccount: account,
83+
accountsCount: 1,
84+
hasUsers: true,
85+
});
86+
const wrapper = shallow(<OnboardingSteps />);
9087

9188
expect(wrapper.find(OnboardingCard).last().prop('description')).toEqual(
9289
'Nice, you’ve invited colleagues to search with you.'
9390
);
9491
});
9592

9693
it('disables link when the user cannot create invitations', () => {
97-
const wrapper = shallow(
98-
<OnboardingSteps
99-
{...defaultServerData}
100-
isFederatedAuth={false}
101-
canCreateInvitations={false}
102-
/>
103-
);
104-
94+
setMockValues({ isFederatedAuth: false, canCreateInvitations: false });
95+
const wrapper = shallow(<OnboardingSteps />);
10596
expect(wrapper.find(OnboardingCard).last().prop('actionPath')).toBe(undefined);
10697
});
10798
});
10899

109100
describe('Org Name', () => {
110101
it('renders button to change name', () => {
111-
const wrapper = shallow(<OnboardingSteps {...defaultServerData} />);
102+
const wrapper = shallow(<OnboardingSteps />);
112103

113104
const button = wrapper
114105
.find(OrgNameOnboarding)
@@ -120,15 +111,13 @@ describe('OnboardingSteps', () => {
120111
});
121112

122113
it('hides card when name has been changed', () => {
123-
const wrapper = shallow(
124-
<OnboardingSteps
125-
{...defaultServerData}
126-
organization={{
127-
name: 'foo',
128-
defaultOrgName: 'bar',
129-
}}
130-
/>
131-
);
114+
setMockValues({
115+
organization: {
116+
name: 'foo',
117+
defaultOrgName: 'bar',
118+
},
119+
});
120+
const wrapper = shallow(<OnboardingSteps />);
132121

133122
expect(wrapper.find(OrgNameOnboarding)).toHaveLength(0);
134123
});

x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_steps.tsx

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import React, { useContext } from 'react';
88
import { i18n } from '@kbn/i18n';
99
import { FormattedMessage } from '@kbn/i18n/react';
10+
import { useValues } from 'kea';
1011

1112
import {
1213
EuiSpacer,
@@ -28,7 +29,7 @@ import { ORG_SOURCES_PATH, USERS_PATH, ORG_SETTINGS_PATH } from '../../routes';
2829

2930
import { ContentSection } from '../shared/content_section';
3031

31-
import { IAppServerData } from './overview';
32+
import { OverviewLogic, IOverviewValues } from './overview_logic';
3233

3334
import { OnboardingCard } from './onboarding_card';
3435

@@ -57,17 +58,19 @@ const ONBOARDING_USERS_CARD_DESCRIPTION = i18n.translate(
5758
{ defaultMessage: 'Invite your colleagues into this organization to search with you.' }
5859
);
5960

60-
export const OnboardingSteps: React.FC<IAppServerData> = ({
61-
hasUsers,
62-
hasOrgSources,
63-
canCreateContentSources,
64-
canCreateInvitations,
65-
accountsCount,
66-
sourcesCount,
67-
fpAccount: { isCurated },
68-
organization: { name, defaultOrgName },
69-
isFederatedAuth,
70-
}) => {
61+
export const OnboardingSteps: React.FC = () => {
62+
const {
63+
hasUsers,
64+
hasOrgSources,
65+
canCreateContentSources,
66+
canCreateInvitations,
67+
accountsCount,
68+
sourcesCount,
69+
fpAccount: { isCurated },
70+
organization: { name, defaultOrgName },
71+
isFederatedAuth,
72+
} = useValues(OverviewLogic) as IOverviewValues;
73+
7174
const accountsPath =
7275
!isFederatedAuth && (canCreateInvitations || isCurated) ? USERS_PATH : undefined;
7376
const sourcesPath = canCreateContentSources || isCurated ? ORG_SOURCES_PATH : undefined;

x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/organization_stats.test.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,27 @@
55
*/
66

77
import '../../../__mocks__/shallow_usecontext.mock';
8+
import './__mocks__/overview_logic.mock';
9+
import { setMockValues } from './__mocks__';
810

911
import React from 'react';
1012
import { shallow } from 'enzyme';
1113
import { EuiFlexGrid } from '@elastic/eui';
1214

1315
import { OrganizationStats } from './organization_stats';
1416
import { StatisticCard } from './statistic_card';
15-
import { defaultServerData } from './overview';
1617

1718
describe('OrganizationStats', () => {
1819
it('renders', () => {
19-
const wrapper = shallow(<OrganizationStats {...defaultServerData} />);
20+
const wrapper = shallow(<OrganizationStats />);
2021

2122
expect(wrapper.find(StatisticCard)).toHaveLength(2);
2223
expect(wrapper.find(EuiFlexGrid).prop('columns')).toEqual(2);
2324
});
2425

2526
it('renders additional cards for federated auth', () => {
26-
const wrapper = shallow(<OrganizationStats {...defaultServerData} isFederatedAuth={false} />);
27+
setMockValues({ isFederatedAuth: false });
28+
const wrapper = shallow(<OrganizationStats />);
2729

2830
expect(wrapper.find(StatisticCard)).toHaveLength(4);
2931
expect(wrapper.find(EuiFlexGrid).prop('columns')).toEqual(4);

0 commit comments

Comments
 (0)