239 lines
7.2 KiB
TypeScript
239 lines
7.2 KiB
TypeScript
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<MenuItem>
|
|
|
|
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`
|
|
<edge-menu-item name='menu' label="${ this.appTitle || "App" }" icon-url='${ this.appIconUrl || MenuIcon }'>
|
|
<edge-menu-sub-item name='home' label='Home' icon-url='${HomeIcon}' link-url='/'></edge-menu-sub-item>
|
|
<slot></slot>
|
|
</edge-menu-item>
|
|
${ 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`
|
|
<edge-menu-sub-item
|
|
name='${ manifest.id }'
|
|
label='${ manifest.title }'
|
|
icon-url='${ iconUrl }'
|
|
link-url='${ manifest.url || '#' }'>
|
|
</edge-menu-sub-item>
|
|
`
|
|
});
|
|
|
|
return html`
|
|
<edge-menu-item name='apps' label='Apps' icon-url='${CloudIcon}'>
|
|
${ apps }
|
|
</edge-menu-item>
|
|
`;
|
|
}
|
|
|
|
_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`
|
|
<edge-menu-item name='profile' label="${profile?.preferred_username || 'Profile'}" icon-url='${UserCircleIcon}'>
|
|
${
|
|
profile ?
|
|
html`<edge-menu-sub-item name='login' label='Logout' icon-url='${LogoutIcon}' link-url='/edge/auth/logout'></edge-menu-sub-item>` :
|
|
html`<edge-menu-sub-item name='login' label='Login' icon-url='${LoginIcon}' link-url='/edge/auth/login'></edge-menu-sub-item>`
|
|
}
|
|
</edge-menu-item>
|
|
`;
|
|
}
|
|
|
|
_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;
|
|
}
|
|
} |