Skip to content

Commit

Permalink
Merge branch 'master' into fix_archives_with_runtime_fields
Browse files Browse the repository at this point in the history
  • Loading branch information
kibanamachine authored Nov 17, 2020
2 parents f3a3d46 + 46d587a commit 1394275
Show file tree
Hide file tree
Showing 48 changed files with 1,060 additions and 221 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,41 @@ export interface User {
groupIds: string[];
}

export interface Features {
basicOrgContext?: FeatureIds[];
basicOrgContextExcludedFeatures?: FeatureIds[];
platinumOrgContext?: FeatureIds[];
platinumPrivateContext: FeatureIds[];
}

export interface Configuration {
isPublicKey: boolean;
needsBaseUrl: boolean;
needsSubdomain?: boolean;
needsConfiguration?: boolean;
hasOauthRedirect: boolean;
baseUrlTitle?: string;
helpText: string;
documentationUrl: string;
applicationPortalUrl?: string;
applicationLinkTitle?: string;
}

export interface SourceDataItem {
name: string;
serviceType: string;
configuration: Configuration;
configured?: boolean;
connected?: boolean;
features?: Features;
objTypes?: string[];
sourceDescription: string;
connectStepDescription: string;
addPath: string;
editPath: string;
accountContextOnly: boolean;
}

export interface ContentSource {
id: string;
serviceType: string;
Expand All @@ -54,6 +89,25 @@ export interface ContentSourceDetails extends ContentSource {
boost: number;
}

export interface ContentSourceStatus {
id: string;
name: string;
service_type: string;
status: {
status: string;
synced_at: string;
error_reason: number;
};
}

export interface Connector {
serviceType: string;
name: string;
configured: boolean;
supportedByLicense: boolean;
accountContextOnly: boolean;
}

export interface SourcePriority {
[id: string]: number;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ import {
CUSTOM_SOURCE_DOCS_URL,
} from '../../routes';

import { FeatureIds } from '../../types';
import { FeatureIds, SourceDataItem } from '../../types';

import { SOURCE_NAMES, SOURCE_OBJ_TYPES, GITHUB_LINK_TITLE } from '../../constants';

Expand Down Expand Up @@ -740,4 +740,4 @@ export const staticSourceData = [
connectStepDescription: connectStepDescription.empty,
accountContextOnly: false,
},
];
] as SourceDataItem[];
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { cloneDeep, findIndex } from 'lodash';

import { kea, MakeLogicType } from 'kea';

import { HttpLogic } from '../../../shared/http';

import {
flashAPIErrors,
setSuccessMessage,
FlashMessagesLogic,
} from '../../../shared/flash_messages';

import { Connector, ContentSourceDetails, ContentSourceStatus, SourceDataItem } from '../../types';

import { staticSourceData } from './source_data';

import { AppLogic } from '../../app_logic';

const ORG_SOURCES_PATH = '/api/workplace_search/org/sources';
const ACCOUNT_SOURCES_PATH = '/api/workplace_search/account/sources';

interface ServerStatuses {
[key: string]: string;
}

export interface ISourcesActions {
setServerSourceStatuses(statuses: ContentSourceStatus[]): ContentSourceStatus[];
onInitializeSources(serverResponse: ISourcesServerResponse): ISourcesServerResponse;
onSetSearchability(
sourceId: string,
searchable: boolean
): { sourceId: string; searchable: boolean };
setAddedSource(
addedSourceName: string,
additionalConfiguration: boolean,
serviceType: string
): { addedSourceName: string; additionalConfiguration: boolean; serviceType: string };
resetFlashMessages(): void;
resetPermissionsModal(): void;
resetSourcesState(): void;
initializeSources(): void;
pollForSourceStatusChanges(): void;
setSourceSearchability(
sourceId: string,
searchable: boolean
): { sourceId: string; searchable: boolean };
}

export interface IPermissionsModalProps {
addedSourceName: string;
serviceType: string;
additionalConfiguration: boolean;
}

type CombinedDataItem = SourceDataItem & ContentSourceDetails;

