import { LitElement, html, css } from 'lit'; import { property, queryAll } from 'lit/decorators.js'; import { CloudIcon, HomeIcon, LoginIcon, LinkIcon, MenuIcon, UserCircleIcon, LogoutIcon } from './icons' import { EVENT_MENU_ITEM_SELECTED, EVENT_MENU_ITEM_UNSELECTED, MenuItem } from './menu-item'; import { MenuSubItem } from './menu-sub-item'; interface Manifest { id: string description: string metadata: { [key: string]: any } tags: string[] title: string version: string url?: string } interface Profile { sub?: string preferred_username?: string iss?: string edge_role?: string edge_tenant?: string edge_entrypoint?: string } const BASE_API_URL = '/edge/api/v1'; enum Roles { visitor = 0, user = 1, superuser = 2, admin = 3, superadmin = 4 } export class Menu extends LitElement { @property({ attribute: 'app-icon-url', type: String }) appIconUrl: string; @property({ attribute: 'app-title', type: String }) appTitle: string; @property({ attribute: 'hidden', type: Boolean }) hidden: boolean; @property() _apps: Manifest[] = [] @property() _profile: Profile static styles = css` :host { position: fixed; top: 0; left: 0; right: 0; height: 60px; background-color: #fff; display: flex; flex-direction: row; align-items: center; justify-content: center; z-index: 9999; } :host([hidden]) { display: none; } `; @queryAll('edge-menu-item') _menuItems: NodeListOf constructor() { super(); this.addEventListener(EVENT_MENU_ITEM_SELECTED, this._handleMenuItemSelected.bind(this)); this.addEventListener(EVENT_MENU_ITEM_UNSELECTED, this._handleMenuItemUnselected.bind(this)); this._fetchApps(); this._fetchProfile(); } render() { const apps = this._renderApps() return html` ${ this._renderApps() } ${ this._renderProfile() } `; } _fetchApps() { return fetch(`${BASE_API_URL}/apps`) .then(res => res.json()) .then(result => { if (result.error) { throw new Error(`Unexpected server error: ${result.error.code}`); } return result.data?.manifests || []; }) .then((manifests: Manifest[]) => { const promises = manifests.map((m: Manifest) => { const fetchOptions: RequestInit = { method: 'POST', body: JSON.stringify({}), headers: { 'Content-Type': 'application/json', } }; return fetch(`${BASE_API_URL}/apps/${m.id}/url`, fetchOptions) .then(res => res.json()) .then(result => { if (result.error) { throw new Error(`Unexpected server error: ${result.error.code}`); } m.url = result.data?.url; return m; }) ; }); return Promise.all(promises); }) .then((manifests: Manifest[]) => { this._apps = manifests; }) .catch(err => console.error(err)) } _fetchProfile() { return fetch(`${BASE_API_URL}/profile`) .then(res => res.json()) .then(result => { if (result.error) { switch (result.error.code) { case "unauthorized": return null; default: throw new Error(`Unexpected server error: ${result.error.code}`); } } return result.data?.profile; }) .then(profile => { this._profile = profile; }) .catch(err => console.error(err)) ; } _renderApps() { const apps = this._apps .filter(manifest => this._canAccess(manifest)) .map(manifest => { const iconUrl = ( ( manifest.url || '') + ( manifest.metadata?.paths?.icon || '' ) ) || LinkIcon; return html` ` }); return html` ${ apps } `; } _canAccess(manifest: Manifest): boolean { const currentRole = this._profile?.edge_role || 'visitor'; const minimumRole = manifest.metadata?.minimumRole || 'visitor'; return Roles[currentRole] >= Roles[minimumRole]; } _renderProfile() { const profile = this._profile; return html` ${ profile && profile.iss != "anon" ? html`` : html`` } `; } _handleMenuItemSelected(evt: CustomEvent) { const selectedMenuItem: HTMLElement = evt.detail.element; selectedMenuItem.classList.add('selected'); selectedMenuItem.classList.remove('unselected'); for (let item, i = 0; (item = this._menuItems[i]); i++) { if (item === selectedMenuItem) continue; item.unselect(); item.classList.add('unselected'); } } _handleMenuItemUnselected(evt: CustomEvent) { const unselectedMenuItem: HTMLElement = evt.detail.element; unselectedMenuItem.classList.remove('selected'); const hasSelectedItem = this.renderRoot.querySelectorAll('edge-menu-item.selected').length !== 0 if (hasSelectedItem) { return } for (let item, i = 0; (item = this._menuItems[i]); i++) { item.classList.remove('unselected'); } } } declare global { interface HTMLElementTagNameMap { "edge-menu": Menu; "edge-menu-item": MenuItem; "edge-menu-sub-item": MenuSubItem; } }