add project

This commit is contained in:
2026-05-05 23:10:05 +03:00
parent b2b066e0d3
commit 53c040161a
46 changed files with 3824 additions and 1073 deletions
+2244 -952
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -10,6 +10,10 @@ export class GlobalConstants {
return environment.default_page;
})();
public static readonly HOST_NAME = (() => {
return environment.host_name;
})();
public static readonly API_URL = (() => {
// ... calculate the value and return it
@@ -10,7 +10,7 @@ import {
} from "@angular/forms";
import {MarkdownComponent} from "ngx-markdown";
import {MatButton} from "@angular/material/button";
import {MatCard, MatCardActions, MatCardContent, MatCardHeader} from "@angular/material/card";
import {MatCard, MatCardActions, MatCardContent} from "@angular/material/card";
import {MatError, MatFormField, MatInput, MatLabel} from "@angular/material/input";
import {MatTab, MatTabGroup} from "@angular/material/tabs";
import {ActivatedRoute, RouterLink} from "@angular/router";
@@ -47,7 +47,6 @@ type TutorialFormValue = {
MatCard,
MatCardActions,
MatCardContent,
MatCardHeader,
MatError,
MatFormField,
MatInput,
@@ -2,7 +2,6 @@ import { Component } from '@angular/core';
import {Tutorial} from '../../../models/tutorial.model';
import {MatCard, MatCardActions, MatCardContent, MatCardImage} from '@angular/material/card';
import {RouterLink} from '@angular/router';
import {MatButton} from '@angular/material/button';
import {TutorialsApiService} from '../tutorials-api.service';
@Component({
@@ -11,7 +10,6 @@ import {TutorialsApiService} from '../tutorials-api.service';
MatCard,
MatCardActions,
RouterLink,
MatButton,
MatCardImage,
MatCardContent
],
@@ -0,0 +1,42 @@
<div class="add-project-container p-4">
<mat-card>
<mat-card-header>
<mat-card-title>Create New SaaS Project</mat-card-title>
<mat-card-subtitle>Define your container infrastructure</mat-card-subtitle>
</mat-card-header>
<mat-card-content class="mt-4">
<form [formGroup]="projectForm" (ngSubmit)="onSubmit()">
<!-- Имя проекта -->
<mat-form-field appearance="outline" class="full-width">
<mat-label>Project Name</mat-label>
<input matInput formControlName="name" placeholder="e.g. My-Awesome-App">
<mat-icon matSuffix>label</mat-icon>
<mat-error *ngIf="projectForm.get('name')?.hasError('required')">Name is required</mat-error>
</mat-form-field>
<!-- Конфигурация Docker Compose -->
<mat-form-field appearance="outline" class="full-width mt-2">
<mat-label>Docker Compose YAML</mat-label>
<textarea matInput formControlName="rawComposeContent"
rows="15"
placeholder="version: '3.8'..."
class="yaml-editor"></textarea>
<mat-hint>Paste your docker-compose.yml content here</mat-hint>
<mat-error *ngIf="projectForm.get('rawComposeContent')?.hasError('required')">
Configuration content is mandatory
</mat-error>
</mat-form-field>
<div class="actions mt-4 d-flex justify-content-end gap-2">
<button mat-button type="button" routerLink="../">Cancel</button>
<button mat-raised-button color="primary" type="submit" [disabled]="projectForm.invalid || isSubmitting">
<mat-icon *ngIf="!isSubmitting">save</mat-icon>
<mat-spinner diameter="20" *ngIf="isSubmitting"></mat-spinner>
Create & Initialize
</button>
</div>
</form>
</mat-card-content>
</mat-card>
</div>
@@ -0,0 +1,17 @@
.full-width {
width: 100%;
}
.yaml-editor {
font-family: 'Fira Code', 'Courier New', monospace;
font-size: 14px;
background-color: #fafafa;
color: #2c3e50;
line-height: 1.5;
white-space: pre;
}
mat-card {
max-width: 900px;
margin: 0 auto;
}
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProjectAddComponent } from './project-add.component';
describe('ProjectAddComponent', () => {
let component: ProjectAddComponent;
let fixture: ComponentFixture<ProjectAddComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ProjectAddComponent]
})
.compileComponents();
fixture = TestBed.createComponent(ProjectAddComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,72 @@
import {Component, CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
import {FormBuilder, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
import {ProjectService} from '../../../services/project.service';
import {Router, RouterLink} from '@angular/router';
import {MatSnackBar} from '@angular/material/snack-bar';
import {MatCard, MatCardContent, MatCardHeader, MatCardSubtitle, MatCardTitle} from '@angular/material/card';
import {MatError, MatFormField, MatHint, MatInputModule, MatLabel} from '@angular/material/input';
import {MatIcon} from '@angular/material/icon';
import {NgIf} from '@angular/common';
import {MatProgressSpinner} from '@angular/material/progress-spinner';
import {MatButton} from '@angular/material/button';
@Component({
selector: 'app-project-add.component',
imports: [
MatInputModule,
MatCard,
MatCardHeader,
MatCardTitle,
MatCardSubtitle,
MatCardContent,
ReactiveFormsModule,
MatFormField,
MatLabel,
MatIcon,
MatError,
MatHint,
NgIf,
MatProgressSpinner,
MatButton,
RouterLink
],
schemas:[CUSTOM_ELEMENTS_SCHEMA],
templateUrl: './project-add.component.html',
styleUrl: './project-add.component.scss',
})
export class ProjectAddComponent {
projectForm: FormGroup;
isSubmitting = false;
constructor(
private fb: FormBuilder,
private projectService: ProjectService,
private router: Router,
private snackBar: MatSnackBar
) {
this.projectForm = this.fb.group({
name: ['', [Validators.required, Validators.minLength(3)]],
rawComposeContent: ['', Validators.required]
});
}
onSubmit(): void {
if (this.projectForm.valid) {
this.isSubmitting = true;
// Отправляем запрос согласно ProjectCreateRequest в Java[cite: 1, 4]
this.projectService.createProject(this.projectForm.value).subscribe({
next: (project) => {
this.snackBar.open(`Project ${project.name} created!`, 'OK', { duration: 3000 });
this.router.navigate(['/main/user/projects']).then(r => console.log('Navigation result:', r)
);
},
error: (err) => {
this.isSubmitting = false;
console.error('Creation error:', err);
this.snackBar.open('Failed to create project. Check YAML syntax.', 'Close');
}
});
}
}
}
@@ -0,0 +1,44 @@
<div class="project-details-wrapper">
<div class="main-header">
<h1>{{ project?.name || 'nt6' }}</h1>
<div class="actions">
<button mat-flat-button color="primary" (click)="deploy()">▶ DEPLOY</button>
<button mat-flat-button (click)="stop()">■ STOP</button>
<button mat-flat-button color="warn" (click)="confirmDelete()">🗑 DELETE PROJECT</button>
</div>
</div>
<div class="section-divider">
<mat-icon>layers</mat-icon>
<h2>CONTAINERS</h2>
</div>
<div class="containers-grid">
@for (container of containers; track container.name) {
<mat-card class="container-card">
<mat-card-content class="card-layout">
<div class="card-left">
<span class="c-name">{{ container.name }}</span>
<span class="c-image">{{ container.image }}</span>
</div>
<div class="card-right">
<div class="state-dot" [class.running]="container.state === 'running'"></div>
<span class="uptime">{{ container.status }}</span>
</div>
</mat-card-content>
</mat-card>
}
</div>
<div class="content-section-terminal">
<div class="terminal-header">
<h3>>_ DOCKER COMPOSE OUTPUT</h3>
<button mat-icon-button (click)="logs = []"><mat-icon>delete_sweep</mat-icon></button>
</div>
<div class="terminal-window" #consoleScroll>
@for (line of logs; track $index) {
<div class="log-line" [innerHTML]="formatLogLine(line)"></div>
}
</div>
</div>
</div>
@@ -0,0 +1,106 @@
.project-details-wrapper {
padding: 24px;
background-color: #fff; // Основной фон страницы как на скрине
.main-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
h1 { font-size: 48px; font-weight: 900; margin: 0; text-transform: uppercase; }
.actions {
display: flex;
gap: 10px;
button {
border-radius: 0; // На скрине кнопки острые
font-weight: bold;
text-transform: uppercase;
}
}
}
}
.section-divider {
display: flex;
align-items: center;
gap: 10px;
margin-top: 40px;
border-bottom: 2px solid #000;
padding-bottom: 10px;
h2 { font-size: 24px; font-weight: 900; text-transform: uppercase; margin: 0; }
}
/* Исправляем карточки под скриншот */
.containers-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
gap: 20px;
margin-top: 20px;
}
.container-card {
background: #f0f0f0 !important; // Серый фон карточки
border: 1px solid #ccc !important;
border-bottom: 4px solid #333 !important; // Твоя фирменная черта
border-radius: 4px !important;
box-shadow: none !important; // Убираем Material-тень
.card-layout {
display: flex;
justify-content: space-between;
padding: 24px;
.card-left {
.c-name { font-size: 20px; font-weight: 800; display: block; }
.c-image { font-size: 12px; color: #666; font-family: 'Fira Code', monospace; }
}
.card-right {
display: flex;
flex-direction: column;
align-items: flex-end;
justify-content: space-between;
.state-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: #ccc;
&.running {
background: #00ff41;
box-shadow: 0 0 8px #00ff41;
}
}
.uptime { font-size: 10px; color: #999; text-transform: uppercase; margin-top: 30px; }
}
}
}
/* Терминал — черная бездна */
.content-section-terminal {
margin-top: 50px;
.terminal-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
h3 { font-size: 22px; font-weight: 800; text-transform: uppercase; margin: 0; }
}
.terminal-window {
background: #000; // Чистый черный
color: #00ff41; // Терминальный зеленый
padding: 20px;
font-family: 'Fira Code', 'Courier New', monospace;
font-size: 14px;
line-height: 1.4;
border-radius: 0;
max-height: 500px;
overflow-y: auto;
}
}
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProjectDetailsComponent } from './project-details.component';
describe('ProjectDetailsComponent', () => {
let component: ProjectDetailsComponent;
let fixture: ComponentFixture<ProjectDetailsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ProjectDetailsComponent]
})
.compileComponents();
fixture = TestBed.createComponent(ProjectDetailsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,146 @@
import { Component, OnInit, OnDestroy, ViewChild, ElementRef, AfterViewChecked } from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
import { CommonModule } from '@angular/common';
import { Subscription, timer, switchMap } from 'rxjs';
import { ProjectService, Project, DockerContainer, ProjectDetailResponse } from '../../../services/project.service';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatDividerModule } from '@angular/material/divider';
import {MatCard, MatCardContent} from '@angular/material/card';
@Component({
selector: 'app-project-details',
standalone: true,
imports: [
CommonModule,
MatIconModule,
MatButtonModule,
MatProgressSpinnerModule,
MatDividerModule,
MatCard,
MatCardContent
],
templateUrl: './project-details.component.html',
styleUrls: ['./project-details.component.scss']
})
export class ProjectDetailsComponent implements OnInit, OnDestroy, AfterViewChecked {
@ViewChild('consoleScroll') private consoleContainer!: ElementRef; // Привязка к #consoleScroll
project?: Project;
config?: any;
containers: DockerContainer[] = [];
logs: string[] = [];
loading = true;
private pollingSub?: Subscription;
private logSub?: Subscription;
constructor(
private route: ActivatedRoute,
private router: Router,
private projectService: ProjectService
) {}
ngOnInit(): void {
const id = this.route.snapshot.paramMap.get('id');
if (id) {
this.loadData(id);
this.startMonitoring(id);
}
}
private loadData(id: string): void {
this.projectService.getProjectDetails(id).subscribe({
next: (data: ProjectDetailResponse) => {
this.project = data.project;
this.config = data.config;
this.loading = false;
this.connectLogs(id);
},
error: (err) => {
console.error('Ошибка загрузки данных проекта:', err);
this.loading = false;
}
});
}
private startMonitoring(id: string): void {
// Опрос состояния контейнеров каждые 5 секунд[cite: 5, 6]
this.pollingSub = timer(0, 5000).pipe(
switchMap(() => this.projectService.getProjectContainers(id))
).subscribe({
next: (data) => this.containers = data,
error: (err) => console.error('Ошибка мониторинга контейнеров:', err)
});
}
private connectLogs(id: string): void {
this.logSub = this.projectService.connectToDockerLogs(id).subscribe({
next: (msg) => this.logs.push(msg),
error: (err) => console.error('WS Error:', err)
});
}
deploy(): void {
if (this.project) this.projectService.deployProject(this.project.id).subscribe();
}
stop(): void {
if (this.project) this.projectService.stopProject(this.project.id).subscribe();
}
ngAfterViewChecked(): void {
this.scrollToBottom();
}
private scrollToBottom(): void {
try {
this.consoleContainer.nativeElement.scrollTop = this.consoleContainer.nativeElement.scrollHeight;
} catch (err) {}
}
ngOnDestroy(): void {
this.pollingSub?.unsubscribe();
this.logSub?.unsubscribe();
}
confirmDelete() {
if (confirm("Уничтожить проект навсегда?")) {
// 1. Сначала «ослепляем» компонент, чтобы он перестал слать запросы
this.pollingSub?.unsubscribe(); // Твой метод, который делает subscription.unsubscribe()
// 2. Только теперь отправляем команду на ликвидацию
if (this.project) this.projectService.deleteProject(this.project.id).subscribe({
next: () => {
console.log("Проект стерт, уходим на дашборд");
this.router.navigate(['/main/user/projects']);
},
error: (err) => {
console.error("Ошибка при удалении, но проект мог уже исчезнуть", err);
// Всё равно уходим, так как оставаться на странице 404 нет смысла
this.router.navigate(['/main/user/projects']);
}
});
}
}
// Метод для окрашивания логов в терминале
formatLogLine(line: string): string {
if (!line) return '';
// Окрашиваем ключевые слова для лучшей читаемости
let formatted = line;
if (line.includes('ERROR') || line.includes('Error') || line.includes('failed')) {
formatted = `<span style="color: #ff5f56; font-weight: bold;">${line}</span>`;
} else if (line.includes('WARN') || line.includes('Warning') || line.includes('Conflict')) {
formatted = `<span style="color: #ffbd2e;">${line}</span>`;
} else if (line.includes('INFO') || line.includes('success') || line.includes('Started')) {
formatted = `<span style="color: #27c93f;">${line}</span>`;
} else if (line.includes('DEBUG')) {
formatted = `<span style="color: #ae81ff;">${line}</span>`;
}
return formatted;
}
}
@@ -0,0 +1 @@
<p>project-edit.component works!</p>
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProjectEditComponent } from './project-edit.component';
describe('ProjectEditComponent', () => {
let component: ProjectEditComponent;
let fixture: ComponentFixture<ProjectEditComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ProjectEditComponent]
})
.compileComponents();
fixture = TestBed.createComponent(ProjectEditComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,11 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-project-edit.component',
imports: [],
templateUrl: './project-edit.component.html',
styleUrl: './project-edit.component.scss',
})
export class ProjectEditComponent {
}
@@ -0,0 +1,42 @@
<div class="dashboard-container">
<div class="dashboard-header">
<h1>Jumbotron Dashboard</h1>
<button mat-flat-button color="primary" class="new-project-btn" (click)='createNewProject()'>
<mat-icon>add</mat-icon>
NEW PROJECT
</button>
</div>
@if (isLoading) {
<div class="loader-container">
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div>
} @else {
<div class="projects-grid">
@for (project of projects; track project.id) {
<mat-card class="project-card" (click)="openProjectDetails(project.id)">
<mat-card-header>
<mat-card-title>{{ project.name }}</mat-card-title>
</mat-card-header>
<mat-card-content>
<div class="status-row">
<mat-icon [style.color]="getStatusColor(project.status)">circle</mat-icon>
<span class="status-text">{{ project.status }}</span>
</div>
<div class="footer-info">
<span class="date-label">Создан:</span>
<span class="date-value">{{ project.createdAt | date:'dd.MM.yyyy HH:mm' }}</span>
</div>
</mat-card-content>
</mat-card>
} @empty {
<div class="empty-state">
<mat-icon>folder_open</mat-icon>
<p>Проектов пока нет. Начните созидание!</p>
</div>
}
</div>
}
</div>
@@ -0,0 +1,110 @@
.dashboard-container {
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
h1 {
font-size: 32px;
font-weight: 500;
margin: 0;
color: #3e87d9; // Белый для контраста с темным фоном
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
}
.new-project-btn {
height: 48px;
padding: 0 24px;
font-weight: 600;
letter-spacing: 0.5px;
border-radius: 8px;
}
.projects-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 24px;
}
.project-card {
border-radius: 12px !important;
background: rgba(255, 255, 255, 0.95) !important;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
border: 1px solid rgba(0,0,0,0.05);
&:hover {
transform: translateY(-8px);
box-shadow: 0 12px 20px rgba(0,0,0,0.2) !important;
}
mat-card-header {
padding: 16px 16px 8px;
}
mat-card-title {
font-size: 1.25rem;
font-weight: 600;
color: #2c3e50;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.status-row {
display: flex;
align-items: center;
gap: 8px;
margin: 12px 0;
mat-icon {
font-size: 16px;
width: 16px;
height: 16px;
}
.status-text {
font-weight: 600;
font-size: 0.85rem;
color: #555;
}
}
.footer-info {
margin-top: 16px;
padding-top: 12px;
border-top: 1px solid #eee;
display: flex;
justify-content: space-between;
font-size: 0.75rem;
.date-label { color: #888; }
.date-value { color: #444; font-weight: 500; }
}
.loader-container {
padding: 40px;
text-align: center;
}
.empty-state {
grid-column: 1 / -1;
text-align: center;
padding: 60px;
color: rgba(255,255,255,0.7);
mat-icon {
font-size: 48px;
width: 48px;
height: 48px;
margin-bottom: 16px;
}
}
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProjectsComponent } from './projects.component';
describe('ProjectsComponent', () => {
let component: ProjectsComponent;
let fixture: ComponentFixture<ProjectsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ProjectsComponent]
})
.compileComponents();
fixture = TestBed.createComponent(ProjectsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,77 @@
import { Component, OnInit } from '@angular/core';
import { Project, ProjectService } from '../../../services/project.service';
import { MatIcon } from '@angular/material/icon';
import { MatProgressBar } from '@angular/material/progress-bar';
import { DatePipe } from '@angular/common';
import { MatButton } from '@angular/material/button';
import { Router } from '@angular/router';
import { MatCard, MatCardContent, MatCardHeader, MatCardTitle } from '@angular/material/card';
@Component({
selector: 'app-projects',
standalone: true,
imports: [
MatIcon,
MatProgressBar,
MatButton,
MatCard,
MatCardHeader,
MatCardTitle,
MatCardContent,
DatePipe
],
templateUrl: './projects.component.html',
styleUrl: './projects.component.scss',
})
export class ProjectsComponent implements OnInit {
projects: Project[] = [];
isLoading = true;
constructor(
private projectService: ProjectService,
private router: Router
) {}
ngOnInit(): void {
this.loadProjects();
}
loadProjects(): void {
this.projectService.getUserProjects().subscribe({
next: (data) => {
// Фикс бага с JSON в имени
this.projects = data.map(p => {
if (p.name && p.name.startsWith('{')) {
try {
const parsed = JSON.parse(p.name);
p.name = parsed.name || 'Untitled Project';
} catch (e) {
p.name = 'Invalid Data';
}
}
return p;
});
this.isLoading = false;
},
error: () => this.isLoading = false
});
}
openProjectDetails(projectId: string): void {
this.router.navigate(['/main/user/projects', projectId, 'details']);
}
createNewProject(): void {
this.router.navigate(['/main/user/projects/add']);
}
getStatusColor(status: string): string {
switch (status) {
case 'RUNNING': return '#4caf50';
case 'STARTING': return '#ffc107';
case 'STOPPED': return '#9e9e9e';
case 'ERROR': return '#f44336';
default: return '#bdbdbd';
}
}
}
@@ -1,20 +1,15 @@
import {Component, CUSTOM_ELEMENTS_SCHEMA, inject, OnInit} from '@angular/core';
import {Component, CUSTOM_ELEMENTS_SCHEMA, inject} from '@angular/core';
import {UserApiService} from '../user-api.service';
import {TokenStorageService} from '../../../services/token-storage.service';
import {EventBusService} from '../../../_shared/event-bus.service';
import {Router, RouterLink} from '@angular/router';
import {AuthService} from '../../../services/auth.service';
import {RouterLink} from '@angular/router';
import {Tutorial} from '../../../models/tutorial.model';
import {MarkdownComponent, MarkdownService} from 'ngx-markdown';
import {MatButton} from '@angular/material/button';
import {MatCard, MatCardActions, MatCardContent, MatCardHeader} from '@angular/material/card';
import {MatCard, MatCardActions, MatCardContent} from '@angular/material/card';
import {MatError, MatFormField, MatInput, MatLabel} from '@angular/material/input';
import {MatTab, MatTabGroup} from '@angular/material/tabs';
import {
AbstractControl,
FormBuilder,
FormControl,
FormGroup,
FormsModule,
ReactiveFormsModule, ValidationErrors,
ValidatorFn,
@@ -24,7 +19,6 @@ import {AngularMarkdownEditorModule, EditorInstance, EditorOption} from 'angular
import {MatDialog} from '@angular/material/dialog';
import {DialogSelectImageComponent} from '../dialog-select-image.component/dialog-select-image.component';
import {DialogUploadImageComponent} from '../dialog-upload-image.component/dialog-upload-image.component';
import {FileUploadComponent} from '../../../components/file-upload.component/file-upload.component';
import {MatDivider} from '@angular/material/divider';
import {GlobalConstants} from '../../../global-constants';
@@ -49,7 +43,6 @@ type TutorialFormValue = {
MatCard,
MatCardActions,
MatCardContent,
MatCardHeader,
MatFormField,
MatInput,
MatLabel,
@@ -42,7 +42,6 @@ type TutorialFormValue = {
MatCard,
MatCardActions,
MatCardContent,
MatCardHeader,
MatFormField,
MatInput,
MatLabel,
@@ -22,7 +22,6 @@ import {
import {DatePipe} from '@angular/common';
import {MatCheckbox} from '@angular/material/checkbox';
import {MatSlideToggle} from '@angular/material/slide-toggle';
import {BreadcrumbsComponent} from '../../../components/breadcrumbs.component/breadcrumbs.component';
@Component({
@@ -44,8 +43,8 @@ import {BreadcrumbsComponent} from '../../../components/breadcrumbs.component/br
MatCheckbox,
DatePipe,
MatCell,
MatSlideToggle,
BreadcrumbsComponent
MatSlideToggle
],
schemas:[CUSTOM_ELEMENTS_SCHEMA],
templateUrl: './tutorials-list.component.html',
@@ -1,19 +1,10 @@
import {Component, CUSTOM_ELEMENTS_SCHEMA, OnInit, ViewChild} from '@angular/core';
import {RouterOutlet} from '@angular/router';
import {MatSidenav} from '@angular/material/sidenav';
import {BreakpointObserver} from '@angular/cdk/layout';
import {SideBarUserComponent} from '../side-bar-user.component/side-bar-user.component';
import {BreadcrumbsComponent} from '../../../components/breadcrumbs.component/breadcrumbs.component';
import {Component, CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
import {InnerLayoutComponent} from '../../../components/inner-layout.component/inner-layout.component';
import {MODERATOR_ROUTS} from '../../moderator-module/moderator.routing';
import {USER_ROUTS} from '../user.routing';
@Component({
selector: 'app-user.component',
imports: [
RouterOutlet,
SideBarUserComponent,
BreadcrumbsComponent,
InnerLayoutComponent
],
templateUrl: './user.component.html',
@@ -50,6 +50,21 @@ export const USER_ROUTS: Routes = [
path:'docker',
loadComponent: () => import('../user-module/docker-management-component/docker-management-component').then((c)=>c.DockerManagementComponent),
data:{title: 'Docker', icon: 'docker', nav:true}
},
{
path: 'projects',
loadComponent: () => import('../user-module/projects.component/projects.component').then((c) => c.ProjectsComponent),
data: { title: 'Dashboard', icon: 'dashboard', nav: true }
},
{
path: 'projects/:id/details',
loadComponent: () => import('../user-module/project-details.component/project-details.component').then((c) => c.ProjectDetailsComponent),
data: { title: 'Project Details' }
},
{
path: 'projects/add',
loadComponent: () => import('../user-module/project-add.component/project-add.component').then((c) => c.ProjectAddComponent),
data: { title: 'New Project' }
}
]
}
@@ -0,0 +1,95 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { GlobalConstants } from '../global-constants';
import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
// Унифицируем название интерфейса для списка контейнеров
export interface DockerContainer {
id: string;
name: string;
image: string;
state: string; // running, exited, etc.
status: string; // Up 2 hours, etc.
}
// Интерфейс Project в соответствии с Project.java[cite: 3]
export interface Project {
id: string;
name: string;
user?: any; // Соответствует полю private User user в Java[cite: 3]
status: string;
localPath: string;
createdAt: Date;
containers?: DockerContainer[];
}
export interface ProjectCreateRequest {
name: string;
rawComposeContent: string;
}
export interface ProjectDetailResponse {
project: Project;
config: {
projectId: string;
rawComposeContent: string;
envVariables: { [key: string]: string };
};
}
@Injectable({
providedIn: 'root'
})
export class ProjectService {
// Используем baseUrl, как определено в начале класса
private baseUrl = GlobalConstants.API_URL;
constructor(private http: HttpClient) {}
// Создание нового проекта[cite: 4, 5]
createProject(request: ProjectCreateRequest): Observable<Project> {
return this.http.post<Project>(`${this.baseUrl}/user/projects`, request);
}
// Методы для Dashboard[cite: 4, 5]
getUserProjects(): Observable<Project[]> {
return this.http.get<Project[]>(`${this.baseUrl}/user/projects`);
}
getProjectDetails(id: string): Observable<ProjectDetailResponse> {
return this.http.get<ProjectDetailResponse>(`${this.baseUrl}/user/projects/${id}/details`);
}
// Управление жизненным циклом (Docker Compose)[cite: 3, 4]
deployProject(id: string): Observable<string> {
return this.http.post(`${this.baseUrl}/user/projects/${id}/deploy`, {}, { responseType: 'text' });
}
stopProject(id: string): Observable<string> {
return this.http.post(`${this.baseUrl}/user/projects/${id}/stop`, {}, { responseType: 'text' });
}
deleteProject(id: string): Observable<any> {
return this.http.delete(`${this.baseUrl}/user/projects/${id}`);
}
// Администрирование[cite: 4]
getAllProjectsAdmin(): Observable<Project[]> {
return this.http.get<Project[]>(`${this.baseUrl}/admin/projects/all`);
}
// WebSocket для трансляции логов в реальном времени[cite: 3, 5]
public connectToDockerLogs(projectId: string): WebSocketSubject<any> {
const socketUrl = this.baseUrl.replace('http', 'ws') + '/ws-jambotron/' + projectId;
return webSocket({
url: socketUrl,
deserializer: (msg) => msg.data
});
}
// ИСПРАВЛЕНО: возвращаем DockerContainer и используем правильный baseUrl[cite: 5]
getProjectContainers(projectId: string): Observable<DockerContainer[]> {
return this.http.get<DockerContainer[]>(`${this.baseUrl}/user/projects/${projectId}/containers`);
}
}
+5
View File
@@ -1,6 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<script>
if (window.global === undefined) {
window.global = window;
}
</script>
<meta charset="utf-8">
<title>JambotronUi</title>
<base href="/">
+5
View File
@@ -1,6 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<script>
if (window.global === undefined) {
window.global = window;
}
</script>
<meta charset="utf-8">
<title>JambotronUi</title>
<base href="/">
+1
View File
@@ -4,6 +4,7 @@
"secure": false,
"changeOrigin": true,
"logLevel": "debug",
"ws": true,
"pathRewrite": {"^/api" : "http://localhost:8081/api"}
}
+3 -3
View File
@@ -1,10 +1,10 @@
{
"/api/**": {
"/api": {
"target": "http://localhost:8082",
"secure": false,
"changeOrigin": true,
"logLevel": "debug",
"pathRewrite": {"^/api" : "http://localhost:8082/api"}
"timeout": 0,
"ws": true
}
}