Files
catalog/ts_web/views/eco-view-saasshare/eco-view-saasshare.ts

1289 lines
36 KiB
TypeScript
Raw Normal View History

2026-01-12 10:57:54 +00:00
import {
customElement,
DeesElement,
type TemplateResult,
html,
property,
css,
cssManager,
state,
} from '@design.estate/dees-element';
import { DeesAppuiSecondarymenu, DeesIcon } from '@design.estate/dees-catalog';
import type { ISecondaryMenuGroup, ISecondaryMenuItem } from '../../elements/interfaces/secondarymenu.js';
import { demo } from './eco-view-saasshare.demo.js';
// Ensure components are registered
DeesAppuiSecondarymenu;
DeesIcon;
declare global {
interface HTMLElementTagNameMap {
'eco-view-saasshare': EcoViewSaasshare;
}
}
export type TSharePanel =
| 'apps'
| 'devices'
| 'permissions'
| 'requests'
| 'activity'
| 'security';
export type TPermissionType =
| 'print'
| 'scan'
| 'storage'
| 'camera'
| 'audio'
| 'display'
| 'network';
export interface ISaasApp {
id: string;
name: string;
domain: string;
icon?: string;
color?: string;
verified: boolean;
lastAccess?: Date;
permissions: ISaasPermission[];
}
export interface ISaasPermission {
type: TPermissionType;
deviceId?: string;
deviceName?: string;
granted: boolean;
grantedAt?: Date;
expiresAt?: Date;
}
export interface IAccessRequest {
id: string;
appId: string;
appName: string;
appDomain: string;
permissionType: TPermissionType;
deviceId?: string;
deviceName?: string;
requestedAt: Date;
status: 'pending' | 'approved' | 'denied';
}
export interface IAccessActivity {
id: string;
appId: string;
appName: string;
permissionType: TPermissionType;
deviceName?: string;
action: string;
timestamp: Date;
}
@customElement('eco-view-saasshare')
export class EcoViewSaasshare extends DeesElement {
public static demo = demo;
public static demoGroup = 'Views';
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
width: 100%;
height: 100%;
background: ${cssManager.bdTheme('#f5f5f7', 'hsl(240 6% 10%)')};
color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')};
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.share-container {
display: flex;
height: 100%;
}
dees-appui-secondarymenu {
flex-shrink: 0;
background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 8%)')};
border-right: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 15%)')};
}
.content {
flex: 1;
overflow-y: auto;
padding: 32px 48px;
}
.panel-header {
margin-bottom: 32px;
display: flex;
align-items: flex-start;
justify-content: space-between;
}
.panel-header-left {
flex: 1;
}
.panel-title {
font-size: 28px;
font-weight: 600;
color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')};
margin-bottom: 8px;
}
.panel-description {
font-size: 14px;
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
}
.header-action {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
background: hsl(217 91% 60%);
color: white;
border: none;
border-radius: 10px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background 0.15s ease;
}
.header-action:hover {
background: hsl(217 91% 55%);
}
.section {
background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 12%)')};
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 18%)')};
border-radius: 12px;
margin-bottom: 24px;
overflow: hidden;
}
.section-title {
padding: 16px 20px 12px;
font-size: 13px;
font-weight: 600;
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 50%)')};
text-transform: uppercase;
letter-spacing: 0.5px;
display: flex;
align-items: center;
justify-content: space-between;
}
.section-count {
background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 20%)')};
padding: 2px 8px;
border-radius: 10px;
font-size: 12px;
font-weight: 600;
}
.app-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 15%)')};
transition: background 0.15s ease;
cursor: pointer;
}
.app-item:first-child {
border-top: none;
}
.app-item:hover {
background: ${cssManager.bdTheme('hsl(0 0% 97%)', 'hsl(240 5% 14%)')};
}
.app-left {
display: flex;
align-items: center;
gap: 14px;
}
.app-icon {
width: 44px;
height: 44px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 20px;
font-weight: 600;
}
.app-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.app-name {
display: flex;
align-items: center;
gap: 8px;
font-size: 15px;
font-weight: 500;
color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')};
}
.verified-badge {
display: flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
background: hsl(142 71% 45% / 0.15);
color: hsl(142 71% 40%);
border-radius: 4px;
font-size: 11px;
font-weight: 600;
}
.app-domain {
font-size: 13px;
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')};
}
.app-right {
display: flex;
align-items: center;
gap: 16px;
}
.permission-badges {
display: flex;
gap: 6px;
}
.permission-badge {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 6px;
background: ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 18%)')};
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
}
.permission-badge.active {
background: hsl(217 91% 60% / 0.15);
color: hsl(217 91% 55%);
}
.last-access {
font-size: 12px;
color: ${cssManager.bdTheme('hsl(0 0% 60%)', 'hsl(0 0% 50%)')};
min-width: 100px;
text-align: right;
}
.request-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 15%)')};
background: ${cssManager.bdTheme('hsl(45 100% 97%)', 'hsl(45 30% 12%)')};
}
.request-item:first-child {
border-top: none;
}
.request-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.request-title {
font-size: 14px;
font-weight: 500;
color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')};
}
.request-detail {
font-size: 13px;
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')};
}
.request-actions {
display: flex;
gap: 8px;
}
.btn {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
border: none;
border-radius: 8px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
}
.btn-approve {
background: hsl(142 71% 45%);
color: white;
}
.btn-approve:hover {
background: hsl(142 71% 40%);
}
.btn-deny {
background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 20%)')};
color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 70%)')};
}
.btn-deny:hover {
background: hsl(0 72% 51%);
color: white;
}
.device-group {
margin-bottom: 24px;
}
.device-header {
display: flex;
align-items: center;
gap: 12px;
padding: 16px 20px;
background: ${cssManager.bdTheme('hsl(0 0% 97%)', 'hsl(240 5% 14%)')};
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 18%)')};
}
.device-icon {
width: 36px;
height: 36px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 20%)')};
color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 70%)')};
}
.device-info h4 {
margin: 0;
font-size: 15px;
font-weight: 500;
color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')};
}
.device-info span {
font-size: 13px;
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')};
}
.device-apps {
padding: 0;
}
.device-app {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 20px 12px 68px;
border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 15%)')};
}
.device-app:first-child {
border-top: none;
}
.device-app-name {
font-size: 14px;
color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 80%)')};
}
.toggle-switch {
position: relative;
width: 44px;
height: 24px;
background: ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(240 5% 25%)')};
border-radius: 12px;
cursor: pointer;
transition: background 0.2s ease;
}
.toggle-switch.active {
background: hsl(217 91% 60%);
}
.toggle-switch::after {
content: '';
position: absolute;
top: 2px;
left: 2px;
width: 20px;
height: 20px;
background: white;
border-radius: 50%;
transition: transform 0.2s ease;
box-shadow: ${cssManager.bdTheme('0 1px 3px rgba(0,0,0,0.2)', 'none')};
}
.toggle-switch.active::after {
transform: translateX(20px);
}
.activity-item {
display: flex;
align-items: flex-start;
gap: 14px;
padding: 14px 20px;
border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 15%)')};
}
.activity-item:first-child {
border-top: none;
}
.activity-icon {
width: 32px;
height: 32px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
background: ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 18%)')};
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
flex-shrink: 0;
}
.activity-content {
flex: 1;
}
.activity-title {
font-size: 14px;
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
margin-bottom: 2px;
}
.activity-title strong {
font-weight: 600;
}
.activity-time {
font-size: 12px;
color: ${cssManager.bdTheme('hsl(0 0% 55%)', 'hsl(0 0% 50%)')};
}
.settings-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 20px;
border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 15%)')};
}
.settings-item:first-child {
border-top: none;
}
.item-left {
display: flex;
align-items: center;
gap: 14px;
}
.item-icon {
width: 32px;
height: 32px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: white;
}
.item-icon.blue { background: hsl(217 91% 60%); }
.item-icon.green { background: hsl(142 71% 45%); }
.item-icon.orange { background: hsl(25 95% 53%); }
.item-icon.red { background: hsl(0 72% 51%); }
.item-icon.purple { background: hsl(262 83% 58%); }
.item-icon.gray { background: hsl(220 9% 46%); }
.item-info {
display: flex;
flex-direction: column;
gap: 2px;
}
.item-label {
font-size: 15px;
font-weight: 500;
color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')};
}
.item-sublabel {
font-size: 13px;
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')};
}
.empty-state {
text-align: center;
padding: 48px 20px;
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')};
}
.empty-state dees-icon {
margin-bottom: 16px;
opacity: 0.5;
}
.empty-state h3 {
margin: 0 0 8px;
font-size: 16px;
font-weight: 500;
color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 70%)')};
}
.empty-state p {
margin: 0;
font-size: 14px;
}
.permission-type-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 20px;
border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 15%)')};
}
.permission-type-row:first-child {
border-top: none;
}
.permission-type-info {
display: flex;
align-items: center;
gap: 14px;
}
.permission-type-details h4 {
margin: 0;
font-size: 15px;
font-weight: 500;
color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')};
}
.permission-type-details span {
font-size: 13px;
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')};
}
.permission-apps-count {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
background: ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 18%)')};
border-radius: 6px;
font-size: 13px;
color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 65%)')};
cursor: pointer;
transition: background 0.15s ease;
}
.permission-apps-count:hover {
background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 22%)')};
}
`,
];
@property({ type: String })
accessor activePanel: TSharePanel = 'apps';
@state()
accessor saasApps: ISaasApp[] = [
{
id: 'google-docs',
name: 'Google Docs',
domain: 'docs.google.com',
color: '#4285F4',
verified: true,
lastAccess: new Date(Date.now() - 1000 * 60 * 5),
permissions: [
{ type: 'print', deviceName: 'HP LaserJet Pro', granted: true },
{ type: 'storage', deviceName: 'Synology NAS', granted: true },
],
},
{
id: 'figma',
name: 'Figma',
domain: 'figma.com',
color: '#F24E1E',
verified: true,
lastAccess: new Date(Date.now() - 1000 * 60 * 30),
permissions: [
{ type: 'display', granted: true },
],
},
{
id: 'zoom',
name: 'Zoom',
domain: 'zoom.us',
color: '#2D8CFF',
verified: true,
lastAccess: new Date(Date.now() - 1000 * 60 * 60 * 2),
permissions: [
{ type: 'camera', deviceName: 'Logitech C920', granted: true },
{ type: 'audio', deviceName: 'Built-in Microphone', granted: true },
{ type: 'display', granted: true },
],
},
{
id: 'notion',
name: 'Notion',
domain: 'notion.so',
color: '#000000',
verified: true,
lastAccess: new Date(Date.now() - 1000 * 60 * 60 * 24),
permissions: [
{ type: 'print', deviceName: 'HP LaserJet Pro', granted: true },
],
},
{
id: 'dropbox',
name: 'Dropbox',
domain: 'dropbox.com',
color: '#0061FF',
verified: true,
lastAccess: new Date(Date.now() - 1000 * 60 * 60 * 48),
permissions: [
{ type: 'storage', deviceName: 'Synology NAS', granted: true },
{ type: 'scan', deviceName: 'Epson Scanner', granted: true },
],
},
];
@state()
accessor accessRequests: IAccessRequest[] = [
{
id: 'req-1',
appId: 'slack',
appName: 'Slack',
appDomain: 'slack.com',
permissionType: 'camera',
deviceName: 'Logitech C920',
requestedAt: new Date(Date.now() - 1000 * 60 * 10),
status: 'pending',
},
{
id: 'req-2',
appId: 'canva',
appName: 'Canva',
appDomain: 'canva.com',
permissionType: 'print',
deviceName: 'HP LaserJet Pro',
requestedAt: new Date(Date.now() - 1000 * 60 * 25),
status: 'pending',
},
];
@state()
accessor activities: IAccessActivity[] = [
{
id: 'act-1',
appId: 'google-docs',
appName: 'Google Docs',
permissionType: 'print',
deviceName: 'HP LaserJet Pro',
action: 'printed document "Q4 Report.pdf"',
timestamp: new Date(Date.now() - 1000 * 60 * 5),
},
{
id: 'act-2',
appId: 'zoom',
appName: 'Zoom',
permissionType: 'camera',
deviceName: 'Logitech C920',
action: 'accessed camera for video call',
timestamp: new Date(Date.now() - 1000 * 60 * 60 * 2),
},
{
id: 'act-3',
appId: 'dropbox',
appName: 'Dropbox',
permissionType: 'scan',
deviceName: 'Epson Scanner',
action: 'scanned 3 pages',
timestamp: new Date(Date.now() - 1000 * 60 * 60 * 5),
},
{
id: 'act-4',
appId: 'figma',
appName: 'Figma',
permissionType: 'display',
action: 'shared screen to external display',
timestamp: new Date(Date.now() - 1000 * 60 * 60 * 24),
},
];
@state()
accessor requireApproval = true;
@state()
accessor autoRevokeInactive = true;
@state()
accessor activityLogging = true;
private getMenuGroups(): ISecondaryMenuGroup[] {
const pendingCount = this.accessRequests.filter(r => r.status === 'pending').length;
return [
{
name: 'Overview',
iconName: 'lucide:share2',
items: [
{
key: 'apps',
iconName: 'lucide:layoutGrid',
action: () => this.activePanel = 'apps',
badge: this.saasApps.length.toString(),
},
{
key: 'requests',
iconName: 'lucide:inbox',
action: () => this.activePanel = 'requests',
badge: pendingCount > 0 ? pendingCount.toString() : undefined,
},
],
},
{
name: 'Browse',
iconName: 'lucide:folder',
items: [
{
key: 'devices',
iconName: 'lucide:hardDrive',
action: () => this.activePanel = 'devices',
},
{
key: 'permissions',
iconName: 'lucide:key',
action: () => this.activePanel = 'permissions',
},
],
},
{
name: 'Monitor',
iconName: 'lucide:activity',
items: [
{
key: 'activity',
iconName: 'lucide:clock',
action: () => this.activePanel = 'activity',
},
{
key: 'security',
iconName: 'lucide:shield',
action: () => this.activePanel = 'security',
},
],
},
];
}
private getSelectedItem(): ISecondaryMenuItem | null {
for (const group of this.getMenuGroups()) {
for (const item of group.items) {
if ('key' in item && item.key === this.activePanel) {
return item;
}
}
}
return null;
}
public render(): TemplateResult {
return html`
<div class="share-container">
<dees-appui-secondarymenu
.heading=${'SaaS Share'}
.groups=${this.getMenuGroups()}
.selectedItem=${this.getSelectedItem()}
></dees-appui-secondarymenu>
<div class="content">
${this.renderActivePanel()}
</div>
</div>
`;
}
private renderActivePanel(): TemplateResult {
switch (this.activePanel) {
case 'apps':
return this.renderAppsPanel();
case 'devices':
return this.renderDevicesPanel();
case 'permissions':
return this.renderPermissionsPanel();
case 'requests':
return this.renderRequestsPanel();
case 'activity':
return this.renderActivityPanel();
case 'security':
return this.renderSecurityPanel();
default:
return this.renderAppsPanel();
}
}
private renderAppsPanel(): TemplateResult {
return html`
<div class="panel-header">
<div class="panel-header-left">
<div class="panel-title">Connected Apps</div>
<div class="panel-description">Manage SaaS applications with access to your peripherals</div>
</div>
<button class="header-action">
<dees-icon .icon=${'lucide:plus'} .iconSize=${16}></dees-icon>
Add App
</button>
</div>
${this.accessRequests.filter(r => r.status === 'pending').length > 0 ? html`
<div class="section">
<div class="section-title">
Pending Requests
<span class="section-count">${this.accessRequests.filter(r => r.status === 'pending').length}</span>
</div>
${this.accessRequests.filter(r => r.status === 'pending').map(request => html`
<div class="request-item">
<div class="request-info">
<div class="request-title">
<strong>${request.appName}</strong> wants access to <strong>${this.getPermissionLabel(request.permissionType)}</strong>
</div>
<div class="request-detail">
${request.deviceName ? `Device: ${request.deviceName}` : ''}
Requested ${this.formatTimeAgo(request.requestedAt)}
</div>
</div>
<div class="request-actions">
<button class="btn btn-approve" @click=${() => this.approveRequest(request.id)}>
<dees-icon .icon=${'lucide:check'} .iconSize=${14}></dees-icon>
Approve
</button>
<button class="btn btn-deny" @click=${() => this.denyRequest(request.id)}>
<dees-icon .icon=${'lucide:x'} .iconSize=${14}></dees-icon>
Deny
</button>
</div>
</div>
`)}
</div>
` : ''}
<div class="section">
<div class="section-title">
All Apps
<span class="section-count">${this.saasApps.length}</span>
</div>
${this.saasApps.map(app => html`
<div class="app-item">
<div class="app-left">
<div class="app-icon" style="background: ${app.color}">
${app.name.charAt(0)}
</div>
<div class="app-info">
<div class="app-name">
${app.name}
${app.verified ? html`
<span class="verified-badge">
<dees-icon .icon=${'lucide:badgeCheck'} .iconSize=${12}></dees-icon>
Verified
</span>
` : ''}
</div>
<div class="app-domain">${app.domain}</div>
</div>
</div>
<div class="app-right">
<div class="permission-badges">
${this.renderPermissionBadges(app.permissions)}
</div>
<div class="last-access">
${app.lastAccess ? this.formatTimeAgo(app.lastAccess) : 'Never'}
</div>
<dees-icon .icon=${'lucide:chevronRight'} .iconSize=${16}></dees-icon>
</div>
</div>
`)}
</div>
`;
}
private renderDevicesPanel(): TemplateResult {
const devices = [
{
name: 'HP LaserJet Pro',
type: 'print',
icon: 'lucide:printer',
apps: ['Google Docs', 'Notion'],
},
{
name: 'Epson Scanner',
type: 'scan',
icon: 'lucide:scan',
apps: ['Dropbox'],
},
{
name: 'Logitech C920',
type: 'camera',
icon: 'lucide:camera',
apps: ['Zoom'],
},
{
name: 'Built-in Microphone',
type: 'audio',
icon: 'lucide:mic',
apps: ['Zoom'],
},
{
name: 'Synology NAS',
type: 'storage',
icon: 'lucide:hardDrive',
apps: ['Google Docs', 'Dropbox'],
},
{
name: 'External Display',
type: 'display',
icon: 'lucide:monitor',
apps: ['Zoom', 'Figma'],
},
];
return html`
<div class="panel-header">
<div class="panel-header-left">
<div class="panel-title">Devices</div>
<div class="panel-description">View which apps have access to each peripheral</div>
</div>
</div>
${devices.map(device => html`
<div class="section device-group">
<div class="device-header">
<div class="device-icon">
<dees-icon .icon=${device.icon} .iconSize=${18}></dees-icon>
</div>
<div class="device-info">
<h4>${device.name}</h4>
<span>${device.apps.length} app${device.apps.length !== 1 ? 's' : ''} with access</span>
</div>
</div>
<div class="device-apps">
${device.apps.map(appName => html`
<div class="device-app">
<span class="device-app-name">${appName}</span>
<div class="toggle-switch active"></div>
</div>
`)}
</div>
</div>
`)}
`;
}
private renderPermissionsPanel(): TemplateResult {
const permissionTypes: { type: TPermissionType; icon: string; label: string; color: string; count: number }[] = [
{ type: 'print', icon: 'lucide:printer', label: 'Printing', color: 'blue', count: 2 },
{ type: 'scan', icon: 'lucide:scan', label: 'Scanning', color: 'purple', count: 1 },
{ type: 'camera', icon: 'lucide:camera', label: 'Camera', color: 'green', count: 1 },
{ type: 'audio', icon: 'lucide:mic', label: 'Microphone', color: 'red', count: 1 },
{ type: 'storage', icon: 'lucide:hardDrive', label: 'Network Storage', color: 'orange', count: 2 },
{ type: 'display', icon: 'lucide:monitor', label: 'Screen Sharing', color: 'gray', count: 2 },
];
return html`
<div class="panel-header">
<div class="panel-header-left">
<div class="panel-title">Permissions</div>
<div class="panel-description">Manage access by permission type</div>
</div>
</div>
<div class="section">
${permissionTypes.map(perm => html`
<div class="permission-type-row">
<div class="permission-type-info">
<div class="item-icon ${perm.color}">
<dees-icon .icon=${perm.icon} .iconSize=${18}></dees-icon>
</div>
<div class="permission-type-details">
<h4>${perm.label}</h4>
<span>${perm.count} app${perm.count !== 1 ? 's' : ''} with access</span>
</div>
</div>
<div class="permission-apps-count">
Manage
<dees-icon .icon=${'lucide:chevronRight'} .iconSize=${14}></dees-icon>
</div>
</div>
`)}
</div>
`;
}
private renderRequestsPanel(): TemplateResult {
const pendingRequests = this.accessRequests.filter(r => r.status === 'pending');
return html`
<div class="panel-header">
<div class="panel-header-left">
<div class="panel-title">Access Requests</div>
<div class="panel-description">Review and manage permission requests from SaaS applications</div>
</div>
</div>
${pendingRequests.length > 0 ? html`
<div class="section">
<div class="section-title">
Pending
<span class="section-count">${pendingRequests.length}</span>
</div>
${pendingRequests.map(request => html`
<div class="request-item">
<div class="request-info">
<div class="request-title">
<strong>${request.appName}</strong> wants access to <strong>${this.getPermissionLabel(request.permissionType)}</strong>
</div>
<div class="request-detail">
${request.deviceName ? `Device: ${request.deviceName}` : ''}
${request.appDomain} Requested ${this.formatTimeAgo(request.requestedAt)}
</div>
</div>
<div class="request-actions">
<button class="btn btn-approve" @click=${() => this.approveRequest(request.id)}>
<dees-icon .icon=${'lucide:check'} .iconSize=${14}></dees-icon>
Approve
</button>
<button class="btn btn-deny" @click=${() => this.denyRequest(request.id)}>
<dees-icon .icon=${'lucide:x'} .iconSize=${14}></dees-icon>
Deny
</button>
</div>
</div>
`)}
</div>
` : html`
<div class="section">
<div class="empty-state">
<dees-icon .icon=${'lucide:inbox'} .iconSize=${48}></dees-icon>
<h3>No pending requests</h3>
<p>When SaaS apps request access to your peripherals, they'll appear here</p>
</div>
</div>
`}
`;
}
private renderActivityPanel(): TemplateResult {
return html`
<div class="panel-header">
<div class="panel-header-left">
<div class="panel-title">Activity Log</div>
<div class="panel-description">Recent peripheral access by SaaS applications</div>
</div>
</div>
<div class="section">
<div class="section-title">Recent Activity</div>
${this.activities.map(activity => html`
<div class="activity-item">
<div class="activity-icon">
<dees-icon .icon=${this.getPermissionIcon(activity.permissionType)} .iconSize=${16}></dees-icon>
</div>
<div class="activity-content">
<div class="activity-title">
<strong>${activity.appName}</strong> ${activity.action}
</div>
<div class="activity-time">${this.formatTimeAgo(activity.timestamp)}</div>
</div>
</div>
`)}
</div>
`;
}
private renderSecurityPanel(): TemplateResult {
return html`
<div class="panel-header">
<div class="panel-header-left">
<div class="panel-title">Security Settings</div>
<div class="panel-description">Configure how SaaS apps can access your peripherals</div>
</div>
</div>
<div class="section">
<div class="section-title">Access Control</div>
<div class="settings-item">
<div class="item-left">
<div class="item-icon blue">
<dees-icon .icon=${'lucide:shieldCheck'} .iconSize=${18}></dees-icon>
</div>
<div class="item-info">
<div class="item-label">Require Approval</div>
<div class="item-sublabel">Ask before granting new app access</div>
</div>
</div>
<div
class="toggle-switch ${this.requireApproval ? 'active' : ''}"
@click=${() => this.requireApproval = !this.requireApproval}
></div>
</div>
<div class="settings-item">
<div class="item-left">
<div class="item-icon orange">
<dees-icon .icon=${'lucide:timer'} .iconSize=${18}></dees-icon>
</div>
<div class="item-info">
<div class="item-label">Auto-revoke Inactive Apps</div>
<div class="item-sublabel">Remove access after 30 days of inactivity</div>
</div>
</div>
<div
class="toggle-switch ${this.autoRevokeInactive ? 'active' : ''}"
@click=${() => this.autoRevokeInactive = !this.autoRevokeInactive}
></div>
</div>
</div>
<div class="section">
<div class="section-title">Logging</div>
<div class="settings-item">
<div class="item-left">
<div class="item-icon purple">
<dees-icon .icon=${'lucide:fileText'} .iconSize=${18}></dees-icon>
</div>
<div class="item-info">
<div class="item-label">Activity Logging</div>
<div class="item-sublabel">Record all peripheral access events</div>
</div>
</div>
<div
class="toggle-switch ${this.activityLogging ? 'active' : ''}"
@click=${() => this.activityLogging = !this.activityLogging}
></div>
</div>
</div>
<div class="section">
<div class="section-title">Danger Zone</div>
<div class="settings-item">
<div class="item-left">
<div class="item-icon red">
<dees-icon .icon=${'lucide:trash2'} .iconSize=${18}></dees-icon>
</div>
<div class="item-info">
<div class="item-label">Revoke All Access</div>
<div class="item-sublabel">Remove all SaaS app permissions</div>
</div>
</div>
<button class="btn btn-deny">Revoke All</button>
</div>
</div>
`;
}
private renderPermissionBadges(permissions: ISaasPermission[]): TemplateResult[] {
const permissionIcons: Record<TPermissionType, string> = {
print: 'lucide:printer',
scan: 'lucide:scan',
storage: 'lucide:hardDrive',
camera: 'lucide:camera',
audio: 'lucide:mic',
display: 'lucide:monitor',
network: 'lucide:wifi',
};
return permissions.map(perm => html`
<div class="permission-badge ${perm.granted ? 'active' : ''}">
<dees-icon .icon=${permissionIcons[perm.type]} .iconSize=${14}></dees-icon>
</div>
`);
}
private getPermissionLabel(type: TPermissionType): string {
const labels: Record<TPermissionType, string> = {
print: 'Printing',
scan: 'Scanning',
storage: 'Storage',
camera: 'Camera',
audio: 'Microphone',
display: 'Display',
network: 'Network',
};
return labels[type];
}
private getPermissionIcon(type: TPermissionType): string {
const icons: Record<TPermissionType, string> = {
print: 'lucide:printer',
scan: 'lucide:scan',
storage: 'lucide:hardDrive',
camera: 'lucide:camera',
audio: 'lucide:mic',
display: 'lucide:monitor',
network: 'lucide:wifi',
};
return icons[type];
}
private formatTimeAgo(date: Date): string {
const now = new Date();
const diff = now.getTime() - date.getTime();
const minutes = Math.floor(diff / 60000);
const hours = Math.floor(diff / 3600000);
const days = Math.floor(diff / 86400000);
if (minutes < 1) return 'Just now';
if (minutes < 60) return `${minutes}m ago`;
if (hours < 24) return `${hours}h ago`;
if (days < 7) return `${days}d ago`;
return date.toLocaleDateString();
}
private approveRequest(requestId: string): void {
this.accessRequests = this.accessRequests.map(r =>
r.id === requestId ? { ...r, status: 'approved' as const } : r
);
this.dispatchEvent(new CustomEvent('request-approved', {
detail: { requestId },
bubbles: true,
composed: true,
}));
}
private denyRequest(requestId: string): void {
this.accessRequests = this.accessRequests.map(r =>
r.id === requestId ? { ...r, status: 'denied' as const } : r
);
this.dispatchEvent(new CustomEvent('request-denied', {
detail: { requestId },
bubbles: true,
composed: true,
}));
}
}