mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
Next (#2170)
* feat: add widgets (#2034) * feat: add Taiga UI library (#1992) * feat: add widgets * update patchdb * right resizable sidebar with widgets * feat: add resizing directive * chore: remove unused code * chore: remove unnecessary dep * feat: `ResponsiveCol` add directive for responsive grid * feat: add widgets edit mode and dialogs * feat: add widgets model and modal * chore: fix import * chore: hide mobile widgets behind flag * chore: add dummy widgets * chore: start working on heath widget and implement other comments * feat: health widget * feat: add saving widgets and sidebar params to patch * feat: preemptive UI update for widgets * update health widget with more accurate states and styling (#2127) * feat: `ResponsiveCol` add directive for responsive grid * chore: some changes after merge Co-authored-by: Matt Hill <matthewonthemoon@gmail.com> Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com> * fix(shared): `ElasticContainer` fix collapsing margin (#2150) * fix(shared): `ElasticContainer` fix collapsing margin * fix toolbar height so titles not chopped --------- Co-authored-by: Matt Hill <matthewonthemoon@gmail.com> * feat: make widgets sidebar width togglable (#2146) * feat: make widgets sidebar width togglable * feat: move widgets under header * chore: fix wide layout * fix(shared): `ResponsiveCol` fix missing grid steps (#2153) * fix widget flag and refactor for non-persistence * default widget flag to false * fix(shared): fix responsive column size (#2159) * fix(shared): fix responsive column size * fix: add responsiveness to all pages * fix responsiveness on more pages * fix: comments * revert some padding changes --------- Co-authored-by: Lucy Cifferello <12953208+elvece@users.noreply.github.com> Co-authored-by: Matt Hill <matthewonthemoon@gmail.com> * chore: add analyzer (#2165) * fix list styling to previous default (#2173) * fix list styling to previous default * dont need important flag --------- Co-authored-by: Alex Inkin <alexander@inkin.ru> Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com>
This commit is contained in:
committed by
Aiden McClelland
parent
aeb6da111b
commit
e867f31c31
@@ -80,7 +80,6 @@ const routes: Routes = [
|
||||
scrollPositionRestoration: 'enabled',
|
||||
preloadingStrategy: PreloadAllModules,
|
||||
initialNavigation: 'disabled',
|
||||
useHash: true,
|
||||
}),
|
||||
],
|
||||
exports: [RouterModule],
|
||||
|
||||
@@ -1,33 +1,77 @@
|
||||
<ion-app appEnter>
|
||||
<ion-content>
|
||||
<ion-split-pane
|
||||
contentId="main-content"
|
||||
[disabled]="!(authService.isVerified$ | async)"
|
||||
(ionSplitPaneVisible)="splitPaneVisible($event)"
|
||||
>
|
||||
<ion-menu contentId="main-content" type="overlay">
|
||||
<ion-content color="light" scrollY="false">
|
||||
<app-menu *ngIf="authService.isVerified$ | async"></app-menu>
|
||||
</ion-content>
|
||||
</ion-menu>
|
||||
<ion-router-outlet
|
||||
id="main-content"
|
||||
class="container"
|
||||
[class.container_offline]="
|
||||
(authService.isVerified$ | async) && !(connection.connected$ | async)
|
||||
"
|
||||
></ion-router-outlet>
|
||||
</ion-split-pane>
|
||||
<tui-root
|
||||
*ngIf="widgetDrawer$ | async as drawer"
|
||||
tuiMode="onDark"
|
||||
[style.--widgets-width.px]="drawer.open ? drawer.width : 0"
|
||||
>
|
||||
<ion-app appEnter>
|
||||
<ion-content>
|
||||
<ion-split-pane
|
||||
contentId="main-content"
|
||||
[disabled]="!(authService.isVerified$ | async)"
|
||||
(ionSplitPaneVisible)="splitPaneVisible($event)"
|
||||
>
|
||||
<ion-menu
|
||||
contentId="main-content"
|
||||
type="overlay"
|
||||
side="start"
|
||||
class="left-menu"
|
||||
>
|
||||
<ion-content color="light" scrollY="false">
|
||||
<app-menu *ngIf="authService.isVerified$ | async"></app-menu>
|
||||
</ion-content>
|
||||
</ion-menu>
|
||||
|
||||
<section appPreloader></section>
|
||||
</ion-content>
|
||||
<ion-footer>
|
||||
<footer appFooter></footer>
|
||||
</ion-footer>
|
||||
<ion-footer
|
||||
*ngIf="(authService.isVerified$ | async) && !(sidebarOpen$ | async)"
|
||||
>
|
||||
<connection-bar></connection-bar>
|
||||
</ion-footer>
|
||||
<toast-container></toast-container>
|
||||
</ion-app>
|
||||
<ion-menu
|
||||
contentId="main-content"
|
||||
type="overlay"
|
||||
side="end"
|
||||
class="right-menu container"
|
||||
[class.container_offline]="
|
||||
(authService.isVerified$ | async) &&
|
||||
!(connection.connected$ | async)
|
||||
"
|
||||
[class.right-menu_hidden]="!drawer.open"
|
||||
[style.--side-width.px]="drawer.width"
|
||||
>
|
||||
<div class="divider">
|
||||
<button
|
||||
class="widgets-button"
|
||||
[class.widgets-button_collapse]="drawer.width === 600"
|
||||
(click)="onResize(drawer)"
|
||||
></button>
|
||||
</div>
|
||||
<widgets *ngIf="drawer.open" [wide]="drawer.width === 600"></widgets>
|
||||
</ion-menu>
|
||||
|
||||
<ion-router-outlet
|
||||
[responsiveColViewport]="viewport"
|
||||
id="main-content"
|
||||
class="container"
|
||||
[class.container_offline]="
|
||||
(authService.isVerified$ | async) &&
|
||||
!(connection.connected$ | async)
|
||||
"
|
||||
>
|
||||
<ion-content
|
||||
#viewport="viewport"
|
||||
responsiveColViewport
|
||||
class="ion-padding with-widgets"
|
||||
style="pointer-events: none; opacity: 0"
|
||||
></ion-content>
|
||||
</ion-router-outlet>
|
||||
</ion-split-pane>
|
||||
|
||||
<section appPreloader></section>
|
||||
</ion-content>
|
||||
<ion-footer>
|
||||
<footer appFooter></footer>
|
||||
</ion-footer>
|
||||
<ion-footer
|
||||
*ngIf="(authService.isVerified$ | async) && !(sidebarOpen$ | async)"
|
||||
>
|
||||
<connection-bar></connection-bar>
|
||||
</ion-footer>
|
||||
<toast-container></toast-container>
|
||||
</ion-app>
|
||||
</tui-root>
|
||||
<tui-theme-night></tui-theme-night>
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
ion-split-pane {
|
||||
tui-root {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.left-menu {
|
||||
--side-max-width: 280px;
|
||||
}
|
||||
|
||||
@@ -12,4 +17,103 @@ ion-split-pane {
|
||||
&_offline {
|
||||
filter: saturate(0.75) contrast(0.85);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 991.499px) {
|
||||
--widgets-width: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.right-menu {
|
||||
--side-max-width: 600px;
|
||||
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
right: 0;
|
||||
left: auto;
|
||||
top: 74px;
|
||||
|
||||
// For some reason *ngIf is broken upon first login
|
||||
&_hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 100%;
|
||||
width: 10px;
|
||||
pointer-events: none;
|
||||
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
|
||||
background: #e2e2e2;
|
||||
|
||||
z-index: 10;
|
||||
opacity: 0.2;
|
||||
transition: opacity 0.3s;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
margin-top: -78px;
|
||||
left: 10px;
|
||||
width: 60px;
|
||||
height: 50px;
|
||||
border-bottom-left-radius: 14px;
|
||||
box-shadow: -14px 0 0 -1px #e2e2e2;
|
||||
}
|
||||
|
||||
&:after {
|
||||
margin-top: 28px;
|
||||
border-radius: 0;
|
||||
border-top-left-radius: 14px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
.widgets-button {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
font-size: 0;
|
||||
left: 100%;
|
||||
width: 16px;
|
||||
height: 60px;
|
||||
margin-top: -30px;
|
||||
border-top-right-radius: 10px;
|
||||
border-bottom-right-radius: 10px;
|
||||
background: inherit;
|
||||
pointer-events: auto;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 3px;
|
||||
width: 2px;
|
||||
height: 8px;
|
||||
background: black;
|
||||
transform: rotate(-45deg);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
&:before {
|
||||
margin-top: -5px;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
&_collapse:before {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
&_collapse:after {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,10 @@ import { PatchMonitorService } from './services/patch-monitor.service'
|
||||
import { ConnectionService } from './services/connection.service'
|
||||
import { Title } from '@angular/platform-browser'
|
||||
import { ServerNameService } from './services/server-name.service'
|
||||
import {
|
||||
ClientStorageService,
|
||||
WidgetDrawer,
|
||||
} from './services/client-storage.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@@ -16,6 +20,7 @@ import { ServerNameService } from './services/server-name.service'
|
||||
export class AppComponent implements OnDestroy {
|
||||
readonly subscription = merge(this.patchData, this.patchMonitor).subscribe()
|
||||
readonly sidebarOpen$ = this.splitPane.sidebarOpen$
|
||||
readonly widgetDrawer$ = this.clientStorageService.widgetDrawer$
|
||||
|
||||
constructor(
|
||||
private readonly titleService: Title,
|
||||
@@ -25,6 +30,7 @@ export class AppComponent implements OnDestroy {
|
||||
private readonly serverNameService: ServerNameService,
|
||||
readonly authService: AuthService,
|
||||
readonly connection: ConnectionService,
|
||||
readonly clientStorageService: ClientStorageService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
@@ -37,6 +43,13 @@ export class AppComponent implements OnDestroy {
|
||||
this.splitPane.sidebarOpen$.next(detail.visible)
|
||||
}
|
||||
|
||||
onResize(drawer: WidgetDrawer) {
|
||||
this.clientStorageService.updateWidgetDrawer({
|
||||
...drawer,
|
||||
width: drawer.width === 400 ? 600 : 400,
|
||||
})
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.subscription.unsubscribe()
|
||||
}
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
import {
|
||||
TuiDialogModule,
|
||||
TuiModeModule,
|
||||
TuiRootModule,
|
||||
TuiThemeNightModule,
|
||||
} from '@taiga-ui/core'
|
||||
import { HttpClientModule } from '@angular/common/http'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor'
|
||||
import { MarkdownModule, SharedPipesModule } from '@start9labs/shared'
|
||||
import {
|
||||
MarkdownModule,
|
||||
ResponsiveColModule,
|
||||
SharedPipesModule,
|
||||
} from '@start9labs/shared'
|
||||
|
||||
import { AppComponent } from './app.component'
|
||||
import { AppRoutingModule } from './app-routing.module'
|
||||
@@ -18,6 +28,7 @@ import { APP_PROVIDERS } from './app.providers'
|
||||
import { PatchDbModule } from './services/patch-db/patch-db.module'
|
||||
import { ToastContainerModule } from './components/toast-container/toast-container.module'
|
||||
import { ConnectionBarComponentModule } from './components/connection-bar/connection-bar.component.module'
|
||||
import { WidgetsPageModule } from './pages/widgets/widgets.module'
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent],
|
||||
@@ -41,6 +52,12 @@ import { ConnectionBarComponentModule } from './components/connection-bar/connec
|
||||
PatchDbModule,
|
||||
ToastContainerModule,
|
||||
ConnectionBarComponentModule,
|
||||
TuiRootModule,
|
||||
TuiDialogModule,
|
||||
TuiModeModule,
|
||||
TuiThemeNightModule,
|
||||
WidgetsPageModule,
|
||||
ResponsiveColModule,
|
||||
],
|
||||
providers: APP_PROVIDERS,
|
||||
bootstrap: [AppComponent],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<backup-drives-header [type]="type"></backup-drives-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<ion-content class="ion-padding with-widgets">
|
||||
<!-- loading -->
|
||||
<text-spinner
|
||||
*ngIf="loading; else loaded"
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
<ng-container *ngIf="enableWidgets">
|
||||
<ion-button class="widgets" color="dark" (click)="onWidgets()">
|
||||
<ion-icon name="grid-outline"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button
|
||||
*ngIf="widgetDrawer$ | async as drawer"
|
||||
class="sidebar"
|
||||
color="dark"
|
||||
(click)="onSidebar(drawer)"
|
||||
>
|
||||
<ion-icon name="grid-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ng-container>
|
||||
<div class="wrapper">
|
||||
<ion-badge
|
||||
*ngIf="!(sidebarOpen$ | async) && (unreadCount$ | async) as unreadCount"
|
||||
|
||||
@@ -2,10 +2,11 @@ import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { BadgeMenuComponent } from './badge-menu.component'
|
||||
import { TuiLetModule } from '@taiga-ui/cdk'
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, IonicModule, TuiLetModule],
|
||||
declarations: [BadgeMenuComponent],
|
||||
imports: [CommonModule, IonicModule],
|
||||
exports: [BadgeMenuComponent],
|
||||
})
|
||||
export class BadgeMenuComponentModule {}
|
||||
|
||||
@@ -1,6 +1,25 @@
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 992px) {
|
||||
.widgets {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
position: relative;
|
||||
margin-right: 1vh;
|
||||
}
|
||||
|
||||
.md-badge {
|
||||
|
||||
@@ -2,6 +2,16 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
import { SplitPaneTracker } from 'src/app/services/split-pane.service'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { TuiDialogService } from '@taiga-ui/core'
|
||||
import { WIDGETS_COMPONENT } from '../../pages/widgets/widgets.page'
|
||||
import { WorkspaceConfig } from '@start9labs/shared'
|
||||
import {
|
||||
ClientStorageService,
|
||||
WidgetDrawer,
|
||||
} from 'src/app/services/client-storage.service'
|
||||
|
||||
const { enableWidgets } =
|
||||
require('../../../../../../config.json') as WorkspaceConfig
|
||||
|
||||
@Component({
|
||||
selector: 'badge-menu-button',
|
||||
@@ -10,11 +20,30 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class BadgeMenuComponent {
|
||||
unreadCount$ = this.patch.watch$('server-info', 'unread-notification-count')
|
||||
sidebarOpen$ = this.splitPane.sidebarOpen$
|
||||
readonly unreadCount$ = this.patch.watch$(
|
||||
'server-info',
|
||||
'unread-notification-count',
|
||||
)
|
||||
readonly sidebarOpen$ = this.splitPane.sidebarOpen$
|
||||
readonly widgetDrawer$ = this.clientStorageService.widgetDrawer$
|
||||
|
||||
readonly enableWidgets = enableWidgets
|
||||
|
||||
constructor(
|
||||
private readonly splitPane: SplitPaneTracker,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly dialog: TuiDialogService,
|
||||
private readonly clientStorageService: ClientStorageService,
|
||||
) {}
|
||||
|
||||
onSidebar(drawer: WidgetDrawer) {
|
||||
this.clientStorageService.updateWidgetDrawer({
|
||||
...drawer,
|
||||
open: !drawer.open,
|
||||
})
|
||||
}
|
||||
|
||||
onWidgets() {
|
||||
this.dialog.open(WIDGETS_COMPONENT, { label: 'Widgets' }).subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
[scrollEvents]="true"
|
||||
(ionScroll)="handleScroll($event)"
|
||||
(ionScrollEnd)="handleScrollEnd()"
|
||||
class="ion-padding"
|
||||
class="ion-padding with-widgets"
|
||||
>
|
||||
<ion-infinite-scroll
|
||||
id="scroller"
|
||||
@@ -73,7 +73,7 @@
|
||||
</ng-container>
|
||||
</ion-content>
|
||||
|
||||
<ion-footer>
|
||||
<ion-footer class="with-widgets">
|
||||
<ion-toolbar>
|
||||
<div class="inline ion-padding-start">
|
||||
<ion-checkbox [(ngModel)]="autoScroll" color="dark"></ion-checkbox>
|
||||
|
||||
@@ -15,12 +15,12 @@ import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||
import {
|
||||
LogsRes,
|
||||
ServerLogsReq,
|
||||
DestroyService,
|
||||
ErrorToastService,
|
||||
toLocalIsoString,
|
||||
Log,
|
||||
DownloadHTMLService,
|
||||
} from '@start9labs/shared'
|
||||
import { TuiDestroyService } from '@taiga-ui/cdk'
|
||||
import { RR } from 'src/app/services/api/api.types'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ConnectionService } from 'src/app/services/connection.service'
|
||||
@@ -39,7 +39,7 @@ var convert = new Convert({
|
||||
selector: 'logs',
|
||||
templateUrl: './logs.component.html',
|
||||
styleUrls: ['./logs.component.scss'],
|
||||
providers: [DestroyService, DownloadHTMLService],
|
||||
providers: [TuiDestroyService, DownloadHTMLService],
|
||||
})
|
||||
export class LogsComponent {
|
||||
@ViewChild(IonContent)
|
||||
@@ -68,7 +68,7 @@ export class LogsComponent {
|
||||
|
||||
constructor(
|
||||
private readonly errToast: ErrorToastService,
|
||||
private readonly destroy$: DestroyService,
|
||||
private readonly destroy$: TuiDestroyService,
|
||||
private readonly api: ApiService,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly downloadHtml: DownloadHTMLService,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<ion-row class="ion-justify-content-center ion-align-items-center">
|
||||
<ion-col
|
||||
*ngFor="let card of cards"
|
||||
responsiveCol
|
||||
sizeLg="4"
|
||||
sizeSm="6"
|
||||
sizeXs="12"
|
||||
|
||||
@@ -2,6 +2,7 @@ import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { ResponsiveColModule } from '@start9labs/shared'
|
||||
import { WidgetListComponent } from './widget-list.component'
|
||||
import { AnyLinkModule } from 'src/app/components/any-link/any-link.component.module'
|
||||
import { WidgetCardComponentModule } from '../widget-card/widget-card.component.module'
|
||||
@@ -14,6 +15,7 @@ import { WidgetCardComponentModule } from '../widget-card/widget-card.component.
|
||||
RouterModule.forChild([]),
|
||||
AnyLinkModule,
|
||||
WidgetCardComponentModule,
|
||||
ResponsiveColModule,
|
||||
],
|
||||
exports: [WidgetListComponent],
|
||||
})
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-top">
|
||||
<ion-content class="ion-padding-top with-widgets">
|
||||
<ion-item-group *ngIf="pkg$ | async as pkg">
|
||||
<!-- ** standard actions ** -->
|
||||
<ion-item-divider>Standard Actions</ion-item-divider>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-top">
|
||||
<ion-content class="ion-padding-top with-widgets">
|
||||
<ion-item-group>
|
||||
<!-- iff ui -->
|
||||
<ng-container *ngIf="ui">
|
||||
@@ -18,9 +18,9 @@
|
||||
<!-- other interface -->
|
||||
<ng-container *ngIf="other.length">
|
||||
<ion-item-divider>Machine Interfaces</ion-item-divider>
|
||||
<div *ngFor="let interface of other" style="margin-bottom: 30px;">
|
||||
<div *ngFor="let interface of other" style="margin-bottom: 30px">
|
||||
<app-interfaces-item [interface]="interface"></app-interfaces-item>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ion-item-group>
|
||||
</ion-content>
|
||||
</ion-content>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { IonicModule } from '@ionic/angular'
|
||||
import { AppListPage } from './app-list.page'
|
||||
import {
|
||||
EmverPipesModule,
|
||||
ResponsiveColModule,
|
||||
TextSpinnerComponentModule,
|
||||
} from '@start9labs/shared'
|
||||
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
|
||||
@@ -35,6 +36,7 @@ const routes: Routes = [
|
||||
RouterModule.forChild(routes),
|
||||
BadgeMenuComponentModule,
|
||||
WidgetListComponentModule,
|
||||
ResponsiveColModule,
|
||||
],
|
||||
declarations: [
|
||||
AppListPage,
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<ion-content class="ion-padding with-widgets">
|
||||
<!-- loaded -->
|
||||
<ng-container *ngIf="pkgs$ | async as pkgs; else loading">
|
||||
<ng-container *ngIf="!pkgs.length; else list">
|
||||
@@ -20,7 +20,12 @@
|
||||
<ng-template #list>
|
||||
<ion-grid>
|
||||
<ion-row>
|
||||
<ion-col *ngFor="let pkg of pkgs" sizeSm="12" sizeLg="6">
|
||||
<ion-col
|
||||
*ngFor="let pkg of pkgs"
|
||||
responsiveCol
|
||||
sizeSm="12"
|
||||
sizeMd="6"
|
||||
>
|
||||
<app-list-pkg
|
||||
*ngIf="pkg | packageInfo | async as info"
|
||||
[pkg]="info"
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<ion-content class="ion-padding with-widgets">
|
||||
<skeleton-list *ngIf="loading"></skeleton-list>
|
||||
<ion-item-group *ngIf="!loading">
|
||||
<ion-item *ngFor="let metric of metrics | keyvalue : asIsOrder">
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-top">
|
||||
<ion-content class="ion-padding-top with-widgets">
|
||||
<text-spinner
|
||||
*ngIf="loading; else loaded"
|
||||
text="Loading Properties"
|
||||
|
||||
@@ -16,11 +16,11 @@ import {
|
||||
PackageMainStatus,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import {
|
||||
DestroyService,
|
||||
ErrorToastService,
|
||||
getPkgId,
|
||||
copyToClipboard,
|
||||
} from '@start9labs/shared'
|
||||
import { TuiDestroyService } from '@taiga-ui/cdk'
|
||||
import { getValueByPointer } from 'fast-json-patch'
|
||||
import { map, takeUntil } from 'rxjs/operators'
|
||||
|
||||
@@ -28,7 +28,7 @@ import { map, takeUntil } from 'rxjs/operators'
|
||||
selector: 'app-properties',
|
||||
templateUrl: './app-properties.page.html',
|
||||
styleUrls: ['./app-properties.page.scss'],
|
||||
providers: [DestroyService],
|
||||
providers: [TuiDestroyService],
|
||||
})
|
||||
export class AppPropertiesPage {
|
||||
loading = true
|
||||
@@ -56,7 +56,7 @@ export class AppPropertiesPage {
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly destroy$: DestroyService,
|
||||
private readonly destroy$: TuiDestroyService,
|
||||
) {}
|
||||
|
||||
ionViewDidEnter() {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { AppShowPage } from './app-show.page'
|
||||
import { EmverPipesModule } from '@start9labs/shared'
|
||||
import { EmverPipesModule, ResponsiveColModule } from '@start9labs/shared'
|
||||
import { StatusComponentModule } from 'src/app/components/status/status.component.module'
|
||||
import { AppConfigPageModule } from 'src/app/modals/app-config/app-config.module'
|
||||
import { LaunchablePipeModule } from 'src/app/pipes/launchable/launchable.module'
|
||||
@@ -55,6 +55,7 @@ const routes: Routes = [
|
||||
EmverPipesModule,
|
||||
LaunchablePipeModule,
|
||||
UiPipeModule,
|
||||
ResponsiveColModule,
|
||||
],
|
||||
})
|
||||
export class AppShowPageModule {}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<app-show-header [pkg]="pkg"></app-show-header>
|
||||
|
||||
<!-- content -->
|
||||
<ion-content class="ion-padding">
|
||||
<ion-content class="ion-padding with-widgets">
|
||||
<!-- ** installing, updating, restoring ** -->
|
||||
<ng-container *ngIf="showProgress(pkg); else installed">
|
||||
<app-show-progress
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<ion-item-divider>Additional Info</ion-item-divider>
|
||||
<ion-grid *ngIf="pkg.manifest as manifest">
|
||||
<ion-row>
|
||||
<ion-col sizeXs="12" sizeMd="6">
|
||||
<ion-col responsiveCol sizeXs="12" sizeMd="6">
|
||||
<ion-item-group>
|
||||
<ion-item>
|
||||
<ion-label>
|
||||
@@ -51,7 +51,7 @@
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
</ion-col>
|
||||
<ion-col sizeXs="12" sizeMd="6">
|
||||
<ion-col responsiveCol sizeXs="12" sizeMd="6">
|
||||
<ion-item-group>
|
||||
<ion-item
|
||||
[href]="manifest['upstream-repo']"
|
||||
|
||||
@@ -16,14 +16,15 @@ import { ConfigSpec } from 'src/app/pkg-config/config-types'
|
||||
import * as yaml from 'js-yaml'
|
||||
import { v4 } from 'uuid'
|
||||
import { DataModel, DevData } from 'src/app/services/patch-db/data-model'
|
||||
import { DestroyService, ErrorToastService } from '@start9labs/shared'
|
||||
import { ErrorToastService } from '@start9labs/shared'
|
||||
import { TuiDestroyService } from '@taiga-ui/cdk'
|
||||
import { takeUntil } from 'rxjs/operators'
|
||||
|
||||
@Component({
|
||||
selector: 'developer-list',
|
||||
templateUrl: 'developer-list.page.html',
|
||||
styleUrls: ['developer-list.page.scss'],
|
||||
providers: [DestroyService],
|
||||
providers: [TuiDestroyService],
|
||||
})
|
||||
export class DeveloperListPage {
|
||||
devData: DevData = {}
|
||||
@@ -34,7 +35,7 @@ export class DeveloperListPage {
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly errToast: ErrorToastService,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly destroy$: DestroyService,
|
||||
private readonly destroy$: TuiDestroyService,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly actionCtrl: ActionSheetController,
|
||||
) {}
|
||||
|
||||
@@ -3,7 +3,11 @@ import { CommonModule } from '@angular/common'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { SharedPipesModule, EmverPipesModule } from '@start9labs/shared'
|
||||
import {
|
||||
SharedPipesModule,
|
||||
EmverPipesModule,
|
||||
ResponsiveColModule,
|
||||
} from '@start9labs/shared'
|
||||
import {
|
||||
FilterPackagesPipeModule,
|
||||
CategoriesModule,
|
||||
@@ -41,6 +45,7 @@ const routes: Routes = [
|
||||
SkeletonModule,
|
||||
MarketplaceSettingsPageModule,
|
||||
StoreIconComponentModule,
|
||||
ResponsiveColModule,
|
||||
],
|
||||
declarations: [MarketplaceListPage],
|
||||
exports: [MarketplaceListPage],
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<ion-content class="ion-padding with-widgets">
|
||||
<ng-container *ngIf="details$ | async as details">
|
||||
<ion-item [color]="details.color">
|
||||
<ion-icon slot="start" name="information-circle-outline"></ion-icon>
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
<ion-grid>
|
||||
<ion-row>
|
||||
<ion-col size="12">
|
||||
<ion-col>
|
||||
<div class="heading">
|
||||
<store-icon
|
||||
class="icon"
|
||||
@@ -38,7 +38,7 @@
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
<ion-row class="ion-align-items-center">
|
||||
<ion-col size="12">
|
||||
<ion-col>
|
||||
<ng-container *ngIf="store$ | async as store; else loading">
|
||||
<marketplace-categories
|
||||
[categories]="store.categories"
|
||||
@@ -55,6 +55,7 @@
|
||||
<ion-row *ngIf="localPkgs$ | async as localPkgs">
|
||||
<ion-col
|
||||
*ngFor="let pkg of filtered"
|
||||
responsiveCol
|
||||
sizeXs="12"
|
||||
sizeSm="12"
|
||||
sizeMd="6"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<marketplace-show-header></marketplace-show-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<ion-content class="ion-padding with-widgets">
|
||||
<ng-container *ngIf="pkg$ | async as pkg else loading">
|
||||
<ng-container *ngIf="pkg | empty; else show">
|
||||
<div
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
<ion-content class="with-widgets">
|
||||
<!-- loading -->
|
||||
<ion-item-group *ngIf="loading; else loaded">
|
||||
<ion-item-divider>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-top">
|
||||
<ion-content class="ion-padding-top with-widgets">
|
||||
<ion-item-group>
|
||||
<!-- about -->
|
||||
<ion-item class="ion-padding-bottom">
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<ion-content class="ion-padding with-widgets">
|
||||
<ion-grid *ngIf="pkgs$ | async as pkgs">
|
||||
<ion-row *ngIf="backupProgress$ | async as backupProgress">
|
||||
<ion-col>
|
||||
|
||||
@@ -13,13 +13,13 @@ import { PatchDB } from 'patch-db-client'
|
||||
import { skip, takeUntil } from 'rxjs/operators'
|
||||
import { MappedBackupTarget } from 'src/app/types/mapped-backup-target'
|
||||
import * as argon2 from '@start9labs/argon2'
|
||||
import { TuiDestroyService } from '@taiga-ui/cdk'
|
||||
import {
|
||||
CifsBackupTarget,
|
||||
DiskBackupTarget,
|
||||
} from 'src/app/services/api/api.types'
|
||||
import { BackupSelectPage } from 'src/app/modals/backup-select/backup-select.page'
|
||||
import { EOSService } from 'src/app/services/eos.service'
|
||||
import { DestroyService } from '@start9labs/shared'
|
||||
import { getServerInfo } from 'src/app/util/get-server-info'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
@@ -27,7 +27,7 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
selector: 'server-backup',
|
||||
templateUrl: './server-backup.page.html',
|
||||
styleUrls: ['./server-backup.page.scss'],
|
||||
providers: [DestroyService],
|
||||
providers: [TuiDestroyService],
|
||||
})
|
||||
export class ServerBackupPage {
|
||||
serviceIds: string[] = []
|
||||
@@ -39,7 +39,7 @@ export class ServerBackupPage {
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly embassyApi: ApiService,
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly destroy$: DestroyService,
|
||||
private readonly destroy$: TuiDestroyService,
|
||||
private readonly eosService: EOSService,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
) {}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<ion-content class="ion-padding with-widgets">
|
||||
<skeleton-list *ngIf="loading" [groups]="2"></skeleton-list>
|
||||
|
||||
<div id="metricSection">
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<ion-content class="ion-padding with-widgets">
|
||||
<!-- loading -->
|
||||
<ng-template #loading>
|
||||
<text-spinner text="Connecting to Embassy"></text-spinner>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
<ion-content class="with-widgets">
|
||||
<ion-item-group *ngIf="server$ | async as server">
|
||||
<ion-item-divider>embassyOS Info</ion-item-divider>
|
||||
<ion-item>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-top">
|
||||
<ion-content class="ion-padding-top with-widgets">
|
||||
<!-- loading -->
|
||||
<ion-item-group *ngIf="loading; else notLoading">
|
||||
<div *ngFor="let entry of ['This Session', 'Other Sessions']">
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-text-center">
|
||||
<ion-content class="ion-text-center with-widgets">
|
||||
<!-- file upload -->
|
||||
<div
|
||||
*ngIf="!toUpload.file; else fileUploaded"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-top">
|
||||
<ion-content class="ion-padding-top with-widgets">
|
||||
<ion-item-group>
|
||||
<!-- always -->
|
||||
<ion-item>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-top">
|
||||
<ion-content class="ion-padding-top with-widgets">
|
||||
<ion-item-group>
|
||||
<!-- always -->
|
||||
<ion-item>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<ion-content class="ion-padding with-widgets">
|
||||
<ion-item-group *ngIf="data$ | async as data">
|
||||
<ng-container *ngFor="let host of data.hosts">
|
||||
<ion-item-divider class="header">
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<div *ngIf="installed$ | async as installed" class="wrapper">
|
||||
<button
|
||||
*ngFor="let widget of widgets | tuiFilter: filter:installed; empty: empty"
|
||||
class="tui-island tui-island_size_l"
|
||||
(click)="context.completeWith(widget)"
|
||||
>
|
||||
<span class="tui-island__title">{{ widget.meta.name }}</span>
|
||||
</button>
|
||||
<ng-template #empty>No additional widgets found</ng-template>
|
||||
</div>
|
||||
@@ -0,0 +1,9 @@
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.tui-island {
|
||||
text-align: left;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { Widget } from '../../../../services/patch-db/data-model'
|
||||
import {
|
||||
POLYMORPHEUS_CONTEXT,
|
||||
PolymorpheusComponent,
|
||||
} from '@tinkoff/ng-polymorpheus'
|
||||
import { TuiDialogContext } from '@taiga-ui/core'
|
||||
import { BUILT_IN_WIDGETS } from '../widgets'
|
||||
|
||||
@Component({
|
||||
selector: 'add-widget',
|
||||
templateUrl: './add.component.html',
|
||||
styleUrls: ['./add.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AddWidgetComponent {
|
||||
readonly context = inject<TuiDialogContext<Widget>>(POLYMORPHEUS_CONTEXT)
|
||||
|
||||
readonly installed$ = inject(PatchDB).watch$('ui', 'widgets')
|
||||
|
||||
readonly widgets = BUILT_IN_WIDGETS
|
||||
|
||||
readonly filter = (widget: Widget, installed: readonly Widget[]) =>
|
||||
!installed.find(({ id }) => id === widget.id)
|
||||
}
|
||||
|
||||
export const ADD_WIDGET = new PolymorpheusComponent<
|
||||
AddWidgetComponent,
|
||||
TuiDialogContext<Widget>
|
||||
>(AddWidgetComponent)
|
||||
@@ -0,0 +1,12 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { TuiFilterPipeModule, TuiForModule } from '@taiga-ui/cdk'
|
||||
|
||||
import { AddWidgetComponent } from './add.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, TuiFilterPipeModule, TuiForModule],
|
||||
declarations: [AddWidgetComponent],
|
||||
exports: [AddWidgetComponent],
|
||||
})
|
||||
export class AddWidgetModule {}
|
||||
@@ -0,0 +1 @@
|
||||
<ion-button class="add">Add to quick launch</ion-button>
|
||||
@@ -0,0 +1,3 @@
|
||||
.add {
|
||||
font-size: 13px;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
|
||||
@Component({
|
||||
selector: 'widget-favorites',
|
||||
templateUrl: './favorites.component.html',
|
||||
styleUrls: ['./favorites.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FavoritesComponent {}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { FavoritesComponent } from './favorites.component'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
|
||||
@NgModule({
|
||||
imports: [IonicModule],
|
||||
declarations: [FavoritesComponent],
|
||||
exports: [FavoritesComponent],
|
||||
})
|
||||
export class FavoritesModule {}
|
||||
@@ -0,0 +1,11 @@
|
||||
<h2 class="widget-title">Service health overview</h2>
|
||||
<tui-ring-chart
|
||||
*ngIf="data$ | async as data"
|
||||
class="ring-chart"
|
||||
[tuiHintContent]="hint"
|
||||
[value]="data"
|
||||
>
|
||||
<ng-template #hint let-index>
|
||||
{{ labels[index] }}: {{ data[index] }}
|
||||
</ng-template>
|
||||
</tui-ring-chart>
|
||||
@@ -0,0 +1,19 @@
|
||||
:host {
|
||||
/* index order must match labels array */
|
||||
--tui-chart-0: var(--ion-color-danger-tint); // error
|
||||
--tui-chart-1: var(--ion-color-success-tint); // healthy
|
||||
--tui-chart-2: var(--ion-color-warning-tint); // needs attention
|
||||
--tui-chart-3: var(--ion-color-step-600); // stopped
|
||||
--tui-chart-4: var(--ion-color-primary-tint); // transitioning
|
||||
}
|
||||
|
||||
.widget-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ring-chart {
|
||||
transform: scale(0.85);
|
||||
margin: 0.6rem auto;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { map } from 'rxjs/operators'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { PrimaryStatus } from 'src/app/services/pkg-status-rendering.service'
|
||||
import { getPackageInfo, PkgInfo } from '../../../../util/get-package-info'
|
||||
|
||||
@Component({
|
||||
selector: 'widget-health',
|
||||
templateUrl: './health.component.html',
|
||||
styleUrls: ['./health.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class HealthComponent {
|
||||
readonly labels = [
|
||||
'Error',
|
||||
'Healthy',
|
||||
'Needs Attention',
|
||||
'Stopped',
|
||||
'Transitioning',
|
||||
] as const
|
||||
|
||||
readonly data$ = inject(PatchDB)
|
||||
.watch$('package-data')
|
||||
.pipe(
|
||||
map(data => {
|
||||
const pkgs = Object.values<PackageDataEntry>(data).map(getPackageInfo)
|
||||
const result = this.labels.reduce<Record<string, number>>(
|
||||
(acc, label) => ({
|
||||
...acc,
|
||||
[label]: this.getCount(label, pkgs),
|
||||
}),
|
||||
{},
|
||||
)
|
||||
|
||||
result['Healthy'] =
|
||||
pkgs.length -
|
||||
result['Error'] -
|
||||
result['Needs Attention'] -
|
||||
result['Stopped'] -
|
||||
result['Transitioning']
|
||||
|
||||
return this.labels.map(label => result[label])
|
||||
}),
|
||||
)
|
||||
|
||||
private getCount(label: string, pkgs: PkgInfo[]): number {
|
||||
switch (label) {
|
||||
case 'Error':
|
||||
return pkgs.filter(
|
||||
a => a.primaryStatus !== PrimaryStatus.Stopped && a.error,
|
||||
).length
|
||||
case 'Needs Attention':
|
||||
return pkgs.filter(a => a.warning).length
|
||||
case 'Stopped':
|
||||
return pkgs.filter(a => a.primaryStatus === PrimaryStatus.Stopped)
|
||||
.length
|
||||
case 'Transitioning':
|
||||
return pkgs.filter(a => a.transitioning).length
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { HealthComponent } from './health.component'
|
||||
import { TuiRingChartModule } from '@taiga-ui/addon-charts'
|
||||
import { TuiHintModule } from '@taiga-ui/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, TuiRingChartModule, TuiHintModule],
|
||||
declarations: [HealthComponent],
|
||||
exports: [HealthComponent],
|
||||
})
|
||||
export class HealthModule {}
|
||||
@@ -0,0 +1,30 @@
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<ion-icon class="stat-icon" name="server-outline"></ion-icon>
|
||||
<div>
|
||||
30%
|
||||
<div class="description">Storage</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<ion-icon class="stat-icon" name="hardware-chip-outline"></ion-icon>
|
||||
<div>
|
||||
10%
|
||||
<div class="description">CPU</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<ion-icon class="stat-icon" name="stats-chart-outline"></ion-icon>
|
||||
<div>
|
||||
10%
|
||||
<div class="description">Memory</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<ion-icon class="stat-icon" name="thermometer-outline"></ion-icon>
|
||||
<div>
|
||||
50.6⁰C
|
||||
<div class="description">Temp</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,34 @@
|
||||
.stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
flex-wrap: wrap;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
|
||||
&_mobile .stat {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.stat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
:host-context(.wrapper_mobile) & {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 32px;
|
||||
margin: 12px;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: #3a7be0;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
|
||||
@Component({
|
||||
selector: 'widget-metrics',
|
||||
templateUrl: './metrics.component.html',
|
||||
styleUrls: ['./metrics.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MetricsComponent {}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { MetricsComponent } from './metrics.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [IonicModule],
|
||||
declarations: [MetricsComponent],
|
||||
exports: [MetricsComponent],
|
||||
})
|
||||
export class MetricsModule {}
|
||||
@@ -0,0 +1,7 @@
|
||||
<iframe
|
||||
class="iframe"
|
||||
src="https://www.youtube.com/embed/dQw4w9WgXcQ"
|
||||
title="YouTube video player"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
@@ -0,0 +1,13 @@
|
||||
:host {
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.iframe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
border-radius: inherit;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
|
||||
@Component({
|
||||
selector: 'widget-network',
|
||||
templateUrl: './network.component.html',
|
||||
styleUrls: ['./network.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class NetworkComponent {}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { NetworkComponent } from './network.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [],
|
||||
declarations: [NetworkComponent],
|
||||
exports: [NetworkComponent],
|
||||
})
|
||||
export class NetworkModule {}
|
||||
@@ -0,0 +1 @@
|
||||
System time and uptime
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
|
||||
@Component({
|
||||
selector: 'widget-uptime',
|
||||
templateUrl: './uptime.component.html',
|
||||
styleUrls: ['./uptime.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class UptimeComponent {}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { UptimeComponent } from './uptime.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [],
|
||||
declarations: [UptimeComponent],
|
||||
exports: [UptimeComponent],
|
||||
})
|
||||
export class UptimeModule {}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Widget } from '../../../services/patch-db/data-model'
|
||||
|
||||
export const BUILT_IN_WIDGETS: readonly Widget[] = [
|
||||
{
|
||||
id: 'favorites',
|
||||
meta: {
|
||||
name: 'Favorites',
|
||||
width: 2,
|
||||
height: 2,
|
||||
mobileWidth: 2,
|
||||
mobileHeight: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'health',
|
||||
meta: {
|
||||
name: 'Service health overview',
|
||||
width: 2,
|
||||
height: 2,
|
||||
mobileWidth: 2,
|
||||
mobileHeight: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'metrics',
|
||||
meta: {
|
||||
name: 'Server metrics',
|
||||
width: 4,
|
||||
height: 1,
|
||||
mobileWidth: 2,
|
||||
mobileHeight: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'network',
|
||||
meta: {
|
||||
name: 'Network',
|
||||
width: 4,
|
||||
height: 2,
|
||||
mobileWidth: 2,
|
||||
mobileHeight: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'uptime',
|
||||
meta: {
|
||||
name: 'System time and uptime',
|
||||
width: 2,
|
||||
height: 2,
|
||||
mobileWidth: 2,
|
||||
mobileHeight: 2,
|
||||
},
|
||||
},
|
||||
]
|
||||
42
frontend/projects/ui/src/app/pages/widgets/widgets.module.ts
Normal file
42
frontend/projects/ui/src/app/pages/widgets/widgets.module.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { TuiLetModule } from '@taiga-ui/cdk'
|
||||
import { TuiLoaderModule } from '@taiga-ui/core'
|
||||
import { TuiTilesModule } from '@taiga-ui/kit'
|
||||
|
||||
import { WidgetsPage } from './widgets.page'
|
||||
import { AddWidgetModule } from './built-in/add/add.module'
|
||||
import { FavoritesModule } from './built-in/favorites/favorites.module'
|
||||
import { HealthModule } from './built-in/health/health.module'
|
||||
import { MetricsModule } from './built-in/metrics/metrics.module'
|
||||
import { NetworkModule } from './built-in/network/network.module'
|
||||
import { UptimeModule } from './built-in/uptime/uptime.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: WidgetsPage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
TuiTilesModule,
|
||||
TuiLetModule,
|
||||
AddWidgetModule,
|
||||
FavoritesModule,
|
||||
HealthModule,
|
||||
MetricsModule,
|
||||
NetworkModule,
|
||||
UptimeModule,
|
||||
RouterModule.forChild(routes),
|
||||
TuiLoaderModule,
|
||||
],
|
||||
declarations: [WidgetsPage],
|
||||
exports: [WidgetsPage],
|
||||
})
|
||||
export class WidgetsPageModule {}
|
||||
55
frontend/projects/ui/src/app/pages/widgets/widgets.page.html
Normal file
55
frontend/projects/ui/src/app/pages/widgets/widgets.page.html
Normal file
@@ -0,0 +1,55 @@
|
||||
<h1 class="heading">
|
||||
<tui-loader
|
||||
*ngIf="pending else buttons"
|
||||
[inheritColor]="true"
|
||||
size="s"
|
||||
class="loader"
|
||||
></tui-loader>
|
||||
<ng-template #buttons>
|
||||
<ion-button fill="clear" color="primary" class="button" (click)="toggle()">
|
||||
<ion-icon
|
||||
[name]="edit ? 'checkmark-outline' : 'pencil-outline'"
|
||||
></ion-icon>
|
||||
{{ edit ? 'Save' : 'Edit'}}
|
||||
</ion-button>
|
||||
<ion-button fill="clear" color="primary" class="button" (click)="add()">
|
||||
<ion-icon name="duplicate-outline"></ion-icon>
|
||||
Add
|
||||
</ion-button>
|
||||
</ng-template>
|
||||
</h1>
|
||||
<!-- TODO: Fix resize lag in Taiga UI -->
|
||||
<tui-tiles
|
||||
class="wrapper"
|
||||
[class.wrapper_wide]="wide"
|
||||
[debounce]="500"
|
||||
[(order)]="order"
|
||||
>
|
||||
<tui-tile
|
||||
*ngFor="let item of items; let index = index; trackBy: trackBy"
|
||||
class="item"
|
||||
[class.item_edit]="edit"
|
||||
[width]="wide ? item.meta.width : item.meta.mobileWidth"
|
||||
[height]="wide ? item.meta.height : item.meta.mobileHeight"
|
||||
[style.order]="order.get(index)"
|
||||
>
|
||||
<div class="content">
|
||||
<ng-container *ngComponentOutlet="components[item.id]"></ng-container>
|
||||
</div>
|
||||
<div tuiTileHandle class="handle"></div>
|
||||
<ion-icon
|
||||
*ngIf="item.id === 'favorites'"
|
||||
name="settings-outline"
|
||||
class="settings"
|
||||
></ion-icon>
|
||||
<ion-icon
|
||||
*ngIf="!pending else loader"
|
||||
name="trash-outline"
|
||||
class="remove"
|
||||
(click)="remove(index)"
|
||||
></ion-icon>
|
||||
<ng-template #loader>
|
||||
<tui-loader [inheritColor]="true" class="pending"></tui-loader>
|
||||
</ng-template>
|
||||
</tui-tile>
|
||||
</tui-tiles>
|
||||
112
frontend/projects/ui/src/app/pages/widgets/widgets.page.scss
Normal file
112
frontend/projects/ui/src/app/pages/widgets/widgets.page.scss
Normal file
@@ -0,0 +1,112 @@
|
||||
:host {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.loader {
|
||||
width: 24px;
|
||||
color: var(--ion-color-tertiary);
|
||||
}
|
||||
|
||||
.heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 44px;
|
||||
font-size: 20px;
|
||||
margin: 14px 0 -20px;
|
||||
padding: 0 40px;
|
||||
|
||||
:host.dialog & {
|
||||
margin: 0 0 -24px;
|
||||
padding: 0;
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
width: 50%;
|
||||
|
||||
ion-icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
gap: 24px;
|
||||
grid-auto-rows: 100px;
|
||||
grid-auto-columns: 1fr;
|
||||
margin: 40px;
|
||||
|
||||
&_wide {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
|
||||
:host.dialog & {
|
||||
margin: 40px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.item {
|
||||
box-shadow: inset 0 0 0 3px rgba(255, 255, 255, 0.1);
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.content {
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
background: #333;
|
||||
border-radius: 24px;
|
||||
padding: 24px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 3px 5px -2px rgba(0, 0, 0, 0.5);
|
||||
transition: opacity 0.3s;
|
||||
|
||||
.item_edit & {
|
||||
opacity: var(--tui-disabled-opacity);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.handle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
cursor: move;
|
||||
|
||||
.item:not(.item_edit) & {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.remove,
|
||||
.settings,
|
||||
.pending {
|
||||
position: absolute;
|
||||
right: 24px;
|
||||
top: 50%;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
transform: translateY(-50%);
|
||||
padding: 10px;
|
||||
box-sizing: content-box;
|
||||
border-radius: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
|
||||
.item_edit & {
|
||||
pointer-events: auto;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.settings {
|
||||
left: 24px;
|
||||
}
|
||||
136
frontend/projects/ui/src/app/pages/widgets/widgets.page.ts
Normal file
136
frontend/projects/ui/src/app/pages/widgets/widgets.page.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
Inject,
|
||||
Input,
|
||||
Optional,
|
||||
Type,
|
||||
} from '@angular/core'
|
||||
import { TuiDialogContext, TuiDialogService } from '@taiga-ui/core'
|
||||
import {
|
||||
PolymorpheusComponent,
|
||||
POLYMORPHEUS_CONTEXT,
|
||||
} from '@tinkoff/ng-polymorpheus'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { DataModel, Widget } from '../../services/patch-db/data-model'
|
||||
import { ApiService } from '../../services/api/embassy-api.service'
|
||||
import { ADD_WIDGET } from './built-in/add/add.component'
|
||||
import { FavoritesComponent } from './built-in/favorites/favorites.component'
|
||||
import { HealthComponent } from './built-in/health/health.component'
|
||||
import { NetworkComponent } from './built-in/network/network.component'
|
||||
import { MetricsComponent } from './built-in/metrics/metrics.component'
|
||||
import { UptimeComponent } from './built-in/uptime/uptime.component'
|
||||
import { take } from 'rxjs/operators'
|
||||
|
||||
@Component({
|
||||
selector: 'widgets',
|
||||
templateUrl: 'widgets.page.html',
|
||||
styleUrls: ['widgets.page.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: {
|
||||
'[class.dialog]': 'context',
|
||||
},
|
||||
})
|
||||
export class WidgetsPage {
|
||||
@Input()
|
||||
wide = false
|
||||
|
||||
edit = false
|
||||
|
||||
order = new Map<number, number>()
|
||||
|
||||
items: readonly Widget[] = []
|
||||
|
||||
pending = true
|
||||
|
||||
readonly components: Record<string, Type<any>> = {
|
||||
health: HealthComponent,
|
||||
favorites: FavoritesComponent,
|
||||
metrics: MetricsComponent,
|
||||
network: NetworkComponent,
|
||||
uptime: UptimeComponent,
|
||||
}
|
||||
|
||||
constructor(
|
||||
@Optional()
|
||||
@Inject(POLYMORPHEUS_CONTEXT)
|
||||
readonly context: TuiDialogContext | null,
|
||||
private readonly dialog: TuiDialogService,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly cdr: ChangeDetectorRef,
|
||||
private readonly api: ApiService,
|
||||
) {
|
||||
this.patch
|
||||
.watch$('ui', 'widgets')
|
||||
.pipe(take(1))
|
||||
.subscribe(items => {
|
||||
this.updateItems(items)
|
||||
this.pending = false
|
||||
})
|
||||
}
|
||||
|
||||
trackBy(_: number, { id }: Widget) {
|
||||
return id
|
||||
}
|
||||
|
||||
toggle() {
|
||||
if (this.edit) {
|
||||
this.updateItems(this.getReordered())
|
||||
}
|
||||
|
||||
this.edit = !this.edit
|
||||
}
|
||||
|
||||
add() {
|
||||
this.dialog.open(ADD_WIDGET, { label: 'Add widget' }).subscribe(widget => {
|
||||
this.addWidget(widget)
|
||||
})
|
||||
}
|
||||
|
||||
remove(index: number) {
|
||||
this.removeWidget(index)
|
||||
}
|
||||
|
||||
private removeWidget(index: number) {
|
||||
this.updateItems(
|
||||
this.getReordered().filter((_, i) => i !== this.order.get(index)),
|
||||
)
|
||||
}
|
||||
|
||||
private addWidget(widget: Widget) {
|
||||
this.updateItems(this.getReordered().concat(widget))
|
||||
}
|
||||
|
||||
private getReordered(): Widget[] {
|
||||
const items: Widget[] = []
|
||||
|
||||
Array.from(this.order.entries()).forEach(([index, order]) => {
|
||||
items[order] = this.items[index]
|
||||
})
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
private updateItems(items: readonly Widget[]) {
|
||||
const previous = this.items
|
||||
|
||||
if (!this.pending) {
|
||||
this.pending = true
|
||||
this.api
|
||||
.setDbValue(['widgets'], items)
|
||||
.catch(() => {
|
||||
this.updateItems(previous)
|
||||
})
|
||||
.finally(() => {
|
||||
this.pending = false
|
||||
this.cdr.markForCheck()
|
||||
})
|
||||
}
|
||||
|
||||
this.items = items
|
||||
this.order = new Map(items.map((_, index) => [index, index]))
|
||||
}
|
||||
}
|
||||
|
||||
export const WIDGETS_COMPONENT = new PolymorpheusComponent(WidgetsPage)
|
||||
@@ -8,11 +8,19 @@ import {
|
||||
PackageState,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { Mock } from './api.fixures'
|
||||
import { BUILT_IN_WIDGETS } from '../../pages/widgets/built-in/widgets'
|
||||
|
||||
export const mockPatchData: DataModel = {
|
||||
ui: {
|
||||
name: `Matt's Embassy`,
|
||||
'ack-welcome': '1.0.0',
|
||||
widgets: BUILT_IN_WIDGETS.filter(
|
||||
({ id }) =>
|
||||
id === 'favorites' ||
|
||||
id === 'health' ||
|
||||
id === 'network' ||
|
||||
id === 'metrics',
|
||||
),
|
||||
marketplace: {
|
||||
'selected-url': 'https://registry.start9.com/',
|
||||
'known-hosts': {
|
||||
|
||||
@@ -1,21 +1,44 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { BehaviorSubject } from 'rxjs'
|
||||
import { ReplaySubject, Subject } from 'rxjs'
|
||||
import { WorkspaceConfig } from '../../../../shared/src/types/workspace-config'
|
||||
import { StorageService } from './storage.service'
|
||||
const SHOW_DEV_TOOLS = 'SHOW_DEV_TOOLS'
|
||||
const SHOW_DISK_REPAIR = 'SHOW_DISK_REPAIR'
|
||||
const WIDGET_DRAWER = 'WIDGET_DRAWER'
|
||||
|
||||
const { enableWidgets } =
|
||||
require('../../../../../config.json') as WorkspaceConfig
|
||||
|
||||
export type WidgetDrawer = {
|
||||
open: boolean
|
||||
width: 400 | 600
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ClientStorageService {
|
||||
readonly showDevTools$ = new BehaviorSubject<boolean>(false)
|
||||
readonly showDiskRepair$ = new BehaviorSubject<boolean>(false)
|
||||
readonly showDevTools$ = new ReplaySubject<boolean>(1)
|
||||
readonly showDiskRepair$ = new ReplaySubject<boolean>(1)
|
||||
readonly widgetDrawer$ = new ReplaySubject<WidgetDrawer>(1)
|
||||
|
||||
constructor(private readonly storage: StorageService) {}
|
||||
|
||||
init() {
|
||||
console.log('EMBAWD', enableWidgets)
|
||||
this.showDevTools$.next(!!this.storage.get(SHOW_DEV_TOOLS))
|
||||
this.showDiskRepair$.next(!!this.storage.get(SHOW_DISK_REPAIR))
|
||||
this.widgetDrawer$.next(
|
||||
enableWidgets
|
||||
? this.storage.get(WIDGET_DRAWER) || {
|
||||
open: true,
|
||||
width: 600,
|
||||
}
|
||||
: {
|
||||
open: false,
|
||||
width: 600,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
toggleShowDevTools(): boolean {
|
||||
@@ -31,4 +54,9 @@ export class ClientStorageService {
|
||||
this.showDiskRepair$.next(newVal)
|
||||
return newVal
|
||||
}
|
||||
|
||||
updateWidgetDrawer(drawer: WidgetDrawer) {
|
||||
this.widgetDrawer$.next(drawer)
|
||||
this.storage.set(WIDGET_DRAWER, drawer)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,20 @@ export interface UIData {
|
||||
}
|
||||
}
|
||||
'ack-instructions': Record<string, boolean>
|
||||
widgets: readonly Widget[]
|
||||
}
|
||||
|
||||
export interface Widget {
|
||||
id: string
|
||||
meta: {
|
||||
name: string
|
||||
width: number
|
||||
height: number
|
||||
mobileWidth: number
|
||||
mobileHeight: number
|
||||
}
|
||||
url?: string
|
||||
settings?: string
|
||||
}
|
||||
|
||||
export interface UIMarketplaceData {
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
DependencyStatus,
|
||||
HealthStatus,
|
||||
PrimaryRendering,
|
||||
PrimaryStatus,
|
||||
renderPkgStatus,
|
||||
StatusRendering,
|
||||
} from '../services/pkg-status-rendering.service'
|
||||
@@ -17,10 +18,12 @@ export function getPackageInfo(entry: PackageDataEntry): PkgInfo {
|
||||
return {
|
||||
entry,
|
||||
primaryRendering,
|
||||
primaryStatus: statuses.primary,
|
||||
installProgress: packageLoadingProgress(entry['install-progress']),
|
||||
error:
|
||||
statuses.health === HealthStatus.Failure ||
|
||||
statuses.dependency === DependencyStatus.Warning,
|
||||
warning: statuses.primary === PrimaryStatus.NeedsConfig,
|
||||
transitioning:
|
||||
primaryRendering.showDots ||
|
||||
statuses.health === HealthStatus.Waiting ||
|
||||
@@ -32,8 +35,10 @@ export function getPackageInfo(entry: PackageDataEntry): PkgInfo {
|
||||
export interface PkgInfo {
|
||||
entry: PackageDataEntry
|
||||
primaryRendering: StatusRendering
|
||||
primaryStatus: PrimaryStatus
|
||||
installProgress: ProgressData | null
|
||||
error: boolean
|
||||
warning: boolean
|
||||
transitioning: boolean
|
||||
sub?: Subscription | null
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ $subheader-height: 48px;
|
||||
}
|
||||
|
||||
.subheader-padding {
|
||||
--padding-top: #{$subheader-height}+10px;
|
||||
--padding-top: #{$subheader-height}+ 10px;
|
||||
}
|
||||
|
||||
.subheader {
|
||||
@@ -188,6 +188,7 @@ ion-icon {
|
||||
}
|
||||
|
||||
ion-toolbar {
|
||||
line-height: 1.1;
|
||||
--min-height: 72px;
|
||||
--background: var(--ion-color-light);
|
||||
}
|
||||
@@ -297,4 +298,15 @@ h2 {
|
||||
|
||||
.fader {
|
||||
animation: flickerAnimation 4s infinite;
|
||||
}
|
||||
|
||||
// TODO: Refactor so header is outside router-outlet
|
||||
ion-content.with-widgets,
|
||||
ion-footer.with-widgets {
|
||||
width: calc(100% - var(--widgets-width));
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 40px;
|
||||
list-style-type: disc;
|
||||
}
|
||||
Reference in New Issue
Block a user