From 82045da5383dd6e8d714a786f8b6791534543a0e Mon Sep 17 00:00:00 2001 From: ChowDPa02K Date: Sun, 19 Apr 2026 14:46:21 +0800 Subject: [PATCH] Update UI for headscale 0.28 API --- README.md | 140 +++++++---- src/app/api.service.ts | 114 ++------- src/app/http-api.interceptor.ts | 11 +- src/app/views/login/login.component.html | 3 +- src/app/views/login/login.component.ts | 20 +- .../machine-manager.component.html | 86 ++----- .../machine-manager.component.ts | 224 +++++++----------- .../user-manager/user-manager.component.html | 16 +- .../user-manager/user-manager.component.ts | 54 ++--- 9 files changed, 277 insertions(+), 391 deletions(-) diff --git a/README.md b/README.md index cc473a2..9acf24a 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,124 @@ -# HeadscaleUi +# Headscale UI [![main](https://github.com/simcu/headscale-ui/actions/workflows/main.yml/badge.svg)](https://github.com/simcu/headscale-ui/actions/workflows/main.yml) -This is a static headscale admin ui, no backend enviroment required +Headscale UI is a static admin interface for Headscale. It does not require its own backend service and can be served by any static web server. -The current release is backward compatible with headscale v0.22 as well as compatible with the future headscale v0.23; +## Compatibility -## Thanks +This fork has been updated for Headscale `v0.28`. -Headscale - https://github.com/juanfont/headscale version: v0.21.0 +Current support status: -UI - https://github.com/NG-ZORRO/ng-zorro-antd version: v15.0.3 +- `v0.28`: supported +- `v0.23` to `v0.27`: login option kept for compatibility, but this release is primarily validated against `v0.28` +- `v0.22` and below: legacy option still exists in the login screen, but not covered by the latest validation -BaseFramework - https://angular.io/ version: 15.2.4 +## What Changed For Headscale v0.28 +Headscale `v0.28` changed several API behaviors compared with older releases. This update aligns the UI with the newer API by: -## ScreenShots +- switching machine operations to the `/api/v1/node` endpoints +- updating user operations to use user IDs where required by newer API handlers +- updating pre-auth key operations to use the current request and response shapes +- updating route approval actions to use node route approval APIs instead of the old route management flow +- adjusting login defaults to prefer `v0.28` +- improving request handling and error messages during login -image -image -image +## API Compatibility Note -## Pre Requirement -1. You need to generate a apikey use headscale-cli -> on your headscale server run: headscale apikeys create -e 9999d -2. You need a static web server space, or use docker -3. (optional) If you deploy it on other domain, you need set headscale api's cors. +This update removes the old version-switching logic for the legacy machine endpoints in the Angular API service. -## Deploy Guide -### A: use static web space -emmm.... I don't know how to describe this action.... it is really very easy... +In practice: -### B: use docker (k8s also, it's a nginx static webserver, no other required) +- the UI now uses the Headscale `v0.28` node APIs as the primary implementation +- legacy `/api/v1/machine/...` request branches were removed from the client service +- legacy standalone route management calls were also removed from the client service +- route updates are now performed through node approved-route APIs -1. run command on your docker enviroment +To reduce UI churn, several Angular service method names still keep the historical `machine*` naming, but they now call `/api/v1/node/...` endpoints internally. -> docker run -d --name headscale-ui -p 8888:80 simcu/headscale-ui +This means the current codebase is no longer a full dual-path implementation for both old `machine` APIs and newer `node` APIs. -2. open http://127.0.0.1:8888/manager/ +## Current Functional Notes -3. (optional) if run it not on same domain with headscale,the system will error, don't warried, only click "Exit" on the top menu, you will redirect to login page. +The `v0.28` flow was validated for: -4. if you deploy this standalone, on login page click "Change Server" then, input server url. -5. input apikey , click "Login" +- login +- user listing, rename, and delete +- node listing +- node registration +- node rename +- node expiry and delete +- route approval and removal through approved routes +- pre-auth key list, create, and expire -### C: use caddy +Known limitation in `v0.28` mode: + +- tag editing is currently shown as read-only in the machine view + +## Requirements + +1. Generate an API key with the Headscale CLI: + + ```bash + headscale apikeys create -e 9999d + ``` + +2. Host the built UI on a static web server, or run it with Docker +3. If the UI is hosted on a different origin from Headscale, configure CORS on the Headscale side + +## Deployment + +### Static Hosting + +Build the application and serve the output directory with any static web server. + +### Docker + +```bash +docker run -d --name headscale-ui -p 8888:80 simcu/headscale-ui +``` + +Then open: + +```text +http://127.0.0.1:8888/manager/ ``` + +If the UI is not hosted on the same origin as Headscale: + +1. open the login page +2. click `Change Server` +3. enter the Headscale server URL +4. enter the API key +5. log in + +### Caddy Example + +```caddy domain.com { - @ui { - path_regexp (/$)|(\.) - } - handle @ui { - root * /www/headscale-ui - try_files {path} /index.html - file_server - } - - reverse_proxy 127.0.0.1:7070 + @ui { + path_regexp (/$)|(\.) + } + + handle @ui { + root * /www/headscale-ui + try_files {path} /index.html + file_server + } + + reverse_proxy 127.0.0.1:7070 } ``` -## Notice -1. ServerUrl and ApiKey are saved in localstorage in plain text. -2. you will only need login once.if apikey is invalid , system will require login again. +## Security Notice + +- `serverUrl` and `apiKey` are stored in `localStorage` in plain text +- users usually only need to log in once +- if the API key becomes invalid, the UI will require login again + +## Credits + +- Headscale: https://github.com/juanfont/headscale +- NG-ZORRO: https://github.com/NG-ZORRO/ng-zorro-antd +- Angular: https://angular.io/ diff --git a/src/app/api.service.ts b/src/app/api.service.ts index 8342cc4..2c15d0e 100644 --- a/src/app/api.service.ts +++ b/src/app/api.service.ts @@ -12,84 +12,36 @@ export class ApiService { ///Machine api start machineList(user: string): Observable { - let version = localStorage.getItem('hsVersion') ?? 'v0.23'; - if (version == 'v0.23'){ - return this.http.get(`/api/v1/node?user=${user}`) - }else{ - return this.http.get(`/api/v1/machine?user=${user}`) - } + const url = user ? `/api/v1/node?user=${encodeURIComponent(user)}` : '/api/v1/node'; + return this.http.get(url); } machineRegister(user: string, key: string): Observable { - let version = localStorage.getItem('hsVersion') ?? 'v0.23'; - if (version == 'v0.23'){ - return this.http.post(`/api/v1/node/register?user=${user}&key=${key}`, null); - }else{ - return this.http.post(`/api/v1/machine/register?user=${user}&key=${key}`, null); - } + return this.http.post(`/api/v1/node/register?user=${encodeURIComponent(user)}&key=${encodeURIComponent(key)}`, null); } machineDetail(machineId: string): Observable { - let version = localStorage.getItem('hsVersion') ?? 'v0.23'; - if (version == 'v0.23'){ - return this.http.get(`/api/v1/node/${machineId}`); - }else{ - return this.http.get(`/api/v1/machine/${machineId}`); - } + return this.http.get(`/api/v1/node/${machineId}`); } machineExpire(machineId: string): Observable { - let version = localStorage.getItem('hsVersion') ?? 'v0.23'; - if (version == 'v0.23'){ - return this.http.post(`/api/v1/node/${machineId}/expire`, null); - }else{ - return this.http.post(`/api/v1/machine/${machineId}/expire`, null); - } + return this.http.post(`/api/v1/node/${machineId}/expire`, null); } machineDelete(machineId: string): Observable { - let version = localStorage.getItem('hsVersion') ?? 'v0.23'; - if (version == 'v0.23'){ - return this.http.delete(`/api/v1/node/${machineId}`); - }else{ - return this.http.delete(`/api/v1/machine/${machineId}`); - } + return this.http.delete(`/api/v1/node/${machineId}`); } machineRename(machineId: string, name: string): Observable { - let version = localStorage.getItem('hsVersion') ?? 'v0.23'; - if (version == 'v0.23'){ - return this.http.post(`/api/v1/node/${machineId}/rename/${name}`, null); - }else{ - return this.http.post(`/api/v1/machine/${machineId}/rename/${name}`, null); - } - } - - machineRoutes(machineId: string): Observable { - let version = localStorage.getItem('hsVersion') ?? 'v0.23'; - if (version == 'v0.23'){ - return this.http.get(`/api/v1/node/${machineId}/routes`); - }else{ - return this.http.get(`/api/v1/machine/${machineId}/routes`); - } + return this.http.post(`/api/v1/node/${machineId}/rename/${encodeURIComponent(name)}`, null); } machineTag(machineId: string, tags: Array): Observable { - let version = localStorage.getItem('hsVersion') ?? 'v0.23'; - if (version == 'v0.23'){ - return this.http.post(`/api/v1/node/${machineId}/tags`, {tags}); - }else{ - return this.http.post(`/api/v1/machine/${machineId}/tags`, {tags}); - } + return this.http.post(`/api/v1/node/${machineId}/tags`, {nodeId: machineId, tags}); } - machineChangeUser(machineId: string, user: string): Observable { - let version = localStorage.getItem('hsVersion') ?? 'v0.23'; - if (version == 'v0.23'){ - return this.http.post(`/api/v1/node/${machineId}/user?user=${user}`, null); - }else{ - return this.http.post(`/api/v1/machine/${machineId}/user?user=${user}`, null); - } + machineSetApprovedRoutes(machineId: string, routes: Array): Observable { + return this.http.post(`/api/v1/node/${machineId}/approve_routes`, {nodeId: machineId, routes}); } @@ -102,51 +54,25 @@ export class ApiService { return this.http.post('/api/v1/user', {name}); } - userDetail(name: string): Observable { - return this.http.get(`/api/v1/user/${name}`); - } - - userDelete(name: string): Observable { - return this.http.delete(`/api/v1/user/${name}`); - } - - userRename(old: string, name: string): Observable { - return this.http.post(`/api/v1/user/${old}/rename/${name}`, {}); + userDelete(id: string): Observable { + return this.http.delete(`/api/v1/user/${id}`); } - - ///route api start - routeList(): Observable { - return this.http.get(`/api/v1/routes`); - } - - routeDelete(id: string): Observable { - return this.http.delete(`/api/v1/routes/${id}`); - } - - routeEnable(id: string): Observable { - return this.http.post(`/api/v1/routes/${id}/enable`, null); - } - - routeDisable(id: string): Observable { - return this.http.post(`/api/v1/routes/${id}/disable`, null); + userRename(id: string, name: string): Observable { + return this.http.post(`/api/v1/user/${id}/rename/${encodeURIComponent(name)}`, {}); } ///preauth key start - preAuthKeyList(user: string): Observable { - var url = `/api/v1/preauthkey` - if (user) { - url = `/api/v1/preauthkey?user=${user}`; - } - return this.http.get(url); + preAuthKeyList(): Observable { + return this.http.get('/api/v1/preauthkey'); } - preAuthKeyAdd(user: string, expiration: string, aclTags: Array = [], reusable = false, ephemeral = false): Observable { - return this.http.post('/api/v1/preauthkey', {user, reusable, ephemeral, aclTags, expiration}) + preAuthKeyAdd(userId: string, expiration: string, aclTags: Array = [], reusable = false, ephemeral = false): Observable { + return this.http.post('/api/v1/preauthkey', {user: userId, reusable, ephemeral, aclTags, expiration}); } - preAuthKeyExpire(user: string, key: string): Observable { - return this.http.post('/api/v1/preauthkey/expire', {user, key}); + preAuthKeyExpire(id: string): Observable { + return this.http.post('/api/v1/preauthkey/expire', {id}); } ///api key start diff --git a/src/app/http-api.interceptor.ts b/src/app/http-api.interceptor.ts index c5794db..f1b474e 100644 --- a/src/app/http-api.interceptor.ts +++ b/src/app/http-api.interceptor.ts @@ -3,22 +3,23 @@ import { HttpRequest, HttpHandler, HttpEvent, - HttpInterceptor, HttpResponse + HttpInterceptor } from '@angular/common/http'; -import {catchError, mergeMap, Observable, of} from 'rxjs'; -import {ActivatedRoute, Router} from '@angular/router'; +import {catchError, Observable, of} from 'rxjs'; +import {Router} from '@angular/router'; import {NzMessageService} from 'ng-zorro-antd/message'; @Injectable() export class HttpApiInterceptor implements HttpInterceptor { - constructor(private router: Router, private msg: NzMessageService, private route: ActivatedRoute) { + constructor(private router: Router, private msg: NzMessageService) { } intercept(request: HttpRequest, next: HttpHandler): Observable> { + const serverKey = localStorage.getItem('serverKey') ?? ''; const resetReq = request.clone({ setHeaders: { - 'Authorization': 'Bearer ' + localStorage.getItem('serverKey') ?? '', + Authorization: serverKey ? `Bearer ${serverKey}` : '', 'Content-Type': 'application/json' }, url: (localStorage.getItem('serverUrl') ?? '') + request.url diff --git a/src/app/views/login/login.component.html b/src/app/views/login/login.component.html index 3843998..7a9f166 100644 --- a/src/app/views/login/login.component.html +++ b/src/app/views/login/login.component.html @@ -48,7 +48,8 @@ nzPlaceHolder="Select Version" style="width: 100%" > - + + diff --git a/src/app/views/login/login.component.ts b/src/app/views/login/login.component.ts index f9d5497..1c0a168 100644 --- a/src/app/views/login/login.component.ts +++ b/src/app/views/login/login.component.ts @@ -9,7 +9,7 @@ import {NzMessageService} from 'ng-zorro-antd/message'; styleUrls: ['./login.component.css'] }) export class LoginComponent implements OnInit { - serverUrl = '/'; + serverUrl = ''; apiKeys = ''; changeServerUrl = ''; hsVersion = ''; @@ -20,8 +20,10 @@ export class LoginComponent implements OnInit { } ngOnInit() { - this.serverUrl = localStorage.getItem('serverUrl') ?? '/'; - let apikey = localStorage.getItem('serverKey') ?? ''; + this.serverUrl = localStorage.getItem('serverUrl') ?? ''; + this.apiKeys = localStorage.getItem('serverKey') ?? ''; + this.hsVersion = localStorage.getItem('hsVersion') ?? 'v0.28'; + let apikey = this.apiKeys; if (apikey) { this.checkLogin(); } @@ -37,10 +39,10 @@ export class LoginComponent implements OnInit { } checkLogin() { - this.api.userList().subscribe(x => { + this.api.userList().subscribe(() => { this.router.navigateByUrl(''); }, error => { - this.msg.error(error.error + 'teste'); + this.msg.error(error.error?.message ?? error.error ?? 'Login failed'); }); } @@ -51,10 +53,8 @@ export class LoginComponent implements OnInit { this.serverUrl = this.changeServerUrl; } - if(!this.hsVersion){ - this.hsVersion = 'v0.23'; - } else { - this.hsVersion = this.hsVersion + if (!this.hsVersion) { + this.hsVersion = 'v0.28'; } localStorage.setItem('serverUrl', this.serverUrl); @@ -63,8 +63,8 @@ export class LoginComponent implements OnInit { } showChangeServer() { - this.changeServerUrl = localStorage.getItem('serverUrl') ?? ''; + this.hsVersion = localStorage.getItem('hsVersion') ?? 'v0.28'; this.changeServerShow = true; } diff --git a/src/app/views/machine-manager/machine-manager.component.html b/src/app/views/machine-manager/machine-manager.component.html index 5519c34..d0e9a9f 100644 --- a/src/app/views/machine-manager/machine-manager.component.html +++ b/src/app/views/machine-manager/machine-manager.component.html @@ -25,7 +25,7 @@ - + {{ data.id }} @@ -39,18 +39,18 @@ Offline - SubNet {{data.subnets_enabled_num}}/{{data.subnets.length}} + {{routeSummaryLabel(data, 'subnet')}} - SubNet {{data.subnets_enabled_num}}/{{data.subnets.length}} + {{routeSummaryLabel(data, 'subnet')}} - ExitNode {{data.exitNode_enabled_num}}/{{data.exitNodes.length}} + {{routeSummaryLabel(data, 'exitNode')}} - ExitNode {{data.exitNode_enabled_num}}/{{data.exitNodes.length}} + {{routeSummaryLabel(data, 'exitNode')}} @@ -69,10 +69,7 @@ Rename Machine
  • - Edit Tags -
  • -
  • - Change Owner + Tags are read-only in 0.28 mode
  • @@ -90,27 +87,16 @@ nzTitle="Created Time">{{data.createdAt|date:'yyyy-MM-dd HH:mm:ss'}} {{data.lastSeen|date:'yyyy-MM-dd HH:mm:ss'}} - {{data.lastSuccessfulUpdate|date:'yyyy-MM-dd HH:mm:ss'}} {{data.registerMethod}} - - None - {{t}} - - - None - {{t}} - - - None - {{t}} + + {{tagLabel(data)}}
    - + IPv4 IPv6  @@ -120,24 +106,14 @@ - - - + + - - - - + {{sni.prefix}}  Enabled @@ -147,19 +123,9 @@ - - - + + - - - @@ -169,27 +135,3 @@ - - - - - - - - - - - {{tag}} - - - New Tag - - - - diff --git a/src/app/views/machine-manager/machine-manager.component.ts b/src/app/views/machine-manager/machine-manager.component.ts index 402ec3a..ce3ddfa 100644 --- a/src/app/views/machine-manager/machine-manager.component.ts +++ b/src/app/views/machine-manager/machine-manager.component.ts @@ -1,4 +1,4 @@ -import {Component, ElementRef, OnInit, ViewChild, ViewContainerRef} from '@angular/core'; +import {Component, OnInit, ViewContainerRef} from '@angular/core'; import {ApiService} from '../../api.service'; import {ActivatedRoute, Router} from '@angular/router'; import {OneInputComponent} from '../../components/one-input/one-input.component'; @@ -17,15 +17,6 @@ export class MachineManagerComponent implements OnInit { user = ''; users: Array = []; - changeOwnerMachine: any = {}; - tagEditMachine: any = {} - changeOwnerUser: string = ''; - - tags = ['Unremovable', 'Tag 2', 'Tag 3']; - inputVisible = false; - inputValue = ''; - @ViewChild('inputElement', {static: false}) inputElement?: ElementRef; - constructor(private api: ApiService, private route: ActivatedRoute, private msg: NzMessageService, private modal: NzModalService, private viewContainerRef: ViewContainerRef, private router: Router) { @@ -50,77 +41,100 @@ export class MachineManagerComponent implements OnInit { } getList() { - let version = localStorage.getItem('serverUrl') ?? 'v0.23'; - - this.api.machineList(this.user).subscribe(x => { - if (version === 'v0.23'){ - this.machines = x.nodes.sort((a: any, b: any) => parseInt(a.id) - parseInt(b.id)); - }else{ - this.machines = x.machines.sort((a: any, b: any) => parseInt(a.id) - parseInt(b.id)); - } - this.getRoutes(); - if (this.tagEditMachine.id) { - this.tagEditMachine = this.machines.find(x => x.id == this.tagEditMachine.id); - } - }); - - - } - - getRoutes() { - this.api.routeList().subscribe(x => { - for (let r of x.routes) { - let m = this.machines.find(x => x.id === r.node.id); - if (m) { - if (['0.0.0.0/0', '::/0'].indexOf(r.prefix) !== -1) { - if (m['exitNodes']) { - m['exitNodes'].push(r); - } else { - m['exitNodes'] = [r] - } - } else { - if (m['subnets']) { - m['subnets'].push(r); - } else { - m['subnets'] = [r] - } - } - } - } - for (let m of this.machines) { - if (m.subnets) { - m['subnets_enabled_num'] = m.subnets.filter((sn: { enabled: any; }) => sn.enabled).length; - } - if (m.exitNodes) { - m['exitNode_enabled_num'] = m.exitNodes.filter((sn: { enabled: any; }) => sn.enabled).length; - } - } - }) + this.api.machineList(this.user).subscribe(x => { + this.machines = (x.nodes ?? []) + .map((node: any) => this.normalizeMachine(node)) + .sort((a: any, b: any) => parseInt(a.id) - parseInt(b.id)); + }); } - onExpandChange(id: string, checked: boolean): void { - if (checked) { - this.expandSet.add(id); - } else { - this.expandSet.delete(id); + normalizeMachine(node: any) { + const approvedRoutes = node.approvedRoutes ?? []; + const routePrefixes = Array.from(new Set([ + ...(node.availableRoutes ?? []), + ...approvedRoutes, + ...(node.subnetRoutes ?? []) + ])); + const routes = routePrefixes.map((prefix: string) => ({ + nodeId: node.id, + prefix, + advertised: (node.availableRoutes ?? []).includes(prefix) || (node.subnetRoutes ?? []).includes(prefix), + enabled: approvedRoutes.includes(prefix), + isPrimary: false + })); + const exitNodes = routes.filter(route => ['0.0.0.0/0', '::/0'].includes(route.prefix)); + const subnets = routes.filter(route => !['0.0.0.0/0', '::/0'].includes(route.prefix)); + + return { + ...node, + tags: node.tags ?? [], + exitNodes, + subnets, + subnets_enabled_num: subnets.filter(route => route.enabled).length, + exitNode_enabled_num: exitNodes.filter(route => route.enabled).length, + lastSuccessfulUpdate: null + }; + } + + setRouteState(machine: any, prefix: string, enabled: boolean) { + const currentRoutes = machine.approvedRoutes ?? []; + const nextRoutes = enabled + ? Array.from(new Set([...currentRoutes, prefix])) + : currentRoutes.filter((route: string) => route !== prefix); + + this.api.machineSetApprovedRoutes(machine.id, nextRoutes).subscribe(() => { + this.msg.success(`Route ${enabled ? 'Enable' : 'Disable'} success`); + this.getList(); + }); + } + + enableRoute(machine: any, prefix: string) { + this.setRouteState(machine, prefix, true); + } + + disableRoute(machine: any, prefix: string) { + this.setRouteState(machine, prefix, false); + } + + routeTrackBy(_: number, route: any) { + return `${route.nodeId}:${route.prefix}`; + } + + machineTrackBy(_: number, machine: any) { + return machine.id; + } + + routeSummaryLabel(machine: any, type: 'subnet' | 'exitNode') { + if (type === 'subnet') { + return `SubNet ${machine.subnets_enabled_num}/${machine.subnets.length}`; } + + return `ExitNode ${machine.exitNode_enabled_num}/${machine.exitNodes.length}`; + } + + tagLabel(machine: any) { + if (machine.tags.length === 0) { + return 'None'; + } + + return machine.tags.join(', '); } renameMachine(machine: any) { this.modal.create({ - nzTitle: `Rename User - ${machine.givenName}`, + nzTitle: `Rename Machine - ${machine.givenName}`, nzComponentParams: {notice: machine.givenName}, nzContent: OneInputComponent, nzViewContainerRef: this.viewContainerRef, nzFooter: null }).afterClose.subscribe(x => { if (x) { - this.api.machineRename(machine.id, x).subscribe(x => { + this.api.machineRename(machine.id, x).subscribe(() => { this.msg.success('Machine Rename success'); this.getList(); - }) + }); } - }) + }); } deleteMachine(machine: any) { @@ -134,30 +148,17 @@ export class MachineManagerComponent implements OnInit { this.api.machineDelete(machine.id).subscribe(_ => { this.msg.success('Machine delete success'); this.getList(); - }) + }); } }); } - enableRoute(id: string) { - this.api.routeEnable(id).subscribe(x => { - this.msg.success('Route Enable success'); - this.getList(); - }) - } - - disableRoute(id: string) { - this.api.routeDisable(id).subscribe(x => { - this.msg.success('Route Disable success'); - this.getList(); - }) - } - - deleteRoute(id: string) { - this.api.routeDelete(id).subscribe(x => { - this.msg.success('Route Delete success'); - this.getList(); - }) + onExpandChange(id: string, checked: boolean): void { + if (checked) { + this.expandSet.add(id); + } else { + this.expandSet.delete(id); + } } userChange(e: any) { @@ -176,61 +177,16 @@ export class MachineManagerComponent implements OnInit { this.modal.create({ nzTitle: `Register Machine For User: ${this.user}`, nzContent: OneInputComponent, - nzComponentParams: {notice: 'nodekey:xxxxxxxxxxxxxxxxxxxxxx'}, + nzComponentParams: {notice: 'Registration ID (24 chars)'}, nzViewContainerRef: this.viewContainerRef, nzFooter: null }).afterClose.subscribe(x => { if (x) { - this.api.machineRegister(this.user, 'nodekey:' + x.replace('nodekey:', '')).subscribe(x => { + this.api.machineRegister(this.user, x.trim()).subscribe(() => { this.msg.success('Register machine success'); this.getList(); - }) + }); } - }) - } - - changeOwner(m: any) { - this.changeOwnerMachine = m; - } - - doChangeOwner() { - if (!this.changeOwnerUser) { - this.msg.error('Must select one user') - return; - } - this.api.machineChangeUser(this.changeOwnerMachine.id, this.changeOwnerUser).subscribe(_ => { - this.msg.success('Machine change owner success') - this.getList(); - this.changeOwnerMachine = {} - }) - } - - handleClose(removedTag: {}): void { - this.tagEditMachine.forcedTags = this.tagEditMachine.forcedTags.filter((tag: {}) => tag !== removedTag); - this.updateTags(this.tagEditMachine.forcedTags); - } - - showInput(): void { - this.inputVisible = true; - setTimeout(() => { - this.inputElement?.nativeElement.focus(); - }, 10); - } - - handleInputConfirm(): void { - if (this.inputValue && this.tagEditMachine.forcedTags.indexOf(this.inputValue) === -1) { - this.tagEditMachine.forcedTags = [...this.tagEditMachine.forcedTags, this.inputValue]; - } - this.inputValue = ''; - this.inputVisible = false; - this.updateTags(this.tagEditMachine.forcedTags); - } - - updateTags(tags: Array) { - this.api.machineTag(this.tagEditMachine.id, tags).subscribe(_ => { - }, _ => { - }, () => { - this.getList(); - }) + }); } } diff --git a/src/app/views/user-manager/user-manager.component.html b/src/app/views/user-manager/user-manager.component.html index 53d3909..3889476 100644 --- a/src/app/views/user-manager/user-manager.component.html +++ b/src/app/views/user-manager/user-manager.component.html @@ -17,13 +17,13 @@ - + {{ data.id }} {{ data.name }} {{ data.createdAt | date: 'yyyy-MM-dd HH:mm:ss' }} -