Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 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
9 changes: 9 additions & 0 deletions lib/katello/plugin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,15 @@
:parent => :lab_features_menu,
:turbolinks => false

menu :labs_menu,
:host_collections_lab,
:url => '/labs/host_collections',
:url_hash => {:controller => 'katello/api/v2/host_collections_controller',
:action => 'index'},
:caption => N_('Host Collections'),
:parent => :lab_features_menu,
:turbolinks => false

extend_template_helpers Katello::KatelloUrlsHelper
extend_template_helpers Katello::Concerns::BaseTemplateScopeExtensions

Expand Down
24 changes: 24 additions & 0 deletions webpack/components/InlineEdit/InlineEdit.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
.inline-edit-display {
align-items: center;

.inline-edit-value {
display: block;
min-height: 1.5rem;
}

button {
visibility: hidden;
}

&:hover button {
visibility: visible;
}
}

.inline-edit-form {
margin-bottom: 0;

.pf-c-form__group-control {
width: 100%;
}
}
171 changes: 171 additions & 0 deletions webpack/components/InlineEdit/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { translate as __ } from 'foremanReact/common/I18n';
import {
Button,
Flex,
FlexItem,
FormGroup,
FormSelect,
FormSelectOption,
TextArea,
TextInput,
} from '@patternfly/react-core';
import { CheckIcon, TimesIcon, PencilAltIcon } from '@patternfly/react-icons';
import './InlineEdit.scss';

const InlineEdit = ({
Copy link
Member

Choose a reason for hiding this comment

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

Katello already has an ActionableDetail component (used in ContentViewInfo) that does this same thing. Make sure to tell your AI to inspect the Katello code base and follow existing patterns - it likes to default to reinventing the wheel.

value, onSave, isRequired, multiline, type, options,
}) => {
const [isEditing, setIsEditing] = useState(false);
const [editValue, setEditValue] = useState(value);

const handleSave = () => {
if (isRequired && !editValue.trim()) {
return;
}
onSave(editValue);
setIsEditing(false);
};

const handleCancel = () => {
setEditValue(value);
setIsEditing(false);
};

const handleKeyDown = (e) => {
if (e.key === 'Enter' && !multiline) {
e.preventDefault();
handleSave();
} else if (e.key === 'Escape') {
handleCancel();
}
};

const renderValue = () => {
if (type === 'select') {
const option = options.find(opt => opt.value === value);
return option?.label || value || __('Not set');
}
return value || __('Not set');
};

const renderEditField = () => {
if (type === 'select') {
return (
<FormSelect
value={editValue}
onChange={newValue => setEditValue(newValue)}
aria-label="inline-edit-select"
ouiaId="inline-edit-select"
>
{options.map(option => (
<FormSelectOption
key={option.value}
value={option.value}
label={option.label}
/>
))}
</FormSelect>
);
}

if (multiline) {
return (
<TextArea
value={editValue}
onChange={(event, newValue) => setEditValue(newValue)}
onKeyDown={handleKeyDown}
aria-label="inline-edit-textarea"
autoFocus
rows={3}
/>
);
}

return (
<TextInput
value={editValue}
onChange={(event, newValue) => setEditValue(newValue)}
onKeyDown={handleKeyDown}
aria-label="inline-edit-input"
autoFocus
isRequired={isRequired}
ouiaId="inline-edit-input"
/>
);
};

if (!isEditing) {
return (
<Flex className="inline-edit-display">
<FlexItem flex={{ default: 'flex_1' }}>
<span className="inline-edit-value">{renderValue()}</span>
</FlexItem>
<FlexItem>
<Button
variant="plain"
aria-label="Edit"
onClick={() => {
setEditValue(value);
setIsEditing(true);
}}
icon={<PencilAltIcon />}
ouiaId="inline-edit-edit-button"
/>
</FlexItem>
</Flex>
);
}

return (
<FormGroup className="inline-edit-form">
<Flex>
<FlexItem flex={{ default: 'flex_1' }}>
{renderEditField()}
</FlexItem>
<FlexItem>
<Button
variant="plain"
aria-label="Save"
onClick={handleSave}
icon={<CheckIcon />}
isDisabled={isRequired && !editValue.trim()}
ouiaId="inline-edit-save-button"
/>
</FlexItem>
<FlexItem>
<Button
variant="plain"
aria-label="Cancel"
onClick={handleCancel}
icon={<TimesIcon />}
ouiaId="inline-edit-cancel-button"
/>
</FlexItem>
</Flex>
</FormGroup>
);
};

InlineEdit.propTypes = {
value: PropTypes.string,
onSave: PropTypes.func.isRequired,
isRequired: PropTypes.bool,
multiline: PropTypes.bool,
type: PropTypes.oneOf(['text', 'select']),
options: PropTypes.arrayOf(PropTypes.shape({
value: PropTypes.string,
label: PropTypes.string,
})),
};

InlineEdit.defaultProps = {
value: '',
isRequired: false,
multiline: false,
type: 'text',
options: [],
};

export default InlineEdit;
10 changes: 10 additions & 0 deletions webpack/containers/Application/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import BootedContainerImages from '../../scenes/BootedContainerImages';
import ContainerImages from '../../scenes/ContainerImages';
import FlatpakRemotes from '../../scenes/FlatpakRemotes';
import FlatpakRemoteDetails from '../../scenes/FlatpakRemotes/Details';
import HostCollections from '../../scenes/HostCollections/HostCollectionsPage';
import HostCollectionDetails from '../../scenes/HostCollections/Details';

