mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 12:11:56 +00:00
feat: add mobile view for dashboard (#2581)
This commit is contained in:
@@ -59,6 +59,10 @@ import { NotificationService } from '../../services/notification.service'
|
|||||||
padding: 0 0.5rem 0 1.75rem;
|
padding: 0 0.5rem 0 1.75rem;
|
||||||
--clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 1.75rem 100%);
|
--clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 1.75rem 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host-context(tui-root._mobile) tui-badged-content {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
],
|
],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { AsyncPipe } from '@angular/common'
|
||||||
|
import { Component, inject } from '@angular/core'
|
||||||
|
import { RouterLink, RouterLinkActive } from '@angular/router'
|
||||||
|
import { TuiTabBarModule } from '@taiga-ui/addon-mobile'
|
||||||
|
import { combineLatest, map, startWith } from 'rxjs'
|
||||||
|
import { SYSTEM_UTILITIES } from 'src/app/apps/portal/constants/system-utilities'
|
||||||
|
import { BadgeService } from 'src/app/apps/portal/services/badge.service'
|
||||||
|
import { NotificationService } from 'src/app/apps/portal/services/notification.service'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
selector: 'app-tabs',
|
||||||
|
template: `
|
||||||
|
<nav tuiTabBar>
|
||||||
|
<a
|
||||||
|
tuiTabBarItem
|
||||||
|
icon="tuiIconGrid"
|
||||||
|
routerLink="/portal/dashboard"
|
||||||
|
routerLinkActive
|
||||||
|
[routerLinkActiveOptions]="{ exact: true }"
|
||||||
|
>
|
||||||
|
Services
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
tuiTabBarItem
|
||||||
|
icon="tuiIconActivity"
|
||||||
|
routerLink="/portal/dashboard"
|
||||||
|
routerLinkActive
|
||||||
|
[routerLinkActiveOptions]="{ exact: true }"
|
||||||
|
[queryParams]="{ tab: 'metrics' }"
|
||||||
|
>
|
||||||
|
Metrics
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
tuiTabBarItem
|
||||||
|
icon="tuiIconSettings"
|
||||||
|
routerLink="/portal/dashboard"
|
||||||
|
routerLinkActive
|
||||||
|
[routerLinkActiveOptions]="{ exact: true }"
|
||||||
|
[queryParams]="{ tab: 'utilities' }"
|
||||||
|
[badge]="(utils$ | async) || 0"
|
||||||
|
>
|
||||||
|
Utilities
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
tuiTabBarItem
|
||||||
|
routerLinkActive
|
||||||
|
routerLink="/portal/system/notifications"
|
||||||
|
icon="tuiIconBell"
|
||||||
|
[badge]="(notification$ | async) || 0"
|
||||||
|
>
|
||||||
|
Notifications
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
`,
|
||||||
|
styles: `
|
||||||
|
:host {
|
||||||
|
display: none;
|
||||||
|
// TODO: Theme
|
||||||
|
--tui-elevation-01: #333;
|
||||||
|
--tui-base-04: var(--tui-clear);
|
||||||
|
backdrop-filter: blur(1rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
[tuiTabBar]::before {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(tui-root._mobile) {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
imports: [AsyncPipe, RouterLink, RouterLinkActive, TuiTabBarModule],
|
||||||
|
})
|
||||||
|
export class TabsComponent {
|
||||||
|
private readonly badge = inject(BadgeService)
|
||||||
|
|
||||||
|
readonly utils$ = combineLatest(
|
||||||
|
Object.keys(SYSTEM_UTILITIES)
|
||||||
|
.filter(key => key !== '/portal/system/notifications')
|
||||||
|
.map(key => this.badge.getCount(key).pipe(startWith(0))),
|
||||||
|
).pipe(map(values => values.reduce((acc, value) => acc + value, 0)))
|
||||||
|
readonly notification$ = inject(NotificationService).unreadCount$
|
||||||
|
}
|
||||||
@@ -1,10 +1,16 @@
|
|||||||
import { CommonModule } from '@angular/common'
|
import { CommonModule } from '@angular/common'
|
||||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
|
||||||
import { NavigationEnd, Router, RouterOutlet } from '@angular/router'
|
import {
|
||||||
|
ActivatedRoute,
|
||||||
|
NavigationEnd,
|
||||||
|
Router,
|
||||||
|
RouterOutlet,
|
||||||
|
} from '@angular/router'
|
||||||
import { tuiDropdownOptionsProvider } from '@taiga-ui/core'
|
import { tuiDropdownOptionsProvider } from '@taiga-ui/core'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { filter } from 'rxjs'
|
import { filter, map } from 'rxjs'
|
||||||
|
import { TabsComponent } from 'src/app/apps/portal/components/tabs.component'
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
import { HeaderComponent } from './components/header/header.component'
|
import { HeaderComponent } from './components/header/header.component'
|
||||||
import { BreadcrumbsService } from './services/breadcrumbs.service'
|
import { BreadcrumbsService } from './services/breadcrumbs.service'
|
||||||
@@ -13,7 +19,8 @@ import { BreadcrumbsService } from './services/breadcrumbs.service'
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
template: `
|
template: `
|
||||||
<header appHeader>{{ name$ | async }}</header>
|
<header appHeader>{{ name$ | async }}</header>
|
||||||
<main><router-outlet /></main>
|
<main [attr.data-dashboard]="tab$ | async"><router-outlet /></main>
|
||||||
|
<app-tabs />
|
||||||
`,
|
`,
|
||||||
styles: [
|
styles: [
|
||||||
`
|
`
|
||||||
@@ -30,7 +37,7 @@ import { BreadcrumbsService } from './services/breadcrumbs.service'
|
|||||||
`,
|
`,
|
||||||
],
|
],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [CommonModule, RouterOutlet, HeaderComponent],
|
imports: [CommonModule, RouterOutlet, HeaderComponent, TabsComponent],
|
||||||
providers: [
|
providers: [
|
||||||
// TODO: Move to global
|
// TODO: Move to global
|
||||||
tuiDropdownOptionsProvider({
|
tuiDropdownOptionsProvider({
|
||||||
@@ -51,4 +58,7 @@ export class PortalComponent {
|
|||||||
})
|
})
|
||||||
|
|
||||||
readonly name$ = inject(PatchDB<DataModel>).watch$('ui', 'name')
|
readonly name$ = inject(PatchDB<DataModel>).watch$('ui', 'name')
|
||||||
|
readonly tab$ = inject(ActivatedRoute).queryParams.pipe(
|
||||||
|
map(params => params['tab']),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,18 @@ import { Manifest } from '@start9labs/marketplace'
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [TuiButtonModule, UILaunchComponent, TuiLetModule, AsyncPipe],
|
imports: [TuiButtonModule, UILaunchComponent, TuiLetModule, AsyncPipe],
|
||||||
providers: [tuiButtonOptionsProvider({ size: 's', appearance: 'none' })],
|
providers: [tuiButtonOptionsProvider({ size: 's', appearance: 'none' })],
|
||||||
styles: ':host { padding: 0; border: none }',
|
styles: `
|
||||||
|
:host {
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(tui-root._mobile) {
|
||||||
|
button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
})
|
})
|
||||||
export class ControlsComponent {
|
export class ControlsComponent {
|
||||||
private readonly errors = inject(DepErrorService)
|
private readonly errors = inject(DepErrorService)
|
||||||
|
|||||||
@@ -74,6 +74,44 @@ import { UtilitiesComponent } from './utilities.component'
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host-context(tui-root._mobile) {
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
border-top: 0;
|
||||||
|
|
||||||
|
app-metrics,
|
||||||
|
app-utilities,
|
||||||
|
app-services {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
time,
|
||||||
|
h2 {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(tui-root._mobile [data-dashboard='metrics']) {
|
||||||
|
app-metrics {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(tui-root._mobile [data-dashboard='utilities']) {
|
||||||
|
app-utilities {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(tui-root._mobile main:not([data-dashboard])) {
|
||||||
|
app-services {
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
imports: [
|
imports: [
|
||||||
ServicesComponent,
|
ServicesComponent,
|
||||||
|
|||||||
@@ -36,6 +36,11 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host-context(tui-root._mobile) {
|
||||||
|
--clip-path: none !important;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -18,18 +18,23 @@ import { getManifest } from 'src/app/util/get-package-data'
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
selector: 'tr[appService]',
|
selector: 'tr[appService]',
|
||||||
template: `
|
template: `
|
||||||
<td><img alt="logo" [src]="pkg.icon" /></td>
|
<td [style.grid-area]="'1 / 1 / 4'">
|
||||||
<td>
|
<a [routerLink]="routerLink"><img alt="logo" [src]="pkg.icon" /></a>
|
||||||
|
</td>
|
||||||
|
<td [style.grid-area]="'1 / 2'">
|
||||||
<a [routerLink]="routerLink">{{ manifest.title }}</a>
|
<a [routerLink]="routerLink">{{ manifest.title }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ manifest.version }}</td>
|
<td [style.grid-area]="'2 / 2'">{{ manifest.version }}</td>
|
||||||
<td appStatus [pkg]="pkg" [hasDepErrors]="hasError(depErrors)"></td>
|
<td
|
||||||
<td [style.text-align]="'center'">
|
[style.grid-area]="'3 / 2'"
|
||||||
|
appStatus
|
||||||
|
[pkg]="pkg"
|
||||||
|
[hasDepErrors]="hasError(depErrors)"
|
||||||
|
></td>
|
||||||
|
<td [style.grid-area]="'2 / 3'" [style.text-align]="'center'">
|
||||||
<fieldset
|
<fieldset
|
||||||
appControls
|
appControls
|
||||||
[disabled]="
|
[disabled]="!installed || !(connected$ | async)"
|
||||||
pkg.stateInfo.state !== 'installed' || !(connected$ | async)
|
|
||||||
"
|
|
||||||
[pkg]="pkg"
|
[pkg]="pkg"
|
||||||
></fieldset>
|
></fieldset>
|
||||||
</td>
|
</td>
|
||||||
@@ -47,6 +52,32 @@ import { getManifest } from 'src/app/util/get-package-data'
|
|||||||
|
|
||||||
a {
|
a {
|
||||||
color: var(--tui-text-01);
|
color: var(--tui-text-01);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(tui-root._mobile) {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
grid-template: 2rem 2rem 2rem/6rem 1fr 2rem;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 4rem;
|
||||||
|
width: 4rem;
|
||||||
|
margin: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
@@ -61,6 +92,10 @@ export class ServiceComponent {
|
|||||||
|
|
||||||
readonly connected$ = inject(ConnectionService).connected$
|
readonly connected$ = inject(ConnectionService).connected$
|
||||||
|
|
||||||
|
get installed(): boolean {
|
||||||
|
return this.pkg.stateInfo.state !== 'installed'
|
||||||
|
}
|
||||||
|
|
||||||
get manifest() {
|
get manifest() {
|
||||||
return getManifest(this.pkg)
|
return getManifest(this.pkg)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,6 +78,20 @@ import { ToManifestPipe } from '../../pipes/to-manifest'
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host-context(tui-root._mobile) {
|
||||||
|
--clip-path: none !important;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [ServiceComponent, AsyncPipe, ToManifestPipe],
|
imports: [ServiceComponent, AsyncPipe, ToManifestPipe],
|
||||||
|
|||||||
@@ -37,6 +37,17 @@ import { InstallingProgressDisplayPipe } from '../service/pipes/install-progress
|
|||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
height: 3rem;
|
height: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host-context(tui-root._mobile) {
|
||||||
|
height: auto;
|
||||||
|
|
||||||
|
tui-loader,
|
||||||
|
tui-icon {
|
||||||
|
position: absolute;
|
||||||
|
top: 1rem;
|
||||||
|
left: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [TuiIconModule, TuiLoaderModule],
|
imports: [TuiIconModule, TuiLoaderModule],
|
||||||
|
|||||||
@@ -54,6 +54,11 @@ import {
|
|||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
styles: `
|
||||||
|
:host-context(tui-root._mobile) *:before {
|
||||||
|
font-size: 1.5rem !important;
|
||||||
|
}
|
||||||
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [TuiButtonModule, TuiHostedDropdownModule, TuiDataListModule],
|
imports: [TuiButtonModule, TuiHostedDropdownModule, TuiDataListModule],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import { BadgeService } from 'src/app/apps/portal/services/badge.service'
|
|||||||
}
|
}
|
||||||
|
|
||||||
.links {
|
.links {
|
||||||
|
width: 100%;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template: 1fr 1fr / 1fr 1fr 1fr;
|
grid-template: 1fr 1fr / 1fr 1fr 1fr;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
@@ -77,6 +78,20 @@ import { BadgeService } from 'src/app/apps/portal/services/badge.service'
|
|||||||
background: var(--tui-clear);
|
background: var(--tui-clear);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host-context(tui-root._mobile) {
|
||||||
|
--clip-path: none !important;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.links {
|
||||||
|
grid-template: 1fr 1fr/1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
font-size: 1rem;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [TuiIconModule, RouterLink, TuiBadgeNotificationModule, AsyncPipe],
|
imports: [TuiIconModule, RouterLink, TuiBadgeNotificationModule, AsyncPipe],
|
||||||
|
|||||||
@@ -53,6 +53,15 @@ import { LogsComponent } from '../../../components/logs/logs.component'
|
|||||||
logs {
|
logs {
|
||||||
height: calc(100% - 4rem);
|
height: calc(100% - 4rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host-context(tui-root._mobile) {
|
||||||
|
--clip-path: none;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 1rem 1rem 0;
|
||||||
|
border: 0.375rem solid transparent;
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
|
|||||||
Reference in New Issue
Block a user