Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[AAP-28420][AAP-AAP-29675][AAP-29676] Add user and teams access tabs to the Event Streams page #3048

Merged
merged 5 commits into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export function useMapContentTypeToDisplayName() {
decisionenvironment: options?.isTitleCase
? t('Decision Environment')
: t('decision environment'),
eventstream: options?.isTitleCase ? t('Event Stream') : t('event stream'),
auditrule: options?.isTitleCase ? t('Rule Audit') : t('rule audit'),
team: options?.isTitleCase ? t('Team') : t('team'),
organization: options?.isTitleCase ? t('Organization') : t('organization'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { EdaCredentialType } from '../../../interfaces/EdaCredentialType';
import { useEdaMultiSelectListView } from '../../../common/useEdaMultiSelectListView';
import { edaAPI } from '../../../common/eda-utils';
import styled from 'styled-components';
import { EdaEventStream } from '../../../interfaces/EdaEventStream';

export type EdaResourceType =
| EdaActivationInstance
Expand All @@ -24,7 +25,8 @@ export type EdaResourceType =
| EdaRulebookActivation
| EdaRuleAudit
| EdaProject
| EdaCredentialType;
| EdaCredentialType
| EdaEventStream;

const resourceToEndpointMapping: { [key: string]: string } = {
'eda.edacredential': edaAPI`/eda-credentials/`,
Expand All @@ -35,6 +37,7 @@ const resourceToEndpointMapping: { [key: string]: string } = {
'eda.credentialtype': edaAPI`/credential-types/`,
'eda.decisionenvironment': edaAPI`/decision-environments/`,
'eda.auditrule': edaAPI`/audit-rules/`,
'eda.eventstream': edaAPI`/event-streams/`,
};

const StyledTitle = styled(Title)`
Expand All @@ -57,6 +60,7 @@ export function EdaSelectResourcesStep() {
'eda.credentialtype': t('Select credential types'),
'eda.decisionenvironment': t('Select decision environments'),
'eda.auditrule': t('Select audit rules'),
'eda.eventstream': t('Select event stream'),
};
}, [t]);
const tableColumns = useMemo<ITableColumn<EdaResourceType>[]>(
Expand Down
4 changes: 4 additions & 0 deletions frontend/eda/access/roles/hooks/useEdaRoleMetadata.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ export function useEdaRoleMetadata(): EdaRoleMetadata {
'shared.change_team': t('Change team'),
'shared.delete_team': t('Delete team'),
'shared.view_team': t('View team'),
'eda.add_eventstream': t('Add event stream'),
'eda.change_eventstream': t('Change event stream'),
'eda.delete_eventstream': t('Delete event stream'),
'eda.view_eventstream': t('View event stream'),
},
},
'eda.project': {
Expand Down
56 changes: 42 additions & 14 deletions frontend/eda/event-streams/EventStreamForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ import { useFormContext, useWatch } from 'react-hook-form';
import { EdaCredentialType } from '../interfaces/EdaCredentialType';
import { useEffect } from 'react';
import { PageFormHidden } from '../../../framework/PageForm/Utils/PageFormHidden';
import { useOptions } from '../../common/crud/useOptions';
import { ActionsResponse, OptionsResponse } from '../interfaces/OptionsResponse';
import { Alert } from '@patternfly/react-core';
import { EventStreamDetails } from './EventStreamPage/EventStreamDetails';

// eslint-disable-next-line react/prop-types
function EventStreamInputs() {
Expand Down Expand Up @@ -216,6 +220,10 @@ export function EditEventStream() {
const navigate = useNavigate();
const params = useParams<{ id?: string }>();
const id = Number(params.id);
const { data } = useOptions<OptionsResponse<ActionsResponse>>(
edaAPI`/event-streams/${params.id ?? ''}/`
);
const canEditEventStream = Boolean(data && data.actions && data.actions['PATCH']);
const { data: eventStream } = useGet<EdaEventStream>(edaAPI`/event-streams/${id.toString()}/`);

const { cache } = useSWRConfig();
Expand Down Expand Up @@ -253,20 +261,40 @@ export function EditEventStream() {
{ label: `${t('Edit')} ${eventStream?.name || t('event stream')}` },
]}
/>
<EdaPageForm
submitText={t('Save event stream')}
onSubmit={onSubmit}
cancelText={t('Cancel')}
onCancel={onCancel}
defaultValue={{
...eventStream,
enabled: !eventStream.test_mode,
organization_id: eventStream.organization?.id,
eda_credential_id: eventStream?.eda_credential?.id,
}}
>
<EventStreamEditInputs />
</EdaPageForm>
{!canEditEventStream ? (
<>
<Alert
variant={'warning'}
isInline
style={{
marginLeft: '24px',
marginRight: '24px',
marginTop: '24px',
paddingLeft: '24px',
paddingTop: '16px',
}}
title={t(
'You do not have permissions to edit this credential. Please contact your organization administrator if there is an issue with your access.'
)}
/>
<EventStreamDetails />
</>
) : (
<EdaPageForm
submitText={t('Save event stream')}
onSubmit={onSubmit}
cancelText={t('Cancel')}
onCancel={onCancel}
defaultValue={{
...eventStream,
enabled: !eventStream.test_mode,
organization_id: eventStream.organization?.id,
eda_credential_id: eventStream?.eda_credential?.id,
}}
>
<EventStreamEditInputs />
</EdaPageForm>
)}
</PageLayout>
);
}
Expand Down
32 changes: 27 additions & 5 deletions frontend/eda/event-streams/EventStreamPage/EventStreamPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,21 @@ import { useDeleteEventStreams } from '../hooks/useDeleteEventStreams';
import { usePatchRequest } from '../../../common/crud/usePatchRequest';
import { useDisableEventStreams } from '../hooks/useDisableEventStreams';
import { EdaResult } from '../../interfaces/EdaResult';
import { useOptions } from '../../../common/crud/useOptions';
import { ActionsResponse, OptionsResponse } from '../../interfaces/OptionsResponse';

export function EventStreamPage() {
const { t } = useTranslation();
const params = useParams<{ id: string }>();
const pageNavigate = usePageNavigate();
const getPageUrl = useGetPageUrl();
const { data } = useOptions<OptionsResponse<ActionsResponse>>(
edaAPI`/event-streams/${params.id ?? ''}/`
);
const canEditEventStream = Boolean(data && data.actions && data.actions['PATCH']);
const { data: eventStream, refresh } = useGet<EdaEventStream>(
edaAPI`/event-streams/${params.id ?? ''}/`
);
const getPageUrl = useGetPageUrl();
const patchRequest = usePatchRequest();
const alertToaster = usePageAlertToaster();
const disableEventStreams = useDisableEventStreams((disabled) => {
Expand Down Expand Up @@ -96,6 +102,10 @@ export function EventStreamPage() {
else void disableEventStreams([eventStream]);
},
isSwitchOn: (eventStream: EdaEventStream) => !eventStream.test_mode,
isDisabled: () =>
canEditEventStream
? ''
: t(`The event stream cannot be edited due to insufficient permission`),
},
{
type: PageActionType.Button,
Expand All @@ -104,6 +114,10 @@ export function EventStreamPage() {
icon: PencilAltIcon,
isPinned: true,
label: t('Edit event stream'),
isDisabled: () =>
canEditEventStream
? ''
: t(`The event stream cannot be edited due to insufficient permission`),
onClick: (eventStream: EdaEventStream) =>
pageNavigate(EdaRoute.EditEventStream, { params: { id: eventStream.id } }),
},
Expand All @@ -115,16 +129,22 @@ export function EventStreamPage() {
selection: PageActionSelection.Single,
icon: TrashIcon,
label: t('Delete event stream'),
isDisabled: () =>
esActivations?.results && esActivations.results.length > 0
? t('To delete this event stream, disconnect it from all rulebook activations')
: undefined,
isDisabled: () => {
if (canEditEventStream) {
return esActivations?.results && esActivations.results.length > 0
? t('To delete this event stream, disconnect it from all rulebook activations')
: '';
} else {
return t(`The event stream cannot be deleted due to insufficient permission`);
}
},
onClick: (eventStream: EdaEventStream) => deleteEventStreams([eventStream]),
isDanger: true,
},
]
: [],
[
canEditEventStream,
deleteEventStreams,
disableEventStreams,
enableEventStream,
Expand Down Expand Up @@ -160,6 +180,8 @@ export function EventStreamPage() {
tabs={[
{ label: t('Details'), page: EdaRoute.EventStreamDetails },
{ label: t('Activations'), page: EdaRoute.EventStreamActivations },
{ label: t('Team Access'), page: EdaRoute.EventStreamTeamAccess },
{ label: t('User Access'), page: EdaRoute.EventStreamUserAccess },
]}
params={{ id: eventStream?.id }}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useParams } from 'react-router-dom';
import { EdaRoute } from '../../main/EdaRoutes';
import { TeamAccess } from '../../../common/access/components/TeamAccess';

export function EventStreamTeamAccess() {
const params = useParams<{ id: string }>();
return (
<TeamAccess
service="eda"
id={params.id || ''}
type={'eventstream'}
addRolesRoute={EdaRoute.EventStreamAddTeams}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useParams } from 'react-router-dom';
import { EdaRoute } from '../../main/EdaRoutes';
import { UserAccess } from '../../../common/access/components/UserAccess';

export function EventStreamUserAccess() {
const params = useParams<{ id: string }>();
return (
<UserAccess
service="eda"
id={params.id || ''}
type={'eventstream'}
addRolesRoute={EdaRoute.EventStreamAddUsers}
/>
);
}
27 changes: 22 additions & 5 deletions frontend/eda/event-streams/EventStreams.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import { useEventStreamActions } from './hooks/useEventStreamActions';
import { useEventStreamColumns } from './hooks/useEventStreamColumns';
import { useEventStreamFilters } from './hooks/useEventStreamFilters';
import { useEventStreamsActions } from './hooks/useEventStreamsActions';
import { PlusCircleIcon } from '@patternfly/react-icons';
import { CubesIcon, PlusCircleIcon } from '@patternfly/react-icons';
import { useOptions } from '../../common/crud/useOptions';
import { ActionsResponse, OptionsResponse } from '../interfaces/OptionsResponse';

export function EventStreams() {
const { t } = useTranslation();
Expand All @@ -21,6 +23,8 @@ export function EventStreams() {
tableColumns,
});
const toolbarActions = useEventStreamsActions(view);
const { data } = useOptions<OptionsResponse<ActionsResponse>>(edaAPI`/event-streams/`);
const canCreateEventStream = Boolean(data && data.actions && data.actions['POST']);
const rowActions = useEventStreamActions(view);
return (
<PageLayout>
Expand All @@ -38,11 +42,24 @@ export function EventStreams() {
toolbarFilters={toolbarFilters}
rowActions={rowActions}
errorStateTitle={t('Error loading event streams')}
emptyStateTitle={t('There are currently no event streams created for your organization.')}
emptyStateDescription={t('Please create an event stream by using the button below.')}
emptyStateTitle={
canCreateEventStream
? t('There are currently no event streams created for your organization.')
: t('You do not have permission to create an event stream.')
}
emptyStateDescription={
canCreateEventStream
? t('Please create an event stream by using the button below.')
: t(
'Please contact your organization administrator if there is an issue with your access.'
)
}
emptyStateIcon={canCreateEventStream ? undefined : CubesIcon}
emptyStateButtonIcon={<PlusCircleIcon />}
emptyStateButtonText={t('Create event stream')}
emptyStateButtonClick={() => pageNavigate(EdaRoute.CreateEventStream)}
emptyStateButtonText={canCreateEventStream ? t('Create event stream') : undefined}
emptyStateButtonClick={
canCreateEventStream ? () => pageNavigate(EdaRoute.CreateEventStream) : undefined
}
{...view}
defaultSubtitle={t('Event stream')}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { edaAPI } from '../../common/eda-utils';
import { EdaEventStreamAddTeams } from './EdaEventStreamAddTeams';

describe('EdaEventStreamAddTeams', () => {
const component = <EdaEventStreamAddTeams />;
const path = '/event-streams/:id/team-access/add-teams';
const initialEntries = [`/event-streams/1/team-access/add-teams`];
const params = {
path,
initialEntries,
};

beforeEach(() => {
cy.intercept('GET', edaAPI`/event-streams/*`, { fixture: 'edaEventStream.json' });
cy.intercept('GET', edaAPI`/teams/?order_by=name*`, { fixture: 'edaTeams.json' });
cy.intercept('GET', edaAPI`/role_definitions/?content_type__model=event-stream*`, {
fixture: 'edaEventStreamRoles.json',
});
cy.mount(component, params);
});
it('should render with correct steps', () => {
cy.get('[data-cy="wizard-nav"] li').eq(0).should('contain.text', 'Select team(s)');
cy.get('[data-cy="wizard-nav"] li').eq(1).should('contain.text', 'Select roles to apply');
cy.get('[data-cy="wizard-nav"] li').eq(2).should('contain.text', 'Review');
cy.get('[data-cy="wizard-nav-item-teams"] button').should('have.class', 'pf-m-current');
cy.get('table tbody').find('tr').should('have.length', 4);
});
it('can filter teams by name', () => {
cy.intercept(edaAPI`/teams/?name=Gal*`, { fixtures: 'edaTeams.json' }).as('nameFilterRequest');
cy.filterTableByText('Gal');
cy.wait('@nameFilterRequest');
cy.clearAllFilters();
});
it('should validate that at least one team is selected for moving to next step', () => {
cy.get('table tbody').find('tr').should('have.length', 4);
cy.clickButton(/^Next$/);
cy.get('.pf-v5-c-alert__title').should('contain.text', 'Select at least one team.');
cy.selectTableRowByCheckbox('name', 'Demo', { disableFilter: true });
cy.clickButton(/^Next$/);
cy.get('[data-cy="wizard-nav-item-teams"] button').should('not.have.class', 'pf-m-current');
cy.get('[data-cy="wizard-nav-item-roles"] button').should('have.class', 'pf-m-current');
});
it('should validate that at least one role is selected for moving to Review step', () => {
cy.selectTableRowByCheckbox('name', 'Demo', { disableFilter: true });
cy.clickButton(/^Next$/);
cy.get('[data-cy="wizard-nav-item-roles"] button').should('have.class', 'pf-m-current');
cy.clickButton(/^Next$/);
cy.get('.pf-v5-c-alert__title').should('contain.text', 'Select at least one role.');
cy.selectTableRowByCheckbox('name', 'EventStream Admin', { disableFilter: true });
cy.clickButton(/^Next$/);
cy.get('[data-cy="wizard-nav-item-roles"] button').should('not.have.class', 'pf-m-current');
cy.get('[data-cy="wizard-nav-item-review"] button').should('have.class', 'pf-m-current');
});
it('should display selected team and role in the Review step', () => {
cy.selectTableRowByCheckbox('name', 'Demo', { disableFilter: true });
cy.clickButton(/^Next$/);
cy.selectTableRowByCheckbox('name', 'EventStream Admin', { disableFilter: true });
cy.clickButton(/^Next$/);
cy.get('[data-cy="wizard-nav-item-review"] button').should('have.class', 'pf-m-current');
cy.get('[data-cy="expandable-section-teams"]').should('contain.text', 'Teams');
cy.get('[data-cy="expandable-section-teams"]').should('contain.text', '1');
cy.get('[data-cy="expandable-section-teams"]').should('contain.text', 'Demo');
cy.get('[data-cy="expandable-section-edaRoles"]').should('contain.text', 'Roles');
cy.get('[data-cy="expandable-section-edaRoles"]').should('contain.text', '1');
cy.get('[data-cy="expandable-section-edaRoles"]').should('contain.text', 'EventStream Admin');
cy.get('[data-cy="expandable-section-edaRoles"]').should(
'contain.text',
'Has all permissions to a single event-stream and its child resources - rulebook'
);
});
it('should trigger bulk action dialog on submit', () => {
cy.intercept('POST', edaAPI`/role_team_assignments/`, {
statusCode: 201,
body: { team: 3, role_definition: 14, content_type: 'eda.event-stream', object_id: 1 },
}).as('createRoleAssignment');
cy.selectTableRowByCheckbox('name', 'Demo', { disableFilter: true });
cy.clickButton(/^Next$/);
cy.selectTableRowByCheckbox('name', 'EventStream Admin', { disableFilter: true });
cy.clickButton(/^Next$/);
cy.clickButton(/^Finish$/);
cy.wait('@createRoleAssignment');
// Bulk action modal is displayed with success
cy.get('.pf-v5-c-modal-box').within(() => {
cy.get('table tbody').find('tr').should('have.length', 1);
cy.get('table tbody').should('contain.text', 'Demo');
cy.get('table tbody').should('contain.text', 'EventStream Admin');
cy.get('div.pf-v5-c-progress__description').should('contain.text', 'Success');
cy.get('div.pf-v5-c-progress__status').should('contain.text', '100%');
});
});
});
Loading
Loading