1289 lines
36 KiB
TypeScript
1289 lines
36 KiB
TypeScript
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,
|
|
}));
|
|
}
|
|
}
|