Skip to content

Commit

Permalink
Add an import button for import/reimport roles in the UI (#4759)
Browse files Browse the repository at this point in the history
Issue: AAH-2833
  • Loading branch information
himdel authored Dec 17, 2023
1 parent cbd2a2d commit 7f0a232
Show file tree
Hide file tree
Showing 11 changed files with 247 additions and 6 deletions.
1 change: 1 addition & 0 deletions CHANGES/2833.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add an import button for import/reimport roles in the UI
1 change: 1 addition & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export { GenericPulpAPI } from './generic-pulp';
export { GroupAPI } from './group';
export { GroupRoleAPI } from './group-role';
export { ImportAPI } from './import';
export { LegacyImportAPI } from './legacy-import';
export { LegacyNamespaceAPI } from './legacy-namespace';
export { LegacyRoleAPI } from './legacy-role';
export { MyDistributionAPI } from './my-distribution';
Expand Down
14 changes: 14 additions & 0 deletions src/api/legacy-import.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { LegacyAPI } from './legacy';

export class API extends LegacyAPI {
apiPath = 'v1/imports/';
sortParam = 'order_by';

// list(params?)

import(data) {
return this.http.post(this.apiPath, data);
}
}

export const LegacyImportAPI = new API();
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export { CollectionHeader } from './headers/collection-header';
export { PartnerHeader } from './headers/partner-header';
export { HelperText } from './helper-text/helper-text';
export { ImportModal } from './import-modal/import-modal';
export { RoleImportForm } from './legacy-role/role-import-form';
export { ListItemActions } from './list-item-actions/list-item-actions';
export { LoadingPageSpinner } from './loading/loading-page-spinner';
export { LoadingPageWithHeader } from './loading/loading-with-header';
Expand Down
137 changes: 137 additions & 0 deletions src/components/legacy-role/role-import-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { Trans, t } from '@lingui/macro';
import { ActionGroup, Button } from '@patternfly/react-core';
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { LegacyImportAPI } from 'src/api';
import { AlertType, DataForm, ExternalLink } from 'src/components';
import { useContext } from 'src/loaders/app-context';
import { Paths, formatPath } from 'src/paths';
import { ErrorMessagesType, handleHttpError, taskAlert } from 'src/utilities';

interface IProps {
addAlert: (alert: AlertType) => void;
}

export const RoleImportForm = ({ addAlert }: IProps) => {
const { queueAlert, user } = useContext();
const [data, setData] = useState<{
alternate_role_name?: string;
github_repo?: string;
github_user?: string;
namespace_id?: string;
}>(user.is_superuser ? {} : { github_user: user.username });
const [errors, setErrors] = useState<ErrorMessagesType>(null);
const navigate = useNavigate();

// TODO user will have their namespace, superuser needs to create+assign
// curl -X POST '{"name": "foobar"}" /api/v1/namespaces/

const formFields = [
{ id: 'github_user', title: t`GitHub user` },
{
id: 'github_repo',
title: t`GitHub repository`,
helper:
!data.github_repo ||
data.github_repo.startsWith('ansible-role-') ||
'ansible-role-'.startsWith(data.github_repo)
? null
: {
variant: 'warning' as const,
text: (
<>
{t`Did you mean ${`ansible-role-${data.github_repo}`}?`}{' '}
<Button
variant='link'
onClick={() =>
updateField(
'github_repo',
`ansible-role-${data.github_repo}`,
)
}
>{t`Change`}</Button>
</>
),
},
},
{
id: 'github_reference',
title: t`GitHub ref (a commit, branch or tag)`,
placeholder: t`Automatic`,
},
{
id: 'alternate_role_name',
title: t`Role name`,
placeholder: t`Only used when a role doesn't have galaxy_info.role_name metadata, and doesn't follow the ansible-role-$name naming convention.`,
},
];

const requiredFields = ['github_user', 'github_repo'];

const nonempty = (o) =>
Object.fromEntries(Object.entries(o).filter(([_k, v]) => v));

const onCancel = () => navigate(formatPath(Paths.standaloneRoles));
const onSaved = ({
data: {
results: [{ pulp_id }],
},
}) => {
// the role import_log tab is not available before the role gets imported, go to list
// TODO .. but we could waitForTask, and go to role on success
queueAlert(taskAlert(pulp_id, t`Import started`));
navigate(formatPath(Paths.standaloneRoles));
};

const onSave = () =>
LegacyImportAPI.import(nonempty(data))
.then(onSaved)
.catch(handleHttpError(t`Failed to import role`, null, addAlert));

const link =
data.github_user &&
data.github_repo &&
`https://github.com/${data.github_user}/${data.github_repo}`;

const anyErrors = !!errors || requiredFields.some((k) => !data[k]);

const formSuffix = (
<>
{link ? (
<div>
<Trans>
Will clone <ExternalLink href={link}>{link}</ExternalLink>
</Trans>
</div>
) : null}
<ActionGroup key='actions'>
<Button type='submit' isDisabled={anyErrors}>
{t`Import`}
</Button>
<Button onClick={onCancel} variant='link'>
{t`Cancel`}
</Button>
</ActionGroup>
</>
);

const updateField = (k, v) => {
setData((data) => ({ ...data, [k]: v }));

if (requiredFields.includes(k) && !v) {
setErrors((errors) => ({ ...errors, [k]: t`Field is required.` }));
}
};

return (
<DataForm
errorMessages={errors || {}}
formFields={formFields}
formSuffix={formSuffix}
model={data}
requiredFields={requiredFields}
updateField={(v, e) => updateField(e.target.id, v)}
onSave={onSave}
/>
);
};
22 changes: 18 additions & 4 deletions src/components/shared/data-form.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import {
Form,
FormGroup,
HelperText,
HelperTextItem,
HelperTextItemProps,
TextInput,
TextInputTypes,
} from '@patternfly/react-core';
import React from 'react';
import React, { ReactNode } from 'react';
import { ErrorMessagesType } from 'src/utilities';

interface IProps {
Expand All @@ -15,10 +18,14 @@ interface IProps {
placeholder?: string;
title: string;
type?: string;
helper?: {
variant: HelperTextItemProps['variant']; // "default" | "error" | "success" | "warning" | "indeterminate"
text: ReactNode;
};
}[];
formPrefix?: React.ReactNode;
formSuffix?: React.ReactNode;
isReadonly: boolean;
formPrefix?: ReactNode;
formSuffix?: ReactNode;
isReadonly?: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
model: Record<string, any>;
requiredFields: string[];
Expand Down Expand Up @@ -70,6 +77,13 @@ export class DataForm extends React.Component<IProps> {
{...(field.type === 'password' ? { autoComplete: 'off' } : {})}
/>
)}
{field.helper ? (
<HelperText>
<HelperTextItem variant={field.helper.variant}>
{field.helper.text}
</HelperTextItem>
</HelperText>
) : null}
</FormGroup>
);
});
Expand Down
54 changes: 54 additions & 0 deletions src/containers/ansible-role/role-import.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { t } from '@lingui/macro';
import React from 'react';
import {
AlertList,
AlertType,
BaseHeader,
Main,
RoleImportForm,
closeAlertMixin,
} from 'src/components';
import { RouteProps, withRouter } from 'src/utilities';

