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

fix(agent): action menu should have flip enabled #584

Merged
merged 1 commit into from
Dec 5, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
214 changes: 132 additions & 82 deletions src/app/Agent/AgentProbeTemplates.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,22 +54,26 @@ import {
TextInput,
StackItem,
Stack,
Dropdown,
DropdownItem,
KebabToggle,
DropdownPosition,
EmptyState,
EmptyStateIcon,
Title,
} from '@patternfly/react-core';
import { SearchIcon, UploadIcon } from '@patternfly/react-icons';
import {
Table,
TableBody,
TableHeader,
TableVariant,
IAction,
IRowData,
IExtraData,
ISortBy,
SortByDirection,
sortable,
Tr,
Td,
ThProps,
TableComposable,
Tbody,
Th,
Thead,
} from '@patternfly/react-table';
import { first } from 'rxjs/operators';
import { LoadingView } from '@app/LoadingView/LoadingView';
Expand All @@ -96,10 +100,24 @@ export const AgentProbeTemplates: React.FunctionComponent<AgentProbeTemplatesPro
const [sortBy, setSortBy] = React.useState({} as ISortBy);
const [isLoading, setIsLoading] = React.useState(false);
const [errorMessage, setErrorMessage] = React.useState('');
const [rowDeleteData, setRowDeleteData] = React.useState({} as IRowData);
const [templateToDelete, setTemplateToDelete] = React.useState(undefined as ProbeTemplate | undefined);
const [warningModalOpen, setWarningModalOpen] = React.useState(false);

const tableColumns = React.useMemo(() => [{ title: 'Name', transforms: [sortable] }, { title: 'XML' }], [sortable]);
const tableColumns: string[] = ['Name', 'XML'];

const getSortParams = React.useCallback(
(columnIndex: number): ThProps['sort'] => ({
sortBy: sortBy,
onSort: (_event, index, direction) => {
setSortBy({
index: index,
direction: direction,
});
},
columnIndex,
}),
[sortBy, setSortBy]
);

const handleTemplates = React.useCallback(
(templates) => {
Expand Down Expand Up @@ -128,10 +146,10 @@ export const AgentProbeTemplates: React.FunctionComponent<AgentProbeTemplatesPro
}, [addSubscription, context.api, setIsLoading, handleTemplates, handleError]);