export interface ISourcesValues {
contentSources: ContentSourceDetails[];
privateContentSources: ContentSourceDetails[];
sourceData: CombinedDataItem[];
availableSources: SourceDataItem[];
configuredSources: SourceDataItem[];
serviceTypes: Connector[];
permissionsModal: IPermissionsModalProps | null;
dataLoading: boolean;
serverStatuses: ServerStatuses | null;
}

interface ISourcesServerResponse {
contentSources: ContentSourceDetails[];
privateContentSources?: ContentSourceDetails[];
serviceTypes: Connector[];
}

export const SourcesLogic = kea<MakeLogicType<ISourcesValues, ISourcesActions>>({
actions: {
setServerSourceStatuses: (statuses: ContentSourceStatus[]) => statuses,
onInitializeSources: (serverResponse: ISourcesServerResponse) => serverResponse,
onSetSearchability: (sourceId: string, searchable: boolean) => ({ sourceId, searchable }),
setAddedSource: (
addedSourceName: string,
additionalConfiguration: boolean,
serviceType: string
) => ({ addedSourceName, additionalConfiguration, serviceType }),
resetFlashMessages: () => true,
resetPermissionsModal: () => true,
resetSourcesState: () => true,
initializeSources: () => true,
pollForSourceStatusChanges: () => true,
setSourceSearchability: (sourceId: string, searchable: boolean) => ({ sourceId, searchable }),
},
reducers: {
contentSources: [
[],
{
onInitializeSources: (_, { contentSources }) => contentSources,
onSetSearchability: (contentSources, { sourceId, searchable }) =>
updateSourcesOnToggle(contentSources, sourceId, searchable),
},
],
privateContentSources: [
[],
{
onInitializeSources: (_, { privateContentSources }) => privateContentSources || [],
onSetSearchability: (privateContentSources, { sourceId, searchable }) =>
updateSourcesOnToggle(privateContentSources, sourceId, searchable),
},
],
serviceTypes: [
[],
{
onInitializeSources: (_, { serviceTypes }) => serviceTypes || [],
},
],
permissionsModal: [
null,
{
setAddedSource: (_, data) => data,
resetPermissionsModal: () => null,
},
],
dataLoading: [
true,
{
onInitializeSources: () => false,
resetSourcesState: () => true,
},
],
serverStatuses: [
null,
{
setServerSourceStatuses: (_, sources) => {
const serverStatuses = {} as ServerStatuses;
sources.forEach((source) => {
serverStatuses[source.id as string] = source.status.status;
});
return serverStatuses;
},
},
],
},
selectors: ({ selectors }) => ({
availableSources: [
() => [selectors.sourceData],
(sourceData: SourceDataItem[]) => sourceData.filter(({ configured }) => !configured),
],
configuredSources: [
() => [selectors.sourceData],
(sourceData: SourceDataItem[]) => sourceData.filter(({ configured }) => configured),
],
sourceData: [
() => [selectors.serviceTypes, selectors.contentSources],
(serviceTypes, contentSources) =>
mergeServerAndStaticData(serviceTypes, staticSourceData, contentSources),
],
}),
listeners: ({ actions, values }) => ({
initializeSources: async () => {
const { isOrganization } = AppLogic.values;
const route = isOrganization ? ORG_SOURCES_PATH : ACCOUNT_SOURCES_PATH;

try {
const response = await HttpLogic.values.http.get(route);
actions.onInitializeSources(response);
} catch (e) {
flashAPIErrors(e);
}

if (isOrganization && !values.serverStatuses) {
// We want to get the initial statuses from the server to compare our polling results to.
const sourceStatuses = await fetchSourceStatuses(isOrganization);
actions.setServerSourceStatuses(sourceStatuses);
}
},
// We poll the server and if the status update, we trigger a new fetch of the sources.
pollForSourceStatusChanges: async () => {
const { isOrganization } = AppLogic.values;
if (!isOrganization) return;
const serverStatuses = values.serverStatuses;

const sourceStatuses = await fetchSourceStatuses(isOrganization);

sourceStatuses.some((source: ContentSourceStatus) => {
if (serverStatuses && serverStatuses[source.id] !== source.status.status) {
return actions.initializeSources();
}
});
},
setSourceSearchability: async ({ sourceId, searchable }) => {
const { isOrganization } = AppLogic.values;
const route = isOrganization
? `/api/workplace_search/org/sources/${sourceId}/searchable`
: `/api/workplace_search/account/sources/${sourceId}/searchable`;

try {
await HttpLogic.values.http.put(route, {
body: JSON.stringify({ searchable }),
});
actions.onSetSearchability(sourceId, searchable);
} catch (e) {
flashAPIErrors(e);
}
},
setAddedSource: ({ addedSourceName, additionalConfiguration }) => {
setSuccessMessage(
[
`Successfully connected ${addedSourceName}.`,
additionalConfiguration ? 'This source requires additional configuration.' : '',
].join(' ')
);
},
resetFlashMessages: () => {
FlashMessagesLogic.actions.clearFlashMessages();
},
}),
});

