adds advanced search functionality and lists correct EEs

This commit is contained in:
Alex Corey
2021-04-26 14:02:22 -04:00
parent e6bde23aea
commit 1d442452b0
10 changed files with 98 additions and 16407 deletions

View File

@@ -35,22 +35,29 @@ function AdHocCommands({ adHocItems, i18n, hasListItems, onLaunchLoading }) {
}, [isKebabified, isWizardOpen, onKebabModalChange]); }, [isKebabified, isWizardOpen, onKebabModalChange]);
const { const {
result: { moduleOptions, credentialTypeId, isAdHocDisabled }, result: {
moduleOptions,
credentialTypeId,
isAdHocDisabled,
organizationId,
},
request: fetchData, request: fetchData,
error: fetchError, error: fetchError,
} = useRequest( } = useRequest(
useCallback(async () => { useCallback(async () => {
const [options, cred] = await Promise.all([ const [options, { data }, cred] = await Promise.all([
InventoriesAPI.readAdHocOptions(id), InventoriesAPI.readAdHocOptions(id),
InventoriesAPI.readDetail(id),
CredentialTypesAPI.read({ namespace: 'ssh' }), CredentialTypesAPI.read({ namespace: 'ssh' }),
]); ]);
return { return {
moduleOptions: options.data.actions.GET.module_name.choices, moduleOptions: options.data.actions.GET.module_name.choices,
credentialTypeId: cred.data.results[0].id, credentialTypeId: cred.data.results[0].id,
isAdHocDisabled: !options.data.actions.POST, isAdHocDisabled: !options.data.actions.POST,
organizationId: data.organization,
}; };
}, [id]), }, [id]),
{ moduleOptions: [], isAdHocDisabled: true } { moduleOptions: [], isAdHocDisabled: true, organizationId: null }
); );
useEffect(() => { useEffect(() => {
fetchData(); fetchData();
@@ -141,6 +148,7 @@ function AdHocCommands({ adHocItems, i18n, hasListItems, onLaunchLoading }) {
{isWizardOpen && ( {isWizardOpen && (
<AdHocCommandsWizard <AdHocCommandsWizard
adHocItems={adHocItems} adHocItems={adHocItems}
organizationId={organizationId}
moduleOptions={moduleOptions} moduleOptions={moduleOptions}
verbosityOptions={verbosityOptions} verbosityOptions={verbosityOptions}
credentialTypeId={credentialTypeId} credentialTypeId={credentialTypeId}

View File

@@ -103,6 +103,7 @@ describe('<AdHocCommands />', () => {
}, },
}, },
}); });
InventoriesAPI.readDetail.mockResolvedValue({ data: { organization: 1 } });
CredentialTypesAPI.read.mockResolvedValue({ CredentialTypesAPI.read.mockResolvedValue({
data: { results: [{ id: 1 }] }, data: { results: [{ id: 1 }] },
}); });
@@ -135,6 +136,10 @@ describe('<AdHocCommands />', () => {
test('should submit properly', async () => { test('should submit properly', async () => {
InventoriesAPI.launchAdHocCommands.mockResolvedValue({ data: { id: 1 } }); InventoriesAPI.launchAdHocCommands.mockResolvedValue({ data: { id: 1 } });
InventoriesAPI.readDetail.mockResolvedValue({
data: { organization: 1 },
});
CredentialsAPI.read.mockResolvedValue({ CredentialsAPI.read.mockResolvedValue({
data: { data: {
results: credentials, results: credentials,
@@ -150,6 +155,9 @@ describe('<AdHocCommands />', () => {
count: 2, count: 2,
}, },
}); });
ExecutionEnvironmentsAPI.readOptions.mockResolvedValue({
data: { actions: { GET: {} } },
});
await act(async () => { await act(async () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<AdHocCommands <AdHocCommands
@@ -275,6 +283,9 @@ describe('<AdHocCommands />', () => {
}, },
}, },
}); });
InventoriesAPI.readDetail.mockResolvedValue({
data: { organization: 1 },
});
CredentialTypesAPI.read.mockResolvedValue({ CredentialTypesAPI.read.mockResolvedValue({
data: { data: {
results: [ results: [
@@ -307,6 +318,9 @@ describe('<AdHocCommands />', () => {
count: 2, count: 2,
}, },
}); });
ExecutionEnvironmentsAPI.readOptions.mockResolvedValue({
data: { actions: { GET: {} } },
});
await act(async () => { await act(async () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<AdHocCommands <AdHocCommands

View File

@@ -1,5 +1,5 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { ExclamationCircleIcon as PFExclamationCircleIcon } from '@patternfly/react-icons'; import { ExclamationCircleIcon as PFExclamationCircleIcon } from '@patternfly/react-icons';
import { Tooltip } from '@patternfly/react-core'; import { Tooltip } from '@patternfly/react-core';
@@ -24,11 +24,11 @@ const ExclamationCircleIcon = styled(PFExclamationCircleIcon)`
function AdHocCommandsWizard({ function AdHocCommandsWizard({
onLaunch, onLaunch,
i18n,
moduleOptions, moduleOptions,
verbosityOptions, verbosityOptions,
onCloseWizard, onCloseWizard,
credentialTypeId, credentialTypeId,
organizationId,
}) { }) {
const [currentStepId, setCurrentStepId] = useState(1); const [currentStepId, setCurrentStepId] = useState(1);
const [enableLaunch, setEnableLaunch] = useState(false); const [enableLaunch, setEnableLaunch] = useState(false);
@@ -58,17 +58,17 @@ function AdHocCommandsWizard({
key: 1, key: 1,
name: hasDetailsStepError ? ( name: hasDetailsStepError ? (
<AlertText> <AlertText>
{i18n._(t`Details`)} {t`Details`}
<Tooltip <Tooltip
position="right" position="right"
content={i18n._(t`This step contains errors`)} content={t`This step contains errors`}
trigger="click mouseenter focus" trigger="click mouseenter focus"
> >
<ExclamationCircleIcon /> <ExclamationCircleIcon />
</Tooltip> </Tooltip>
</AlertText> </AlertText>
) : ( ) : (
i18n._(t`Details`) t`Details`
), ),
component: ( component: (
<AdHocDetailsStep <AdHocDetailsStep
@@ -77,20 +77,25 @@ function AdHocCommandsWizard({
/> />
), ),
enableNext: enabledNextOnDetailsStep(), enableNext: enabledNextOnDetailsStep(),
nextButtonText: i18n._(t`Next`), nextButtonText: t`Next`,
}, },
{ {
id: 2, id: 2,
key: 2, key: 2,
name: t`Execution Environment`, name: t`Execution Environment`,
component: <AdHocExecutionEnvironmentStep />, component: (
<AdHocExecutionEnvironmentStep organizationId={organizationId} />
),
// Removed this line when https://github.com/patternfly/patternfly-react/issues/5729 is fixed
stepNavItemProps: { style: { 'white-space': 'nowrap' } },
enableNext: true, enableNext: true,
nextButtonText: t`Next`,
canJumpTo: currentStepId >= 2, canJumpTo: currentStepId >= 2,
}, },
{ {
id: 3, id: 3,
key: 3, key: 3,
name: i18n._(t`Machine credential`), name: t`Machine credential`,
component: ( component: (
<AdHocCredentialStep <AdHocCredentialStep
credentialTypeId={credentialTypeId} credentialTypeId={credentialTypeId}
@@ -98,7 +103,7 @@ function AdHocCommandsWizard({
/> />
), ),
enableNext: enableLaunch && Object.values(errors).length === 0, enableNext: enableLaunch && Object.values(errors).length === 0,
nextButtonText: i18n._(t`Launch`), nextButtonText: t`Launch`,
canJumpTo: currentStepId >= 2, canJumpTo: currentStepId >= 2,
}, },
]; ];
@@ -115,10 +120,10 @@ function AdHocCommandsWizard({
onLaunch(values); onLaunch(values);
}} }}
steps={steps} steps={steps}
title={i18n._(t`Run command`)} title={t`Run command`}
nextButtonText={currentStep.nextButtonText || undefined} nextButtonText={currentStep.nextButtonText || undefined}
backButtonText={i18n._(t`Back`)} backButtonText={t`Back`}
cancelButtonText={i18n._(t`Cancel`)} cancelButtonText={t`Cancel`}
/> />
); );
} }
@@ -149,4 +154,4 @@ FormikApp.propTypes = {
onCloseWizard: PropTypes.func.isRequired, onCloseWizard: PropTypes.func.isRequired,
credentialTypeId: PropTypes.number.isRequired, credentialTypeId: PropTypes.number.isRequired,
}; };
export default withI18n()(FormikApp); export default FormikApp;

View File

@@ -41,6 +41,7 @@ describe('<AdHocCommandsWizard/>', () => {
verbosityOptions={verbosityOptions} verbosityOptions={verbosityOptions}
onCloseWizard={() => {}} onCloseWizard={() => {}}
credentialTypeId={1} credentialTypeId={1}
organizationId={1}
/> />
); );
}); });
@@ -108,6 +109,9 @@ describe('<AdHocCommandsWizard/>', () => {
count: 2, count: 2,
}, },
}); });
ExecutionEnvironmentsAPI.readOptions.mockResolvedValue({
data: { actions: { GET: {} } },
});
CredentialsAPI.read.mockResolvedValue({ CredentialsAPI.read.mockResolvedValue({
data: { data: {
results: [ results: [

View File

@@ -22,10 +22,13 @@ describe('<AdHocExecutionEnvironmentStep />', () => {
count: 2, count: 2,
}, },
}); });
ExecutionEnvironmentsAPI.readOptions.mockResolvedValue({
data: { actions: { GET: {} } },
});
await act(async () => { await act(async () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<Formik> <Formik>
<AdHocExecutionEnvironmentStep /> <AdHocExecutionEnvironmentStep organizationId={1} />
</Formik> </Formik>
); );
}); });

View File

@@ -6,18 +6,18 @@ import { Form, FormGroup } from '@patternfly/react-core';
import { ExecutionEnvironmentsAPI } from '../../api'; import { ExecutionEnvironmentsAPI } from '../../api';
import Popover from '../Popover'; import Popover from '../Popover';
import { parseQueryString, getQSConfig } from '../../util/qs'; import { parseQueryString, getQSConfig, mergeParams } from '../../util/qs';
import useRequest from '../../util/useRequest'; import useRequest from '../../util/useRequest';
import ContentError from '../ContentError'; import ContentError from '../ContentError';
import ContentLoading from '../ContentLoading'; import ContentLoading from '../ContentLoading';
import OptionsList from '../OptionsList'; import OptionsList from '../OptionsList';
const QS_CONFIG = getQSConfig('execution_environemts', { const QS_CONFIG = getQSConfig('execution_environments', {
page: 1, page: 1,
page_size: 5, page_size: 5,
order_by: 'name', order_by: 'name',
}); });
function AdHocExecutionEnvironmentStep() { function AdHocExecutionEnvironmentStep({ organizationId }) {
const history = useHistory(); const history = useHistory();
const [executionEnvironmentField, , executionEnvironmentHelpers] = useField( const [executionEnvironmentField, , executionEnvironmentHelpers] = useField(
'execution_environment' 'execution_environment'
@@ -26,21 +26,51 @@ function AdHocExecutionEnvironmentStep() {
error, error,
isLoading, isLoading,
request: fetchExecutionEnvironments, request: fetchExecutionEnvironments,
result: { executionEnvironments, executionEnvironmentsCount }, result: {
executionEnvironments,
executionEnvironmentsCount,
relatedSearchableKeys,
searchableKeys,
},
} = useRequest( } = useRequest(
useCallback(async () => { useCallback(async () => {
const params = parseQueryString(QS_CONFIG, history.location.search); const params = parseQueryString(QS_CONFIG, history.location.search);
const globallyAvailableParams = { or__organization__isnull: 'True' };
const organizationIdParams = organizationId
? { or__organization__id: organizationId }
: {};
const { const [
data: { results, count }, {
} = await ExecutionEnvironmentsAPI.read(params); data: { results, count },
},
actionsResponse,
] = await Promise.all([
ExecutionEnvironmentsAPI.read(
mergeParams(params, {
...globallyAvailableParams,
...organizationIdParams,
})
),
ExecutionEnvironmentsAPI.readOptions(),
]);
return { return {
executionEnvironments: results, executionEnvironments: results,
executionEnvironmentsCount: count, executionEnvironmentsCount: count,
relatedSearchableKeys: (
actionsResponse?.data?.related_search_fields || []
).map(val => val.slice(0, -8)),
searchableKeys: Object.keys(
actionsResponse.data.actions?.GET || {}
).filter(key => actionsResponse.data.actions?.GET[key].filterable),
}; };
}, [history.location.search]), }, [history.location.search, organizationId]),
{ executionEnvironments: [], executionEnvironmentsCount: 0 } {
executionEnvironments: [],
executionEnvironmentsCount: 0,
relatedSearchableKeys: [],
searchableKeys: [],
}
); );
useEffect(() => { useEffect(() => {
@@ -62,11 +92,12 @@ function AdHocExecutionEnvironmentStep() {
aria-label={t`Execution Environments`} aria-label={t`Execution Environments`}
labelIcon={ labelIcon={
<Popover <Popover
content={t`Select the Execution Environment you want this command to run inside`} content={t`Select the Execution Environment you want this command to run inside.`}
/> />
} }
> >
<OptionsList <OptionsList
isLoading={isLoading}
value={executionEnvironmentField.value || []} value={executionEnvironmentField.value || []}
options={executionEnvironments} options={executionEnvironments}
optionCount={executionEnvironmentsCount} optionCount={executionEnvironmentsCount}
@@ -75,7 +106,7 @@ function AdHocExecutionEnvironmentStep() {
searchColumns={[ searchColumns={[
{ {
name: t`Name`, name: t`Name`,
key: 'name', key: 'name__icontains',
isDefault: true, isDefault: true,
}, },
{ {
@@ -94,6 +125,8 @@ function AdHocExecutionEnvironmentStep() {
}, },
]} ]}
name="execution_environment" name="execution_environment"
searchableKeys={searchableKeys}
relatedSearchableKeys={relatedSearchableKeys}
selectItem={value => { selectItem={value => {
executionEnvironmentHelpers.setValue([value]); executionEnvironmentHelpers.setValue([value]);
}} }}

View File

@@ -92,11 +92,11 @@ describe('<InventoryDetail />', () => {
expectDetailToMatch(wrapper, 'Type', 'Inventory'); expectDetailToMatch(wrapper, 'Type', 'Inventory');
const org = wrapper.find('Detail[label="Organization"]'); const org = wrapper.find('Detail[label="Organization"]');
expect(org.prop('value')).toMatchInlineSnapshot(` expect(org.prop('value')).toMatchInlineSnapshot(`
<ForwardRef <Link
to="/organizations/1/details" to="/organizations/1/details"
> >
The Organization The Organization
</ForwardRef> </Link>
`); `);
const vars = wrapper.find('VariablesDetail'); const vars = wrapper.find('VariablesDetail');
expect(vars).toHaveLength(1); expect(vars).toHaveLength(1);