feat(sdk,client): add menu to help navigation between apps
All checks were successful
arcad/edge/pipeline/head This commit looks good
All checks were successful
arcad/edge/pipeline/head This commit looks good
This commit is contained in:
239
pkg/sdk/client/src/components/menu.ts
Normal file
239
pkg/sdk/client/src/components/menu.ts
Normal file
@ -0,0 +1,239 @@
|
||||
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;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user