Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature partial updates #1035

Merged
11 commits merged into from
Nov 9, 2020
Merged
Prev Previous commit
Next Next commit
extracted software information deduction & rendering
Changelog: None
Signed-off-by: Manuel Zedel <[email protected]>
  • Loading branch information
mzedel committed Oct 23, 2020
commit 1adb17f7572171391bdfae8da3708a6d921a5007
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`ArtifactPayload Component renders correctly 1`] = `
Array [
<p
className="margin-bottom-none"
>
test
</p>,
<ul
className="MuiList-root list-horizontal-flex MuiList-padding"
style={
Object {
"paddingTop": 0,
}
}
>
<div
onClick={[Function]}
>
<li
className="MuiListItem-root attributes MuiListItem-gutters Mui-disabled"
disabled={true}
>
<div
className="MuiListItemText-root MuiListItemText-multiline"
>
<span
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
>
primary text
</span>
<div
className="MuiTypography-root MuiListItemText-secondary MuiTypography-body2 MuiTypography-colorTextSecondary MuiTypography-displayBlock"
title="secondary text"
>
<div>
<span
className="inventory-text"
style={Object {}}
>
secondary text
</span>

</div>
</div>
</div>
</li>
<hr
className="MuiDivider-root"
/>
</div>
</ul>,
]
`;
22 changes: 22 additions & 0 deletions src/js/components/artifacts/artifactmetadatalist.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react';

import { List } from '@material-ui/core';

import ExpandableAttribute from '../common/expandable-attribute';

export const ArtifactMetadataList = ({ metaInfo = { content: [] } }) => {
return (
!!metaInfo.content.length && (
<>
<p className="margin-bottom-none">{metaInfo.title}</p>
<List className="list-horizontal-flex" style={{ paddingTop: 0 }}>
{metaInfo.content.map((info, index) => (
<ExpandableAttribute key={`software-info-${index}`} {...info} />
))}
</List>
</>
)
);
};

export default ArtifactMetadataList;
14 changes: 14 additions & 0 deletions src/js/components/artifacts/artifactmetadatalist.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react';
import renderer from 'react-test-renderer';
import ArtifactMetadataList from './artifactmetadatalist';
import { undefineds } from '../../../../tests/mockData';

describe('ArtifactPayload Component', () => {
it('renders correctly', () => {
const tree = renderer
.create(<ArtifactMetadataList metaInfo={{ content: [{ key: 'custom-key', primary: 'primary text', secondary: 'secondary text' }], title: 'test' }} />)
.toJSON();
expect(tree).toMatchSnapshot();
expect(JSON.stringify(tree)).toEqual(expect.not.stringMatching(undefineds));
});
});
35 changes: 10 additions & 25 deletions src/js/components/artifacts/selectedartifact.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import {
} from '@material-ui/icons';

import { getArtifactUrl, showRemoveArtifactDialog } from '../../actions/releaseActions';
import { extractSoftwareInformation } from '../../helpers';
import { colors } from '../../themes/mender-theme';
import ExpandableAttribute from '../common/expandable-attribute';
import ArtifactPayload from './artifactPayload';

const styles = {
Expand All @@ -34,6 +34,11 @@ const styles = {
}
};

const softwareTitleMap = {
'rootfs-image.version': { title: 'System filesystem', priority: 0 },
'rootfs-image.checksum': { title: 'checksum', priority: 1 }
};

export const SelectedArtifact = ({ artifact, editArtifact, getArtifactUrl, onExpansion, showRemoveArtifactDialog }) => {
const [descEdit, setDescEdit] = useState(false);
const [description, setDescription] = useState(artifact.description || '-');
Expand Down Expand Up @@ -93,19 +98,9 @@ export const SelectedArtifact = ({ artifact, editArtifact, getArtifactUrl, onExp
}, []);
};

const extractSoftwareInformation = (capabilities = {}) => {
return Object.entries(capabilities).reduce((accu, item) => {
const parts = item[0].split('.');
if (parts.length > 2 && parts[2].endsWith('.version')) {
const content = ['Software filesystem', 'Software name', 'Software version'].map((item, index) => (
<ExpandableAttribute key={`${parts[0]}-info-${index}`} primary={item} secondary={parts[index]} />
));
accu.push({ title: [parts[0]], content });
}
return accu;
}, []);
};