const fetchSourceStatuses = async (isOrganization: boolean) => {
const route = isOrganization ? ORG_SOURCES_PATH : ACCOUNT_SOURCES_PATH;
let response;

try {
response = await HttpLogic.values.http.get(route);
SourcesLogic.actions.setServerSourceStatuses(response);
} catch (e) {
flashAPIErrors(e);
}

return response;
};

const updateSourcesOnToggle = (
contentSources: ContentSourceDetails[],
sourceId: string,
searchable: boolean
): ContentSourceDetails[] => {
if (!contentSources) return [];
const sources = cloneDeep(contentSources) as ContentSourceDetails[];
const index = findIndex(sources, ({ id }) => id === sourceId);
const updatedSource = sources[index];
sources[index] = {
...updatedSource,
searchable,
};
return sources;
};

/**
* We have 3 different data sets we have to combine in the UI. The first is the static (`staticSourceData`)
* data that contains the UI componets, such as the Path for React Router and the copy and images.
*
* The second is the base list of available sources that the server sends back in the collection,
* `availableTypes` that is the source of truth for the name and whether the source has been configured.
*
* Fnally, also in the collection response is the current set of connected sources. We check for the
* existence of a `connectedSource` of the type in the loop and set `connected` to true so that the UI
* can diplay "Add New" instead of "Connect", the latter of which is displated only when a connector
* has been configured but there are no connected sources yet.
*/
const mergeServerAndStaticData = (
serverData: ContentSourceDetails[],
staticData: SourceDataItem[],
contentSources: ContentSourceDetails[]
) => {
const combined = [] as CombinedDataItem[];
serverData.forEach((serverItem) => {
const type = serverItem.serviceType;
const staticItem = staticData.find(({ serviceType }) => serviceType === type);
const connectedSource = contentSources.find(({ serviceType }) => serviceType === type);
combined.push({
...serverItem,
...staticItem,
connected: !!connectedSource,
} as CombinedDataItem);
});

return combined;
};
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@ describe('sources routes', () => {
});
});

describe('PUT /api/workplace_search/sources/{id}/searchable', () => {
describe('PUT /api/workplace_search/account/sources/{id}/searchable', () => {
let mockRouter: MockRouter;

beforeEach(() => {
Expand All @@ -421,7 +421,7 @@ describe('sources routes', () => {
it('creates a request handler', () => {
mockRouter = new MockRouter({
method: 'put',
path: '/api/workplace_search/sources/{id}/searchable',
path: '/api/workplace_search/account/sources/{id}/searchable',
payload: 'body',
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ export function registerAccountSourceSearchableRoute({
}: RouteDependencies) {
router.put(
{
path: '/api/workplace_search/sources/{id}/searchable',
path: '/api/workplace_search/account/sources/{id}/searchable',
validate: {
body: schema.object({
searchable: schema.boolean(),
Expand Down
Loading

0 comments on commit 1394275

Please sign in to comment.