Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ import React from 'react';
import Permitted from 'foremanReact/components/Permitted';
import { AdPermissions } from '../../constants/foremanAnsibleDirectorPermissions';
import AnsibleContentPage from './AnsibleContentPage';
import { ForceTaxonomy } from '../common/ForceTaxonomy';

// TODO: This wrapper will be used for permission management
const AnsibleContentPageWrapper: React.FC = () => (
<Permitted requiredPermissions={[AdPermissions.ansibleContent.view]}>
<AnsibleContentPage />
<ForceTaxonomy organization>
<AnsibleContentPage />
</ForceTaxonomy>
</Permitted>
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ import Permitted from 'foremanReact/components/Permitted';

import AnsibleEnvironmentsPage from './AnsibleEnvironmentsPage';
import { AdPermissions } from '../../constants/foremanAnsibleDirectorPermissions';
import { ForceTaxonomy } from '../common/ForceTaxonomy';

const AnsibleEnvironmentsPageWrapper: React.FC = () => (
<Permitted requiredPermissions={[AdPermissions.ansibleLcePaths.view]}>
<AnsibleEnvironmentsPage />
<ForceTaxonomy organization>
<AnsibleEnvironmentsPage />
</ForceTaxonomy>
</Permitted>
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ import React from 'react';
import Permitted from 'foremanReact/components/Permitted';
import AnsibleExecutionEnvPage from './AnsibleExecutionEnvPage';
import { AdPermissions } from '../../constants/foremanAnsibleDirectorPermissions';
import { ForceTaxonomy } from '../common/ForceTaxonomy';

const AnsibleExecutionEnvPageWrapper: React.FC = () => (
<Permitted requiredPermissions={[AdPermissions.executionEnvironments.view]}>
<AnsibleExecutionEnvPage />
<ForceTaxonomy organization>
<AnsibleExecutionEnvPage />
</ForceTaxonomy>
</Permitted>
);

Expand Down
242 changes: 242 additions & 0 deletions webpack/components/common/ForceTaxonomy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
import React, { ReactElement, useEffect } from 'react';
import {
useForemanOrganization,
useForemanLocation,
} from 'foremanReact/Root/Context/ForemanContext';
import { translate as _ } from 'foremanReact/common/I18n';

import {
Card,
CardTitle,
CardBody,
CardFooter,
Bullseye,
ActionList,
ActionListItem,
Button,
Form,
TextVariants,
TextContent,
Text,
} from '@patternfly/react-core';

import axios, { AxiosResponse } from 'axios';
import { foremanUrl } from 'foremanReact/common/helpers';
import { addToast } from 'foremanReact/components/ToastsList';
import { useDispatch } from 'react-redux';

import { TaxonSelector } from './components/TaxonSelector';

interface ForceTaxonomyProps {
organization?: boolean;
location?: boolean;
children: React.ReactNode;
}

export const ForceTaxonomy = ({
organization,
location = false,
children = false,
}: ForceTaxonomyProps): ReactElement => {
const [selectedOrganization, setSelectedOrganization] = React.useState<
string
>('');
const [selectedLocation, setSelectedLocation] = React.useState<string>('');

const currentOrganization = useForemanOrganization();
const currentLocation = useForemanLocation();

const organizationUpdateRequired =
organization && currentOrganization === undefined;
const locationUpdateRequired = location && currentLocation === undefined;

useEffect(() => {
if (organizationUpdateRequired && locationUpdateRequired) {
document.title = _('Organization/Location required!');
} else if (locationUpdateRequired) {
document.title = _('Location required!');
} else if (organizationUpdateRequired) {
document.title = _('Organization required!');
}
}, [organizationUpdateRequired, locationUpdateRequired]);

const dispatch = useDispatch();

const card = (content: ReactElement): ReactElement => (
<Bullseye>
<Card ouiaId="BasicCard" style={{ minHeight: '30vh', minWidth: '50vh' }}>
{content}
</Card>
</Bullseye>
);

const updateTaxonomy = async (): Promise<void> => {
const updateTaxon = async ({
type,
endpoint,
}: {
type: 'organization' | 'location';
endpoint: string;
}): Promise<void> => {
try {
await axios.get(foremanUrl(endpoint));
dispatch(
addToast({
type: 'success',
key: `UPDATE_CURRENT_${type.toUpperCase()}_SUCC`,
message: `Successfully updated the current ${type}!`,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The message needs a translation.

sticky: false,
})
);
} catch (e) {
dispatch(
addToast({
type: 'danger',
key: `UPDATE_CURRENT_${type.toUpperCase()}_ERR`,
message: `Updating the current ${type} failed with error code "${
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The message needs a translation.

(e as {
response: AxiosResponse;
}).response.status
}".`,
sticky: false,
})
);
}
};

await Promise.all([
organizationUpdateRequired &&
updateTaxon({
type: 'organization',
endpoint: `/organizations/${selectedOrganization}/select`,
}),
locationUpdateRequired &&
updateTaxon({
type: 'location',
endpoint: `/locations/${selectedLocation}/select`,
}),
]);

// Unfortunately, the context is hydrated during the initial mounting of the root React App.
// There is currently no mechanism to refresh it out of tree. Therefore, a reload is needed here.
window.location.reload();
};

if (organizationUpdateRequired && locationUpdateRequired) {
return card(
<>
<CardTitle>
<TextContent>
<Text component={TextVariants.h2}>
{_('An organization and a location are required to proceed.')}
</Text>
</TextContent>
</CardTitle>
<CardBody>
<Form>
<TaxonSelector
type="organization"
selected={selectedOrganization}
onSelect={taxonId => setSelectedOrganization(taxonId)}
/>
<TaxonSelector
type="location"
selected={selectedLocation}
onSelect={taxonId => setSelectedLocation(taxonId)}
/>
</Form>
</CardBody>
<CardFooter>
<ActionList>
<ActionListItem>
<Button
variant="primary"
id="single-group-next-button"
isDisabled={
selectedOrganization === '' || selectedLocation === ''
}
onClick={() => updateTaxonomy()}
>
{_('Save')}
</Button>
</ActionListItem>
</ActionList>
</CardFooter>
</>
);
} else if (locationUpdateRequired) {
return card(
<>
<CardTitle>
<TextContent>
<Text component={TextVariants.h2}>
{_('A location is required to proceed.')}
</Text>
</TextContent>
</CardTitle>
<CardBody>
<Form>
<TaxonSelector
type="location"
selected={selectedLocation}
onSelect={taxonId => {
setSelectedLocation(taxonId);
}}
/>
</Form>
</CardBody>
<CardFooter>
<ActionList>
<ActionListItem>
<Button
variant="primary"
id="single-group-next-button"
isDisabled={selectedLocation === ''}
onClick={() => updateTaxonomy()}
>
{_('Save')}
</Button>
</ActionListItem>
</ActionList>
</CardFooter>
</>
);
} else if (organizationUpdateRequired) {
return card(
<>
<CardTitle>
<TextContent>
<Text component={TextVariants.h2}>
{_('An organization is required to proceed.')}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
{_('An organization is required to proceed.')}
{_('Select an organization.')}

Please check if all of those texts end with a dot or not.

Same suggestion applies to line 173 and 131.

</Text>
</TextContent>
</CardTitle>
<CardBody>
<Form>
<TaxonSelector
type="organization"
selected={selectedOrganization}
onSelect={taxonId => setSelectedOrganization(taxonId)}
/>
</Form>
</CardBody>
<CardFooter>
<ActionList>
<ActionListItem>
<Button
variant="primary"
id="single-group-next-button"
isDisabled={selectedOrganization === ''}
onClick={() => updateTaxonomy()}
>
{_('Save')}
</Button>
</ActionListItem>
</ActionList>
</CardFooter>
</>
);
}

return <>{children}</>;
};
81 changes: 81 additions & 0 deletions webpack/components/common/components/TaxonSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import React, { ReactElement } from 'react';

import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
import { foremanUrl } from 'foremanReact/common/helpers';
import { translate as _, sprintf as __ } from 'foremanReact/common/I18n';
import {
EmptyState,
EmptyStateHeader,
EmptyStateIcon,
FormGroup,
FormSelect,
FormSelectOption,
Spinner,
} from '@patternfly/react-core';

import { Taxon } from '../../../types/common';

interface TaxonSelectorProps {
type: 'organization' | 'location';
selected: string;
onSelect: (taxonId: string) => void;
}

export const TaxonSelector = ({
type,
selected,
onSelect,
}: TaxonSelectorProps): ReactElement => {
const taxonResponse = useAPI<{ results: Taxon[] }>(
'get',
foremanUrl(`/api/v2/${type}s`)
);

if (taxonResponse.status === 'ERROR') {
// TODO: Handle error
} else if (taxonResponse.status === 'RESOLVED') {
return (
<FormGroup
label={type === 'organization' ? _('Organization') : _('Location')}
isRequired
>
<FormSelect
value={selected}
onChange={(_event, value) => {
onSelect(value);
}}
>
{[
<FormSelectOption
key="placeholder"
value=""
label={
type === 'organization'
? _('Select an organization')
: _('Select a location')
}
isPlaceholder
/>,
...taxonResponse.response.results.map(taxon => (
<FormSelectOption
key={taxon.id}
value={taxon.id}
label={taxon.title}
/>
)),
]}
</FormSelect>
</FormGroup>
);
}

return (
<EmptyState>
<EmptyStateHeader
titleText={__(_('Loading %(taxon)s information...'), { taxon: type })}
headingLevel="h4"
icon={<EmptyStateIcon icon={Spinner} />}
/>
</EmptyState>
);
};
11 changes: 11 additions & 0 deletions webpack/types/common.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface Organization {
id: number;
title: string;
}

export interface Location {
id: number;
title: string;
}

export interface Taxon extends Organization, Location {}
Loading
Loading