Skip to content

Commit

Permalink
Improve Performance of DBus Interface Handling and ListItem Rendering (
Browse files Browse the repository at this point in the history
…#270)

* Added memo around ListItem

* Removed history

* Rewrite Demo

* Subscription handling rewrite

* Fixed number tinker
  • Loading branch information
SindriTh authored Nov 21, 2023
1 parent dc1ba09 commit dda16fd
Show file tree
Hide file tree
Showing 9 changed files with 299 additions and 399 deletions.
126 changes: 44 additions & 82 deletions cockpit/IPC/src/Components/IOList/ListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,53 +4,47 @@ import React, { ChangeEvent, ReactElement } from 'react';
import {
ClipboardCopy,
ClipboardCopyVariant,
DataListAction,
DataListCell,
DataListItem,
DataListItemCells,
DataListItemRow,
Dropdown,
DropdownItem,
DropdownList,
MenuToggleElement,
Tooltip,
} from '@patternfly/react-core';
import Circle from 'src/Components/Simple/Circle';
import { removeSlotOrg } from 'src/Components/Form/WidgetFunctions';
import CustomMenuToggle from 'src/Components/Dropdown/CustomMenuToggle';
import StringTinker from 'src/Components/Tinker/StringTinker';
import BoolTinker from 'src/Components/Tinker/BoolTinker';
import NumberTinker from 'src/Components/Tinker/NumberTinker';

interface ListItemProps {
dbusInterface: any,
index: number,
activeDropdown: number | null,
dropdownRefs: any,
onToggleClick: any,
setModalOpen: any,
onCheck: (checked: boolean) => void,
isChecked: boolean,
data: any,
}

// eslint-disable-next-line react/function-component-definition
const ListItem: React.FC<ListItemProps> = ({
dbusInterface, index, activeDropdown, dropdownRefs, onToggleClick, setModalOpen, onCheck,
dbusInterface,
onCheck, isChecked, data,
}) => {
const isMobile = window.innerWidth < 768;
/**
* Handles the content of the secondary column for booleans
* @param data The data to be displayed
* @returns ReactElement to be displayed
*/
function handleBoolContent(data: any): ReactElement {
function handleBoolContent(booldata: any): ReactElement {
return (
<Tooltip
content={`Value is ${data.Value ? 'true' : 'false'}`}
content={booldata !== null && isChecked ? `Value is ${booldata ? 'true' : 'false'}` : 'Unknown'} // NOSONAR
enableFlip
distance={5}
entryDelay={1000}
>
<Circle size="1rem" color={data.Value ? 'green' : 'red'} />
{ isChecked && booldata !== null
? <Circle size="1rem" color={booldata ? 'green' : 'red'} />
: <Circle size="1rem" color="#888888" />}
</Tooltip>
);
}
Expand All @@ -60,23 +54,28 @@ const ListItem: React.FC<ListItemProps> = ({
* @param data The data to be displayed
* @returns ReactElement to be displayed
*/
function handleStringContent(data: any): ReactElement {
return (
<p style={{ marginBottom: '0px' }}>{data.Value}</p>
);
function handleStringContent(stringdata: any): ReactElement {
if (!isChecked || stringdata === null) {
return (<p style={{ marginBottom: '0px' }}>Unknown</p>);
}
return (<p style={{ marginBottom: '0px' }}>{stringdata}</p>);
}

/**
* Handles the content of the secondary column for JSON objects
* @param data The data to be displayed
* @returns ReactElement to be displayed
*/
function handleJSONContent(data: any): ReactElement {
function handleJSONContent(stringdata: any): ReactElement {
if (!isChecked || stringdata === null) {
return (<p style={{ marginBottom: '0px' }}>Unknown</p>);
}

// Test if string is JSON
let isJSON = false;
if (data.Value[0] === '{' || data.Value[0] === '[') {
if (stringdata[0] === '{' || stringdata[0] === '[') {
try {
JSON.parse(data.Value);
JSON.parse(stringdata);
isJSON = true;
} catch {
isJSON = false;
Expand All @@ -92,16 +91,16 @@ const ListItem: React.FC<ListItemProps> = ({
clickTip="Copied"
variant={ClipboardCopyVariant.expansion}
style={{
alignSelf: 'baseline', marginTop: '-.85rem', zIndex: 1, maxWidth: '25vw',
alignSelf: 'baseline', marginTop: '-.25rem', zIndex: 1, maxWidth: '25vw',
}}
>
{JSON.stringify(JSON.parse(data.Value), null, 2)}
{JSON.stringify(JSON.parse(stringdata), null, 2)}
</ClipboardCopy>
);
}

return (
<p style={{ marginBottom: '0px' }}>{data.Value}</p>
<p style={{ marginBottom: '0px' }}>{stringdata}</p>
);
}

Expand All @@ -110,23 +109,23 @@ const ListItem: React.FC<ListItemProps> = ({
* @param data Interface data
* @returns ReactElement to be displayed
*/
function getSecondaryContent(data: any): ReactElement | null {
const internals = (interfacedata: any) => {
switch (interfacedata.type) {
function getSecondaryContent(interfaceData: any): ReactElement | null {
const internals = (secData: any) => {
switch (secData.type) {
case 'boolean':
return handleBoolContent(interfacedata.proxy.data);
return handleBoolContent(data);

case 'string':
return handleJSONContent(interfacedata.proxy.data);
return handleJSONContent(data);
// If the string is not JSON, it falls back to string

case 'object':
return handleJSONContent(interfacedata.proxy.data);
return handleJSONContent(data);

case 'number':
case 'integer':
case 'double':
return handleStringContent(interfacedata.proxy.data);
return handleStringContent(data);

default:
return <>Type Error</>;
Expand All @@ -142,7 +141,7 @@ const ListItem: React.FC<ListItemProps> = ({
padding: isMobile ? '10px' : '0px',
}}
>
{internals(data)}
{internals(interfaceData)}
</div>
);
}
Expand All @@ -152,22 +151,22 @@ const ListItem: React.FC<ListItemProps> = ({
* @param data Interface data
* @returns ReactElement to be displayed
*/
function getTinkerInterface(data: any): React.ReactElement | null {
function getTinkerInterface(interfaceData: any): React.ReactElement | null {
const internals = (interfacedata: any) => {
switch (interfacedata.type) {
case 'boolean':
return <BoolTinker data={interfacedata} />;
return <BoolTinker interfaceData={interfacedata} isChecked={isChecked} />;

case 'string':
return <StringTinker data={interfacedata} />;
return <StringTinker interfaceData={interfacedata} isChecked={isChecked} />;

case 'object':
return <StringTinker data={interfacedata} />;
return <StringTinker interfaceData={interfacedata} isChecked={isChecked} />;

case 'number':
case 'integer':
case 'double':
return <NumberTinker data={interfacedata} />;
return <NumberTinker interfaceData={interfacedata} isChecked={isChecked} />;

default:
return <>Type Error</>;
Expand All @@ -181,35 +180,34 @@ const ListItem: React.FC<ListItemProps> = ({
justifyContent: 'start',
}}
>
{internals(data)}
{internals(interfaceData)}
</div>
);
}
const onSelect = (_event: React.MouseEvent<Element, MouseEvent> | undefined, value: string | number | undefined) => {
console.log(value);
};

const handleCheckboxChange = (event: ChangeEvent<HTMLInputElement>) => {
onCheck(event.target.checked);
};

return (
<DataListItem aria-labelledby="check-action-item1" key={dbusInterface.proxy.iface + dbusInterface.process}>
<DataListItem aria-labelledby="check-action-item1" key={dbusInterface.interfaceName}>
<DataListItemRow size={10}>
<DataListCell key="checkbox" style={{ display: 'flex', alignItems: 'center', marginRight: '1rem' }}>
<input
type="checkbox"
onChange={handleCheckboxChange}
aria-label={`Select ${dbusInterface.proxy.iface}`}
aria-label={`Select ${dbusInterface.interfaceName}`}
checked={isChecked}
/>
</DataListCell>
<DataListItemCells
dataListCells={[
<DataListCell key="primary content" style={{ textAlign: 'left' }}>
<p className="PrimaryText">{removeSlotOrg(dbusInterface.proxy.iface)}</p>
<p className="PrimaryText">{removeSlotOrg(dbusInterface.interfaceName)}</p>
</DataListCell>,
<DataListCell
key={`${dbusInterface.interfaceName}-secondary-content`}
className="SecondaryText"
style={{
textAlign: 'right',
height: '100%',
Expand All @@ -223,6 +221,7 @@ const ListItem: React.FC<ListItemProps> = ({
? (
<DataListCell
key={`${dbusInterface.interfaceName}-tinker-content`}
className="TinkerText"
style={{
textAlign: 'right',
height: '100%',
Expand All @@ -236,43 +235,6 @@ const ListItem: React.FC<ListItemProps> = ({
: null,
]}
/>
<DataListAction
aria-labelledby="check-action-item1 check-action-action1"
id="check-action-action1"
aria-label="Actions"
isPlainButtonAction
>
<Dropdown
className="DropdownItem"
key={`${dbusInterface.proxy.iface}${dbusInterface.process}`}
toggle={(toggleRef: React.Ref<MenuToggleElement>) => ( // NOSONAR
<CustomMenuToggle
toggleRef={(ref) => {
if (typeof toggleRef === 'function') {
toggleRef(ref);
}
dropdownRefs.current[index] = ref; // Store the ref for the dropdown
}}
onClick={() => onToggleClick(index)}
isExpanded={dbusInterface.dropdown}
/>
)}
isOpen={activeDropdown === index}
onSelect={onSelect}
popperProps={{ enableFlip: true }}
>
<DropdownList>
<DropdownItem key="history" style={{ textDecoration: 'none' }} isDisabled> View History </DropdownItem>
<DropdownItem
key={`watch-${dbusInterface.interfaceName}-dd`}
style={{ textDecoration: 'none' }}
onMouseDown={() => setModalOpen(index)}
>
Watch
</DropdownItem>
</DropdownList>
</Dropdown>
</DataListAction>
</DataListItemRow>
</DataListItem>
);
Expand Down
35 changes: 22 additions & 13 deletions cockpit/IPC/src/Components/Tinker/BoolTinker.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,41 @@
/* eslint-disable react/function-component-definition */
import React from 'react';
import { TFC_DBUS_DOMAIN, TFC_DBUS_ORGANIZATION } from 'src/variables';
import { AlertVariant, Switch } from '@patternfly/react-core';
import { useAlertContext } from '../Alert/AlertContext';

interface BoolTinkerIface {
data: any
interfaceData: any;
isChecked: boolean;
}

const BoolTinker: React.FC<BoolTinkerIface> = ({ data }) => {
const BoolTinker: React.FC<BoolTinkerIface> = ({ interfaceData, isChecked }) => {
const { addAlert } = useAlertContext();
const [value, setValue] = React.useState(data.proxy.data.Value);
const [value, setValue] = React.useState(interfaceData.data);
const slotPath = `/${TFC_DBUS_DOMAIN}/${TFC_DBUS_ORGANIZATION}/Slots`;
const signalPath = `/${TFC_DBUS_DOMAIN}/${TFC_DBUS_ORGANIZATION}/Signals`;

const handleInputChange = (newValue: boolean) => {
data.proxy.Tinker(newValue ? 1 : 0).then(() => {
addAlert(`Value of ${data.interfaceName} has been set ${newValue ? 'true' : 'false'}`, AlertVariant.success);
setValue(newValue);
}).catch(() => {
addAlert(`Error setting value of ${data.interfaceName}`, AlertVariant.danger);
const handleInputChange = async (newValue: boolean) => {
const client = window.cockpit.dbus(interfaceData.process, { bus: 'system', superuser: 'try' });
const proxy = client.proxy(interfaceData.interfaceName, interfaceData.direction === 'slot' ? slotPath : signalPath);
await proxy.wait().then(() => {
proxy.Tinker(newValue ? 1 : 0).then(() => {
addAlert(`Value of ${interfaceData.interfaceName} has been set ${newValue ? 'true' : 'false'}`, AlertVariant.success);
setValue(newValue);
}).catch((e: any) => {
addAlert(`Error setting value of ${interfaceData.interfaceName}`, AlertVariant.danger);
console.log(e);
});
});
};

return (
<Switch
aria-label={`tinker-{${data.iface}}-{${data.process}}}`}
isChecked={value}
aria-label={`tinker-{${interfaceData.interfaceName}}-{${interfaceData.process}}}`}
isChecked={isChecked ? value : false}
onChange={(e, val) => handleInputChange(val)}
isDisabled={false}
key={`${data.iface}-${data.process}`}
isDisabled={!isChecked}
key={`${interfaceData.interfaceName}-${interfaceData.process}`}
/>
);
};
Expand Down
Loading

0 comments on commit dda16fd

Please sign in to comment.