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

[NEW][ENTERPRISE] Device Management #25791

Merged
merged 115 commits into from
Jul 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
115 commits
Select commit Hold shift + click to select a range
f46f5e4
add device management permissions (#25419)
yash-rajpal May 6, 2022
56df9a4
Chore: Store login token on session collection (#25054)
albuquerquefabio May 15, 2022
0b2fed2
Merge branch 'develop' into new/device-management
yash-rajpal May 17, 2022
1cf533c
Merge branch 'develop' into new/device-management
albuquerquefabio May 18, 2022
6d4c4a9
Merge branch 'develop' into new/device-management
debdutdeb May 24, 2022
c882bb7
Merge branch 'new/device-management' of github.com:RocketChat/Rocket.…
yash-rajpal May 25, 2022
0cb20c8
[NEW] Session endpoints (#25232)
albuquerquefabio Jun 1, 2022
0179e1b
Squashed commit of the following:
albuquerquefabio Jun 2, 2022
7814649
fix conflicts
yash-rajpal Jun 6, 2022
7dcc483
[FIX] Session with loginToken not being saved after login
albuquerquefabio Jun 7, 2022
3ff4caf
merge develop, fix conflicts
yash-rajpal Jun 8, 2022
ef83ca0
oops
yash-rajpal Jun 8, 2022
85a26cd
Listen session login
albuquerquefabio Jun 8, 2022
4f434ae
Fix import order
albuquerquefabio Jun 8, 2022
5af8802
merge develop, fix conflicts
yash-rajpal Jun 14, 2022
4911a08
Merge branch 'develop' into new/device-management
yash-rajpal Jun 16, 2022
11ef4da
Settings Migration to add a css email style
albuquerquefabio Jun 21, 2022
33aea75
Squashed commit of the following:
albuquerquefabio Jun 21, 2022
602d107
Merge branch 'develop' into new/device-management
yash-rajpal Jun 21, 2022
e884589
Fix prevent device management migrations
albuquerquefabio Jun 21, 2022
6c349e1
Device Management email template
albuquerquefabio Jun 21, 2022
48408ab
Update en.i18n.json
albuquerquefabio Jun 21, 2022
f67c505
Merge branch 'new/device-management' into new/login-send-notification
albuquerquefabio Jun 21, 2022
dc554e8
Regression: Send param logoutBy when admin logout an user (#25833)
albuquerquefabio Jun 22, 2022
cfdcecb
[NEW] [Device Management] Manage Devices Admin Table (#25773)
yash-rajpal Jun 22, 2022
0cb30a0
[NEW] [Device Management] Logged out banner on Login Form (#25849)
yash-rajpal Jun 23, 2022
60f9c4b
[NEW] [Device Management] User Preferences Table (#25904)
yash-rajpal Jun 24, 2022
8c7dd6d
Chore: MDM Session sort by loginAt (#25997)
albuquerquefabio Jun 27, 2022
0d160a8
Build a search term
albuquerquefabio Jun 27, 2022
16cf82e
Merge branch 'new/device-management' into chore/device-management-search
albuquerquefabio Jun 27, 2022
23a57e9
Remove duplicate code
albuquerquefabio Jun 27, 2022
953c037
New service for device management events
albuquerquefabio Jun 28, 2022
913927a
Device Management service
albuquerquefabio Jun 28, 2022
b6ed272
Update tsconfig.json
albuquerquefabio Jun 28, 2022
a23134f
[NEW] Modal Management (#25995)
albuquerquefabio Jun 28, 2022
5a70dc6
Merge branch 'develop' into new/device-management
debdutdeb Jun 28, 2022
7ff916e
Chore: Move Modal* model references to models package
debdutdeb Jun 28, 2022
317752a
Chore: Fix more develop merge issues
debdutdeb Jun 28, 2022
42eeae1
Merge branch 'new/device-management' into new/login-send-notification
debdutdeb Jun 28, 2022
bfbe832
Merge remote-tracking branch 'origin' into new/device-management
debdutdeb Jun 28, 2022
fb8a11c
Merge
albuquerquefabio Jun 28, 2022
a22dfff
Change suggested texts for MDM (#26020)
csuadev Jun 28, 2022
3a33cf5
fix lint
yash-rajpal Jun 28, 2022
ae62d41
Regrassion
albuquerquefabio Jun 28, 2022
c9c2306
Merge branch 'new/device-management' into new/login-send-notification
albuquerquefabio Jun 28, 2022
1976e66
Remove unncessary definitions
albuquerquefabio Jun 28, 2022
4acf8ad
Fix ISessionModel
albuquerquefabio Jun 28, 2022
8a4e9cb
Fix some imports and types
albuquerquefabio Jun 28, 2022
17beb04
Fix some lint issues
albuquerquefabio Jun 28, 2022
d4def4b
[NEW] Device management feature modal (#26006)
csuadev Jun 28, 2022
95ef420
fix conflicts and merge dev
yash-rajpal Jun 28, 2022
c8f3ef6
update mdm migrations
yash-rajpal Jun 28, 2022
9903dba
Changes requested
albuquerquefabio Jun 28, 2022
8c61fdc
ahhh
yash-rajpal Jun 28, 2022
9de8138
Merge branch 'new/device-management' into chore/device-management-search
albuquerquefabio Jun 29, 2022
77400c5
hidde searchTerm when undefined
albuquerquefabio Jun 29, 2022
3110cb3
fix conflict: update migration
yash-rajpal Jun 29, 2022
016256b
'IUser' is declared but its value is never read
albuquerquefabio Jun 30, 2022
cd9e84c
Merge branch 'develop' into new/device-management
albuquerquefabio Jun 30, 2022
201ce7b
Merge remote-tracking branch 'origin/develop' into new/device-management
ggazzo Jun 30, 2022
2dc6ba8
remove meteor startup
yash-rajpal Jun 30, 2022
add14f0
Chore: session search by regex
albuquerquefabio Jun 30, 2022
99b1312
Merge branch 'chore/device-management-search' into new/device-management
sampaiodiego Jun 30, 2022
a41942a
Chore migration email style
albuquerquefabio Jun 29, 2022
53e4deb
Merge branch 'new/login-send-notification' into new/device-management
sampaiodiego Jun 30, 2022
9d9e135
Changes requested
albuquerquefabio Jun 30, 2022
cd9b5df
Validate sort keys
albuquerquefabio Jun 30, 2022
970683b
Merge remote-tracking branch 'origin/develop' into new/device-management
sampaiodiego Jun 30, 2022
62fe99a
Fix some types
albuquerquefabio Jun 30, 2022
eb84dfd
Update getClientAddress.ts
albuquerquefabio Jun 30, 2022
0624591
add docs link to mdm feature modal
yash-rajpal Jun 30, 2022
3ed86de
Fix modal types
albuquerquefabio Jun 30, 2022
2ddabe9
TOTP undefined headers fix
yash-rajpal Jun 30, 2022
3304af3
Merge branch 'new/device-management' of https://github.com/RocketChat…
sampaiodiego Jun 30, 2022
3f22002
Remove Modal models
sampaiodiego Jun 30, 2022
50dd0a8
Remove modal APIs
sampaiodiego Jun 30, 2022
cc2782d
use banners to display mdm modal
sampaiodiego Jun 30, 2022
8456718
remove modal
sampaiodiego Jun 30, 2022
9628a3d
escape filter
sampaiodiego Jun 30, 2022
f176cfa
additional fixes
sampaiodiego Jun 30, 2022
23eeb62
set lastActivity on logout
sampaiodiego Jun 30, 2022
4808e9a
Merge branch 'new/device-management' of https://github.com/RocketChat…
albuquerquefabio Jun 30, 2022
5d6b51e
Partial index with background task
albuquerquefabio Jun 30, 2022
f1583b9
remove missing modal files
sampaiodiego Jul 1, 2022
fd7827e
fix user.autocomplete types
sampaiodiego Jul 1, 2022
f16baf0
adjust typings
sampaiodiego Jul 1, 2022
e98bec1
missing changes
sampaiodiego Jul 1, 2022
dc1e0b7
pick session
sampaiodiego Jul 1, 2022
a62b245
Merge branch 'new/device-management' of https://github.com/RocketChat…
sampaiodiego Jul 1, 2022
5023783
Merge remote-tracking branch 'origin/develop' into new/device-management
sampaiodiego Jul 1, 2022
e5dcbb8
Merge branch 'develop' into new/device-management
albuquerquefabio Jul 1, 2022
79fbc29
remove api-client headers fix
yash-rajpal Jul 1, 2022
bb445d3
Chore: Room access validation may be called without user information …
pierre-lehnen-rc Jul 1, 2022
626e0ab
[FIX] Unable to close chats when comments is disabled (#26057)
murtaza98 Jul 1, 2022
c35c393
Regression: Unhandled Exceptions metric causing a secondary exception…
pierre-lehnen-rc Jul 1, 2022
a7b0083
Chore: Allow endpoints to optionally require authentication (#26084)
pierre-lehnen-rc Jul 1, 2022
b18ca65
Merge branch 'develop' of github.com:RocketChat/Rocket.Chat into new/…
tassoevan Jul 1, 2022
31b0e15
Adjust indent
tassoevan Jul 1, 2022
8715ab7
Promise.await increase setting
sampaiodiego Jul 1, 2022
01e794b
try/catch index removal
sampaiodiego Jul 1, 2022
7b9b099
Merge remote-tracking branch 'origin/develop' into new/device-management
sampaiodiego Jul 1, 2022
62937b6
don't touch it
sampaiodiego Jul 1, 2022
956f707
validate sort with .some
albuquerquefabio Jul 1, 2022
234d935
sessions.ts
albuquerquefabio Jul 1, 2022
41e5b06
Update en.i18n.json
albuquerquefabio Jul 1, 2022
f87a62c
sort keys validations
albuquerquefabio Jul 1, 2022
0b02b47
Merge remote-tracking branch 'origin/develop' into new/device-management
pierre-lehnen-rc Jul 2, 2022
a3dd844
Removed unnecessary dependency
pierre-lehnen-rc Jul 2, 2022
f7b9b5e
Merge branch 'develop' into new/device-management
albuquerquefabio Jul 2, 2022
2d942bf
Merge remote-tracking branch 'origin/develop' into new/device-management
sampaiodiego Jul 2, 2022
180fff1
add fallbacks
sampaiodiego Jul 2, 2022
fb2bf70
Merge remote-tracking branch 'origin/develop' into new/device-management
sampaiodiego Jul 2, 2022
f5702c2
Fixed auto complete typing
pierre-lehnen-rc Jul 2, 2022
298dd37
Merge remote-tracking branch 'origin/develop' into new/device-management
pierre-lehnen-rc Jul 2, 2022
cedede3
undo autocomplete changes
sampaiodiego Jul 2, 2022
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
5 changes: 3 additions & 2 deletions apps/meteor/app/api/server/lib/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Users } from '@rocket.chat/models';

import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';

type UserAutoComplete = Required<Pick<IUser, '_id' | 'name' | 'username' | 'nickname' | 'status' | 'avatarETag'>>;
export async function findUsersToAutocomplete({
uid,
selector,
Expand All @@ -16,7 +17,7 @@ export async function findUsersToAutocomplete({
term: string;
};
}): Promise<{
items: Required<Pick<IUser, '_id' | 'name' | 'username' | 'nickname' | 'status' | 'avatarETag'>>[];
items: UserAutoComplete[];
}> {
if (!(await hasPermissionAsync(uid, 'view-outside-room'))) {
return { items: [] };
Expand All @@ -37,7 +38,7 @@ export async function findUsersToAutocomplete({
limit: 10,
};

const users = await Users.findActiveByUsernameOrNameRegexWithExceptionsAndConditions(
const users = await Users.findActiveByUsernameOrNameRegexWithExceptionsAndConditions<UserAutoComplete>(
new RegExp(escapeRegExp(selector.term), 'i'),
exceptions,
conditions,
Expand Down
3 changes: 3 additions & 0 deletions apps/meteor/app/lib/server/startup/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ settingsRegistry.addGroup('Email', function () {
}
.social {
font-size: 12px
}
.rc-color {
color: #F5455C;
}
`,
{
Expand Down
1 change: 0 additions & 1 deletion apps/meteor/app/models/server/models/Users.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ export class Users extends Base {
this.tryEnsureIndex({ statusLivechat: 1 }, { sparse: true });
this.tryEnsureIndex({ extension: 1 }, { sparse: true, unique: true });
this.tryEnsureIndex({ language: 1 }, { sparse: true });

this.tryEnsureIndex({ 'active': 1, 'services.email2fa.enabled': 1 }, { sparse: true }); // used by statistics
this.tryEnsureIndex({ 'active': 1, 'services.totp.enabled': 1 }, { sparse: true }); // used by statistics

Expand Down
58 changes: 36 additions & 22 deletions apps/meteor/app/statistics/server/lib/SAUMonitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import { Meteor } from 'meteor/meteor';
import { SyncedCron } from 'meteor/littledata:synced-cron';
import UAParser from 'ua-parser-js';
import mem from 'mem';
import type { ISession, ISessionDevice, ISocketConnection, IUser } from '@rocket.chat/core-typings';
import type { ISession, ISessionDevice, ISocketConnectionLogged, IUser } from '@rocket.chat/core-typings';
import { Sessions, Users } from '@rocket.chat/models';

import { UAParserMobile, UAParserDesktop } from './UAParserCustom';
import { aggregates } from '../../../../server/models/raw/Sessions';
import { Logger } from '../../../../server/lib/logger/Logger';
import { getMostImportantRole } from '../../../../lib/roles/getMostImportantRole';
import { sauEvents } from '../../../../server/services/sauMonitor/events';
import { getClientAddress } from '../../../../server/lib/getClientAddress';

type DateObj = { day: number; month: number; year: number };

Expand Down Expand Up @@ -121,20 +122,25 @@ export class SAUMonitorClass {
if (!this.isRunning()) {
return;
}
const { id: sessionId } = connection;

await Sessions.logoutByInstanceIdAndSessionIdAndUserId(connection.instanceId, connection.id, userId);
await Sessions.logoutBySessionIdAndUserId({ sessionId, userId });
sampaiodiego marked this conversation as resolved.
Show resolved Hide resolved
});
}

private async _handleSession(
connection: ISocketConnection,
connection: ISocketConnectionLogged,
params: Pick<ISession, 'userId' | 'mostImportantRole' | 'loginAt' | 'day' | 'month' | 'year' | 'roles'>,
): Promise<void> {
const data = this._getConnectionInfo(connection, params);

if (!data) {
return;
}
await Sessions.createOrUpdate(data);

const searchTerm = this._getSearchTerm(data);

await Sessions.insertOne({ ...data, searchTerm, createdAt: new Date() });
}

private async _finishSessionsFromDate(yesterday: Date, today: Date): Promise<void> {
Expand Down Expand Up @@ -181,30 +187,37 @@ export class SAUMonitorClass {
// TODO missing an action to perform on dangling sessions (for example remove sessions not closed one month ago)
}

private _getSearchTerm(session: Omit<ISession, '_id' | '_updatedAt' | 'createdAt' | 'searchTerm'>): string {
return [session.device?.name, session.device?.type, session.device?.os.name, session.sessionId, session.userId]
.filter(Boolean)
.join('');
}

private _getConnectionInfo(
connection: ISocketConnection,
connection: ISocketConnectionLogged,
params: Pick<ISession, 'userId' | 'mostImportantRole' | 'loginAt' | 'day' | 'month' | 'year' | 'roles'>,
): Omit<ISession, '_id' | '_updatedAt' | 'createdAt'> | undefined {
): Omit<ISession, '_id' | '_updatedAt' | 'createdAt' | 'searchTerm'> | undefined {
if (!connection) {
return;
}

const ip = connection.clientAddress || connection.httpHeaders?.['x-real-ip'] || connection.httpHeaders?.['x-forwarded-for'];
const ip = getClientAddress(connection);

const host = connection.httpHeaders?.host || '';
const host = connection.httpHeaders?.host ?? '';

return {
type: 'session',
sessionId: connection.id,
instanceId: connection.instanceId,
ip: (Array.isArray(ip) ? ip[0] : ip) || '',
...(connection.loginToken && { loginToken: connection.loginToken }),
ip,
host,
...this._getUserAgentInfo(connection),
...params,
};
}

private _getUserAgentInfo(connection: ISocketConnection): { device: ISessionDevice } | undefined {
private _getUserAgentInfo(connection: ISocketConnectionLogged): { device: ISessionDevice } | undefined {
if (!connection?.httpHeaders?.['user-agent']) {
return;
}
Expand Down Expand Up @@ -315,13 +328,6 @@ export class SAUMonitorClass {
date.setDate(date.getDate() - 0); // yesterday
const yesterday = getDateObj(date);

const match = {
type: 'session',
year: { $lte: yesterday.year },
month: { $lte: yesterday.month },
day: { $lte: yesterday.day },
};

for await (const record of aggregates.dailySessionsOfYesterday(Sessions.col, yesterday)) {
await Sessions.updateOne(
{ _id: `${record.userId}-${record.year}-${record.month}-${record.day}` },
Expand All @@ -330,11 +336,19 @@ export class SAUMonitorClass {
);
}

await Sessions.updateMany(match, {
$set: {
type: 'computed-session',
_computedAt: new Date(),
await Sessions.updateMany(
{
type: 'session',
year: { $lte: yesterday.year },
month: { $lte: yesterday.month },
day: { $lte: yesterday.day },
},
});
{
$set: {
type: 'computed-session',
_computedAt: new Date(),
},
},
);
}
}
1 change: 1 addition & 0 deletions apps/meteor/app/ui-login/client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ import './username/username.html';
import './login/form';
import './login/services';
import './username/username';
import './login/startup';
3 changes: 3 additions & 0 deletions apps/meteor/app/ui-login/client/login/layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
<section class="rc-old full-page color-tertiary-font-color" style="{{#with backgroundUrl}}background-image: url('{{.}}'){{/with}}">
<div class="wrapper">
{{> loginLayoutHeader}}
{{#if showForcedLogoutBanner}}
{{> loggedOutBanner}}
{{/if}}
{{> loginForm}}
{{> loginLayoutFooter}}
</div>
Expand Down
4 changes: 4 additions & 0 deletions apps/meteor/app/ui-login/client/login/layout.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Session } from 'meteor/session';
import { Template } from 'meteor/templating';

import { settings } from '../../../settings';
Expand All @@ -10,4 +11,7 @@ Template.loginLayout.helpers({
return `${prefix}/${asset.url || asset.defaultUrl}`;
}
},
showForcedLogoutBanner() {
return Session.get('force_logout');
},
});
19 changes: 19 additions & 0 deletions apps/meteor/app/ui-login/client/login/startup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Meteor } from 'meteor/meteor';
import { Session } from 'meteor/session';
import { Tracker } from 'meteor/tracker';

import { Notifications } from '../../../notifications/client';

Meteor.startup(() => {
Tracker.autorun(() => {
const userId = Meteor.userId();

if (!userId) {
return;
}

Notifications.onUser('force_logout', () => {
Session.set('force_logout', true);
});
});
});
2 changes: 1 addition & 1 deletion apps/meteor/app/utils/client/lib/RestApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ APIClient.use(async function (request, next) {
return resolve(
next(request[0], request[1], {
...request[2],
headers: { ...request[2].headers, 'x-2fa-code': code, 'x-2fa-method': method },
headers: { ...request[2]?.headers, 'x-2fa-code': code, 'x-2fa-method': method },
}),
);
},
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/client/components/GenericModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ const GenericModal: FC<GenericModalProps> = ({
<Modal.Header>
{renderIcon(icon, variant)}
<Modal.Title>{title ?? t('Are_you_sure')}</Modal.Title>
<Modal.Close onClick={onClose} />
<Modal.Close title={t('Close')} onClick={onClose} />
</Modal.Header>
<Modal.Content fontScale='p2'>{children}</Modal.Content>
<Modal.Footer>
Expand Down
14 changes: 14 additions & 0 deletions apps/meteor/client/startup/banners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,28 @@ import { Tracker } from 'meteor/tracker';

import { Notifications } from '../../app/notifications/client';
import { APIClient } from '../../app/utils/client';
import DeviceManagementFeatureModal from '../../ee/client/deviceManagement/components/featureModal/DeviceManagementFeatureModal';
import * as banners from '../lib/banners';
import { imperativeModal } from '../lib/imperativeModal';

const fetchInitialBanners = async (): Promise<void> => {
const response = await APIClient.get('/v1/banners', {
platform: BannerPlatform.Web,
});

for (const banner of response.banners) {
if (banner._id === 'device-management') {
setTimeout(() => {
imperativeModal.open({
component: DeviceManagementFeatureModal,
props: {
close: imperativeModal.close,
},
});
}, 2000);
continue;
}

banners.open({
...banner.view,
viewId: banner.view.viewId || banner._id,
Expand Down
4 changes: 4 additions & 0 deletions apps/meteor/client/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,7 @@ createTemplateForComponent('roomNotFound', () => import('./views/room/Room/RoomN
createTemplateForComponent('ComposerNotAvailablePhoneCalls', () => import('./components/voip/composer/NotAvailableOnCall'), {
renderContainerView: () => HTML.DIV({ style: 'display: flex; height: 100%; width: 100%' }),
});

createTemplateForComponent('loggedOutBanner', () => import('../ee/client/components/deviceManagement/LoggedOutBanner'), {
renderContainerView: () => HTML.DIV({ style: 'max-width: 520px; margin: 0 auto;' }),
});
2 changes: 2 additions & 0 deletions apps/meteor/ee/app/license/server/bundles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type BundleFeature =
| 'scalability'
| 'teams-mention'
| 'saml-enterprise'
| 'device-management'
| 'oauth-enterprise'
| 'federation'
| 'videoconference-enterprise';
Expand All @@ -32,6 +33,7 @@ const bundles: IBundle = {
'teams-mention',
'saml-enterprise',
'oauth-enterprise',
'device-management',
'federation',
'videoconference-enterprise',
],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Banner, Box, Icon } from '@rocket.chat/fuselage';
import { useTranslation } from '@rocket.chat/ui-contexts';
import React, { ReactElement } from 'react';

const LoggedOutBanner = (): ReactElement => {
const t = useTranslation();

return (
<Banner variant='warning' icon={<Icon name='warning' size={24} />}>
<Box textAlign='left'>{t('Logged_Out_Banner_Text')}</Box>
</Banner>
);
};

export default LoggedOutBanner;
25 changes: 25 additions & 0 deletions apps/meteor/ee/client/deviceManagement/components/DeviceIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Box, Icon } from '@rocket.chat/fuselage';
import React, { ComponentProps, ReactElement } from 'react';

const iconMap: Record<string, ComponentProps<typeof Icon>['name']> = {
browser: 'desktop',
mobile: 'mobile',
};

const DeviceIcon = ({ deviceType }: { deviceType: string }): ReactElement => (
<Box
is='span'
display='inline-flex'
alignItems='center'
justifyContent='center'
p='x4'
bg='neutral-500-50'
size='x24'
borderRadius='full'
mie='x8'
>
<Icon name={iconMap[deviceType]} size='x14' color='info' />
</Box>
);

export default DeviceIcon;
Loading