diff --git a/x-pack/plugins/osquery/cypress/integration/all/alerts.spec.ts b/x-pack/plugins/osquery/cypress/integration/all/alerts.spec.ts index 153fd5d58791e..90492838bd2c3 100644 --- a/x-pack/plugins/osquery/cypress/integration/all/alerts.spec.ts +++ b/x-pack/plugins/osquery/cypress/integration/all/alerts.spec.ts @@ -17,6 +17,7 @@ import { import { preparePack } from '../../tasks/packs'; import { closeModalIfVisible } from '../../tasks/integrations'; import { navigateTo } from '../../tasks/navigation'; +import { RESULTS_TABLE, RESULTS_TABLE_BUTTON } from '../../screens/live_query'; describe('Alert Event Details', () => { before(() => { @@ -58,8 +59,16 @@ describe('Alert Event Details', () => { cy.getBySel('expand-event').first().click(); cy.getBySel('take-action-dropdown-btn').click(); cy.getBySel('osquery-action-item').click(); + cy.contains('1 agent selected.'); inputQuery('select * from uptime;'); submitQuery(); checkResults(); + + cy.getBySel(RESULTS_TABLE).within(() => { + cy.getBySel(RESULTS_TABLE_BUTTON).should('not.exist'); + }); + cy.contains('Save for later').click(); + cy.contains('Save query'); + cy.contains(/^Save$/); }); }); diff --git a/x-pack/plugins/osquery/cypress/integration/all/live_query.spec.ts b/x-pack/plugins/osquery/cypress/integration/all/live_query.spec.ts index d6af17596d89a..c33763bb2bff4 100644 --- a/x-pack/plugins/osquery/cypress/integration/all/live_query.spec.ts +++ b/x-pack/plugins/osquery/cypress/integration/all/live_query.spec.ts @@ -15,7 +15,11 @@ import { typeInECSFieldInput, typeInOsqueryFieldInput, } from '../../tasks/live_query'; -import { RESULTS_TABLE_CELL_WRRAPER } from '../../screens/live_query'; +import { + RESULTS_TABLE, + RESULTS_TABLE_BUTTON, + RESULTS_TABLE_CELL_WRRAPER, +} from '../../screens/live_query'; import { getAdvancedButton } from '../../screens/integrations'; describe('ALL - Live Query', () => { @@ -48,6 +52,9 @@ describe('ALL - Live Query', () => { submitQuery(); checkResults(); + cy.getBySel(RESULTS_TABLE).within(() => { + cy.getBySel(RESULTS_TABLE_BUTTON).should('exist'); + }); cy.react(RESULTS_TABLE_CELL_WRRAPER, { props: { id: 'message', index: 1 }, }); diff --git a/x-pack/plugins/osquery/cypress/integration/roles/reader.spec.ts b/x-pack/plugins/osquery/cypress/integration/roles/reader.spec.ts index d3a00f970322b..ddbca76efcf16 100644 --- a/x-pack/plugins/osquery/cypress/integration/roles/reader.spec.ts +++ b/x-pack/plugins/osquery/cypress/integration/roles/reader.spec.ts @@ -46,6 +46,11 @@ describe('Reader - only READ', () => { cy.contains('Update query').should('not.exist'); cy.contains(`Delete query`).should('not.exist'); }); + it('should not be able to enter live queries with just read and no run saved queries', () => { + navigateTo('/app/osquery/live_queries/new'); + cy.waitForReact(1000); + cy.contains('Permission denied'); + }); it('should not be able to play in live queries history', () => { navigateTo('/app/osquery/live_queries'); cy.waitForReact(1000); diff --git a/x-pack/plugins/osquery/cypress/screens/live_query.ts b/x-pack/plugins/osquery/cypress/screens/live_query.ts index 54c19fe508705..ce29edc2c9187 100644 --- a/x-pack/plugins/osquery/cypress/screens/live_query.ts +++ b/x-pack/plugins/osquery/cypress/screens/live_query.ts @@ -10,6 +10,7 @@ export const ALL_AGENTS_OPTION = '[title="All agents"]'; export const LIVE_QUERY_EDITOR = '#osquery_editor'; export const SUBMIT_BUTTON = '#submit-button'; +export const RESULTS_TABLE = 'osqueryResultsTable'; export const RESULTS_TABLE_BUTTON = 'dataGridFullScreenButton'; export const RESULTS_TABLE_CELL_WRRAPER = 'EuiDataGridHeaderCellWrapper'; export const getSavedQueriesDropdown = () => diff --git a/x-pack/plugins/osquery/cypress/tasks/live_query.ts b/x-pack/plugins/osquery/cypress/tasks/live_query.ts index 2e199ae453f1b..b3006a3d1074a 100644 --- a/x-pack/plugins/osquery/cypress/tasks/live_query.ts +++ b/x-pack/plugins/osquery/cypress/tasks/live_query.ts @@ -16,6 +16,7 @@ export const selectAllAgents = () => { cy.react('EuiComboBox', { props: { placeholder: 'Select agents or groups' } }).type( '{downArrow}{enter}{esc}' ); + cy.contains('1 agent selected.'); }; export const inputQuery = (query: string) => cy.get(LIVE_QUERY_EDITOR).type(query); diff --git a/x-pack/plugins/osquery/public/agents/agents_table.tsx b/x-pack/plugins/osquery/public/agents/agents_table.tsx index 105518537384f..fac74086670c2 100644 --- a/x-pack/plugins/osquery/public/agents/agents_table.tsx +++ b/x-pack/plugins/osquery/public/agents/agents_table.tsx @@ -83,6 +83,37 @@ const AgentsTableComponent: React.FC = ({ agentSelection, onCh const [numAgentsSelected, setNumAgentsSelected] = useState(0); const defaultValueInitialized = useRef(false); + const onSelection = useCallback( + (selection: GroupOption[]) => { + // TODO?: optimize this by making the selection computation incremental + const { + newAgentSelection, + selectedAgents, + selectedGroups, + }: { + newAgentSelection: AgentSelection; + selectedAgents: AgentOptionValue[]; + selectedGroups: SelectedGroups; + } = generateAgentSelection(selection); + if (newAgentSelection.allAgentsSelected) { + setNumAgentsSelected(totalNumAgents); + } else { + const checkAgent = generateAgentCheck(selectedGroups); + setNumAgentsSelected( + // filter out all the agents counted by selected policies and platforms + selectedAgents.filter(checkAgent).length + + // add the number of agents added via policy and platform groups + getNumAgentsInGrouping(selectedGroups) - + // subtract the number of agents double counted by policy/platform selections + getNumOverlapped(selectedGroups, groups.overlap) + ); + } + onChange(newAgentSelection); + setSelectedOptions(selection); + }, + [groups, onChange, totalNumAgents] + ); + useEffect(() => { const handleSelectedOptions = (selection: string[], label: string) => { const agentOptions = find(['label', label], options); @@ -95,7 +126,7 @@ const AgentsTableComponent: React.FC = ({ agentSelection, onCh }); if (defaultOptions?.length) { - setSelectedOptions(defaultOptions); + onSelection(defaultOptions); defaultValueInitialized.current = true; } } @@ -105,7 +136,7 @@ const AgentsTableComponent: React.FC = ({ agentSelection, onCh const allAgentsOptions = find(['label', ALL_AGENTS_LABEL], options); if (allAgentsOptions?.options) { - setSelectedOptions(allAgentsOptions.options); + onSelection(allAgentsOptions.options); defaultValueInitialized.current = true; } } @@ -118,7 +149,7 @@ const AgentsTableComponent: React.FC = ({ agentSelection, onCh handleSelectedOptions(agentSelection.agents, AGENT_SELECTION_LABEL); } } - }, [agentSelection, options, selectedOptions]); + }, [agentSelection, onSelection, options, selectedOptions]); useEffect(() => { if (agentsFetched && groupsFetched) { @@ -142,37 +173,6 @@ const AgentsTableComponent: React.FC = ({ agentSelection, onCh grouper, ]); - const onSelection = useCallback( - (selection: GroupOption[]) => { - // TODO?: optimize this by making the selection computation incremental - const { - newAgentSelection, - selectedAgents, - selectedGroups, - }: { - newAgentSelection: AgentSelection; - selectedAgents: AgentOptionValue[]; - selectedGroups: SelectedGroups; - } = generateAgentSelection(selection); - if (newAgentSelection.allAgentsSelected) { - setNumAgentsSelected(totalNumAgents); - } else { - const checkAgent = generateAgentCheck(selectedGroups); - setNumAgentsSelected( - // filter out all the agents counted by selected policies and platforms - selectedAgents.filter(checkAgent).length + - // add the number of agents added via policy and platform groups - getNumAgentsInGrouping(selectedGroups) - - // subtract the number of agents double counted by policy/platform selections - getNumOverlapped(selectedGroups, groups.overlap) - ); - } - onChange(newAgentSelection); - setSelectedOptions(selection); - }, - [groups, onChange, totalNumAgents] - ); - const renderOption = useCallback((option, searchVal, contentClassName) => { const { label, value } = option; return value?.groupType === AGENT_GROUP_KEY.Agent ? ( diff --git a/x-pack/plugins/osquery/public/components/layouts/header.tsx b/x-pack/plugins/osquery/public/components/layouts/header.tsx index 5e8ed0923a0d9..6049e1d7bacb0 100644 --- a/x-pack/plugins/osquery/public/components/layouts/header.tsx +++ b/x-pack/plugins/osquery/public/components/layouts/header.tsx @@ -35,6 +35,7 @@ const Tabs = styled(EuiTabs)` `; export interface HeaderProps { + children?: React.ReactNode; maxWidth?: number; leftColumn?: JSX.Element; rightColumn?: JSX.Element; @@ -55,7 +56,8 @@ const HeaderColumns: React.FC> = memo( HeaderColumns.displayName = 'HeaderColumns'; -export const Header: React.FC = ({ +const HeaderComponent: React.FC = ({ + children, leftColumn, rightColumn, rightColumnGrow, @@ -71,6 +73,7 @@ export const Header: React.FC = ({ rightColumn={rightColumn} rightColumnGrow={rightColumnGrow} /> + {children} {tabs ? ( @@ -92,3 +95,5 @@ export const Header: React.FC = ({ ); + +export const Header = React.memo(HeaderComponent); diff --git a/x-pack/plugins/osquery/public/components/layouts/with_header.tsx b/x-pack/plugins/osquery/public/components/layouts/with_header.tsx index a620194b37877..97db914fedcf2 100644 --- a/x-pack/plugins/osquery/public/components/layouts/with_header.tsx +++ b/x-pack/plugins/osquery/public/components/layouts/with_header.tsx @@ -16,12 +16,14 @@ export interface WithHeaderLayoutProps extends HeaderProps { restrictHeaderWidth?: number; 'data-test-subj'?: string; children?: React.ReactNode; + headerChildren?: React.ReactNode; } export const WithHeaderLayout: React.FC = ({ restrictWidth, restrictHeaderWidth, children, + headerChildren, 'data-test-subj': dataTestSubj, ...rest }) => ( @@ -30,7 +32,9 @@ export const WithHeaderLayout: React.FC = ({ maxWidth={restrictHeaderWidth} data-test-subj={dataTestSubj ? `${dataTestSubj}_header` : undefined} {...rest} - /> + > + {headerChildren} + ; -const OsqueryIconComponent: React.FC = (props) => { - const [Icon, setIcon] = useState(null); - - // FIXME: This is a hack to force the icon to be loaded asynchronously. - useEffect(() => { - const interval = setInterval(() => { - setIcon(); - }, 0); - - return () => clearInterval(interval); - }, [props, setIcon]); - - return Icon; -}; +const OsqueryIconComponent: React.FC = (props) => ( + +); export const OsqueryIcon = React.memo(OsqueryIconComponent); diff --git a/x-pack/plugins/osquery/public/live_queries/form/index.tsx b/x-pack/plugins/osquery/public/live_queries/form/index.tsx index 9164266d6a8c5..a6481d420f4d1 100644 --- a/x-pack/plugins/osquery/public/live_queries/form/index.tsx +++ b/x-pack/plugins/osquery/public/live_queries/form/index.tsx @@ -62,6 +62,7 @@ interface LiveQueryFormProps { ecsMappingField?: boolean; formType?: FormType; enabled?: boolean; + hideFullscreen?: true; } const LiveQueryFormComponent: React.FC = ({ @@ -72,6 +73,7 @@ const LiveQueryFormComponent: React.FC = ({ ecsMappingField = true, formType = 'steps', enabled = true, + hideFullscreen, }) => { const ecsFieldRef = useRef(); const permissions = useKibana().services.application.capabilities.osquery; @@ -146,7 +148,7 @@ const LiveQueryFormComponent: React.FC = ({ onSubmit: async (formData, isValid) => { const ecsFieldValue = await ecsFieldRef?.current?.validate(); - if (isValid) { + if (isValid && !!ecsFieldValue) { try { await mutateAsync( pickBy( @@ -268,9 +270,9 @@ const LiveQueryFormComponent: React.FC = ({ const ecsFieldProps = useMemo( () => ({ - isDisabled: !permissions.writeSavedQueries, + isDisabled: !permissions.writeLiveQueries, }), - [permissions.writeSavedQueries] + [permissions.writeLiveQueries] ); const isSavedQueryDisabled = useMemo( @@ -388,9 +390,14 @@ const LiveQueryFormComponent: React.FC = ({ const resultsStepContent = useMemo( () => actionId ? ( - + ) : null, - [actionId, agentIds, data?.actions] + [actionId, agentIds, data?.actions, hideFullscreen] ); const formSteps: EuiContainedStepProps[] = useMemo( diff --git a/x-pack/plugins/osquery/public/live_queries/index.tsx b/x-pack/plugins/osquery/public/live_queries/index.tsx index bf2186c1a3e50..fdde03d6076a6 100644 --- a/x-pack/plugins/osquery/public/live_queries/index.tsx +++ b/x-pack/plugins/osquery/public/live_queries/index.tsx @@ -28,6 +28,7 @@ interface LiveQueryProps { ecsMappingField?: boolean; enabled?: boolean; formType?: 'steps' | 'simple'; + hideFullscreen?: true; } const LiveQueryComponent: React.FC = ({ @@ -44,6 +45,7 @@ const LiveQueryComponent: React.FC = ({ ecsMappingField, formType, enabled, + hideFullscreen, }) => { const { data: hasActionResultsPrivileges, isLoading } = useActionResultsPrivileges(); @@ -113,6 +115,7 @@ const LiveQueryComponent: React.FC = ({ onSuccess={onSuccess} formType={formType} enabled={enabled} + hideFullscreen={hideFullscreen} /> ); }; diff --git a/x-pack/plugins/osquery/public/packs/form/index.tsx b/x-pack/plugins/osquery/public/packs/form/index.tsx index f327239560345..41f51181aa304 100644 --- a/x-pack/plugins/osquery/public/packs/form/index.tsx +++ b/x-pack/plugins/osquery/public/packs/form/index.tsx @@ -48,10 +48,14 @@ const CommonUseField = getUseField({ component: Field }); interface PackFormProps { defaultValue?: OsqueryManagerPackagePolicy; editMode?: boolean; + isReadOnly?: boolean; } -const PackFormComponent: React.FC = ({ defaultValue, editMode = false }) => { - const isReadOnly = !!defaultValue?.read_only; +const PackFormComponent: React.FC = ({ + defaultValue, + editMode = false, + isReadOnly = false, +}) => { const [showConfirmationModal, setShowConfirmationModal] = useState(false); const handleHideConfirmationModal = useCallback(() => setShowConfirmationModal(false), []); diff --git a/x-pack/plugins/osquery/public/packs/queries/ecs_mapping_editor_field.tsx b/x-pack/plugins/osquery/public/packs/queries/ecs_mapping_editor_field.tsx index df8e083737559..b5bfe1a234d91 100644 --- a/x-pack/plugins/osquery/public/packs/queries/ecs_mapping_editor_field.tsx +++ b/x-pack/plugins/osquery/public/packs/queries/ecs_mapping_editor_field.tsx @@ -16,6 +16,7 @@ import { isArray, map, reduce, + trim, get, } from 'lodash'; import React, { @@ -127,6 +128,10 @@ const StyledFieldSpan = styled.span` padding-bottom: 0 !important; `; +const DescriptionWrapper = styled(EuiFlexItem)` + overflow: hidden; +`; + // align the icon to the inputs const StyledSemicolonWrapper = styled.div` margin-top: 8px; @@ -181,9 +186,9 @@ const ECSComboboxFieldComponent: React.FC = ({ const renderOption = useCallback( (option, searchValue, contentClassName) => ( { @@ -197,11 +202,11 @@ const ECSComboboxFieldComponent: React.FC = ({ - - + + {option.value.description} - - + + ), [] @@ -344,9 +349,9 @@ const OsqueryColumnFieldComponent: React.FC = ({ const renderOsqueryOption = useCallback( (option, searchValue, contentClassName) => ( @@ -354,11 +359,11 @@ const OsqueryColumnFieldComponent: React.FC = ({ - - + + {option.value.description} - - + + ), [] @@ -367,7 +372,11 @@ const OsqueryColumnFieldComponent: React.FC = ({ const handleChange = useCallback( (newSelectedOptions) => { setSelected(newSelectedOptions); - setValue(newSelectedOptions[0]?.label ?? ''); + setValue( + isArray(newSelectedOptions) + ? map(newSelectedOptions, 'label') + : newSelectedOptions[0]?.label ?? '' + ); }, [setValue, setSelected] ); @@ -384,16 +393,20 @@ const OsqueryColumnFieldComponent: React.FC = ({ const handleCreateOption = useCallback( (newOption: string) => { + const trimmedNewOption = trim(newOption); + + if (!trimmedNewOption.length) return; + if (euiFieldProps.singleSelection === false) { - setValue([newOption]); + setValue([trimmedNewOption]); if (resultValue.value.length) { - setValue([...castArray(resultValue.value), newOption]); + setValue([...castArray(resultValue.value), trimmedNewOption]); } else { - setValue([newOption]); + setValue([trimmedNewOption]); } inputRef.current?.blur(); } else { - setValue(newOption); + setValue(trimmedNewOption); } }, [euiFieldProps.singleSelection, resultValue.value, setValue] @@ -416,6 +429,17 @@ const OsqueryColumnFieldComponent: React.FC = ({ [onTypeChange, resultType.value] ); + useEffect(() => { + if (euiFieldProps?.singleSelection && isArray(resultValue.value)) { + setValue(resultValue.value.join(' ')); + } + + if (!euiFieldProps?.singleSelection && !isArray(resultValue.value)) { + setValue(resultValue.value.length ? [resultValue.value] : []); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [euiFieldProps?.singleSelection, setValue]); + useEffect(() => { setSelected(() => { if (!resultValue.value.length) return []; @@ -705,7 +729,7 @@ export const ECSMappingEditorForm = forwardRef = ({ @@ -53,6 +54,7 @@ const ResultsTableComponent: React.FC = ({ agentIds, startDate, endDate, + hideFullscreen, }) => { const [isLive, setIsLive] = useState(true); const { data: hasActionResultsPrivileges } = useActionResultsPrivileges(); @@ -307,6 +309,7 @@ const ResultsTableComponent: React.FC = ({ const toolbarVisibility = useMemo( () => ({ showDisplaySelector: false, + showFullScreenSelector: !hideFullscreen, additionalControls: ( <> = ({ ), }), - [actionId, endDate, startDate] + [actionId, endDate, startDate, hideFullscreen] ); useEffect( @@ -368,6 +371,7 @@ const ResultsTableComponent: React.FC = ({ // @ts-expect-error update types { return ( - {permissions.runSavedQueries || permissions.writeLiveQueries ? ( + {(permissions.runSavedQueries && permissions.readSavedQueries) || + permissions.writeLiveQueries ? ( ) : ( diff --git a/x-pack/plugins/osquery/public/routes/packs/edit/index.tsx b/x-pack/plugins/osquery/public/routes/packs/edit/index.tsx index 341312a45ae8a..64d3c4699708f 100644 --- a/x-pack/plugins/osquery/public/routes/packs/edit/index.tsx +++ b/x-pack/plugins/osquery/public/routes/packs/edit/index.tsx @@ -8,10 +8,12 @@ import { EuiButton, EuiButtonEmpty, + EuiCallOut, EuiConfirmModal, EuiFlexGroup, EuiFlexItem, EuiLoadingContent, + EuiSpacer, EuiText, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -33,6 +35,7 @@ const EditPackPageComponent = () => { const { isLoading, data } = usePack({ packId }); const deletePackMutation = useDeletePack({ packId, withRedirect: true }); + const isReadOnly = useMemo(() => !!data?.read_only, [data]); useBreadcrumbs('pack_edit', { packId: data?.id ?? '', @@ -97,11 +100,36 @@ const EditPackPageComponent = () => { [handleDeleteClick] ); + const HeaderContent = useMemo( + () => + isReadOnly ? ( + <> + + + + + + ) : null, + [isReadOnly] + ); + if (isLoading) return null; return ( - - {!data ? : } + + {!data ? ( + + ) : ( + + )} {isDeleteModalVisible ? ( = ({ @@ -24,6 +25,7 @@ const ResultTabsComponent: React.FC = ({ agentIds, endDate, startDate, + hideFullscreen, }) => { const tabs = useMemo( () => [ @@ -38,6 +40,7 @@ const ResultTabsComponent: React.FC = ({ agentIds={agentIds} startDate={startDate} endDate={endDate} + hideFullscreen={hideFullscreen} /> ), @@ -57,7 +60,7 @@ const ResultTabsComponent: React.FC = ({ ), }, ], - [actionId, agentIds, endDate, startDate] + [actionId, agentIds, endDate, startDate, hideFullscreen] ); return ( diff --git a/x-pack/plugins/osquery/public/saved_queries/form/use_saved_query_form.tsx b/x-pack/plugins/osquery/public/saved_queries/form/use_saved_query_form.tsx index b3e0cab60851e..39e2f6ca51cbc 100644 --- a/x-pack/plugins/osquery/public/saved_queries/form/use_saved_query_form.tsx +++ b/x-pack/plugins/osquery/public/saved_queries/form/use_saved_query_form.tsx @@ -50,7 +50,7 @@ export const useSavedQueryForm = ({ onSubmit: async (formData, isValid) => { const ecsFieldValue = await savedQueryFormRef?.current?.validateEcsMapping(); - if (isValid) { + if (isValid && !!ecsFieldValue) { try { await handleSubmit({ ...formData, diff --git a/x-pack/plugins/osquery/public/saved_queries/saved_query_flyout.tsx b/x-pack/plugins/osquery/public/saved_queries/saved_query_flyout.tsx index 7339ff20093fc..69fe9442a1cbb 100644 --- a/x-pack/plugins/osquery/public/saved_queries/saved_query_flyout.tsx +++ b/x-pack/plugins/osquery/public/saved_queries/saved_query_flyout.tsx @@ -48,7 +48,14 @@ const SavedQueryFlyoutComponent: React.FC = ({ defaultValue return ( - +

diff --git a/x-pack/plugins/osquery/public/shared_components/osquery_action/index.tsx b/x-pack/plugins/osquery/public/shared_components/osquery_action/index.tsx index 0ffd1cb6cc8a1..a0ee8bf314e57 100644 --- a/x-pack/plugins/osquery/public/shared_components/osquery_action/index.tsx +++ b/x-pack/plugins/osquery/public/shared_components/osquery_action/index.tsx @@ -20,9 +20,14 @@ import { useIsOsqueryAvailable } from './use_is_osquery_available'; interface OsqueryActionProps { agentId?: string; formType: 'steps' | 'simple'; + hideFullscreen?: true; } -const OsqueryActionComponent: React.FC = ({ agentId, formType = 'simple' }) => { +const OsqueryActionComponent: React.FC = ({ + agentId, + formType = 'simple', + hideFullscreen, +}) => { const permissions = useKibana().services.application.capabilities.osquery; const emptyPrompt = useMemo( @@ -134,18 +139,18 @@ const OsqueryActionComponent: React.FC = ({ agentId, formTyp ); } - return ; + return ; }; const OsqueryAction = React.memo(OsqueryActionComponent); // @ts-expect-error update types -const OsqueryActionWrapperComponent = ({ services, agentId, formType }) => ( +const OsqueryActionWrapperComponent = ({ services, agentId, formType, hideFullscreen }) => ( - + diff --git a/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_flyout.tsx b/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_flyout.tsx index 3262fc36abf75..0001c2966c1bc 100644 --- a/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_flyout.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_flyout.tsx @@ -45,7 +45,7 @@ export const OsqueryFlyout: React.FC = ({ agentId, onClose } - +