Device manager - label devices as inactive (PSG-638) (#9175)
* filter devices by security recommendation * display inactive status on device tile * unify DeviceSecurityVariation type, add correct icon to inactive ui * tidy * avoid dead code warningpull/28788/head^2
parent
d21498de94
commit
4a5ed2f899
|
@ -18,7 +18,6 @@ limitations under the License.
|
|||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
@ -27,15 +26,21 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_DeviceTile_metadata {
|
||||
margin-top: 2px;
|
||||
margin-top: $spacing-4;
|
||||
font-size: $font-12px;
|
||||
color: $secondary-content;
|
||||
line-height: $font-14px;
|
||||
}
|
||||
|
||||
.mx_DeviceTile_inactiveIcon {
|
||||
height: 14px;
|
||||
margin-right: $spacing-8;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.mx_DeviceTile_actions {
|
||||
display: grid;
|
||||
grid-gap: $spacing-8;
|
||||
grid-auto-flow: column;
|
||||
|
||||
margin-left: $spacing-8;
|
||||
}
|
||||
|
|
|
@ -20,12 +20,7 @@ import React from 'react';
|
|||
import { Icon as VerifiedIcon } from '../../../../../res/img/e2e/verified.svg';
|
||||
import { Icon as UnverifiedIcon } from '../../../../../res/img/e2e/warning.svg';
|
||||
import { Icon as InactiveIcon } from '../../../../../res/img/element-icons/settings/inactive.svg';
|
||||
|
||||
export enum DeviceSecurityVariation {
|
||||
Verified = 'Verified',
|
||||
Unverified = 'Unverified',
|
||||
Inactive = 'Inactive',
|
||||
}
|
||||
import { DeviceSecurityVariation } from './filter';
|
||||
interface Props {
|
||||
variation: DeviceSecurityVariation;
|
||||
heading: string;
|
||||
|
|
|
@ -16,13 +16,14 @@ limitations under the License.
|
|||
|
||||
import React, { Fragment } from "react";
|
||||
|
||||
import { Icon as InactiveIcon } from '../../../../../res/img/element-icons/settings/inactive.svg';
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { formatDate, formatRelativeTime } from "../../../../DateUtils";
|
||||
import TooltipTarget from "../../elements/TooltipTarget";
|
||||
import { Alignment } from "../../elements/Tooltip";
|
||||
import Heading from "../../typography/Heading";
|
||||
import { INACTIVE_DEVICE_AGE_MS, isDeviceInactive } from "./filter";
|
||||
import { DeviceWithVerification } from "./useOwnDevices";
|
||||
|
||||
export interface DeviceTileProps {
|
||||
device: DeviceWithVerification;
|
||||
children?: React.ReactNode;
|
||||
|
@ -45,7 +46,8 @@ const DeviceTileName: React.FC<{ device: DeviceWithVerification }> = ({ device }
|
|||
</Heading>;
|
||||
};
|
||||
|
||||
const MS_6_DAYS = 6 * 24 * 60 * 60 * 1000;
|
||||
const MS_DAY = 24 * 60 * 60 * 1000;
|
||||
const MS_6_DAYS = 6 * MS_DAY;
|
||||
const formatLastActivity = (timestamp: number, now = new Date().getTime()): string => {
|
||||
// less than a week ago
|
||||
if (timestamp + MS_6_DAYS >= now) {
|
||||
|
@ -56,18 +58,40 @@ const formatLastActivity = (timestamp: number, now = new Date().getTime()): stri
|
|||
return formatRelativeTime(new Date(timestamp));
|
||||
};
|
||||
|
||||
const DeviceMetadata: React.FC<{ value: string, id: string }> = ({ value, id }) => (
|
||||
const getInactiveMetadata = (device: DeviceWithVerification): { id: string, value: React.ReactNode } | undefined => {
|
||||
const isInactive = isDeviceInactive(device);
|
||||
|
||||
if (!isInactive) {
|
||||
return undefined;
|
||||
}
|
||||
const inactiveAgeDays = Math.round(INACTIVE_DEVICE_AGE_MS / MS_DAY);
|
||||
return { id: 'inactive', value: (
|
||||
<>
|
||||
<InactiveIcon className="mx_DeviceTile_inactiveIcon" />
|
||||
{
|
||||
_t('Inactive for %(inactiveAgeDays)s+ days', { inactiveAgeDays }) +
|
||||
` (${formatLastActivity(device.last_seen_ts)})`
|
||||
}
|
||||
</>),
|
||||
};
|
||||
};
|
||||
|
||||
const DeviceMetadata: React.FC<{ value: string | React.ReactNode, id: string }> = ({ value, id }) => (
|
||||
value ? <span data-testid={`device-metadata-${id}`}>{ value }</span> : null
|
||||
);
|
||||
|
||||
const DeviceTile: React.FC<DeviceTileProps> = ({ device, children, onClick }) => {
|
||||
const inactive = getInactiveMetadata(device);
|
||||
const lastActivity = device.last_seen_ts && `${_t('Last activity')} ${formatLastActivity(device.last_seen_ts)}`;
|
||||
const verificationStatus = device.isVerified ? _t('Verified') : _t('Unverified');
|
||||
const metadata = [
|
||||
{ id: 'isVerified', value: verificationStatus },
|
||||
{ id: 'lastActivity', value: lastActivity },
|
||||
{ id: 'lastSeenIp', value: device.last_seen_ip },
|
||||
];
|
||||
// if device is inactive, don't display last activity or verificationStatus
|
||||
const metadata = inactive
|
||||
? [inactive, { id: 'lastSeenIp', value: device.last_seen_ip }]
|
||||
: [
|
||||
{ id: 'isVerified', value: verificationStatus },
|
||||
{ id: 'lastActivity', value: lastActivity },
|
||||
{ id: 'lastSeenIp', value: device.last_seen_ip },
|
||||
];
|
||||
|
||||
return <div className="mx_DeviceTile" data-testid={`device-tile-${device.device_id}`}>
|
||||
<div className="mx_DeviceTile_info" onClick={onClick}>
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
|
||||
import DeviceTile from './DeviceTile';
|
||||
import { filterDevicesBySecurityRecommendation } from './filter';
|
||||
import { DevicesDictionary, DeviceWithVerification } from './useOwnDevices';
|
||||
|
||||
interface Props {
|
||||
|
@ -27,8 +28,9 @@ interface Props {
|
|||
const sortDevicesByLatestActivity = (left: DeviceWithVerification, right: DeviceWithVerification) =>
|
||||
(right.last_seen_ts || 0) - (left.last_seen_ts || 0);
|
||||
|
||||
const getSortedDevices = (devices: DevicesDictionary) =>
|
||||
Object.values(devices).sort(sortDevicesByLatestActivity);
|
||||
const getFilteredSortedDevices = (devices: DevicesDictionary) =>
|
||||
filterDevicesBySecurityRecommendation(Object.values(devices), [])
|
||||
.sort(sortDevicesByLatestActivity);
|
||||
|
||||
/**
|
||||
* Filtered list of devices
|
||||
|
@ -36,7 +38,7 @@ const getSortedDevices = (devices: DevicesDictionary) =>
|
|||
* TODO(kerrya) Filtering to added as part of PSG-648
|
||||
*/
|
||||
const FilteredDeviceList: React.FC<Props> = ({ devices }) => {
|
||||
const sortedDevices = getSortedDevices(devices);
|
||||
const sortedDevices = getFilteredSortedDevices(devices);
|
||||
|
||||
return <ol className='mx_FilteredDeviceList'>
|
||||
{ sortedDevices.map((device) =>
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
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 { DeviceWithVerification } from "./useOwnDevices";
|
||||
|
||||
export enum DeviceSecurityVariation {
|
||||
Verified = 'Verified',
|
||||
Unverified = 'Unverified',
|
||||
Inactive = 'Inactive',
|
||||
}
|
||||
|
||||
type DeviceFilterCondition = (device: DeviceWithVerification) => boolean;
|
||||
|
||||
export const INACTIVE_DEVICE_AGE_MS = 7.776e+9; // 90 days
|
||||
|
||||
export const isDeviceInactive: DeviceFilterCondition = device =>
|
||||
!!device.last_seen_ts && device.last_seen_ts < Date.now() - INACTIVE_DEVICE_AGE_MS;
|
||||
|
||||
const filters: Record<DeviceSecurityVariation, DeviceFilterCondition> = {
|
||||
[DeviceSecurityVariation.Verified]: device => !!device.isVerified,
|
||||
[DeviceSecurityVariation.Unverified]: device => !device.isVerified,
|
||||
[DeviceSecurityVariation.Inactive]: isDeviceInactive,
|
||||
};
|
||||
|
||||
export const filterDevicesBySecurityRecommendation = (
|
||||
devices: DeviceWithVerification[],
|
||||
securityVariations: DeviceSecurityVariation[],
|
||||
) => {
|
||||
const activeFilters = securityVariations.map(variation => filters[variation]);
|
||||
if (!activeFilters.length) {
|
||||
return devices;
|
||||
}
|
||||
return devices.filter(device => activeFilters.every(filter => filter(device)));
|
||||
};
|
|
@ -20,10 +20,11 @@ import { _t } from "../../../../../languageHandler";
|
|||
import Spinner from '../../../elements/Spinner';
|
||||
import { useOwnDevices } from '../../devices/useOwnDevices';
|
||||
import DeviceTile from '../../devices/DeviceTile';
|
||||
import DeviceSecurityCard, { DeviceSecurityVariation } from '../../devices/DeviceSecurityCard';
|
||||
import DeviceSecurityCard from '../../devices/DeviceSecurityCard';
|
||||
import SettingsSubsection from '../../shared/SettingsSubsection';
|
||||
import SettingsTab from '../SettingsTab';
|
||||
import FilteredDeviceList from '../../devices/FilteredDeviceList';
|
||||
import { DeviceSecurityVariation } from '../../devices/filter';
|
||||
import SettingsTab from '../SettingsTab';
|
||||
|
||||
const SessionManagerTab: React.FC = () => {
|
||||
const { devices, currentDeviceId, isLoading } = useOwnDevices();
|
||||
|
|
|
@ -1703,6 +1703,7 @@
|
|||
"Device": "Device",
|
||||
"IP address": "IP address",
|
||||
"Session details": "Session details",
|
||||
"Inactive for %(inactiveAgeDays)s+ days": "Inactive for %(inactiveAgeDays)s+ days",
|
||||
"Verified": "Verified",
|
||||
"Unverified": "Unverified",
|
||||
"Unable to remove contact information": "Unable to remove contact information",
|
||||
|
|
|
@ -17,9 +17,8 @@ limitations under the License.
|
|||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import DeviceSecurityCard, {
|
||||
DeviceSecurityVariation,
|
||||
} from '../../../../../src/components/views/settings/devices/DeviceSecurityCard';
|
||||
import DeviceSecurityCard from '../../../../../src/components/views/settings/devices/DeviceSecurityCard';
|
||||
import { DeviceSecurityVariation } from '../../../../../src/components/views/settings/devices/filter';
|
||||
|
||||
describe('<DeviceSecurityCard />', () => {
|
||||
const defaultProps = {
|
||||
|
|
|
@ -109,5 +109,18 @@ describe('<DeviceTile />', () => {
|
|||
const { getByTestId } = render(getComponent({ device }));
|
||||
expect(getByTestId('device-metadata-lastActivity').textContent).toEqual('Last activity Dec 29, 2021');
|
||||
});
|
||||
|
||||
it('renders with inactive notice when last activity was more than 90 days ago', () => {
|
||||
const device: IMyDevice = {
|
||||
device_id: '123',
|
||||
last_seen_ip: '1.2.3.4',
|
||||
last_seen_ts: now - (MS_DAY * 100),
|
||||
};
|
||||
const { getByTestId, queryByTestId } = render(getComponent({ device }));
|
||||
expect(getByTestId('device-metadata-inactive').textContent).toEqual('Inactive for 90+ days (Dec 4, 2021)');
|
||||
// last activity and verification not shown when inactive
|
||||
expect(queryByTestId('device-metadata-lastActivity')).toBeFalsy();
|
||||
expect(queryByTestId('device-metadata-verificationStatus')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
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 {
|
||||
DeviceSecurityVariation,
|
||||
filterDevicesBySecurityRecommendation,
|
||||
} from "../../../../../src/components/views/settings/devices/filter";
|
||||
|
||||
const MS_DAY = 86400000;
|
||||
describe('filterDevicesBySecurityRecommendation()', () => {
|
||||
const unverifiedNoMetadata = { device_id: 'unverified-no-metadata', isVerified: false };
|
||||
const verifiedNoMetadata = { device_id: 'verified-no-metadata', isVerified: true };
|
||||
const hundredDaysOld = { device_id: '100-days-old', isVerified: true, last_seen_ts: Date.now() - (MS_DAY * 100) };
|
||||
const hundredDaysOldUnverified = {
|
||||
device_id: 'unverified-100-days-old',
|
||||
isVerified: false,
|
||||
last_seen_ts: Date.now() - (MS_DAY * 100),
|
||||
};
|
||||
const fiftyDaysOld = { device_id: '50-days-old', isVerified: true, last_seen_ts: Date.now() - (MS_DAY * 50) };
|
||||
|
||||
const devices = [
|
||||
unverifiedNoMetadata,
|
||||
verifiedNoMetadata,
|
||||
hundredDaysOld,
|
||||
hundredDaysOldUnverified,
|
||||
fiftyDaysOld,
|
||||
];
|
||||
|
||||
it('returns all devices when no securityRecommendations are passed', () => {
|
||||
expect(filterDevicesBySecurityRecommendation(devices, [])).toBe(devices);
|
||||
});
|
||||
|
||||
it('returns devices older than 90 days as inactive', () => {
|
||||
expect(filterDevicesBySecurityRecommendation(devices, [DeviceSecurityVariation.Inactive])).toEqual([
|
||||
// devices without ts metadata are not filtered as inactive
|
||||
hundredDaysOld,
|
||||
hundredDaysOldUnverified,
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns correct devices for verified filter', () => {
|
||||
expect(filterDevicesBySecurityRecommendation(devices, [DeviceSecurityVariation.Verified])).toEqual([
|
||||
verifiedNoMetadata,
|
||||
hundredDaysOld,
|
||||
fiftyDaysOld,
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns correct devices for unverified filter', () => {
|
||||
expect(filterDevicesBySecurityRecommendation(devices, [DeviceSecurityVariation.Unverified])).toEqual([
|
||||
unverifiedNoMetadata,
|
||||
hundredDaysOldUnverified,
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns correct devices for combined verified and inactive filters', () => {
|
||||
expect(filterDevicesBySecurityRecommendation(
|
||||
devices,
|
||||
[DeviceSecurityVariation.Unverified, DeviceSecurityVariation.Inactive],
|
||||
)).toEqual([
|
||||
hundredDaysOldUnverified,
|
||||
]);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue