[Infra UI] Replace redux source slice with constate container (#26121)

This commit is contained in:
Felix Stürmer 2018-12-05 18:42:11 +01:00 committed by GitHub
parent aaa809249e
commit d8f487d331
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 798 additions and 473 deletions

View file

@ -153,6 +153,7 @@
"chroma-js": "^1.3.6",
"classnames": "2.2.5",
"concat-stream": "1.5.1",
"constate": "^0.9.0",
"copy-to-clipboard": "^3.0.8",
"cronstrue": "^1.51.0",
"d3": "3.5.6",

View file

@ -705,6 +705,52 @@ export namespace WaffleNodesQuery {
value: number;
};
}
export namespace SourceQuery {
export type Variables = {
sourceId?: string | null;
};
export type Query = {
__typename?: 'Query';
source: Source;
};
export type Source = {
__typename?: 'InfraSource';
id: string;
configuration: Configuration;
status: Status;
};
export type Configuration = {
__typename?: 'InfraSourceConfiguration';
metricAlias: string;
logAlias: string;
fields: Fields;
};
export type Fields = {
__typename?: 'InfraSourceFields';
container: string;
host: string;
pod: string;
};
export type Status = {
__typename?: 'InfraSourceStatus';
indexFields: IndexFields[];
logIndicesExist: boolean;
metricIndicesExist: boolean;
};
export type IndexFields = {
__typename?: 'InfraIndexField';
name: string;
type: string;
searchable: boolean;
aggregatable: boolean;
};
}
export namespace LogEntries {
export type Variables = {
sourceId?: string | null;
@ -800,52 +846,6 @@ export namespace LogSummary {
entriesCount: number;
};
}
export namespace SourceQuery {
export type Variables = {
sourceId?: string | null;
};
export type Query = {
__typename?: 'Query';
source: Source;
};
export type Source = {
__typename?: 'InfraSource';
id: string;
configuration: Configuration;
status: Status;
};
export type Configuration = {
__typename?: 'InfraSourceConfiguration';
metricAlias: string;
logAlias: string;
fields: Fields;
};
export type Fields = {
__typename?: 'InfraSourceFields';
container: string;
host: string;
pod: string;
};
export type Status = {
__typename?: 'InfraSourceStatus';
indexFields: IndexFields[];
logIndicesExist: boolean;
metricIndicesExist: boolean;
};
export type IndexFields = {
__typename?: 'InfraIndexField';
name: string;
type: string;
searchable: boolean;
aggregatable: boolean;
};
}
export namespace InfraTimeKeyFields {
export type Fragment = {

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { I18nProvider } from '@kbn/i18n/react';
import { Provider as ConstateProvider } from 'constate';
import { createHashHistory } from 'history';
import React from 'react';
import { ApolloProvider } from 'react-apollo';
@ -16,6 +16,7 @@ import { ThemeProvider } from 'styled-components';
// TODO use theme provided from parentApp when kibana supports it
import { EuiErrorBoundary } from '@elastic/eui';
import * as euiVars from '@elastic/eui/dist/eui_theme_k6_light.json';
import { I18nProvider } from '@kbn/i18n/react';
import { InfraFrontendLibs } from '../lib/lib';
import { PageRouter } from '../routes';
import { createStore } from '../store';
@ -32,13 +33,15 @@ export async function startApp(libs: InfraFrontendLibs) {
libs.framework.render(
<I18nProvider>
<EuiErrorBoundary>
<ReduxStoreProvider store={store}>
<ApolloProvider client={libs.apolloClient}>
<ThemeProvider theme={{ eui: euiVars }}>
<PageRouter history={history} />
</ThemeProvider>
</ApolloProvider>
</ReduxStoreProvider>
<ConstateProvider devtools>
<ReduxStoreProvider store={store}>
<ApolloProvider client={libs.apolloClient}>
<ThemeProvider theme={{ eui: euiVars }}>
<PageRouter history={history} />
</ThemeProvider>
</ApolloProvider>
</ReduxStoreProvider>
</ConstateProvider>
</EuiErrorBoundary>
</I18nProvider>
);

View file

@ -0,0 +1,56 @@
/*
* 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 {
EuiButton,
EuiCallOut,
EuiFlexGroup,
EuiFlexItem,
EuiPageBody,
EuiPageContent,
EuiPageContentBody,
} from '@elastic/eui';
import React from 'react';
import styled from 'styled-components';
import { FlexPage } from './page';
interface Props {
detailedMessage?: React.ReactNode;
retry?: () => void;
shortMessage: React.ReactNode;
}
export const ErrorPage: React.SFC<Props> = ({ detailedMessage, retry, shortMessage }) => (
<FlexPage>
<EuiPageBody>
<MinimumPageContent
horizontalPosition="center"
verticalPosition="center"
panelPaddingSize="none"
>
<EuiPageContentBody>
<EuiCallOut color="danger" iconType="cross" title="An error occurred">
<EuiFlexGroup>
<EuiFlexItem>{shortMessage}</EuiFlexItem>
{retry ? (
<EuiFlexItem grow={false}>
<EuiButton onClick={retry} iconType="refresh">
Try again
</EuiButton>
</EuiFlexItem>
) : null}
</EuiFlexGroup>
{detailedMessage ? <div>{detailedMessage}</div> : null}
</EuiCallOut>
</EuiPageContentBody>
</MinimumPageContent>
</EuiPageBody>
</FlexPage>
);
const MinimumPageContent = styled(EuiPageContent)`
min-width: 50vh;
`;

View file

@ -7,31 +7,49 @@
import React from 'react';
import { connect } from 'react-redux';
import { StaticIndexPattern } from 'ui/index_patterns';
import { logFilterActions, logFilterSelectors, State } from '../../store';
import { FilterQuery } from '../../store/local/log_filter';
import { convertKueryToElasticSearchQuery } from '../../utils/kuery';
import { asChildFunctionRenderer } from '../../utils/typed_react';
import { bindPlainActionCreators } from '../../utils/typed_redux';
import { replaceStateKeyInQueryString, UrlStateContainer } from '../../utils/url_state';
interface WithLogFilterProps {
indexPattern: StaticIndexPattern;
}
const withLogFilter = connect(
(state: State) => ({
filterQuery: logFilterSelectors.selectLogFilterQuery(state),
filterQueryDraft: logFilterSelectors.selectLogFilterQueryDraft(state),
isFilterQueryDraftValid: logFilterSelectors.selectIsLogFilterQueryDraftValid(state),
}),
bindPlainActionCreators({
applyFilterQuery: logFilterActions.applyLogFilterQuery,
applyFilterQueryFromKueryExpression: (expression: string) =>
logFilterActions.applyLogFilterQuery({
kind: 'kuery',
expression,
}),
setFilterQueryDraft: logFilterActions.setLogFilterQueryDraft,
setFilterQueryDraftFromKueryExpression: (expression: string) =>
logFilterActions.setLogFilterQueryDraft({
kind: 'kuery',
expression,
}),
})
(dispatch, ownProps: WithLogFilterProps) =>
bindPlainActionCreators({
applyFilterQuery: (query: FilterQuery) =>
logFilterActions.applyLogFilterQuery({
query,
serializedQuery: convertKueryToElasticSearchQuery(
query.expression,
ownProps.indexPattern
),
}),
applyFilterQueryFromKueryExpression: (expression: string) =>
logFilterActions.applyLogFilterQuery({
query: {
kind: 'kuery',
expression,
},
serializedQuery: convertKueryToElasticSearchQuery(expression, ownProps.indexPattern),
}),
setFilterQueryDraft: logFilterActions.setLogFilterQueryDraft,
setFilterQueryDraftFromKueryExpression: (expression: string) =>
logFilterActions.setLogFilterQueryDraft({
kind: 'kuery',
expression,
}),
})(dispatch)
);
export const WithLogFilter = asChildFunctionRenderer(withLogFilter);
@ -42,8 +60,10 @@ export const WithLogFilter = asChildFunctionRenderer(withLogFilter);
type LogFilterUrlState = ReturnType<typeof logFilterSelectors.selectLogFilterQuery>;
export const WithLogFilterUrlState = () => (
<WithLogFilter>
type WithLogFilterUrlStateProps = WithLogFilterProps;
export const WithLogFilterUrlState: React.SFC<WithLogFilterUrlStateProps> = ({ indexPattern }) => (
<WithLogFilter indexPattern={indexPattern}>
{({ applyFilterQuery, filterQuery }) => (
<UrlStateContainer
urlState={filterQuery}

View file

@ -7,32 +7,50 @@
import React from 'react';
import { connect } from 'react-redux';
import { sharedSelectors, State, waffleFilterActions, waffleFilterSelectors } from '../../store';
import { StaticIndexPattern } from 'ui/index_patterns';
import { State, waffleFilterActions, waffleFilterSelectors } from '../../store';
import { FilterQuery } from '../../store/local/waffle_filter';
import { convertKueryToElasticSearchQuery } from '../../utils/kuery';
import { asChildFunctionRenderer } from '../../utils/typed_react';
import { bindPlainActionCreators } from '../../utils/typed_redux';
import { UrlStateContainer } from '../../utils/url_state';
interface WithWaffleFilterProps {
indexPattern: StaticIndexPattern;
}
export const withWaffleFilter = connect(
(state: State) => ({
filterQuery: waffleFilterSelectors.selectWaffleFilterQuery(state),
filterQueryDraft: waffleFilterSelectors.selectWaffleFilterQueryDraft(state),
filterQueryAsJson: sharedSelectors.selectWaffleFilterQueryAsJson(state),
filterQueryAsJson: waffleFilterSelectors.selectWaffleFilterQueryAsJson(state),
isFilterQueryDraftValid: waffleFilterSelectors.selectIsWaffleFilterQueryDraftValid(state),
}),
bindPlainActionCreators({
applyFilterQuery: waffleFilterActions.applyWaffleFilterQuery,
applyFilterQueryFromKueryExpression: (expression: string) =>
waffleFilterActions.applyWaffleFilterQuery({
kind: 'kuery',
expression,
}),
setFilterQueryDraft: waffleFilterActions.setWaffleFilterQueryDraft,
setFilterQueryDraftFromKueryExpression: (expression: string) =>
waffleFilterActions.setWaffleFilterQueryDraft({
kind: 'kuery',
expression,
}),
})
(dispatch, ownProps: WithWaffleFilterProps) =>
bindPlainActionCreators({
applyFilterQuery: (query: FilterQuery) =>
waffleFilterActions.applyWaffleFilterQuery({
query,
serializedQuery: convertKueryToElasticSearchQuery(
query.expression,
ownProps.indexPattern
),
}),
applyFilterQueryFromKueryExpression: (expression: string) =>
waffleFilterActions.applyWaffleFilterQuery({
query: {
kind: 'kuery',
expression,
},
serializedQuery: convertKueryToElasticSearchQuery(expression, ownProps.indexPattern),
}),
setFilterQueryDraft: waffleFilterActions.setWaffleFilterQueryDraft,
setFilterQueryDraftFromKueryExpression: (expression: string) =>
waffleFilterActions.setWaffleFilterQueryDraft({
kind: 'kuery',
expression,
}),
})
);
export const WithWaffleFilter = asChildFunctionRenderer(withWaffleFilter);
@ -43,8 +61,12 @@ export const WithWaffleFilter = asChildFunctionRenderer(withWaffleFilter);
type WaffleFilterUrlState = ReturnType<typeof waffleFilterSelectors.selectWaffleFilterQuery>;
export const WithWaffleFilterUrlState = () => (
<WithWaffleFilter>
type WithWaffleFilterUrlStateProps = WithWaffleFilterProps;
export const WithWaffleFilterUrlState: React.SFC<WithWaffleFilterUrlStateProps> = ({
indexPattern,
}) => (
<WithWaffleFilter indexPattern={indexPattern}>
{({ applyFilterQuery, filterQuery }) => (
<UrlStateContainer
urlState={filterQuery}

View file

@ -5,18 +5,12 @@
*/
import React from 'react';
import { connect } from 'react-redux';
import { AutocompleteSuggestion, getAutocompleteProvider } from 'ui/autocomplete_providers';
import { StaticIndexPattern } from 'ui/index_patterns';
import { sourceSelectors, State } from '../store';
import { RendererFunction } from '../utils/typed_react';
const withIndexPattern = connect((state: State) => ({
indexPattern: sourceSelectors.selectDerivedIndexPattern(state),
}));
interface WithKueryAutocompletionLifecycleProps {
children: RendererFunction<{
isLoadingSuggestions: boolean;
@ -36,73 +30,71 @@ interface WithKueryAutocompletionLifecycleState {
suggestions: AutocompleteSuggestion[];
}
export const WithKueryAutocompletion = withIndexPattern(
class WithKueryAutocompletionLifecycle extends React.Component<
WithKueryAutocompletionLifecycleProps,
WithKueryAutocompletionLifecycleState
> {
public readonly state: WithKueryAutocompletionLifecycleState = {
currentRequest: null,
suggestions: [],
export class WithKueryAutocompletion extends React.Component<
WithKueryAutocompletionLifecycleProps,
WithKueryAutocompletionLifecycleState
> {
public readonly state: WithKueryAutocompletionLifecycleState = {
currentRequest: null,
suggestions: [],
};
public render() {
const { currentRequest, suggestions } = this.state;
return this.props.children({
isLoadingSuggestions: currentRequest !== null,
loadSuggestions: this.loadSuggestions,
suggestions,
});
}
private loadSuggestions = async (
expression: string,
cursorPosition: number,
maxSuggestions?: number
) => {
const { indexPattern } = this.props;
const autocompletionProvider = getAutocompleteProvider('kuery');
const config = {
get: () => true,
};
public render() {
const { currentRequest, suggestions } = this.state;
return this.props.children({
isLoadingSuggestions: currentRequest !== null,
loadSuggestions: this.loadSuggestions,
suggestions,
});
if (!autocompletionProvider) {
return;
}
private loadSuggestions = async (
expression: string,
cursorPosition: number,
maxSuggestions?: number
) => {
const { indexPattern } = this.props;
const autocompletionProvider = getAutocompleteProvider('kuery');
const config = {
get: () => true,
};
const getSuggestions = autocompletionProvider({
config,
indexPatterns: [indexPattern],
boolFilter: [],
});
if (!autocompletionProvider) {
return;
}
this.setState({
currentRequest: {
expression,
cursorPosition,
},
suggestions: [],
});
const getSuggestions = autocompletionProvider({
config,
indexPatterns: [indexPattern],
boolFilter: [],
});
const suggestions = await getSuggestions({
query: expression,
selectionStart: cursorPosition,
selectionEnd: cursorPosition,
});
this.setState({
currentRequest: {
expression,
cursorPosition,
},
suggestions: [],
});
const suggestions = await getSuggestions({
query: expression,
selectionStart: cursorPosition,
selectionEnd: cursorPosition,
});
this.setState(
state =>
state.currentRequest &&
state.currentRequest.expression !== expression &&
state.currentRequest.cursorPosition !== cursorPosition
? state // ignore this result, since a newer request is in flight
: {
...state,
currentRequest: null,
suggestions: maxSuggestions ? suggestions.slice(0, maxSuggestions) : suggestions,
}
);
};
}
);
this.setState(
state =>
state.currentRequest &&
state.currentRequest.expression !== expression &&
state.currentRequest.cursorPosition !== cursorPosition
? state // ignore this result, since a newer request is in flight
: {
...state,
currentRequest: null,
suggestions: maxSuggestions ? suggestions.slice(0, maxSuggestions) : suggestions,
}
);
};
}

View file

@ -1,18 +0,0 @@
/*
* 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 { connect } from 'react-redux';
import { sourceSelectors, State } from '../store';
import { asChildFunctionRenderer } from '../utils/typed_react';
export const withSource = connect((state: State) => ({
configuredFields: sourceSelectors.selectSourceFields(state),
logIndicesExist: sourceSelectors.selectSourceLogIndicesExist(state),
metricIndicesExist: sourceSelectors.selectSourceMetricIndicesExist(state),
}));
export const WithSource = asChildFunctionRenderer(withSource);

View file

@ -4,6 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { loadSourceActionCreators } from './operations/load';
export const loadSource = loadSourceActionCreators.resolve;
export { SourceErrorPage } from './source_error_page';
export { SourceLoadingPage } from './source_loading_page';
export { WithSource } from './with_source';

View file

@ -0,0 +1,22 @@
/*
* 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 React from 'react';
import { ErrorPage } from '../../components/error_page';
interface SourceErrorPageProps {
errorMessage: string;
retry: () => void;
}
export const SourceErrorPage: React.SFC<SourceErrorPageProps> = ({ errorMessage, retry }) => (
<ErrorPage
shortMessage="Failed to load data sources."
detailedMessage={<code>{errorMessage}</code>}
retry={retry}
/>
);

View file

@ -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;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { LoadingPage } from '../../components/loading_page';
export const SourceLoadingPage: React.SFC = () => <LoadingPage message="Loading data sources" />;

View file

@ -0,0 +1,150 @@
/*
* 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 { ApolloClient } from 'apollo-client';
import { Container as ConstateContainer, OnMount } from 'constate';
import React from 'react';
import { ApolloConsumer } from 'react-apollo';
import { createSelector } from 'reselect';
import { oc } from 'ts-optchain';
import { StaticIndexPattern } from 'ui/index_patterns';
import { memoizeLast } from 'ui/utils/memoize';
import { SourceQuery } from '../../../common/graphql/types';
import {
createStatusActions,
createStatusSelectors,
Operation,
OperationStatus,
StatusHistoryUpdater,
} from '../../utils/operation_status';
import { inferActionMap, inferEffectMap, inferSelectorMap } from '../../utils/typed_constate';
import { RendererFunction } from '../../utils/typed_react';
import { sourceQuery } from './query_source.gql_query';
type Operations = Operation<'load', SourceQuery.Variables>;
interface State {
operationStatusHistory: Array<OperationStatus<Operations>>;
source: SourceQuery.Source | undefined;
}
const createContainerProps = memoizeLast((sourceId: string, apolloClient: ApolloClient<any>) => {
const initialState: State = {
operationStatusHistory: [],
source: undefined,
};
const actions = inferActionMap<State>()({
...createStatusActions((updater: StatusHistoryUpdater<Operations>) => (state: State) => ({
...state,
operationStatusHistory: updater(state.operationStatusHistory),
})),
});
const getDerivedIndexPattern = createSelector(
(state: State) => oc(state).source.status.indexFields([]),
(state: State) => oc(state).source.configuration.logAlias(),
(state: State) => oc(state).source.configuration.metricAlias(),
(indexFields, logAlias, metricAlias) => ({
fields: indexFields,
title: `${logAlias},${metricAlias}`,
})
);
const selectors = inferSelectorMap<State>()({
...createStatusSelectors(({ operationStatusHistory }: State) => operationStatusHistory),
getConfiguredFields: () => state => oc(state).source.configuration.fields(),
getLogIndicesExist: () => state => oc(state).source.status.logIndicesExist(),
getMetricIndicesExist: () => state => oc(state).source.status.metricIndicesExist(),
getDerivedIndexPattern: () => getDerivedIndexPattern,
});
const effects = inferEffectMap<State>()({
load: () => ({ setState }) => {
const variables = {
sourceId,
};
setState(actions.startOperation({ name: 'load', parameters: variables }));
apolloClient
.query<SourceQuery.Query, SourceQuery.Variables>({
query: sourceQuery,
fetchPolicy: 'no-cache',
variables,
})
.then(
result =>
setState(state => ({
...actions.finishOperation({ name: 'load', parameters: variables })(state),
source: result.data.source,
})),
error =>
setState(state => ({
...actions.failOperation({ name: 'load', parameters: variables }, `${error}`)(state),
}))
);
},
});
const onMount: OnMount<State> = props => {
effects.load()(props);
};
return {
actions,
context: `source-${sourceId}`,
effects,
initialState,
key: `source-${sourceId}`,
onMount,
selectors,
};
});
interface WithSourceProps {
children: RendererFunction<{
configuredFields?: SourceQuery.Fields;
derivedIndexPattern: StaticIndexPattern;
hasFailed: boolean;
isLoading: boolean;
lastFailureMessage?: string;
load: () => void;
logIndicesExist?: boolean;
metricIndicesExist?: boolean;
}>;
}
export const WithSource: React.SFC<WithSourceProps> = ({ children }) => (
<ApolloConsumer>
{client => (
<ConstateContainer pure {...createContainerProps('default', client)}>
{({
getConfiguredFields,
getDerivedIndexPattern,
getHasFailed,
getIsInProgress,
getLastFailureMessage,
getLogIndicesExist,
getMetricIndicesExist,
load,
}) =>
children({
configuredFields: getConfiguredFields(),
derivedIndexPattern: getDerivedIndexPattern(),
hasFailed: getHasFailed(),
isLoading: getIsInProgress(),
lastFailureMessage: getLastFailureMessage(),
load,
logIndicesExist: getLogIndicesExist(),
metricIndicesExist: getMetricIndicesExist(),
})
}
</ConstateContainer>
)}
</ApolloConsumer>
);

View file

@ -4,9 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import React from 'react';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { HomePageContent } from './page_content';
import { HomeToolbar } from './toolbar';
@ -19,30 +20,43 @@ import { WithWaffleFilterUrlState } from '../../containers/waffle/with_waffle_fi
import { WithWaffleOptionsUrlState } from '../../containers/waffle/with_waffle_options';
import { WithWaffleTimeUrlState } from '../../containers/waffle/with_waffle_time';
import { WithKibanaChrome } from '../../containers/with_kibana_chrome';
import { WithSource } from '../../containers/with_source';
import { SourceErrorPage, SourceLoadingPage, WithSource } from '../../containers/with_source';
interface HomePageProps {
intl: InjectedIntl;
}
export const HomePage = injectI18n(
class extends React.PureComponent<HomePageProps, {}> {
class extends React.Component<HomePageProps, {}> {
public static displayName = 'HomePage';
public render() {
const { intl } = this.props;
return (
<ColumnarPage>
<Header appendSections={<InfrastructureBetaBadgeHeaderSection />} />
<WithSource>
{({ metricIndicesExist }) =>
metricIndicesExist || metricIndicesExist === null ? (
{({
derivedIndexPattern,
hasFailed,
isLoading,
lastFailureMessage,
load,
metricIndicesExist,
}) =>
metricIndicesExist ? (
<>
<WithWaffleTimeUrlState />
<WithWaffleFilterUrlState />
<WithWaffleFilterUrlState indexPattern={derivedIndexPattern} />
<WithWaffleOptionsUrlState />
<Header appendSections={<InfrastructureBetaBadgeHeaderSection />} />
<HomeToolbar />
<HomePageContent />
</>
) : isLoading ? (
<SourceLoadingPage />
) : hasFailed ? (
<SourceErrorPage errorMessage={lastFailureMessage || ''} retry={load} />
) : (
<WithKibanaChrome>
{({ basePath }) => (

View file

@ -19,10 +19,10 @@ import { WithSource } from '../../containers/with_source';
export const HomePageContent: React.SFC = () => (
<PageContent>
<WithSource>
{({ configuredFields }) => (
{({ configuredFields, derivedIndexPattern }) => (
<WithOptions>
{({ wafflemap, sourceId }) => (
<WithWaffleFilter>
<WithWaffleFilter indexPattern={derivedIndexPattern}>
{({ filterQueryAsJson, applyFilterQuery }) => (
<WithWaffleTime>
{({ currentTimeRange, isAutoReloading }) => (

View file

@ -21,6 +21,7 @@ import { WithWaffleFilter } from '../../containers/waffle/with_waffle_filters';
import { WithWaffleOptions } from '../../containers/waffle/with_waffle_options';
import { WithWaffleTime } from '../../containers/waffle/with_waffle_time';
import { WithKueryAutocompletion } from '../../containers/with_kuery_autocompletion';
import { WithSource } from '../../containers/with_source';
const getTitle = (nodeType: string) => {
const TITLES = {
@ -78,32 +79,36 @@ export const HomeToolbar = injectI18n(({ intl }) => (
</EuiFlexGroup>
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween" gutterSize="m">
<EuiFlexItem>
<WithKueryAutocompletion>
{({ isLoadingSuggestions, loadSuggestions, suggestions }) => (
<WithWaffleFilter>
{({
applyFilterQueryFromKueryExpression,
filterQueryDraft,
isFilterQueryDraftValid,
setFilterQueryDraftFromKueryExpression,
}) => (
<AutocompleteField
isLoadingSuggestions={isLoadingSuggestions}
isValid={isFilterQueryDraftValid}
loadSuggestions={loadSuggestions}
onChange={setFilterQueryDraftFromKueryExpression}
onSubmit={applyFilterQueryFromKueryExpression}
placeholder={intl.formatMessage({
id: 'xpack.infra.homePage.toolbar.kqlSearchFieldPlaceholder',
defaultMessage: 'Search for infrastructure data… (e.g. host.name:host-1)',
})}
suggestions={suggestions}
value={filterQueryDraft ? filterQueryDraft.expression : ''}
/>
<WithSource>
{({ derivedIndexPattern }) => (
<WithKueryAutocompletion indexPattern={derivedIndexPattern}>
{({ isLoadingSuggestions, loadSuggestions, suggestions }) => (
<WithWaffleFilter indexPattern={derivedIndexPattern}>
{({
applyFilterQueryFromKueryExpression,
filterQueryDraft,
isFilterQueryDraftValid,
setFilterQueryDraftFromKueryExpression,
}) => (
<AutocompleteField
isLoadingSuggestions={isLoadingSuggestions}
isValid={isFilterQueryDraftValid}
loadSuggestions={loadSuggestions}
onChange={setFilterQueryDraftFromKueryExpression}
onSubmit={applyFilterQueryFromKueryExpression}
placeholder={intl.formatMessage({
id: 'xpack.infra.homePage.toolbar.kqlSearchFieldPlaceholder',
defaultMessage: 'Search for infrastructure data… (e.g. host.name:host-1)',
})}
suggestions={suggestions}
value={filterQueryDraft ? filterQueryDraft.expression : ''}
/>
)}
</WithWaffleFilter>
)}
</WithWaffleFilter>
</WithKueryAutocompletion>
)}
</WithKueryAutocompletion>
</WithSource>
</EuiFlexItem>
<WithWaffleOptions>
{({ changeMetric, changeGroupBy, groupBy, metric, nodeType }) => (

View file

@ -20,40 +20,54 @@ import { WithLogMinimapUrlState } from '../../containers/logs/with_log_minimap';
import { WithLogPositionUrlState } from '../../containers/logs/with_log_position';
import { WithLogTextviewUrlState } from '../../containers/logs/with_log_textview';
import { WithKibanaChrome } from '../../containers/with_kibana_chrome';
import { WithSource } from '../../containers/with_source';
import { SourceErrorPage, SourceLoadingPage, WithSource } from '../../containers/with_source';
interface Props {
intl: InjectedIntl;
}
export const LogsPage = injectI18n(
class extends React.Component<Props> {
public static displayName = 'LogsPage';
public render() {
const { intl } = this.props;
return (
<ColumnarPage>
<Header
appendSections={<LogsBetaBadgeHeaderSection />}
breadcrumbs={[
{
text: intl.formatMessage({
id: 'xpack.infra.logsPage.logsBreadcrumbsText',
defaultMessage: 'Logs',
}),
},
]}
/>
<WithSource>
{({ logIndicesExist }) =>
logIndicesExist || logIndicesExist === null ? (
{({
derivedIndexPattern,
hasFailed,
isLoading,
lastFailureMessage,
load,
logIndicesExist,
}) =>
logIndicesExist ? (
<>
<WithLogFilterUrlState />
<WithLogFilterUrlState indexPattern={derivedIndexPattern} />
<WithLogPositionUrlState />
<WithLogMinimapUrlState />
<WithLogTextviewUrlState />
<Header
appendSections={<LogsBetaBadgeHeaderSection />}
breadcrumbs={[
{
text: intl.formatMessage({
id: 'xpack.infra.logsPage.logsBreadcrumbsText',
defaultMessage: 'Logs',
}),
},
]}
/>
<LogsToolbar />
<LogsPageContent />
</>
) : isLoading ? (
<SourceLoadingPage />
) : hasFailed ? (
<SourceErrorPage errorMessage={lastFailureMessage || ''} retry={load} />
) : (
<WithKibanaChrome>
{({ basePath }) => (

View file

@ -20,38 +20,42 @@ import { WithLogMinimap } from '../../containers/logs/with_log_minimap';
import { WithLogPosition } from '../../containers/logs/with_log_position';
import { WithLogTextview } from '../../containers/logs/with_log_textview';
import { WithKueryAutocompletion } from '../../containers/with_kuery_autocompletion';
import { WithSource } from '../../containers/with_source';
export const LogsToolbar = injectI18n(({ intl }) => (
<Toolbar>
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween" gutterSize="none">
<EuiFlexItem>
<WithKueryAutocompletion>
{({ isLoadingSuggestions, loadSuggestions, suggestions }) => (
<WithLogFilter>
{({
applyFilterQueryFromKueryExpression,
/* filterQuery,*/
filterQueryDraft,
isFilterQueryDraftValid,
setFilterQueryDraftFromKueryExpression,
}) => (
<AutocompleteField
isLoadingSuggestions={isLoadingSuggestions}
isValid={isFilterQueryDraftValid}
loadSuggestions={loadSuggestions}
onChange={setFilterQueryDraftFromKueryExpression}
onSubmit={applyFilterQueryFromKueryExpression}
placeholder={intl.formatMessage({
id: 'xpack.infra.logsPage.toolbar.kqlSearchFieldPlaceholder',
defaultMessage: 'Search for log entries… (e.g. host.name:host-1)',
})}
suggestions={suggestions}
value={filterQueryDraft ? filterQueryDraft.expression : ''}
/>
<WithSource>
{({ derivedIndexPattern }) => (
<WithKueryAutocompletion indexPattern={derivedIndexPattern}>
{({ isLoadingSuggestions, loadSuggestions, suggestions }) => (
<WithLogFilter indexPattern={derivedIndexPattern}>
{({
applyFilterQueryFromKueryExpression,
filterQueryDraft,
isFilterQueryDraftValid,
setFilterQueryDraftFromKueryExpression,
}) => (
<AutocompleteField
isLoadingSuggestions={isLoadingSuggestions}
isValid={isFilterQueryDraftValid}
loadSuggestions={loadSuggestions}
onChange={setFilterQueryDraftFromKueryExpression}
onSubmit={applyFilterQueryFromKueryExpression}
placeholder={intl.formatMessage({
id: 'xpack.infra.logsPage.toolbar.kqlSearchFieldPlaceholder',
defaultMessage: 'Search for log entries… (e.g. host.name:host-1)',
})}
suggestions={suggestions}
value={filterQueryDraft ? filterQueryDraft.expression : ''}
/>
)}
</WithLogFilter>
)}
</WithLogFilter>
</WithKueryAutocompletion>
)}
</WithKueryAutocompletion>
</WithSource>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<LogCustomizationMenu>

View file

@ -6,10 +6,10 @@
import actionCreatorFactory from 'typescript-fsa';
import { FilterQuery } from './reducer';
import { FilterQuery, SerializedFilterQuery } from './reducer';
const actionCreator = actionCreatorFactory('x-pack/infra/local/log_filter');
export const setLogFilterQueryDraft = actionCreator<FilterQuery>('SET_LOG_FILTER_QUERY_DRAFT');
export const applyLogFilterQuery = actionCreator<FilterQuery>('APPLY_LOG_FILTER_QUERY');
export const applyLogFilterQuery = actionCreator<SerializedFilterQuery>('APPLY_LOG_FILTER_QUERY');

View file

@ -15,8 +15,13 @@ export interface KueryFilterQuery {
export type FilterQuery = KueryFilterQuery;
export interface SerializedFilterQuery {
query: FilterQuery;
serializedQuery: string;
}
export interface LogFilterState {
filterQuery: KueryFilterQuery | null;
filterQuery: SerializedFilterQuery | null;
filterQueryDraft: KueryFilterQuery | null;
}
@ -33,6 +38,6 @@ export const logFilterReducer = reducerWithInitialState(initialLogFilterState)
.case(applyLogFilterQuery, (state, filterQuery) => ({
...state,
filterQuery,
filterQueryDraft: filterQuery,
filterQueryDraft: filterQuery.query,
}))
.build();

View file

@ -10,7 +10,11 @@ import { fromKueryExpression } from '@kbn/es-query';
import { LogFilterState } from './reducer';
export const selectLogFilterQuery = (state: LogFilterState) => state.filterQuery;
export const selectLogFilterQuery = (state: LogFilterState) =>
state.filterQuery ? state.filterQuery.query : null;
export const selectLogFilterQueryAsJson = (state: LogFilterState) =>
state.filterQuery ? state.filterQuery.serializedQuery : null;
export const selectLogFilterQueryDraft = (state: LogFilterState) => state.filterQueryDraft;

View file

@ -6,7 +6,7 @@
import actionCreatorFactory from 'typescript-fsa';
import { FilterQuery } from './reducer';
import { FilterQuery, SerializedFilterQuery } from './reducer';
const actionCreator = actionCreatorFactory('x-pack/infra/local/waffle_filter');
@ -14,4 +14,6 @@ export const setWaffleFilterQueryDraft = actionCreator<FilterQuery>(
'SET_WAFFLE_FILTER_QUERY_DRAFT'
);
export const applyWaffleFilterQuery = actionCreator<FilterQuery>('APPLY_WAFFLE_FILTER_QUERY');
export const applyWaffleFilterQuery = actionCreator<SerializedFilterQuery>(
'APPLY_WAFFLE_FILTER_QUERY'
);

View file

@ -15,8 +15,13 @@ export interface KueryFilterQuery {
export type FilterQuery = KueryFilterQuery;
export interface SerializedFilterQuery {
query: FilterQuery;
serializedQuery: string;
}
export interface WaffleFilterState {
filterQuery: KueryFilterQuery | null;
filterQuery: SerializedFilterQuery | null;
filterQueryDraft: KueryFilterQuery | null;
}
@ -33,6 +38,6 @@ export const waffleFilterReducer = reducerWithInitialState(initialWaffleFilterSt
.case(applyWaffleFilterQuery, (state, filterQuery) => ({
...state,
filterQuery,
filterQueryDraft: filterQuery,
filterQueryDraft: filterQuery.query,
}))
.build();

View file

@ -10,7 +10,11 @@ import { fromKueryExpression } from '@kbn/es-query';
import { WaffleFilterState } from './reducer';
export const selectWaffleFilterQuery = (state: WaffleFilterState) => state.filterQuery;
export const selectWaffleFilterQuery = (state: WaffleFilterState) =>
state.filterQuery ? state.filterQuery.query : null;
export const selectWaffleFilterQueryAsJson = (state: WaffleFilterState) =>
state.filterQuery ? state.filterQuery.serializedQuery : null;
export const selectWaffleFilterQueryDraft = (state: WaffleFilterState) => state.filterQueryDraft;

View file

@ -6,4 +6,3 @@
export { logEntriesActions } from './log_entries';
export { logSummaryActions } from './log_summary';
export { sourceActions } from './source';

View file

@ -8,11 +8,6 @@ import { combineEpics } from 'redux-observable';
import { createLogEntriesEpic } from './log_entries';
import { createLogSummaryEpic } from './log_summary';
import { createSourceEpic } from './source';
export const createRemoteEpic = <State>() =>
combineEpics(
createLogEntriesEpic<State>(),
createLogSummaryEpic<State>(),
createSourceEpic<State>()
);
combineEpics(createLogEntriesEpic<State>(), createLogSummaryEpic<State>());

View file

@ -7,22 +7,18 @@
import { combineReducers } from 'redux';
import { initialLogEntriesState, logEntriesReducer, LogEntriesState } from './log_entries';
import { initialLogSummaryState, logSummaryReducer, LogSummaryState } from './log_summary';
import { initialSourceState, sourceReducer, SourceState } from './source';
export interface RemoteState {
logEntries: LogEntriesState;
logSummary: LogSummaryState;
source: SourceState;
}
export const initialRemoteState = {
logEntries: initialLogEntriesState,
logSummary: initialLogSummaryState,
source: initialSourceState,
};
export const remoteReducer = combineReducers<RemoteState>({
logEntries: logEntriesReducer,
logSummary: logSummaryReducer,
source: sourceReducer,
});

View file

@ -8,7 +8,6 @@ import { globalizeSelectors } from '../../utils/typed_redux';
import { logEntriesSelectors as innerLogEntriesSelectors } from './log_entries';
import { logSummarySelectors as innerLogSummarySelectors } from './log_summary';
import { RemoteState } from './reducer';
import { sourceSelectors as innerSourceSelectors } from './source';
export const logEntriesSelectors = globalizeSelectors(
(state: RemoteState) => state.logEntries,
@ -19,8 +18,3 @@ export const logSummarySelectors = globalizeSelectors(
(state: RemoteState) => state.logSummary,
innerLogSummarySelectors
);
export const sourceSelectors = globalizeSelectors(
(state: RemoteState) => state.source,
innerSourceSelectors
);

View file

@ -1,22 +0,0 @@
/*
* 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 { Action } from 'redux';
import { combineEpics, Epic, EpicWithState } from 'redux-observable';
import { of } from 'rxjs';
import { loadSource } from './actions';
import { loadSourceEpic } from './operations/load';
export const createSourceEpic = <State>() =>
combineEpics(createSourceEffectsEpic<State>(), loadSourceEpic as EpicWithState<
typeof loadSourceEpic,
State
>);
export const createSourceEffectsEpic = <State>(): Epic<Action, Action, State, {}> => action$ => {
return of(loadSource({ sourceId: 'default' }));
};

View file

@ -1,13 +0,0 @@
/*
* 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 * as sourceActions from './actions';
import * as sourceSelectors from './selectors';
export { sourceActions, sourceSelectors };
export * from './epic';
export * from './reducer';
export { initialSourceState, SourceState } from './state';

View file

@ -1,30 +0,0 @@
/*
* 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 { SourceQuery } from '../../../../../common/graphql/types';
import {
createGraphqlOperationActionCreators,
createGraphqlOperationReducer,
createGraphqlQueryEpic,
} from '../../../../utils/remote_state/remote_graphql_state';
import { initialSourceState } from '../state';
import { sourceQuery } from './query_source.gql_query';
const operationKey = 'load';
export const loadSourceActionCreators = createGraphqlOperationActionCreators<
SourceQuery.Query,
SourceQuery.Variables
>('source', operationKey);
export const loadSourceReducer = createGraphqlOperationReducer(
operationKey,
initialSourceState,
loadSourceActionCreators,
(state, action) => action.payload.result.data.source
);
export const loadSourceEpic = createGraphqlQueryEpic(sourceQuery, loadSourceActionCreators);

View file

@ -1,13 +0,0 @@
/*
* 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 reduceReducers from 'reduce-reducers';
import { Reducer } from 'redux';
import { loadSourceReducer } from './operations/load';
import { SourceState } from './state';
export const sourceReducer = reduceReducers(loadSourceReducer) as Reducer<SourceState>;

View file

@ -1,64 +0,0 @@
/*
* 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 { createSelector } from 'reselect';
import { createGraphqlStateSelectors } from '../../../utils/remote_state/remote_graphql_state';
import { SourceRemoteState } from './state';
const sourceStatusGraphqlStateSelectors = createGraphqlStateSelectors<SourceRemoteState>();
export const selectSource = sourceStatusGraphqlStateSelectors.selectData;
export const selectSourceConfiguration = createSelector(
selectSource,
source => (source ? source.configuration : null)
);
export const selectSourceLogAlias = createSelector(
selectSourceConfiguration,
configuration => (configuration ? configuration.logAlias : null)
);
export const selectSourceMetricAlias = createSelector(
selectSourceConfiguration,
configuration => (configuration ? configuration.metricAlias : null)
);
export const selectSourceFields = createSelector(
selectSourceConfiguration,
configuration => (configuration ? configuration.fields : null)
);
export const selectSourceStatus = createSelector(
selectSource,
source => (source ? source.status : null)
);
export const selectSourceLogIndicesExist = createSelector(
selectSourceStatus,
sourceStatus => (sourceStatus ? sourceStatus.logIndicesExist : null)
);
export const selectSourceMetricIndicesExist = createSelector(
selectSourceStatus,
sourceStatus => (sourceStatus ? sourceStatus.metricIndicesExist : null)
);
export const selectSourceIndexFields = createSelector(
selectSourceStatus,
sourceStatus => (sourceStatus ? sourceStatus.indexFields : [])
);
export const selectDerivedIndexPattern = createSelector(
selectSourceIndexFields,
selectSourceLogAlias,
selectSourceMetricAlias,
(indexFields, logAlias, metricAlias) => ({
fields: indexFields,
title: `${logAlias},${metricAlias}`,
})
);

View file

@ -1,16 +0,0 @@
/*
* 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 { SourceQuery } from '../../../../common/graphql/types';
import {
createGraphqlInitialState,
GraphqlState,
} from '../../../utils/remote_state/remote_graphql_state';
export type SourceRemoteState = SourceQuery.Source;
export type SourceState = GraphqlState<SourceRemoteState>;
export const initialSourceState = createGraphqlInitialState<SourceRemoteState>();

View file

@ -6,8 +6,6 @@
import { createSelector } from 'reselect';
import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query';
import { getLogEntryAtTime } from '../utils/log_entry';
import { globalizeSelectors } from '../utils/typed_redux';
import {
@ -24,7 +22,6 @@ import { State } from './reducer';
import {
logEntriesSelectors as remoteLogEntriesSelectors,
logSummarySelectors as remoteLogSummarySelectors,
sourceSelectors as remoteSourceSelectors,
} from './remote';
/**
@ -50,7 +47,6 @@ const selectRemote = (state: State) => state.remote;
export const logEntriesSelectors = globalizeSelectors(selectRemote, remoteLogEntriesSelectors);
export const logSummarySelectors = globalizeSelectors(selectRemote, remoteLogSummarySelectors);
export const sourceSelectors = globalizeSelectors(selectRemote, remoteSourceSelectors);
/**
* shared selectors
@ -75,34 +71,4 @@ export const sharedSelectors = {
(entries, lastVisiblePosition) =>
lastVisiblePosition ? getLogEntryAtTime(entries, lastVisiblePosition) : null
),
selectLogFilterQueryAsJson: createSelector(
logFilterSelectors.selectLogFilterQuery,
sourceSelectors.selectDerivedIndexPattern,
(filterQuery, indexPattern) => {
try {
return filterQuery
? JSON.stringify(
toElasticsearchQuery(fromKueryExpression(filterQuery.expression), indexPattern)
)
: null;
} catch (err) {
return null;
}
}
),
selectWaffleFilterQueryAsJson: createSelector(
waffleFilterSelectors.selectWaffleFilterQuery,
sourceSelectors.selectDerivedIndexPattern,
(filterQuery, indexPattern) => {
try {
return filterQuery
? JSON.stringify(
toElasticsearchQuery(fromKueryExpression(filterQuery.expression), indexPattern)
)
: null;
} catch (err) {
return null;
}
}
),
};

View file

@ -13,10 +13,10 @@ import {
createRootEpic,
initialState,
logEntriesSelectors,
logFilterSelectors,
logPositionSelectors,
metricTimeSelectors,
reducer,
sharedSelectors,
State,
waffleTimeSelectors,
} from '.';
@ -45,7 +45,7 @@ export function createStore({ apolloClient, observableApi }: StoreDependencies)
selectHasMoreLogEntriesAfterEnd: logEntriesSelectors.selectHasMoreAfterEnd,
selectHasMoreLogEntriesBeforeStart: logEntriesSelectors.selectHasMoreBeforeStart,
selectIsAutoReloadingLogEntries: logPositionSelectors.selectIsAutoReloading,
selectLogFilterQueryAsJson: sharedSelectors.selectLogFilterQueryAsJson,
selectLogFilterQueryAsJson: logFilterSelectors.selectLogFilterQueryAsJson,
selectLogTargetPosition: logPositionSelectors.selectTargetPosition,
selectVisibleLogMidpointOrTarget: logPositionSelectors.selectVisibleMidpointOrTarget,
selectVisibleLogSummary: logPositionSelectors.selectVisibleSummary,

View file

@ -0,0 +1,21 @@
/*
* 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 { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query';
import { StaticIndexPattern } from 'ui/index_patterns';
export const convertKueryToElasticSearchQuery = (
kueryExpression: string,
indexPattern: StaticIndexPattern
) => {
try {
return kueryExpression
? JSON.stringify(toElasticsearchQuery(fromKueryExpression(kueryExpression), indexPattern))
: '';
} catch (err) {
return '';
}
};

View file

@ -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;
* you may not use this file except in compliance with the Elastic License.
*/
import last from 'lodash/fp/last';
import { oc } from 'ts-optchain';
export interface InProgressStatus<O extends Operation<string, any>> {
operation: O;
status: 'in-progress';
time: number;
}
export interface SucceededStatus<O extends Operation<string, any>> {
operation: O;
status: 'succeeded';
time: number;
}
export interface FailedStatus<O extends Operation<string, any>> {
message: string;
operation: O;
status: 'failed';
time: number;
}
const isFailedStatus = <O extends Operation<string, any>>(
status: OperationStatus<O>
): status is FailedStatus<O> => status.status === 'failed';
export type OperationStatus<O extends Operation<string, any>> =
| InProgressStatus<O>
| SucceededStatus<O>
| FailedStatus<O>;
export interface Operation<Name extends string, Parameters> {
name: Name;
parameters: Parameters;
}
export const createStatusSelectors = <S extends {}>(
selectStatusHistory: (state: S) => Array<OperationStatus<any>>
) => ({
getIsInProgress: () => (state: S) =>
oc(last(selectStatusHistory(state))).status() === 'in-progress',
getHasSucceeded: () => (state: S) =>
oc(last(selectStatusHistory(state))).status() === 'succeeded',
getHasFailed: () => (state: S) => oc(last(selectStatusHistory(state))).status() === 'failed',
getLastFailureMessage: () => (state: S) =>
oc(last(selectStatusHistory(state).filter(isFailedStatus))).message(),
});
export type StatusHistoryUpdater<Operations extends Operation<string, any>> = (
statusHistory: Array<OperationStatus<Operations>>
) => Array<OperationStatus<Operations>>;
export const createStatusActions = <S extends {}, Operations extends Operation<string, any>>(
updateStatusHistory: (updater: StatusHistoryUpdater<Operations>) => (state: S) => S
) => ({
startOperation: (operation: Operations) =>
updateStatusHistory(statusHistory => [
...statusHistory,
{
operation,
status: 'in-progress',
time: Date.now(),
},
]),
finishOperation: (operation: Operations) =>
updateStatusHistory(statusHistory => [
...statusHistory,
{
operation,
status: 'succeeded',
time: Date.now(),
},
]),
failOperation: (operation: Operations, message: string) =>
updateStatusHistory(statusHistory => [
...statusHistory,
{
message,
operation,
status: 'failed',
time: Date.now(),
},
]),
});

View file

@ -0,0 +1,101 @@
/*
* 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.
*/
/**
* The helper types and functions below are designed to be used with constate
* v0.9. From version 1.0 the use of react hooks probably makes them
* unnecessary.
*
* The `inferActionMap`, `inferEffectMap` and `inferSelectorMap` functions
* remove the necessity to type out the child-facing interfaces as suggested in
* the constate typescript documentation by inferring the `ActionMap`,
* `EffectMap` and `SelectorMap` types from the object passed as an argument.
* At runtime these functions just return their first argument without
* modification.
*
* Until partial type argument inference is (hopefully) introduced with
* TypeScript 3.3, the functions are split into two nested functions to allow
* for specifying the `State` type argument while leaving the other type
* arguments for inference by the compiler.
*
* Example Usage:
*
* ```typescript
* const actions = inferActionMap<State>()({
* increment: (amount: number) => state => ({ ...state, count: state.count + amount }),
* });
* // actions has type ActionMap<State, { increment: (amount: number) => void; }>
* ```
*/
import { ActionMap, EffectMap, EffectProps, SelectorMap } from 'constate';
/**
* actions
*/
type InferredAction<State, Action> = Action extends (...args: infer A) => (state: State) => State
? (...args: A) => void
: never;
type InferredActions<State, Actions> = ActionMap<
State,
{ [K in keyof Actions]: InferredAction<State, Actions[K]> }
>;
export const inferActionMap = <State extends any>() => <
Actions extends {
[key: string]: (...args: any[]) => (state: State) => State;
}
>(
actionMap: Actions
): InferredActions<State, Actions> => actionMap as any;
/**
* effects
*/
type InferredEffect<State, Effect> = Effect extends (
...args: infer A
) => (props: EffectProps<State>) => infer R
? (...args: A) => R
: never;
type InferredEffects<State, Effects> = EffectMap<
State,
{ [K in keyof Effects]: InferredEffect<State, Effects[K]> }
>;
export const inferEffectMap = <State extends any>() => <
Effects extends {
[key: string]: (...args: any[]) => (props: EffectProps<State>) => any;
}
>(
effectMap: Effects
): InferredEffects<State, Effects> => effectMap as any;
/**
* selectors
*/
type InferredSelector<State, Selector> = Selector extends (
...args: infer A
) => (state: State) => infer R
? (...args: A) => R
: never;
type InferredSelectors<State, Selectors> = SelectorMap<
State,
{ [K in keyof Selectors]: InferredSelector<State, Selectors[K]> }
>;
export const inferSelectorMap = <State extends any>() => <
Selectors extends {
[key: string]: (...args: any[]) => (state: State) => any;
}
>(
selectorMap: Selectors
): InferredSelectors<State, Selectors> => selectorMap as any;

View file

@ -6,7 +6,7 @@
import expect from 'expect.js';
import { SourceQuery } from '../../../../plugins/infra/common/graphql/types';
import { sourceQuery } from '../../../../plugins/infra/public/store/remote/source/operations/query_source.gql_query';
import { sourceQuery } from '../../../../plugins/infra/public/containers/with_source/query_source.gql_query';
import { KbnTestProvider } from './types';

View file

@ -5739,6 +5739,11 @@ constants-browserify@^1.0.0:
resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=
constate@^0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/constate/-/constate-0.9.0.tgz#877197ef8fbcacee95672a7e98f7b21dec818891"
integrity sha512-Cgkqefi4GrepnA7gwqbrsU+Kf/xl0sPv3O1UNE/vUZtTgpWlkd+/rSZjjd7npICtExn6yuICro7Vb2zoFCnS/A==
contains-path@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a"