const handleDelete = React.useCallback(
(rowData) => {
(template: ProbeTemplate) => {
addSubscription(
context.api
.deleteCustomProbeTemplate(rowData[0])
.deleteCustomProbeTemplate(template.name)
.pipe(first())
.subscribe(() => {
/** Do nothing. Notifications hook will handle */
Expand All @@ -141,68 +159,14 @@ export const AgentProbeTemplates: React.FunctionComponent<AgentProbeTemplatesPro
[addSubscription, context.api]
);

const handleDeleteButton = React.useCallback(
(rowData) => {
if (context.settings.deletionDialogsEnabledFor(DeleteWarningType.DeleteEventTemplates)) {
setRowDeleteData(rowData);
setWarningModalOpen(true);
} else {
handleDelete(rowData);
}
},
[context.settings, setWarningModalOpen, setRowDeleteData, handleDelete]
);

const handleWarningModalAccept = React.useCallback(() => {
handleDelete(rowDeleteData);
}, [handleDelete, rowDeleteData]);
handleDelete(templateToDelete!);
}, [handleDelete, templateToDelete]);

const handleWarningModalClose = React.useCallback(() => {
setWarningModalOpen(false);
}, [setWarningModalOpen]);

const handleSort = React.useCallback(
(event, index, direction) => {
setSortBy({ index, direction });
},
[setSortBy]
);

const handleInsert = React.useCallback(
(rowData) => {
addSubscription(
context.api
.insertProbes(rowData[0])
.pipe(first())
.subscribe(() => {})
);
},
[addSubscription, context.api]
);

const actionResolver = React.useCallback(
(rowData: IRowData, extraData: IExtraData) => {
if (typeof extraData.rowIndex == 'undefined') {
return [];
}
return [
{
title: 'Insert Probes...',
onClick: (event, rowId, rowData) => handleInsert(rowData),
isDisabled: !props.agentDetected,
},
{
isSeparator: true,
},
{
title: 'Delete',
onClick: (event, rowId, rowData) => handleDeleteButton(rowData),
},
] as IAction[];
},
[handleInsert, handleDeleteButton, props.agentDetected]
);

const handleTemplateUpload = React.useCallback(() => {
setUploadModalOpen(true);
}, [setUploadModalOpen]);
Expand Down Expand Up @@ -270,8 +234,51 @@ export const AgentProbeTemplates: React.FunctionComponent<AgentProbeTemplatesPro
setFilteredTemplates([...filtered]);
}, [filterText, templates, sortBy, setFilteredTemplates]);

const displayTemplates = React.useMemo(
() => filteredTemplates.map((t: ProbeTemplate) => [t.name, t.xml]),
const handleDeleteAction = React.useCallback(
(template: ProbeTemplate) => {
if (context.settings.deletionDialogsEnabledFor(DeleteWarningType.DeleteEventTemplates)) {
setTemplateToDelete(template);
setWarningModalOpen(true);
} else {
handleDelete(template);
}
},
[context.settings, setWarningModalOpen, setTemplateToDelete, handleDelete]
);

const handleInsertAction = React.useCallback(
(template: ProbeTemplate) => {
addSubscription(
context.api
.insertProbes(template.name)
.pipe(first())
.subscribe(() => {})
);
},
[addSubscription, context.api]
);

const templateRows = React.useMemo(
() =>
filteredTemplates.map((t: ProbeTemplate, index) => {
return (
<Tr key={`probe-template-${index}`}>
<Td key={`probe-template-name-${index}`} dataLabel={tableColumns[0]}>
{t.name}
</Td>
<Td key={`probe-template-xml-${index}`} dataLabel={tableColumns[1]}>
{t.xml}
</Td>
<Td key={`probe-template-action-${index}`} isActionCell style={{ paddingRight: '0' }}>
<AgentTemplateAction
template={t}
onDelete={handleDeleteAction}
onInsert={props.agentDetected ? handleInsertAction : undefined}
/>
</Td>
</Tr>
);
}),
[filteredTemplates]
);

Expand Down Expand Up @@ -303,6 +310,7 @@ export const AgentProbeTemplates: React.FunctionComponent<AgentProbeTemplatesPro
placeholder="Filter..."
aria-label="Probe template filter"
onChange={setFilterText}
value={filterText}
/>
</ToolbarItem>
</ToolbarGroup>
Expand All @@ -321,19 +329,19 @@ export const AgentProbeTemplates: React.FunctionComponent<AgentProbeTemplatesPro
/>
</ToolbarContent>
</Toolbar>
{displayTemplates.length ? (
<Table
aria-label="Probe Templates Table"
variant={TableVariant.compact}
cells={tableColumns}
rows={displayTemplates}
actionResolver={actionResolver}
sortBy={sortBy}
onSort={handleSort}
>
<TableHeader />
<TableBody />
</Table>
{templateRows.length ? (
<TableComposable aria-label="Probe Templates Table" variant={TableVariant.compact}>
<Thead>
<Tr>
{tableColumns.map((column, index) => (
<Th key={`probe-template-header-${column}`} sort={getSortParams(index)}>
{column}
</Th>
))}
</Tr>
</Thead>
<Tbody>{...templateRows}</Tbody>
</TableComposable>
) : (
<EmptyState>
<EmptyStateIcon icon={SearchIcon} />
Expand Down Expand Up @@ -464,3 +472,45 @@ export const AgentProbeTemplateUploadModal: React.FunctionComponent<AgentProbeTe
</Modal>
);
};

export interface AgentTemplateActionProps {
template: ProbeTemplate;
onInsert?: (template: ProbeTemplate) => void;
onDelete: (template: ProbeTemplate) => void;
}

export const AgentTemplateAction: React.FunctionComponent<AgentTemplateActionProps> = (props) => {
const [isOpen, setIsOpen] = React.useState(false);

const actionItems = React.useMemo(() => {
return [
{
key: 'insert-template',
title: 'Insert Probes...',
onClick: () => props.onInsert && props.onInsert(props.template),
isDisabled: !props.onInsert,
},
{
key: 'delete-template',
title: 'Delete',
onClick: () => props.onDelete(props.template),
},
];
}, [props.onInsert, props.onDelete, props.template]);

return (
<Dropdown
isPlain
isOpen={isOpen}
toggle={<KebabToggle id="probe-template-toggle-kebab" onToggle={setIsOpen} />}
menuAppendTo={document.body}
position={DropdownPosition.right}
isFlipEnabled
dropdownItems={actionItems.map((action) => (
<DropdownItem key={action.key} onClick={action.onClick} isDisabled={action.isDisabled}>
{action.title}
</DropdownItem>
))}
/>
);
};
35 changes: 7 additions & 28 deletions src/test/Agent/AgentProbeTemplates.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,23 +35,21 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import * as React from 'react';
import renderer, { act } from 'react-test-renderer';
import { act as doAct, cleanup, screen, waitFor, within } from '@testing-library/react';
import '@testing-library/jest-dom';
import { of } from 'rxjs';
import { AgentProbeTemplates } from '@app/Agent/AgentProbeTemplates';
import { DeleteProbeTemplates } from '@app/Modal/DeleteWarningUtils';
import { ProbeTemplate } from '@app/Shared/Services/Api.service';
import {
MessageMeta,
MessageType,
NotificationCategory,
NotificationMessage,
} from '@app/Shared/Services/NotificationChannel.service';
import { ServiceContext, defaultServices } from '@app/Shared/Services/Services';
import { DeleteProbeTemplates } from '@app/Modal/DeleteWarningUtils';
import { AgentProbeTemplates } from '@app/Agent/AgentProbeTemplates';
import { defaultServices } from '@app/Shared/Services/Services';
import '@testing-library/jest-dom';
import { act as doAct, cleanup, screen, waitFor, within } from '@testing-library/react';
import * as React from 'react';
import { of } from 'rxjs';
import { renderWithServiceContext } from '../Common';
import { NotificationsContext, NotificationsInstance } from '@app/Notifications/Notifications';

const mockMessageType = { type: 'application', subtype: 'json' } as MessageType;

Expand Down Expand Up @@ -98,8 +96,6 @@ const uploadRequestSpy = jest.spyOn(defaultServices.api, 'addCustomProbeTemplate

jest
.spyOn(defaultServices.api, 'getProbeTemplates')
.mockReturnValueOnce(of([mockProbeTemplate])) // renders Correctly

.mockReturnValueOnce(of([mockProbeTemplate])) // should add a probe template after receiving a notification
.mockReturnValueOnce(of([mockProbeTemplate, mockAnotherProbeTemplate]))

Expand All @@ -110,9 +106,6 @@ jest

jest
.spyOn(defaultServices.notificationChannel, 'messages')
.mockReturnValueOnce(of()) // renders correctly
.mockReturnValueOnce(of())

.mockReturnValueOnce(of(mockCreateTemplateNotification)) // adds a template after receiving a notification
.mockReturnValueOnce(of())

Expand All @@ -124,20 +117,6 @@ jest
describe('<AgentProbeTemplates />', () => {
afterEach(cleanup);

it('renders correctly', async () => {
let tree;
await act(async () => {
tree = renderer.create(
<ServiceContext.Provider value={defaultServices}>
<NotificationsContext.Provider value={NotificationsInstance}>
<AgentProbeTemplates agentDetected={true} />
</NotificationsContext.Provider>
</ServiceContext.Provider>
);
});
expect(tree.toJSON()).toMatchSnapshot();
});

it('should add a probe template after receiving a notification', async () => {
renderWithServiceContext(<AgentProbeTemplates agentDetected={true} />);

Expand Down
Loading