Skip to content

Commit

Permalink
Add event source mappings in Rulebook Activation form (#2907)
Browse files Browse the repository at this point in the history
  • Loading branch information
lgalis authored Aug 12, 2024
1 parent 6e083a8 commit 417b4db
Show file tree
Hide file tree
Showing 6 changed files with 388 additions and 33 deletions.
9 changes: 9 additions & 0 deletions frontend/eda/interfaces/EdaSource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Source } from './generated/eda-api';
export type EdaSource = Source;

export type EdaSourceEventMapping = {
source_name: string;
webhook_id: string;
webhook_name: string;
rulebook_hash: string;
};
6 changes: 6 additions & 0 deletions frontend/eda/interfaces/generated/eda-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1552,6 +1552,12 @@ export enum ScmTypeEnum {
Git = 'git',
}

export interface Source {
name: string;
source_info: string;
rulebook_hash: string;
}

/**
* * `starting` - starting
* * `running` - running
Expand Down
88 changes: 55 additions & 33 deletions frontend/eda/rulebook-activations/RulebookActivationForm.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useCallback } from 'react';
import { useWatch } from 'react-hook-form';
import React, { useCallback, useEffect, useState } from 'react';
import { useFormContext, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { Link, useNavigate } from 'react-router-dom';
import {
Expand All @@ -12,8 +12,10 @@ import {
PageLayout,
compareStrings,
useGetPageUrl,
usePageDialog,
usePageNavigate,
} from '../../../framework';
import { Button, Label, Tooltip } from '@patternfly/react-core';
import { PageFormAsyncSelect } from '../../../framework/PageForm/Inputs/PageFormAsyncSelect';
import { PageFormSection } from '../../../framework/PageForm/Utils/PageFormSection';
import { requestGet, swrOptions } from '../../common/crud/Data';
Expand All @@ -37,8 +39,11 @@ import { EdaProjectCell } from '../projects/components/EdaProjectCell';
import { PageFormSelectOrganization } from '../access/organizations/components/PageFormOrganizationSelect';
import useSWR from 'swr';
import { EdaOrganization } from '../interfaces/EdaOrganization';
import { EdaWebhook } from '../interfaces/EdaWebhook';
import { PageFormMultiSelect } from '../../../framework/PageForm/Inputs/PageFormMultiSelect';
import { SourceEventStreamMappingModal } from './components/SourceEventStreamMapping';
import { EdaSourceEventMapping } from '../interfaces/EdaSource';
import { PageFormGroup } from '../../../framework/PageForm/Inputs/PageFormGroup';
import jsyaml from 'js-yaml';
import { LabelGroupWrapper } from '../../common/label-group-wrapper';

export function CreateRulebookActivation() {
const { t } = useTranslation();
Expand Down Expand Up @@ -94,7 +99,6 @@ export function CreateRulebookActivation() {
restart_policy: RestartPolicyEnum.OnFailure,
log_level: LogLevelEnum.Error,
is_enabled: true,
swap_single_source: false,
}}
>
<RulebookActivationInputs />
Expand All @@ -106,6 +110,8 @@ export function CreateRulebookActivation() {
export function RulebookActivationInputs() {
const { t } = useTranslation();
const getPageUrl = useGetPageUrl();
const [sourceMappings, setSourceMappings] = useState<EdaSourceEventMapping[] | undefined>([]);
const { register, setValue } = useFormContext();
const restartPolicyHelpBlock = (
<>
<p>
Expand Down Expand Up @@ -138,8 +144,8 @@ export function RulebookActivationInputs() {
const { data: tokens } = useGet<EdaResult<AwxToken>>(
edaAPI`/users/me/awx-tokens/?page=1&page_size=200`
);
const { data: webhooks } = useGet<EdaResult<EdaWebhook>>(edaAPI`/webhooks/?page=1&page_size=200`);

const [_, setDialog] = usePageDialog();
const RESTART_OPTIONS = [
{ label: t('On failure'), value: 'on-failure' },
{ label: t('Always'), value: 'always' },
Expand All @@ -156,6 +162,10 @@ export function RulebookActivationInputs() {
name: 'project_id',
}) as number;

const rulebook = useWatch<IEdaRulebookActivationInputs>({
name: 'rulebook',
}) as EdaRulebook;

const query = useCallback(async () => {
const response = await requestGet<EdaResult<EdaRulebook>>(
projectId !== undefined
Expand All @@ -168,6 +178,14 @@ export function RulebookActivationInputs() {
});
}, [projectId]);

useEffect(() => {
setValue('source_mappings', jsyaml.dump(sourceMappings));
}, [setValue, sourceMappings]);

useEffect(() => {
setSourceMappings(undefined);
}, [rulebook, setSourceMappings]);

return (
<>
<PageFormTextInput<IEdaRulebookActivationInputs>
Expand Down Expand Up @@ -215,7 +233,37 @@ export function RulebookActivationInputs() {
isRequired
labelHelp={t('Rulebooks will be shown according to the project selected.')}
labelHelpTitle={t('Rulebook')}
additionalControls={
<Button
variant="link"
data-cy={'manage_event_stream'}
isDisabled={!rulebook}
onClick={() =>
setDialog(
<SourceEventStreamMappingModal
rulebook={rulebook}
mappings={sourceMappings}
setSourceMappings={setSourceMappings}
/>
)
}
>{`${t('Manage event streams')}`}</Button>
}
/>
{sourceMappings && sourceMappings.length > 0 && (
<PageFormGroup label={t('Event streams')}>
<LabelGroupWrapper>
{sourceMappings.map((map) => (
<>
<Tooltip content={<div>{map.source_name}</div>}>
<Label>{map.webhook_name}</Label>
</Tooltip>
</>
))}
</LabelGroupWrapper>
</PageFormGroup>
)}
<input type="hidden" {...register(`source_mappings`)} />
<PageFormCredentialSelect<{ credential_refs: string; id: string }>
name="credential_refs"
credentialKinds={['vault,cloud']}
Expand Down Expand Up @@ -289,32 +337,6 @@ export function RulebookActivationInputs() {
labelHelp={t('Optional service name.')}
labelHelpTitle={t('Service name')}
/>
<PageFormSection>
<PageFormMultiSelect<IEdaRulebookActivationInputs>
name="webhooks"
label={t('Event stream(s)')}
options={
webhooks?.results
? webhooks.results.map((item) => ({
label: item?.name || '',
value: `${item.id}`,
}))
: []
}
placeholder={t('Select event stream(s)')}
footer={<Link to={getPageUrl(EdaRoute.CreateWebhook)}>Create event stream</Link>}
/>
<PageFormSwitch<IEdaRulebookActivationInputs>
id="swap_single_source"
name="swap_single_source"
label={t('Swap single source?')}
labelHelp={t(
'Event streams can be used to swap out one or more sources in your rulebook, the name of the source and the event stream have to match.'
)}
labelHelpTitle={t('Swap single source')}
/>
</PageFormSection>

<PageFormSwitch<IEdaRulebookActivationInputs>
id="rulebook-activation"
name="is_enabled"
Expand Down Expand Up @@ -345,5 +367,5 @@ type IEdaRulebookActivationInputs = Omit<EdaRulebookActivationCreate, 'event_str
project_id: string;
awx_token_id: number;
credential_refs?: EdaCredential[] | null;
swap_single_source: boolean;
source_mappings: EdaSourceEventMapping[];
};
108 changes: 108 additions & 0 deletions frontend/eda/rulebook-activations/components/SourceEventMapFields.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { Button, FormFieldGroupExpandable, FormFieldGroupHeader } from '@patternfly/react-core';
import { TrashIcon } from '@patternfly/react-icons';
import { useTranslation } from 'react-i18next';
import { useFormContext, useWatch } from 'react-hook-form';
import { PageFormTextArea } from '../../../../framework';
import { EdaSource, EdaSourceEventMapping } from '../../interfaces/EdaSource';
import { edaAPI } from '../../common/eda-utils';
import { EdaResult } from '../../interfaces/EdaResult';
import { EdaRulebook } from '../../interfaces/EdaRulebook';
import { useGet } from '../../../common/crud/useGet';
import { EdaWebhook } from '../../interfaces/EdaWebhook';
import { PageFormSingleSelect } from '../../../../framework/PageForm/Inputs/PageFormSingleSelect';

export function SourceEventMapFields(props: {
index: number;
rulebook: EdaRulebook;
source_mappings: EdaSourceEventMapping;
onDelete: (id: number) => void;
}) {
const { t } = useTranslation();
const { index, onDelete } = props;
const { register, setValue } = useFormContext();

const { data: sources } = useGet<EdaResult<EdaSource>>(
edaAPI`/rulebooks/` + `${props?.rulebook?.id}/sources/?page=1&page_size=200`
);
const { data: events } = useGet<EdaResult<EdaWebhook>>(edaAPI`/webhooks/?page=1&page_size=200`);

const selectedSource = useWatch({ name: `mappings.${index}.source_name` }) as string;
let srcIndex = -1;
if (sources?.results) {
srcIndex = sources.results.findIndex((source) => source.name === selectedSource);

if (srcIndex > -1) {
setValue(`${index}.source_info`, sources.results[srcIndex].source_info);
setValue(`mappings.${index}.rulebook_hash`, sources.results[srcIndex].rulebook_hash);
}
}
const selectedEvent = useWatch({ name: `mappings.${index}.webhook_id` }) as number;
let evIndex = -1;
if (events?.results) {
evIndex = events.results.findIndex((event) => event.id === selectedEvent);

if (evIndex > -1) {
setValue(`mappings.${index}.webhook_name`, events.results[evIndex].name);
}
}

return (
<FormFieldGroupExpandable
isExpanded
header={
<FormFieldGroupHeader
titleText={{ text: t('Mapping ') + `${index + 1}`, id: `Mapping ${index}` }}
data-cy={'mapping-header-' + `${index}`}
actions={
<Button
id={`map-delete-${index}`}
icon={<TrashIcon />}
aria-label={t('Delete map')}
onClick={() => onDelete(index)}
variant="plain"
/>
}
/>
}
>
<PageFormSingleSelect
name={`mappings.${index}.source_name`}
label={t('Source')}
placeholder={t('Select source')}
isRequired
labelHelp={t('Sources in the rulebook.')}
labelHelpTitle={t('Sources')}
options={
sources?.results
? sources.results.map((item: { name: string }) => ({
label: item.name,
value: item.name,
}))
: []
}
/>
<PageFormSingleSelect
name={`mappings.${index}.webhook_id`}
label={t('Event stream')}
placeholder={t('Select event stream')}
isRequired
labelHelp={t('Event stream to swap with the source.')}
labelHelpTitle={t('Event streams')}
options={
events?.results
? events.results.map((item: { name: string; id: number }) => ({
label: item.name,
value: item.id,
}))
: []
}
/>
<PageFormTextArea
name={`${index}.source_info`}
label={t('Preview of source from rulebook')}
isReadOnly
/>
<input type="hidden" {...register(`mappings.${index}.rulebook_hash`)} />
</FormFieldGroupExpandable>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { edaAPI } from '../../common/eda-utils';
import { SourceEventStreamMappingModal } from './SourceEventStreamMapping';

describe('SourceEventStreamMapping.cy.ts', () => {
beforeEach(() => {
cy.intercept(
{ method: 'GET', url: edaAPI`/rulebooks/1/sources/?page=1&page_size=200` },
{
count: 3,
next: null,
previous: null,
page_size: 10,
page: 1,
results: [
{
name: '__SOURCE_1',
source_info: 'ansible.eda.sample1:\n sample1: 10\n',
rulebook_hash: 'hash_1',
},
{
name: '__SOURCE_2',
source_info: 'ansible.eda.sample2:\n sample2: 10\n',
rulebook_hash: 'hash_2',
},
{
name: '__SOURCE_3',
source_info: 'ansible.eda.sample3:\n sample3: 10\n',
rulebook_hash: 'hash_3',
},
],
}
);
cy.intercept(
{ method: 'GET', url: edaAPI`/webhooks/*` },
{
fixture: 'edaWebhooks.json',
}
);
});

it('Renders the correct rulebook activations columns', () => {
cy.mount(
<SourceEventStreamMappingModal
rulebook={{
id: 1,
name: 'sample_rulebook.yml',
description: '',
rulesets:
'---\n- name: Test run job templates\n hosts: all\n sources:\n - ansible.eda.range:\n limit: 10\n rules:\n - name: "1 job template"\n condition: event.i >= 0\n action:\n run_job_template:\n name: Demo Job Template\n organization: Default\n job_args:\n extra_vars:\n hello: Fred\n retries: 1\n delay: 10\n',
project_id: 2,
organization_id: 1,
created_at: '2024-08-07T22:29:42.081976Z',
modified_at: '2024-08-07T22:29:42.081983Z',
}}
mappings={undefined}
setSourceMappings={() => undefined}
/>
);
cy.get('.pf-v5-c-modal-box__title-text').should('contain', 'Event streams');
cy.contains(
'Event streams represent server side webhooks which ease the routing issues related to running webhooks individually in a container or a pod. ' +
'You can swap the sources in your rulebook with a matching event stream. Typically the sources to swap out are of the type ansible.eda.rulebook, ' +
'but you may also be able to swap out your own webhook source plugins. The swapping process replaces just the source type and args and leaves your ' +
'filters intact. We swap out the webhook source type with a source of type ansible.eda.pg_listener.'
).should('be.visible');
cy.get('[data-cy="rulebook"]').should('contain.text', 'sample_rulebook.yml');
cy.get('[data-cy="number-of-sources"]').should('contain.text', '3');
cy.get('[data-cy="add_event_stream"]').should('contain.text', 'Add event stream');
cy.clickButton(/^Add event stream$/);
cy.get('[data-cy="mapping-header-0"]').should('contain.text', 'Mapping 1');
cy.get('[data-cy="mappings-0-source-name"]').click();
cy.get('#--source-1 > .pf-v5-c-menu__item-main > .pf-v5-c-menu__item-text').click();
cy.get('[data-cy="0-source-info"]').should(
'contain.text',
'ansible.eda.sample1:\n sample1: 10\n'
);
});
});
Loading

0 comments on commit 417b4db

Please sign in to comment.