const softwareInformation = Object.values(
extractSoftwareInformation(artifact.artifact_provides, softwareTitleMap, ['Software filesystem', 'Software name', 'Software version'])
).map(content => ({ title: 'Software versioning information', content }))[0];
const artifactMetaInfo = [
{ title: 'Software versioning information', content: extractSoftwareInformation(artifact.artifact_provides) },
{ title: 'Artifact dependencies', content: transformArtifactCapabilities(artifact.artifact_depends) },
Expand Down Expand Up @@ -149,17 +144,7 @@ export const SelectedArtifact = ({ artifact, editArtifact, getArtifactUrl, onExp
<ListItemText primary="Signed" secondary={artifact.signed ? <CheckCircleOutlineIcon className="green" /> : <CancelOutlinedIcon className="red" />} />
</ListItem>
</List>
{artifactMetaInfo.map(
(info, index) =>
!!info.content.length && (
<React.Fragment key={`artifact-info-${index}`}>
<p className="margin-bottom-none">{info.title}</p>
<List className="list-horizontal-flex" style={{ paddingTop: 0 }}>
{info.content}
</List>
</React.Fragment>
)
)}
<ArtifactMetadataList metaInfo={softwareInformation} />
<Accordion
square
expanded={showPayloads}
Expand Down
71 changes: 24 additions & 47 deletions src/js/components/devices/device-details/deviceinventory.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,77 +5,54 @@ import copy from 'copy-to-clipboard';
import { Button, List } from '@material-ui/core';
import { Link as LinkIcon, Replay as ReplayIcon } from '@material-ui/icons';

import { extractSoftware, extractSoftwareInformation } from '../../../helpers';
import ExpandableAttribute from '../../common/expandable-attribute';
import ForwardingLink from '../../common/forwardlink';
import { inlineHeadingStyle } from '../../artifacts/artifactPayload';

const softwareAttributeNames = ['rootfs-image', 'data-partition', 'artifact_name'];
const listItemTextClass = { secondary: 'inventory-text' };

const softwareTitleMap = {
'rootfs-image.version': { title: 'System software version', priority: 0 },
'rootfs-image.checksum': { title: 'checksum', priority: 1 }
};

export const DeviceInventory = ({ attributes, id, setSnackbar, unauthorized }) => {
const copyLinkToClipboard = () => {
const location = window.location.href.substring(0, window.location.href.indexOf('/devices') + '/devices'.length);
copy(`${location}?id=${id}`);
setSnackbar('Link copied to clipboard');
};

const { softwareAttributes, deviceInventory } = Object.entries(attributes)
const softwareInfo = extractSoftware(attributes);
const deviceInventory = Object.entries(attributes)
.sort((a, b) => a[0].localeCompare(b[0]))
.reduce(
(accu, attribute, i) => {
const softwareAttribute = softwareAttributeNames.find(item => attribute[0].startsWith(item));
if (softwareAttribute) {
if (!accu.softwareAttributes[softwareAttribute]) {
accu.softwareAttributes[softwareAttribute] = [];
}
accu.softwareAttributes[softwareAttribute].push({ key: attribute[0], value: attribute[1] });
} else {
const secondaryText = Array.isArray(attribute[1]) ? attribute[1].join(',') : attribute[1];
accu.deviceInventory.push(<ExpandableAttribute key={`info-${i}`} primary={attribute[0]} secondary={secondaryText} textClasses={listItemTextClass} />);
}
return accu;
},
{ softwareAttributes: {}, deviceInventory: [] }
);

const mapLayerInformation = (layerInfo, i) => {
let primary = layerInfo.key;
let secondary = layerInfo.value;
let priority = i + 2;
const infoItems = layerInfo.key.split('.');
switch (infoItems.length) {
case 2:
primary = layerInfo.key === 'rootfs-image.version' ? 'System software version' : primary;
priority = i;
break;
case 3:
primary = infoItems[1];
break;
default:
break;
}
const component = <ExpandableAttribute key={`software-info-${i}`} primary={primary} secondary={secondary} textClasses={listItemTextClass} />;
return { priority, component };
};
.reduce((accu, attribute, i) => {
const softwareAttribute = softwareInfo.find(info => attribute[0].startsWith(info));
if (!softwareAttribute) {
const secondaryText = Array.isArray(attribute[1]) ? attribute[1].join(',') : attribute[1];
accu.push(<ExpandableAttribute key={`info-${i}`} primary={attribute[0]} secondary={secondaryText} textClasses={listItemTextClass} />);
}
return accu;
}, []);

const layers = Object.entries(softwareAttributes).map(layer => {
const items = layer[1]
.map(mapLayerInformation)
.sort((a, b) => a.priority.localeCompare(b.priority))
.map(item => item.component);
return { title: layer[0], items };
});
const softwareInformation = Object.entries(extractSoftwareInformation(attributes, softwareTitleMap)).map(item => ({
title: item[0],
content: item[1].map((info, index) => (
<ExpandableAttribute key={`${item[0]}-info-${index}`} primary={info.primary} secondary={info.secondary} textClasses={listItemTextClass} />
))
}));

return (
<>
<div className={`device-inventory bordered ${unauthorized ? 'hidden' : 'report-list'}`}>
<h4>Device inventory</h4>
<div className="file-details">
<h4 style={inlineHeadingStyle}>Installed software</h4>
{layers.map((layer, layerIndex) => (
{softwareInformation.map((layer, layerIndex) => (
<div className="flexbox column" key={`layer-${layerIndex}`}>
<div className="margin-top-small">{layer.title}</div>
<List>{layer.items}</List>
<List>{layer.content}</List>
</div>
))}
</div>
Expand Down
55 changes: 55 additions & 0 deletions src/js/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -513,3 +513,58 @@ export const getSnackbarMessage = (skipped, done) => {
const doneText = done ? `${done} ${pluralize('device', done)} ${pluralize('was', done)} updated successfully. ` : '';
return `${doneText}${skipText}`;
};

export const extractSoftware = (capabilities = {}) =>
Object.keys(capabilities).reduce((accu, item) => {
if (item.endsWith('.version')) {
accu.push(item.substring(0, item.indexOf('.')));
}
return accu;
}, []);

export const extractSoftwareInformation = (capabilities = {}, softwareTitleMap = {}, softwareHeaderList = []) => {
const mapLayerInformation = (key, value, i) => {
let primary = key;
let secondary = value;
let priority = i + Object.keys(softwareTitleMap);
let result = [];
const infoItems = key.split('.');
if (infoItems.length === 2) {
primary = softwareTitleMap[key] ? softwareTitleMap[key].title : primary;
priority = softwareTitleMap[key] ? softwareTitleMap[key].priority : i;
result.push({ priority, primary, secondary });
} else if (infoItems.length >= 3) {
primary = softwareTitleMap[key] ? softwareTitleMap[key].title : infoItems[1];
// this is required with the "secondary" assignment in the softwareHeaderList.map to support keys with more than 2 dots
const punctuated = infoItems.length > softwareHeaderList.length ? infoItems.slice(1, infoItems.length - 1).join('.') : null;
const things = softwareHeaderList.length
? softwareHeaderList.map((item, index) => ({
priority: index,
primary: item,
secondary: infoItems[index] === 'version' && index === infoItems.length - 1 ? value : punctuated ?? infoItems[index]
}))
: [{ priority, primary, secondary }];
result.push(...things);
} else {
result.push({ priority, primary, secondary });
}
return result;
};

const softwareInformation = extractSoftware(capabilities);
const softwareLayers = Object.entries(capabilities).reduce((accu, item, index) => {
const softwareAttribute = softwareInformation.find(info => item[0].startsWith(info));
if (softwareAttribute) {
if (!accu[softwareAttribute]) {
accu[softwareAttribute] = [];
}
accu[softwareAttribute].push(...mapLayerInformation(item[0], item[1], index));
}
return accu;
}, {});

return Object.entries(softwareLayers).reduce((accu, item) => {
accu[item[0]] = item[1].sort((a, b) => a.priority - b.priority);
return accu;
}, {});
};