mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +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
@@ -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)
|
||||
Reference in New Issue
Block a user