|
|
|
|
@@ -1,5 +1,5 @@
|
|
|
|
|
import { Component, OnInit, OnDestroy, inject } from '@angular/core';
|
|
|
|
|
import { NgIf, NgFor, DatePipe } from '@angular/common';
|
|
|
|
|
import { NgIf, NgFor, NgSwitch, NgSwitchCase, NgSwitchDefault, DatePipe } from '@angular/common';
|
|
|
|
|
import { FormsModule } from '@angular/forms';
|
|
|
|
|
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
|
|
|
|
import { MatToolbarModule } from '@angular/material/toolbar';
|
|
|
|
|
@@ -7,6 +7,7 @@ import { MatButtonModule } from '@angular/material/button';
|
|
|
|
|
import { MatIconModule } from '@angular/material/icon';
|
|
|
|
|
import { MatInputModule } from '@angular/material/input';
|
|
|
|
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
|
|
|
import { MatSelectModule } from '@angular/material/select';
|
|
|
|
|
import { MatCardModule } from '@angular/material/card';
|
|
|
|
|
import { MatDividerModule } from '@angular/material/divider';
|
|
|
|
|
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
|
|
|
|
@@ -15,16 +16,16 @@ import { Subject } from 'rxjs';
|
|
|
|
|
import { takeUntil } from 'rxjs/operators';
|
|
|
|
|
import { SiteService } from '../../core/services/site.service';
|
|
|
|
|
import { DeploymentService } from '../../core/services/deployment.service';
|
|
|
|
|
import { SiteResponse } from '../../core/models/site';
|
|
|
|
|
import { SelfHostedConfigResponse } from '../../core/models/deployment';
|
|
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
|
selector: 'app-settings',
|
|
|
|
|
standalone: true,
|
|
|
|
|
imports: [
|
|
|
|
|
NgIf, NgFor, DatePipe, FormsModule, RouterModule,
|
|
|
|
|
NgIf, NgFor, NgSwitch, NgSwitchCase, NgSwitchDefault, DatePipe, FormsModule, RouterModule,
|
|
|
|
|
MatToolbarModule, MatButtonModule, MatIconModule,
|
|
|
|
|
MatInputModule, MatFormFieldModule, MatCardModule,
|
|
|
|
|
MatDividerModule, MatSnackBarModule, MatProgressSpinnerModule,
|
|
|
|
|
MatInputModule, MatFormFieldModule, MatSelectModule,
|
|
|
|
|
MatCardModule, MatDividerModule, MatSnackBarModule, MatProgressSpinnerModule,
|
|
|
|
|
],
|
|
|
|
|
template: `
|
|
|
|
|
<div class="h-screen flex flex-col bg-gray-50">
|
|
|
|
|
@@ -66,6 +67,38 @@ import { SiteResponse } from '../../core/models/site';
|
|
|
|
|
</mat-card-content>
|
|
|
|
|
</mat-card>
|
|
|
|
|
|
|
|
|
|
<mat-card class="p-4" appearance="outlined">
|
|
|
|
|
<mat-card-content class="!p-0">
|
|
|
|
|
<h4 class="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-4">Deployment Target</h4>
|
|
|
|
|
<p class="text-sm text-gray-600 mb-3">
|
|
|
|
|
Choose where your site is published. Git push always runs if a remote is configured.
|
|
|
|
|
</p>
|
|
|
|
|
<mat-form-field appearance="outline" class="w-full">
|
|
|
|
|
<mat-label>Target</mat-label>
|
|
|
|
|
<mat-select [(ngModel)]="deploymentTarget" (selectionChange)="onDeploymentTargetChange()">
|
|
|
|
|
<mat-option value="NONE">None</mat-option>
|
|
|
|
|
<mat-option value="GIT_PAGES">GitHub / GitLab Pages</mat-option>
|
|
|
|
|
<mat-option value="SELF_HOSTED">Self-Hosted Server</mat-option>
|
|
|
|
|
</mat-select>
|
|
|
|
|
</mat-form-field>
|
|
|
|
|
<div class="text-xs text-gray-500 space-y-1 mt-1">
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
<span class="w-3 h-3 rounded-full shrink-0"
|
|
|
|
|
[class.bg-green-500]="hasGitRemote"
|
|
|
|
|
[class.bg-gray-300]="!hasGitRemote"></span>
|
|
|
|
|
<span>Git Remote: {{ hasGitRemote ? 'Connected' : 'Not configured' }}</span>
|
|
|
|
|
<button mat-button (click)="goToGitSettings()" class="!text-xs !min-w-0 !px-1">Manage</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
<span class="w-3 h-3 rounded-full shrink-0"
|
|
|
|
|
[class.bg-green-500]="hasSelfHostedConfig"
|
|
|
|
|
[class.bg-gray-300]="!hasSelfHostedConfig"></span>
|
|
|
|
|
<span>Self-Hosted: {{ hasSelfHostedConfig ? 'Configured' : 'Not configured' }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</mat-card-content>
|
|
|
|
|
</mat-card>
|
|
|
|
|
|
|
|
|
|
<mat-card class="p-4" appearance="outlined">
|
|
|
|
|
<mat-card-content class="!p-0">
|
|
|
|
|
<h4 class="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-4">Export</h4>
|
|
|
|
|
@@ -92,7 +125,13 @@ import { SiteResponse } from '../../core/models/site';
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<p class="text-sm text-gray-600 mb-3" *ngIf="siteStatus === 'DRAFT'">
|
|
|
|
|
Publish your site to make it live at your subdomain. The latest version will be rendered and deployed.
|
|
|
|
|
Publish your site to make it live
|
|
|
|
|
<ng-container [ngSwitch]="deploymentTarget">
|
|
|
|
|
<span *ngSwitchCase="'GIT_PAGES'">on GitHub/GitLab Pages</span>
|
|
|
|
|
<span *ngSwitchCase="'SELF_HOSTED'">on your own server</span>
|
|
|
|
|
<span *ngSwitchDefault>.</span>
|
|
|
|
|
</ng-container>
|
|
|
|
|
. The latest version will be rendered and deployed.
|
|
|
|
|
</p>
|
|
|
|
|
<p class="text-sm text-gray-600 mb-3" *ngIf="siteStatus === 'PUBLISHED' && liveUrl">
|
|
|
|
|
Your site is live at <a [href]="liveUrl" target="_blank" class="text-blue-600 underline">{{ liveUrl }}</a>
|
|
|
|
|
@@ -114,6 +153,66 @@ import { SiteResponse } from '../../core/models/site';
|
|
|
|
|
</mat-card-content>
|
|
|
|
|
</mat-card>
|
|
|
|
|
|
|
|
|
|
<mat-card class="p-4" appearance="outlined" *ngIf="deploymentTarget === 'SELF_HOSTED'">
|
|
|
|
|
<mat-card-content class="!p-0">
|
|
|
|
|
<h4 class="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-4">Self-Hosted Server</h4>
|
|
|
|
|
<div class="space-y-3">
|
|
|
|
|
<mat-form-field appearance="outline" class="w-full">
|
|
|
|
|
<mat-label>Domain</mat-label>
|
|
|
|
|
<input matInput [(ngModel)]="shDomain" [disabled]="shSaving" placeholder="www.example.com" />
|
|
|
|
|
</mat-form-field>
|
|
|
|
|
<div class="flex gap-3">
|
|
|
|
|
<mat-form-field appearance="outline" class="flex-1">
|
|
|
|
|
<mat-label>SSH Host</mat-label>
|
|
|
|
|
<input matInput [(ngModel)]="shSshHost" [disabled]="shSaving" placeholder="192.168.1.1" />
|
|
|
|
|
</mat-form-field>
|
|
|
|
|
<mat-form-field appearance="outline" class="w-24">
|
|
|
|
|
<mat-label>Port</mat-label>
|
|
|
|
|
<input matInput type="number" [(ngModel)]="shSshPort" [disabled]="shSaving" />
|
|
|
|
|
</mat-form-field>
|
|
|
|
|
</div>
|
|
|
|
|
<mat-form-field appearance="outline" class="w-full">
|
|
|
|
|
<mat-label>SSH User</mat-label>
|
|
|
|
|
<input matInput [(ngModel)]="shSshUser" [disabled]="shSaving" placeholder="deploy" />
|
|
|
|
|
</mat-form-field>
|
|
|
|
|
<mat-form-field appearance="outline" class="w-full">
|
|
|
|
|
<mat-label>Deploy Path</mat-label>
|
|
|
|
|
<input matInput [(ngModel)]="shDeployPath" [disabled]="shSaving" />
|
|
|
|
|
<mat-hint>Use {{ '{' }}domain{{ '}' }} as placeholder: /var/www/{{ '{' }}domain{{ '}' }}/public</mat-hint>
|
|
|
|
|
</mat-form-field>
|
|
|
|
|
<mat-form-field appearance="outline" class="w-full">
|
|
|
|
|
<mat-label>Nginx Sites Path</mat-label>
|
|
|
|
|
<input matInput [(ngModel)]="shNginxSitesPath" [disabled]="shSaving" />
|
|
|
|
|
<mat-hint>Path to nginx sites-available directory</mat-hint>
|
|
|
|
|
</mat-form-field>
|
|
|
|
|
<div class="bg-gray-50 rounded-lg p-3 space-y-2" *ngIf="shPublicKey">
|
|
|
|
|
<div class="text-xs font-semibold text-gray-500 uppercase tracking-wide">Public Key</div>
|
|
|
|
|
<p class="text-xs text-gray-600">
|
|
|
|
|
Add this key to <code class="bg-gray-200 px-1 rounded">~/.ssh/authorized_keys</code> on your server for the SSH user above.
|
|
|
|
|
</p>
|
|
|
|
|
<pre class="text-xs bg-gray-800 text-green-300 rounded p-2 overflow-x-auto whitespace-pre-wrap break-all max-h-28 overflow-y-auto">{{ shPublicKey }}</pre>
|
|
|
|
|
<button mat-stroked-button (click)="copyPublicKey()" class="!text-xs">
|
|
|
|
|
<mat-icon>content_copy</mat-icon>
|
|
|
|
|
{{ shPublicKeyCopied ? 'Copied!' : 'Copy' }}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<p class="text-xs text-gray-500" *ngIf="shDeployedAt">
|
|
|
|
|
Last deployed: {{ shDeployedAt }}
|
|
|
|
|
</p>
|
|
|
|
|
<div class="flex gap-3">
|
|
|
|
|
<button mat-flat-button color="primary" [disabled]="shSaving || !shDomain.trim() || !shSshHost.trim() || !shSshUser.trim()" (click)="saveSelfHostedConfig()">
|
|
|
|
|
<mat-icon>save</mat-icon>
|
|
|
|
|
{{ shSaving ? 'Saving...' : 'Save Config' }}
|
|
|
|
|
</button>
|
|
|
|
|
<button mat-stroked-button color="warn" *ngIf="hasSelfHostedConfig" [disabled]="shSaving" (click)="removeSelfHostedConfig()">
|
|
|
|
|
<mat-icon>delete</mat-icon>
|
|
|
|
|
Remove
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</mat-card-content>
|
|
|
|
|
</mat-card>
|
|
|
|
|
|
|
|
|
|
<mat-card class="p-4" appearance="outlined">
|
|
|
|
|
<mat-card-content class="!p-0">
|
|
|
|
|
<div class="flex items-center justify-between">
|
|
|
|
|
@@ -124,7 +223,7 @@ import { SiteResponse } from '../../core/models/site';
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<p class="text-sm text-gray-600 mt-2">
|
|
|
|
|
Connect a GitHub or GitLab repository to automatically push changes when you publish.
|
|
|
|
|
Connect a GitHub or GitLab repository. Git push runs on every publish regardless of deployment target.
|
|
|
|
|
</p>
|
|
|
|
|
</mat-card-content>
|
|
|
|
|
</mat-card>
|
|
|
|
|
@@ -147,10 +246,25 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
|
|
|
|
siteSubdomain = '';
|
|
|
|
|
siteStatus: 'DRAFT' | 'PUBLISHED' = 'DRAFT';
|
|
|
|
|
liveUrl = '';
|
|
|
|
|
deploymentTarget: 'GIT_PAGES' | 'SELF_HOSTED' | 'NONE' = 'NONE';
|
|
|
|
|
hasGitRemote = false;
|
|
|
|
|
hasSelfHostedConfig = false;
|
|
|
|
|
saving = false;
|
|
|
|
|
exporting = false;
|
|
|
|
|
publishing = false;
|
|
|
|
|
publishStatusText = '';
|
|
|
|
|
|
|
|
|
|
shDomain = '';
|
|
|
|
|
shSshHost = '';
|
|
|
|
|
shSshPort = 22;
|
|
|
|
|
shSshUser = '';
|
|
|
|
|
shDeployPath = '/var/www/{domain}/public';
|
|
|
|
|
shNginxSitesPath = '/etc/nginx/sites-available';
|
|
|
|
|
shPublicKey = '';
|
|
|
|
|
shPublicKeyCopied = false;
|
|
|
|
|
shDeployedAt: string | null = null;
|
|
|
|
|
shSaving = false;
|
|
|
|
|
|
|
|
|
|
private destroy$ = new Subject<void>();
|
|
|
|
|
|
|
|
|
|
ngOnInit(): void {
|
|
|
|
|
@@ -173,11 +287,64 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
|
|
|
|
this.siteSubdomain = site.subdomain || '';
|
|
|
|
|
this.siteStatus = site.status;
|
|
|
|
|
this.liveUrl = site.publishedUrl || '';
|
|
|
|
|
this.deploymentTarget = site.deploymentTarget || 'NONE';
|
|
|
|
|
this.loadGitStatus();
|
|
|
|
|
this.loadSelfHostedConfig();
|
|
|
|
|
},
|
|
|
|
|
error: () => this.snackBar.open('Failed to load site', 'Close', { duration: 3000 }),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private loadGitStatus(): void {
|
|
|
|
|
if (!this.siteId) return;
|
|
|
|
|
this.deploymentService.getGitStatus(this.siteId)
|
|
|
|
|
.pipe(takeUntil(this.destroy$))
|
|
|
|
|
.subscribe({
|
|
|
|
|
next: (status) => { this.hasGitRemote = status.connected; },
|
|
|
|
|
error: () => {},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private loadSelfHostedConfig(): void {
|
|
|
|
|
if (!this.siteId) return;
|
|
|
|
|
this.siteService.getSelfHostedConfig(this.siteId)
|
|
|
|
|
.pipe(takeUntil(this.destroy$))
|
|
|
|
|
.subscribe({
|
|
|
|
|
next: (config) => {
|
|
|
|
|
if ('configured' in config && !config.configured) {
|
|
|
|
|
this.hasSelfHostedConfig = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const c = config as SelfHostedConfigResponse;
|
|
|
|
|
this.hasSelfHostedConfig = true;
|
|
|
|
|
this.shDomain = c.domain;
|
|
|
|
|
this.shSshHost = c.sshHost;
|
|
|
|
|
this.shSshPort = c.sshPort;
|
|
|
|
|
this.shSshUser = c.sshUser;
|
|
|
|
|
this.shDeployPath = c.deployPath;
|
|
|
|
|
this.shNginxSitesPath = c.nginxSitesPath;
|
|
|
|
|
this.shPublicKey = c.publicKey;
|
|
|
|
|
this.shDeployedAt = c.deployedAt;
|
|
|
|
|
},
|
|
|
|
|
error: () => { this.hasSelfHostedConfig = false; },
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onDeploymentTargetChange(): void {
|
|
|
|
|
if (!this.siteId) return;
|
|
|
|
|
this.saving = true;
|
|
|
|
|
this.siteService.update(this.siteId, {
|
|
|
|
|
name: this.siteName,
|
|
|
|
|
description: this.siteDescription,
|
|
|
|
|
subdomain: this.siteSubdomain,
|
|
|
|
|
deploymentTarget: this.deploymentTarget,
|
|
|
|
|
}).pipe(takeUntil(this.destroy$))
|
|
|
|
|
.subscribe({
|
|
|
|
|
next: () => { this.saving = false; },
|
|
|
|
|
error: () => { this.saving = false; },
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
saveSettings(): void {
|
|
|
|
|
if (!this.siteId || !this.siteName.trim()) return;
|
|
|
|
|
this.saving = true;
|
|
|
|
|
@@ -185,6 +352,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
|
|
|
|
name: this.siteName,
|
|
|
|
|
description: this.siteDescription,
|
|
|
|
|
subdomain: this.siteSubdomain,
|
|
|
|
|
deploymentTarget: this.deploymentTarget,
|
|
|
|
|
}).pipe(takeUntil(this.destroy$))
|
|
|
|
|
.subscribe({
|
|
|
|
|
next: () => {
|
|
|
|
|
@@ -198,6 +366,63 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
saveSelfHostedConfig(): void {
|
|
|
|
|
if (!this.siteId) return;
|
|
|
|
|
this.shSaving = true;
|
|
|
|
|
this.siteService.saveSelfHostedConfig(this.siteId, {
|
|
|
|
|
domain: this.shDomain,
|
|
|
|
|
sshHost: this.shSshHost,
|
|
|
|
|
sshPort: this.shSshPort,
|
|
|
|
|
sshUser: this.shSshUser,
|
|
|
|
|
deployPath: this.shDeployPath,
|
|
|
|
|
nginxSitesPath: this.shNginxSitesPath,
|
|
|
|
|
}).pipe(takeUntil(this.destroy$))
|
|
|
|
|
.subscribe({
|
|
|
|
|
next: (config) => {
|
|
|
|
|
this.shSaving = false;
|
|
|
|
|
this.hasSelfHostedConfig = true;
|
|
|
|
|
this.shPublicKey = config.publicKey;
|
|
|
|
|
this.shDeployedAt = config.deployedAt;
|
|
|
|
|
this.deploymentTarget = 'SELF_HOSTED';
|
|
|
|
|
this.snackBar.open('Self-hosted config saved', 'Close', { duration: 2000 });
|
|
|
|
|
},
|
|
|
|
|
error: () => {
|
|
|
|
|
this.shSaving = false;
|
|
|
|
|
this.snackBar.open('Failed to save self-hosted config', 'Close', { duration: 3000 });
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
removeSelfHostedConfig(): void {
|
|
|
|
|
if (!this.siteId) return;
|
|
|
|
|
this.shSaving = true;
|
|
|
|
|
this.siteService.deleteSelfHostedConfig(this.siteId)
|
|
|
|
|
.pipe(takeUntil(this.destroy$))
|
|
|
|
|
.subscribe({
|
|
|
|
|
next: () => {
|
|
|
|
|
this.shSaving = false;
|
|
|
|
|
this.hasSelfHostedConfig = false;
|
|
|
|
|
this.shDomain = '';
|
|
|
|
|
this.shSshHost = '';
|
|
|
|
|
this.shSshPort = 22;
|
|
|
|
|
this.shSshUser = '';
|
|
|
|
|
this.shDeployPath = '/var/www/{domain}/public';
|
|
|
|
|
this.shNginxSitesPath = '/etc/nginx/sites-available';
|
|
|
|
|
this.shPublicKey = '';
|
|
|
|
|
this.shPublicKeyCopied = false;
|
|
|
|
|
this.shDeployedAt = null;
|
|
|
|
|
if (this.deploymentTarget === 'SELF_HOSTED') {
|
|
|
|
|
this.deploymentTarget = 'NONE';
|
|
|
|
|
}
|
|
|
|
|
this.snackBar.open('Self-hosted config removed', 'Close', { duration: 2000 });
|
|
|
|
|
},
|
|
|
|
|
error: () => {
|
|
|
|
|
this.shSaving = false;
|
|
|
|
|
this.snackBar.open('Failed to remove self-hosted config', 'Close', { duration: 3000 });
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
exportSite(): void {
|
|
|
|
|
if (!this.siteId) return;
|
|
|
|
|
this.exporting = true;
|
|
|
|
|
@@ -263,6 +488,14 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
copyPublicKey(): void {
|
|
|
|
|
if (!this.shPublicKey) return;
|
|
|
|
|
navigator.clipboard.writeText(this.shPublicKey).then(() => {
|
|
|
|
|
this.shPublicKeyCopied = true;
|
|
|
|
|
setTimeout(() => { this.shPublicKeyCopied = false; }, 2000);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
goBack(): void {
|
|
|
|
|
if (this.siteId) this.router.navigate(['/llm', this.siteId]);
|
|
|
|
|
}
|
|
|
|
|
|