interface RoleState {
alerts: AlertType[];
}

class AnsibleRoleImport extends React.Component<RouteProps, RoleState> {
constructor(props) {
super(props);
this.state = {
alerts: [],
};
}

private addAlert(alert: AlertType) {
this.setState({
alerts: [...this.state.alerts, alert],
});
}

private get closeAlert() {
return closeAlertMixin('alerts');
}

render() {
const { alerts } = this.state;
const addAlert = (alert) => this.addAlert(alert);
const closeAlert = (i) => this.closeAlert(i);

return (
<>
<AlertList alerts={alerts} closeAlert={closeAlert} />
<BaseHeader title={t`Import role`} />
<Main>
<section className='body'>
<RoleImportForm addAlert={addAlert} />
</section>
</Main>
</>
);
}
}

export default withRouter(AnsibleRoleImport);
19 changes: 17 additions & 2 deletions src/containers/ansible-role/role-list.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { t } from '@lingui/macro';
import { DataList } from '@patternfly/react-core';
import { Button, DataList } from '@patternfly/react-core';
import React from 'react';
import { LegacyRoleAPI, LegacyRoleListType, TagAPI } from 'src/api';
import {
Expand All @@ -14,6 +14,8 @@ import {
Pagination,
closeAlertMixin,
} from 'src/components';
import { AppContext } from 'src/loaders/app-context';
import { Paths, formatPath } from 'src/paths';
import {
ParamHelper,
RouteProps,
Expand All @@ -35,6 +37,8 @@ interface RolesState {
}

class AnsibleRoleList extends React.Component<RouteProps, RolesState> {
static contextType = AppContext;

constructor(props) {
super(props);

Expand All @@ -58,6 +62,9 @@ class AnsibleRoleList extends React.Component<RouteProps, RolesState> {
}

componentDidMount() {
this.setState({ alerts: this.context.alerts || [] });
this.context.setAlerts([]);

this.query(this.state.params);
}

Expand Down Expand Up @@ -163,6 +170,14 @@ class AnsibleRoleList extends React.Component<RouteProps, RolesState> {
) : (
<div>
<HubListToolbar
buttons={[
<Button
key='import'
onClick={() =>
this.props.navigate(formatPath(Paths.standaloneRoleImport))
}
>{t`Import role`}</Button>,
]}
count={count}
filterConfig={filterConfig}
ignoredParams={['page', 'page_size', 'sort']}
Expand All @@ -182,7 +197,7 @@ class AnsibleRoleList extends React.Component<RouteProps, RolesState> {
<LegacyRoleListItem
key={lrole.id}
role={lrole}
show_thumbnail={true}
show_thumbnail
/>
))}
</DataList>
Expand Down
1 change: 1 addition & 0 deletions src/containers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export { default as AnsibleRepositoryList } from './ansible-repository/list';
export { default as AnsibleRoleNamespaceDetail } from './ansible-role/namespace-detail';
export { default as AnsibleRoleNamespaceList } from './ansible-role/namespace-list';
export { default as AnsibleRoleDetail } from './ansible-role/role-detail';
export { default as AnsibleRoleImport } from './ansible-role/role-import';
export { default as AnsibleRoleList } from './ansible-role/role-list';
export { default as CertificationDashboard } from './certification-dashboard/certification-dashboard';
export { default as CollectionContent } from './collection-detail/collection-content';
Expand Down
2 changes: 2 additions & 0 deletions src/loaders/standalone/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
AnsibleRepositoryEdit,
AnsibleRepositoryList,
AnsibleRoleDetail,
AnsibleRoleImport,
AnsibleRoleList,
AnsibleRoleNamespaceDetail,
AnsibleRoleNamespaceList,
Expand Down Expand Up @@ -216,6 +217,7 @@ export class StandaloneRoutes extends React.Component<IRoutesProps> {
},
{ component: AnsibleRoleNamespaceList, path: Paths.standaloneNamespaces },
{ component: AnsibleRoleDetail, path: Paths.standaloneRole },
{ component: AnsibleRoleImport, path: Paths.standaloneRoleImport },
{ component: AnsibleRoleList, path: Paths.standaloneRoles },
{
component: TaskListView,
Expand Down
1 change: 1 addition & 0 deletions src/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export enum Paths {
logout = '/logout',
landingPage = '/',
standaloneRole = '/standalone/roles/:namespace/:name/:tab?',
standaloneRoleImport = '/standalone/roles/import',
standaloneRoles = '/standalone/roles',
standaloneNamespace = '/standalone/namespaces/:namespaceid',
standaloneNamespaces = '/standalone/namespaces',
Expand Down

0 comments on commit 7f0a232

Please sign in to comment.