Skip to content

Commit

Permalink
[ML] Transforms: Fixes missing number of transform nodes and error re…
Browse files Browse the repository at this point in the history
…porting in stats bar. (#93956)

- Adds a Kibana API endpoint transforms/_nodes
- Adds number of nodes to the stats bar in the transforms list.
- Shows a callout when no transform nodes are available.
- Disable all actions except delete when no transform nodes are available.
- Disables the create button when no transform nodes are available.
  • Loading branch information
walterra authored Mar 16, 2021
1 parent d02169e commit f3b74b4
Show file tree
Hide file tree
Showing 28 changed files with 368 additions and 72 deletions.
1 change: 1 addition & 0 deletions src/core/public/doc_links/doc_links_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ export class DocLinksService {
elasticsearch: {
indexModules: `${ELASTICSEARCH_DOCS}index-modules.html`,
mapping: `${ELASTICSEARCH_DOCS}mapping.html`,
nodeRoles: `${ELASTICSEARCH_DOCS}modules-node.html#node-roles`,
remoteClusters: `${ELASTICSEARCH_DOCS}modules-remote-clusters.html`,
remoteClustersProxy: `${ELASTICSEARCH_DOCS}modules-remote-clusters.html#proxy-mode`,
remoteClusersProxySettings: `${ELASTICSEARCH_DOCS}modules-remote-clusters.html#remote-cluster-proxy-settings`,
Expand Down
5 changes: 5 additions & 0 deletions x-pack/plugins/transform/common/api_schemas/transforms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ import type { TransformId, TransformPivotConfig } from '../types/transform';

import { transformStateSchema, runtimeMappingsSchema } from './common';

// GET transform nodes
export interface GetTransformNodesResponseSchema {
count: number;
}

// GET transforms
export const getTransformsRequestSchema = schema.arrayOf(
schema.object({
Expand Down
9 changes: 9 additions & 0 deletions x-pack/plugins/transform/common/api_schemas/type_guards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type { DeleteTransformsResponseSchema } from './delete_transforms';
import type { StartTransformsResponseSchema } from './start_transforms';
import type { StopTransformsResponseSchema } from './stop_transforms';
import type {
GetTransformNodesResponseSchema,
GetTransformsResponseSchema,
PostTransformsPreviewResponseSchema,
PutTransformsResponseSchema,
Expand All @@ -35,6 +36,14 @@ const isGenericResponseSchema = <T>(arg: any): arg is T => {
);
};

export const isGetTransformNodesResponseSchema = (
arg: unknown
): arg is GetTransformNodesResponseSchema => {
return (
isPopulatedObject(arg) && {}.hasOwnProperty.call(arg, 'count') && typeof arg.count === 'number'
);
};

export const isGetTransformsResponseSchema = (arg: unknown): arg is GetTransformsResponseSchema => {
return isGenericResponseSchema<GetTransformsResponseSchema>(arg);
};
Expand Down
8 changes: 8 additions & 0 deletions x-pack/plugins/transform/public/app/hooks/use_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import type {
StopTransformsResponseSchema,
} from '../../../common/api_schemas/stop_transforms';
import type {
GetTransformNodesResponseSchema,
GetTransformsResponseSchema,
PostTransformsPreviewRequestSchema,
PostTransformsPreviewResponseSchema,
Expand Down Expand Up @@ -66,6 +67,13 @@ export const useApi = () => {

return useMemo(
() => ({
async getTransformNodes(): Promise<GetTransformNodesResponseSchema | HttpFetchError> {
try {
return await http.get(`${API_BASE_PATH}transforms/_nodes`);
} catch (e) {
return e;
}
},
async getTransform(
transformId: TransformId
): Promise<GetTransformsResponseSchema | HttpFetchError> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const useDocumentationLinks = () => {
return {
esAggsCompositeMissingBucket: deps.docLinks.links.aggs.composite_missing_bucket,
esIndicesCreateIndex: deps.docLinks.links.apis.createIndex,
esNodeRoles: deps.docLinks.links.elasticsearch.nodeRoles,
esPluginDocBasePath: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/plugins/${DOC_LINK_VERSION}/`,
esQueryDsl: deps.docLinks.links.query.queryDsl,
esTransform: deps.docLinks.links.transforms.guide,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { HttpFetchError } from 'src/core/public';

import {
isGetTransformNodesResponseSchema,
isGetTransformsResponseSchema,
isGetTransformsStatsResponseSchema,
} from '../../../common/api_schemas/type_guards';
Expand All @@ -22,6 +23,7 @@ export type GetTransforms = (forceRefresh?: boolean) => void;

export const useGetTransforms = (
setTransforms: React.Dispatch<React.SetStateAction<TransformListRow[]>>,
setTransformNodes: React.Dispatch<React.SetStateAction<number>>,
setErrorMessage: React.Dispatch<React.SetStateAction<HttpFetchError | undefined>>,
setIsInitialized: React.Dispatch<React.SetStateAction<boolean>>,
blockRefresh: boolean
Expand All @@ -40,17 +42,20 @@ export const useGetTransforms = (
}

const fetchOptions = { asSystemRequest: true };
const transformNodes = await api.getTransformNodes();
const transformConfigs = await api.getTransforms(fetchOptions);
const transformStats = await api.getTransformsStats(fetchOptions);

if (
!isGetTransformsResponseSchema(transformConfigs) ||
!isGetTransformsStatsResponseSchema(transformStats)
!isGetTransformsStatsResponseSchema(transformStats) ||
!isGetTransformNodesResponseSchema(transformNodes)
) {
// An error is followed immediately by setting the state to idle.
// This way we're able to treat ERROR as a one-time-event like REFRESH.
refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.ERROR);
refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.IDLE);
setTransformNodes(0);
setTransforms([]);

setIsInitialized(true);
Expand Down Expand Up @@ -86,6 +91,7 @@ export const useGetTransforms = (
return reducedtableRows;
}, [] as TransformListRow[]);

setTransformNodes(transformNodes.count);
setTransforms(tableRows);
setErrorMessage(undefined);
setIsInitialized(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ export const hasPrivilegeFactory = (privileges: Privileges | undefined | null) =

// create the text for button's tooltips if the user
// doesn't have the permission to press that button
export function createCapabilityFailureMessage(capability: keyof Capabilities) {
export function createCapabilityFailureMessage(
capability: keyof Capabilities | 'noTransformNodes'
) {
let message = '';

switch (capability) {
Expand All @@ -80,6 +82,12 @@ export function createCapabilityFailureMessage(capability: keyof Capabilities) {
defaultMessage: 'You do not have permission to delete transforms.',
});
break;

case 'noTransformNodes':
message = i18n.translate('xpack.transform.capability.noPermission.noTransformNodesTooltip', {
defaultMessage: 'There are no transform nodes available.',
});
break;
}

return i18n.translate('xpack.transform.capability.pleaseContactAdministratorTooltip', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,7 @@ export const StepDefineForm: FC<StepDefineFormProps> = React.memo((props) => {
stepDefineForm.advancedPivotEditor.actions.setAdvancedPivotEditorApplyButtonEnabled(false);
};

const { esQueryDsl } = useDocumentationLinks();
const { esTransformPivot } = useDocumentationLinks();
const { esQueryDsl, esTransformPivot } = useDocumentationLinks();

const advancedEditorsSidebarWidth = '220px';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { useAppDependencies, useToastNotifications } from '../../../../app_depen
import { cloneActionNameText, CloneActionName } from './clone_action_name';

export type CloneAction = ReturnType<typeof useCloneAction>;
export const useCloneAction = (forceDisable: boolean) => {
export const useCloneAction = (forceDisable: boolean, transformNodes: number) => {
const history = useHistory();
const appDeps = useAppDependencies();
const savedObjectsClient = appDeps.savedObjects.client;
Expand Down Expand Up @@ -72,14 +72,14 @@ export const useCloneAction = (forceDisable: boolean) => {
const action: TransformListAction = useMemo(
() => ({
name: (item: TransformListRow) => <CloneActionName disabled={!canCreateTransform} />,
enabled: () => canCreateTransform && !forceDisable,
enabled: () => canCreateTransform && !forceDisable && transformNodes > 0,
description: cloneActionNameText,
icon: 'copy',
type: 'icon',
onClick: clickHandler,
'data-test-subj': 'transformActionClone',
}),
[canCreateTransform, forceDisable, clickHandler]
[canCreateTransform, forceDisable, clickHandler, transformNodes]
);

return { action };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { AuthorizationContext } from '../../../../lib/authorization';

import { editActionNameText, EditActionName } from './edit_action_name';

export const useEditAction = (forceDisable: boolean) => {
export const useEditAction = (forceDisable: boolean, transformNodes: number) => {
const { canCreateTransform } = useContext(AuthorizationContext).capabilities;

const [config, setConfig] = useState<TransformConfigUnion>();
Expand All @@ -28,14 +28,14 @@ export const useEditAction = (forceDisable: boolean) => {
const action: TransformListAction = useMemo(
() => ({
name: () => <EditActionName />,
enabled: () => canCreateTransform || !forceDisable,
enabled: () => canCreateTransform && !forceDisable && transformNodes > 0,
description: editActionNameText,
icon: 'pencil',
type: 'icon',
onClick: (item: TransformListRow) => showFlyout(item.config),
'data-test-subj': 'transformActionEdit',
}),
[canCreateTransform, forceDisable]
[canCreateTransform, forceDisable, transformNodes]
);

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ describe('Transform: Transform List Actions <StartAction />', () => {
const props: StartActionNameProps = {
forceDisable: false,
items: [item],
transformNodes: 1,
};

const wrapper = shallow(<StartActionName {...props} />);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ export const startActionNameText = i18n.translate(

export const isStartActionDisabled = (
items: TransformListRow[],
canStartStopTransform: boolean
canStartStopTransform: boolean,
transformNodes: number
) => {
// Disable start for batch transforms which have completed.
const completedBatchTransform = items.some((i: TransformListRow) => isCompletedBatchTransform(i));
Expand All @@ -36,15 +37,24 @@ export const isStartActionDisabled = (
);

return (
!canStartStopTransform || completedBatchTransform || startedTransform || items.length === 0
!canStartStopTransform ||
completedBatchTransform ||
startedTransform ||
items.length === 0 ||
transformNodes === 0
);
};

export interface StartActionNameProps {
items: TransformListRow[];
forceDisable?: boolean;
transformNodes: number;
}
export const StartActionName: FC<StartActionNameProps> = ({ items, forceDisable }) => {
export const StartActionName: FC<StartActionNameProps> = ({
items,
forceDisable,
transformNodes,
}) => {
const { canStartStopTransform } = useContext(AuthorizationContext).capabilities;
const isBulkAction = items.length > 1;

Expand Down Expand Up @@ -89,7 +99,7 @@ export const StartActionName: FC<StartActionNameProps> = ({ items, forceDisable
);
}

const actionIsDisabled = isStartActionDisabled(items, canStartStopTransform);
const actionIsDisabled = isStartActionDisabled(items, canStartStopTransform, transformNodes);

let content: string | undefined;
if (actionIsDisabled && items.length > 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { useStartTransforms } from '../../../../hooks';
import { isStartActionDisabled, startActionNameText, StartActionName } from './start_action_name';

export type StartAction = ReturnType<typeof useStartAction>;
export const useStartAction = (forceDisable: boolean) => {
export const useStartAction = (forceDisable: boolean, transformNodes: number) => {
const { canStartStopTransform } = useContext(AuthorizationContext).capabilities;

const startTransforms = useStartTransforms();
Expand All @@ -43,17 +43,22 @@ export const useStartAction = (forceDisable: boolean) => {
const action: TransformListAction = useMemo(
() => ({
name: (item: TransformListRow) => (
<StartActionName items={[item]} forceDisable={forceDisable} />
<StartActionName
items={[item]}
forceDisable={forceDisable}
transformNodes={transformNodes}
/>
),
available: (item: TransformListRow) => item.stats.state === TRANSFORM_STATE.STOPPED,
enabled: (item: TransformListRow) => !isStartActionDisabled([item], canStartStopTransform),
enabled: (item: TransformListRow) =>
!isStartActionDisabled([item], canStartStopTransform, transformNodes),
description: startActionNameText,
icon: 'play',
type: 'icon',
onClick: (item: TransformListRow) => openModal([item]),
'data-test-subj': 'transformActionStart',
}),
[canStartStopTransform, forceDisable]
[canStartStopTransform, forceDisable, transformNodes]
);

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jest.mock('../../../../../shared_imports');

describe('Transform: Transform List <CreateTransformButton />', () => {
test('Minimal initialization', () => {
const wrapper = shallow(<CreateTransformButton onClick={jest.fn()} />);
const wrapper = shallow(<CreateTransformButton onClick={jest.fn()} transformNodes={1} />);

expect(wrapper).toMatchSnapshot();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,20 @@ import {

interface CreateTransformButtonProps {
onClick: MouseEventHandler<HTMLButtonElement>;
transformNodes: number;
}

export const CreateTransformButton: FC<CreateTransformButtonProps> = ({ onClick }) => {
export const CreateTransformButton: FC<CreateTransformButtonProps> = ({
onClick,
transformNodes,
}) => {
const { capabilities } = useContext(AuthorizationContext);

const disabled =
!capabilities.canCreateTransform ||
!capabilities.canPreviewTransform ||
!capabilities.canStartStopTransform;
!capabilities.canStartStopTransform ||
transformNodes === 0;

const createTransformButton = (
<EuiButton
Expand All @@ -45,7 +50,12 @@ export const CreateTransformButton: FC<CreateTransformButtonProps> = ({ onClick

if (disabled) {
return (
<EuiToolTip position="top" content={createCapabilityFailureMessage('canCreateTransform')}>
<EuiToolTip
position="top"
content={createCapabilityFailureMessage(
transformNodes > 0 ? 'canCreateTransform' : 'noTransformNodes'
)}
>
{createTransformButton}
</EuiToolTip>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,8 @@ describe('Transform: Transform List <TransformList />', () => {
test('Minimal initialization', () => {
const wrapper = shallow(
<TransformList
errorMessage={undefined}
isInitialized={true}
onCreateTransform={jest.fn()}
transformNodes={1}
transforms={[]}
transformsLoading={false}
/>
Expand Down
Loading

0 comments on commit f3b74b4

Please sign in to comment.