// eslint-disable-next-line import/prefer-default-export
export const links = [
Expand Down Expand Up @@ -106,4 +108,12 @@ export const links = [
component: WithOrganization(withHeader(FlatpakRemoteDetails, { title: __('Flatpak Remote Details') })),
exact: false,
},
{
path: 'labs/host_collections/:id([0-9]+)',
component: WithOrganization(withHeader(HostCollectionDetails, { title: __('Host Collection Details') })),
},
{
path: 'labs/host_collections',
component: WithOrganization(withHeader(HostCollections, { title: __('Host collections') })),
},
];
2 changes: 2 additions & 0 deletions webpack/redux/reducers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { moduleStreamDetails } from '../../scenes/ModuleStreams/Details';
import { contentViewDetails } from '../../scenes/ContentViews/Details';
import hostDetails from '../../components/extensions/HostDetails/HostDetailsReducer';
import searchBar from '../../components/extensions/SearchBar/SearchBarReducer';
import hostCollections from '../../scenes/HostCollections/HostCollectionsReducer';

export default combineReducers({
organization,
Expand All @@ -26,5 +27,6 @@ export default combineReducers({
contentViewDetails,
hostDetails,
searchBar,
hostCollections,
...organizationProductsReducers,
});
117 changes: 117 additions & 0 deletions webpack/scenes/HostCollections/Copy/CopyHostCollectionModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import PropTypes from 'prop-types';
import { translate as __ } from 'foremanReact/common/I18n';
import {
Modal,
ModalVariant,
Form,
FormGroup,
TextInput,
ActionGroup,
Button,
} from '@patternfly/react-core';
import { addToast } from 'foremanReact/components/ToastsList/slice';
import { copyHostCollection } from '../HostCollectionsActions';

const CopyHostCollectionModal = ({ isOpen, onClose, hostCollection }) => {
const dispatch = useDispatch();
const [newName, setNewName] = useState(`Copy of ${hostCollection?.name || ''}`);
const [copying, setCopying] = useState(false);

const handleSuccess = (data) => {
setCopying(false);
dispatch(addToast({
type: 'success',
message: __('Host collection copied successfully'),
}));
onClose();
window.location.href = `/labs/host_collections/${data.id}`;
};
Comment on lines +19 to +30
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion: Directly setting window.location.href for navigation may bypass SPA routing.

Use the router's navigation method (such as history.push) to maintain SPA behavior and prevent unnecessary page reloads.

Suggested change
const [newName, setNewName] = useState(`Copy of ${hostCollection?.name || ''}`);
const [copying, setCopying] = useState(false);
const handleSuccess = (data) => {
setCopying(false);
dispatch(addToast({
type: 'success',
message: __('Host collection copied successfully'),
}));
onClose();
window.location.href = `/labs/host_collections/${data.id}`;
};
import { useHistory } from 'react-router-dom';
const [newName, setNewName] = useState(`Copy of ${hostCollection?.name || ''}`);
const [copying, setCopying] = useState(false);
const history = useHistory();
const handleSuccess = (data) => {
setCopying(false);
dispatch(addToast({
type: 'success',
message: __('Host collection copied successfully'),
}));
onClose();
history.push(`/labs/host_collections/${data.id}`);
};


const handleError = (error) => {
Copy link
Member

Choose a reason for hiding this comment

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

This seems like wheel-reinvention too. Tell it to look in webpack/scenes/Tasks/helpers.js and reuse stuff from there.

setCopying(false);
const errorMsg =
error?.response?.data?.error?.full_messages?.[0] ||
error?.response?.data?.displayMessage ||
__('Failed to copy host collection');
dispatch(addToast({
type: 'error',
message: errorMsg,
}));
};

const onCopy = () => {
setCopying(true);
dispatch(copyHostCollection(hostCollection.id, newName, handleSuccess, handleError));
};

const submitDisabled = !newName?.trim().length || copying;

return (
<Modal
ouiaId="copy-host-collection-modal"
title={__('Copy host collection')}
variant={ModalVariant.small}
isOpen={isOpen}
onClose={onClose}
appendTo={document.body}
>
<Form
onSubmit={(e) => {
e.preventDefault();
onCopy();
}}
>
<FormGroup label={__('Name')} isRequired fieldId="new-name">
<TextInput
isRequired
type="text"
id="new-name"
aria-label="new-name"
ouiaId="new-name-input"
name="new-name"
value={newName}
onChange={(_event, value) => setNewName(value)}
isDisabled={copying}
/>
</FormGroup>
<ActionGroup>
<Button
ouiaId="copy-button"
aria-label="copy"
variant="primary"
isDisabled={submitDisabled}
isLoading={copying}
type="submit"
>
{__('Copy')}
</Button>
<Button
ouiaId="cancel-button"
variant="link"
onClick={onClose}
isDisabled={copying}
>
{__('Cancel')}
</Button>
</ActionGroup>
</Form>
</Modal>
);
};

CopyHostCollectionModal.propTypes = {
isOpen: PropTypes.bool,
onClose: PropTypes.func.isRequired,
hostCollection: PropTypes.shape({
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
name: PropTypes.string.isRequired,
}).isRequired,
};

CopyHostCollectionModal.defaultProps = {
isOpen: false,
};

export default CopyHostCollectionModal;
Loading
Loading