Device manager - device details section (#9169)

* add device metadata table

* test devicedetails

* handle missing last activity metadata

* remove debugs

* revert tile style changes

* fuss with indentation

* code formatting
This commit is contained in:
Kerry 2022-08-11 11:13:17 +02:00 committed by GitHub
parent 988cd335db
commit 09aade2907
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 371 additions and 5 deletions

View file

@ -27,6 +27,7 @@
@import "./components/views/location/_ZoomButtons.pcss"; @import "./components/views/location/_ZoomButtons.pcss";
@import "./components/views/messages/_MBeaconBody.pcss"; @import "./components/views/messages/_MBeaconBody.pcss";
@import "./components/views/messages/shared/_MediaProcessingError.pcss"; @import "./components/views/messages/shared/_MediaProcessingError.pcss";
@import "./components/views/settings/devices/_DeviceDetails.pcss";
@import "./components/views/settings/devices/_DeviceTile.pcss"; @import "./components/views/settings/devices/_DeviceTile.pcss";
@import "./components/views/settings/devices/_SelectableDeviceTile.pcss"; @import "./components/views/settings/devices/_SelectableDeviceTile.pcss";
@import "./components/views/settings/shared/_SettingsSubsection.pcss"; @import "./components/views/settings/shared/_SettingsSubsection.pcss";

View file

@ -0,0 +1,70 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_DeviceDetails {
display: flex;
flex-direction: column;
width: 100%;
padding: $spacing-16;
border-radius: 8px;
border: 1px solid $system;
}
.mx_DeviceDetails_section {
padding-bottom: $spacing-16;
margin-bottom: $spacing-16;
border-bottom: 1px solid $system;
&:last-child {
padding-bottom: 0;
border-bottom: 0;
margin-bottom: 0;
}
}
.mx_DeviceDetails_sectionHeading {
margin: 0;
}
.mxDeviceDetails_metadataTable {
font-size: $font-12px;
color: $secondary-content;
width: 100%;
margin-top: $spacing-20;
border-spacing: 0;
th {
text-transform: uppercase;
font-weight: normal;
text-align: left;
}
td {
padding-top: $spacing-8;
}
.mxDeviceDetails_metadataLabel {
width: 160px;
}
.mxDeviceDetails_metadataValue {
color: $primary-content;
}
}

View file

@ -17,6 +17,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import { IMyDevice } from 'matrix-js-sdk/src/client'; import { IMyDevice } from 'matrix-js-sdk/src/client';
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import classNames from 'classnames';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
@ -113,8 +114,6 @@ export default class DevicesPanelEntry extends React.Component<IProps, IState> {
}; };
public render(): JSX.Element { public render(): JSX.Element {
const myDeviceClass = this.props.isOwnDevice ? " mx_DevicesPanel_myDevice" : '';
let iconClass = ''; let iconClass = '';
let verifyButton: JSX.Element; let verifyButton: JSX.Element;
if (this.props.verified !== null) { if (this.props.verified !== null) {
@ -160,7 +159,7 @@ export default class DevicesPanelEntry extends React.Component<IProps, IState> {
}; };
if (this.props.isOwnDevice) { if (this.props.isOwnDevice) {
return <div className={"mx_DevicesPanel_device" + myDeviceClass}> return <div className={classNames("mx_DevicesPanel_device", "mx_DevicesPanel_myDevice")}>
<div className="mx_DevicesPanel_deviceTrust"> <div className="mx_DevicesPanel_deviceTrust">
<span className={"mx_DevicesPanel_icon mx_E2EIcon " + iconClass} /> <span className={"mx_DevicesPanel_icon mx_E2EIcon " + iconClass} />
</div> </div>
@ -171,7 +170,7 @@ export default class DevicesPanelEntry extends React.Component<IProps, IState> {
} }
return ( return (
<div className={"mx_DevicesPanel_device" + myDeviceClass}> <div className="mx_DevicesPanel_device">
<SelectableDeviceTile device={deviceWithVerification} onClick={this.onDeviceToggled} isSelected={this.props.selected}> <SelectableDeviceTile device={deviceWithVerification} onClick={this.onDeviceToggled} isSelected={this.props.selected}>
{ buttons } { buttons }
</SelectableDeviceTile> </SelectableDeviceTile>

View file

@ -0,0 +1,79 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { IMyDevice } from 'matrix-js-sdk/src/matrix';
import { formatDate } from '../../../../DateUtils';
import { _t } from '../../../../languageHandler';
import Heading from '../../typography/Heading';
interface Props {
device: IMyDevice;
}
interface MetadataTable {
heading?: string;
values: { label: string, value?: string | React.ReactNode }[];
}
const DeviceDetails: React.FC<Props> = ({ device }) => {
const metadata: MetadataTable[] = [
{
values: [
{ label: _t('Session ID'), value: device.device_id },
{
label: _t('Last activity'),
value: device.last_seen_ts && formatDate(new Date(device.last_seen_ts)),
},
],
},
{
heading: _t('Device'),
values: [
{ label: _t('IP address'), value: device.last_seen_ip },
],
},
];
return <div className='mx_DeviceDetails'>
<section className='mx_DeviceDetails_section'>
<Heading size='h3'>{ device.display_name ?? device.device_id }</Heading>
</section>
<section className='mx_DeviceDetails_section'>
<p className='mx_DeviceDetails_sectionHeading'>{ _t('Session details') }</p>
{ metadata.map(({ heading, values }, index) => <table
className='mxDeviceDetails_metadataTable'
key={index}
>
{ heading &&
<thead>
<tr><th>{ heading }</th></tr>
</thead>
}
<tbody>
{ values.map(({ label, value }) => <tr key={label}>
<td className='mxDeviceDetails_metadataLabel'>{ label }</td>
<td className='mxDeviceDetails_metadataValue'>{ value }</td>
</tr>) }
</tbody>
</table>,
) }
</section>
</div>;
};
export default DeviceDetails;

View file

@ -1692,7 +1692,11 @@
"Sign out devices|other": "Sign out devices", "Sign out devices|other": "Sign out devices",
"Sign out devices|one": "Sign out device", "Sign out devices|one": "Sign out device",
"Authentication": "Authentication", "Authentication": "Authentication",
"Session ID": "Session ID",
"Last activity": "Last activity", "Last activity": "Last activity",
"Device": "Device",
"IP address": "IP address",
"Session details": "Session details",
"Verified": "Verified", "Verified": "Verified",
"Unverified": "Unverified", "Unverified": "Unverified",
"Unable to remove contact information": "Unable to remove contact information", "Unable to remove contact information": "Unable to remove contact information",
@ -2720,7 +2724,6 @@
"Confirm by comparing the following with the User Settings in your other session:": "Confirm by comparing the following with the User Settings in your other session:", "Confirm by comparing the following with the User Settings in your other session:": "Confirm by comparing the following with the User Settings in your other session:",
"Confirm this user's session by comparing the following with their User Settings:": "Confirm this user's session by comparing the following with their User Settings:", "Confirm this user's session by comparing the following with their User Settings:": "Confirm this user's session by comparing the following with their User Settings:",
"Session name": "Session name", "Session name": "Session name",
"Session ID": "Session ID",
"Session key": "Session key", "Session key": "Session key",
"If they don't match, the security of your communication may be compromised.": "If they don't match, the security of your communication may be compromised.", "If they don't match, the security of your communication may be compromised.": "If they don't match, the security of your communication may be compromised.",
"Verify session": "Verify session", "Verify session": "Verify session",

View file

@ -0,0 +1,53 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { render } from '@testing-library/react';
import DeviceDetails from '../../../../../src/components/views/settings/devices/DeviceDetails';
describe('<DeviceDetails />', () => {
const baseDevice = {
device_id: 'my-device',
};
const defaultProps = {
device: baseDevice,
};
const getComponent = (props = {}) => <DeviceDetails {...defaultProps} {...props} />;
// 14.03.2022 16:15
const now = 1647270879403;
jest.useFakeTimers();
beforeEach(() => {
jest.setSystemTime(now);
});
it('renders device without metadata', () => {
const { container } = render(getComponent());
expect(container).toMatchSnapshot();
});
it('renders device with metadata', () => {
const device = {
...baseDevice,
display_name: 'My Device',
last_seen_ip: '123.456.789',
last_seen_ts: now - 60000000,
};
const { container } = render(getComponent({ device }));
expect(container).toMatchSnapshot();
});
});

View file

@ -0,0 +1,161 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<DeviceDetails /> renders device with metadata 1`] = `
<div>
<div
class="mx_DeviceDetails"
>
<section
class="mx_DeviceDetails_section"
>
<h3
class="mx_Heading_h3"
>
My Device
</h3>
</section>
<section
class="mx_DeviceDetails_section"
>
<p
class="mx_DeviceDetails_sectionHeading"
>
Session details
</p>
<table
class="mxDeviceDetails_metadataTable"
>
<tbody>
<tr>
<td
class="mxDeviceDetails_metadataLabel"
>
Session ID
</td>
<td
class="mxDeviceDetails_metadataValue"
>
my-device
</td>
</tr>
<tr>
<td
class="mxDeviceDetails_metadataLabel"
>
Last activity
</td>
<td
class="mxDeviceDetails_metadataValue"
>
Sun 22:34
</td>
</tr>
</tbody>
</table>
<table
class="mxDeviceDetails_metadataTable"
>
<thead>
<tr>
<th>
Device
</th>
</tr>
</thead>
<tbody>
<tr>
<td
class="mxDeviceDetails_metadataLabel"
>
IP address
</td>
<td
class="mxDeviceDetails_metadataValue"
>
123.456.789
</td>
</tr>
</tbody>
</table>
</section>
</div>
</div>
`;
exports[`<DeviceDetails /> renders device without metadata 1`] = `
<div>
<div
class="mx_DeviceDetails"
>
<section
class="mx_DeviceDetails_section"
>
<h3
class="mx_Heading_h3"
>
my-device
</h3>
</section>
<section
class="mx_DeviceDetails_section"
>
<p
class="mx_DeviceDetails_sectionHeading"
>
Session details
</p>
<table
class="mxDeviceDetails_metadataTable"
>
<tbody>
<tr>
<td
class="mxDeviceDetails_metadataLabel"
>
Session ID
</td>
<td
class="mxDeviceDetails_metadataValue"
>
my-device
</td>
</tr>
<tr>
<td
class="mxDeviceDetails_metadataLabel"
>
Last activity
</td>
<td
class="mxDeviceDetails_metadataValue"
/>
</tr>
</tbody>
</table>
<table
class="mxDeviceDetails_metadataTable"
>
<thead>
<tr>
<th>
Device
</th>
</tr>
</thead>
<tbody>
<tr>
<td
class="mxDeviceDetails_metadataLabel"
>
IP address
</td>
<td
class="mxDeviceDetails_metadataValue"
/>
</tr>
</tbody>
</table>
</section>
</div>
</div>
`;