ciyidogan commited on
Commit
ca23205
·
verified ·
1 Parent(s): ddc7db2

Upload 45 files

Browse files
flare-ui/angular.json CHANGED
@@ -1,116 +1,116 @@
1
- {
2
- "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3
- "version": 1,
4
- "newProjectRoot": "projects",
5
- "projects": {
6
- "flare-ui": {
7
- "projectType": "application",
8
- "schematics": {
9
- "@schematics/angular:component": {
10
- "style": "scss"
11
- }
12
- },
13
- "root": "",
14
- "sourceRoot": "src",
15
- "prefix": "app",
16
- "architect": {
17
- "build": {
18
- "builder": "@angular-devkit/build-angular:browser",
19
- "options": {
20
- "outputPath": "dist/flare-ui",
21
- "index": "src/index.html",
22
- "main": "src/main.ts",
23
- "polyfills": [
24
- "zone.js"
25
- ],
26
- "tsConfig": "tsconfig.app.json",
27
- "inlineStyleLanguage": "scss",
28
- "assets": [
29
- "src/favicon.ico",
30
- "src/assets"
31
- ],
32
- "styles": [
33
- "src/styles.scss"
34
- ],
35
- "scripts": []
36
- },
37
- "configurations": {
38
- "production": {
39
- "budgets": [
40
- {
41
- "type": "initial",
42
- "maximumWarning": "1mb",
43
- "maximumError": "2mb"
44
- },
45
- {
46
- "type": "anyComponentStyle",
47
- "maximumWarning": "4kb",
48
- "maximumError": "8kb"
49
- }
50
- ],
51
- "fileReplacements": [
52
- {
53
- "replace": "src/environments/environment.ts",
54
- "with": "src/environments/environment.prod.ts"
55
- }
56
- ],
57
- "outputHashing": "all"
58
- },
59
- "development": {
60
- "buildOptimizer": false,
61
- "optimization": false,
62
- "vendorChunk": true,
63
- "extractLicenses": false,
64
- "sourceMap": true,
65
- "namedChunks": true
66
- }
67
- },
68
- "defaultConfiguration": "production"
69
- },
70
- "serve": {
71
- "builder": "@angular-devkit/build-angular:dev-server",
72
- "configurations": {
73
- "production": {
74
- "browserTarget": "flare-ui:build:production"
75
- },
76
- "development": {
77
- "browserTarget": "flare-ui:build:development"
78
- }
79
- },
80
- "defaultConfiguration": "development",
81
- "options": {
82
- "proxyConfig": "src/proxy.conf.json"
83
- }
84
- },
85
- "extract-i18n": {
86
- "builder": "@angular-devkit/build-angular:extract-i18n",
87
- "options": {
88
- "browserTarget": "flare-ui:build"
89
- }
90
- },
91
- "test": {
92
- "builder": "@angular-devkit/build-angular:karma",
93
- "options": {
94
- "polyfills": [
95
- "zone.js",
96
- "zone.js/testing"
97
- ],
98
- "tsConfig": "tsconfig.spec.json",
99
- "inlineStyleLanguage": "scss",
100
- "assets": [
101
- "src/favicon.ico",
102
- "src/assets"
103
- ],
104
- "styles": [
105
- "src/styles.scss"
106
- ],
107
- "scripts": []
108
- }
109
- }
110
- }
111
- }
112
- },
113
- "cli": {
114
- "analytics": false
115
- }
116
  }
 
1
+ {
2
+ "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3
+ "version": 1,
4
+ "newProjectRoot": "projects",
5
+ "projects": {
6
+ "flare-ui": {
7
+ "projectType": "application",
8
+ "schematics": {
9
+ "@schematics/angular:component": {
10
+ "style": "scss"
11
+ }
12
+ },
13
+ "root": "",
14
+ "sourceRoot": "src",
15
+ "prefix": "app",
16
+ "architect": {
17
+ "build": {
18
+ "builder": "@angular-devkit/build-angular:browser",
19
+ "options": {
20
+ "outputPath": "dist/flare-ui",
21
+ "index": "src/index.html",
22
+ "main": "src/main.ts",
23
+ "polyfills": [
24
+ "zone.js"
25
+ ],
26
+ "tsConfig": "tsconfig.app.json",
27
+ "inlineStyleLanguage": "scss",
28
+ "assets": [
29
+ "src/favicon.ico",
30
+ "src/assets"
31
+ ],
32
+ "styles": [
33
+ "src/styles.scss"
34
+ ],
35
+ "scripts": []
36
+ },
37
+ "configurations": {
38
+ "production": {
39
+ "budgets": [
40
+ {
41
+ "type": "initial",
42
+ "maximumWarning": "1mb",
43
+ "maximumError": "2mb"
44
+ },
45
+ {
46
+ "type": "anyComponentStyle",
47
+ "maximumWarning": "4kb",
48
+ "maximumError": "8kb"
49
+ }
50
+ ],
51
+ "fileReplacements": [
52
+ {
53
+ "replace": "src/environments/environment.ts",
54
+ "with": "src/environments/environment.prod.ts"
55
+ }
56
+ ],
57
+ "outputHashing": "all"
58
+ },
59
+ "development": {
60
+ "buildOptimizer": false,
61
+ "optimization": false,
62
+ "vendorChunk": true,
63
+ "extractLicenses": false,
64
+ "sourceMap": true,
65
+ "namedChunks": true
66
+ }
67
+ },
68
+ "defaultConfiguration": "production"
69
+ },
70
+ "serve": {
71
+ "builder": "@angular-devkit/build-angular:dev-server",
72
+ "configurations": {
73
+ "production": {
74
+ "browserTarget": "flare-ui:build:production"
75
+ },
76
+ "development": {
77
+ "browserTarget": "flare-ui:build:development"
78
+ }
79
+ },
80
+ "defaultConfiguration": "development",
81
+ "options": {
82
+ "proxyConfig": "src/proxy.conf.json"
83
+ }
84
+ },
85
+ "extract-i18n": {
86
+ "builder": "@angular-devkit/build-angular:extract-i18n",
87
+ "options": {
88
+ "browserTarget": "flare-ui:build"
89
+ }
90
+ },
91
+ "test": {
92
+ "builder": "@angular-devkit/build-angular:karma",
93
+ "options": {
94
+ "polyfills": [
95
+ "zone.js",
96
+ "zone.js/testing"
97
+ ],
98
+ "tsConfig": "tsconfig.spec.json",
99
+ "inlineStyleLanguage": "scss",
100
+ "assets": [
101
+ "src/favicon.ico",
102
+ "src/assets"
103
+ ],
104
+ "styles": [
105
+ "src/styles.scss"
106
+ ],
107
+ "scripts": []
108
+ }
109
+ }
110
+ }
111
+ }
112
+ },
113
+ "cli": {
114
+ "analytics": false
115
+ }
116
  }
flare-ui/package.json CHANGED
@@ -1,41 +1,41 @@
1
- {
2
- "name": "flare-ui",
3
- "version": "0.1.0",
4
- "scripts": {
5
- "ng": "ng",
6
- "start": "ng serve",
7
- "build": "ng build --configuration production",
8
- "watch": "ng build --watch --configuration development",
9
- "test": "ng test"
10
- },
11
- "private": true,
12
- "dependencies": {
13
- "@angular/animations": "^17.0.0",
14
- "@angular/cdk": "^17.0.0",
15
- "@angular/common": "^17.0.0",
16
- "@angular/compiler": "^17.0.0",
17
- "@angular/core": "^17.0.0",
18
- "@angular/forms": "^17.0.0",
19
- "@angular/material": "^17.0.0",
20
- "@angular/platform-browser": "^17.0.0",
21
- "@angular/platform-browser-dynamic": "^17.0.0",
22
- "@angular/router": "^17.0.0",
23
- "rxjs": "~7.8.0",
24
- "tslib": "^2.3.0",
25
- "zone.js": "~0.14.0"
26
- },
27
- "devDependencies": {
28
- "@angular-devkit/build-angular": "^17.0.0",
29
- "@angular/cli": "^17.0.0",
30
- "@angular/compiler-cli": "^17.0.0",
31
- "@types/jasmine": "~5.1.0",
32
- "@types/node": "^20.0.0",
33
- "jasmine-core": "~5.1.0",
34
- "karma": "~6.4.0",
35
- "karma-chrome-launcher": "~3.2.0",
36
- "karma-coverage": "~2.2.0",
37
- "karma-jasmine": "~5.1.0",
38
- "karma-jasmine-html-reporter": "~2.1.0",
39
- "typescript": "~5.2.2"
40
- }
41
  }
 
1
+ {
2
+ "name": "flare-ui",
3
+ "version": "0.1.0",
4
+ "scripts": {
5
+ "ng": "ng",
6
+ "start": "ng serve",
7
+ "build": "ng build --configuration production",
8
+ "watch": "ng build --watch --configuration development",
9
+ "test": "ng test"
10
+ },
11
+ "private": true,
12
+ "dependencies": {
13
+ "@angular/animations": "^17.0.0",
14
+ "@angular/cdk": "^17.0.0",
15
+ "@angular/common": "^17.0.0",
16
+ "@angular/compiler": "^17.0.0",
17
+ "@angular/core": "^17.0.0",
18
+ "@angular/forms": "^17.0.0",
19
+ "@angular/material": "^17.0.0",
20
+ "@angular/platform-browser": "^17.0.0",
21
+ "@angular/platform-browser-dynamic": "^17.0.0",
22
+ "@angular/router": "^17.0.0",
23
+ "rxjs": "~7.8.0",
24
+ "tslib": "^2.3.0",
25
+ "zone.js": "~0.14.0"
26
+ },
27
+ "devDependencies": {
28
+ "@angular-devkit/build-angular": "^17.0.0",
29
+ "@angular/cli": "^17.0.0",
30
+ "@angular/compiler-cli": "^17.0.0",
31
+ "@types/jasmine": "~5.1.0",
32
+ "@types/node": "^20.0.0",
33
+ "jasmine-core": "~5.1.0",
34
+ "karma": "~6.4.0",
35
+ "karma-chrome-launcher": "~3.2.0",
36
+ "karma-coverage": "~2.2.0",
37
+ "karma-jasmine": "~5.1.0",
38
+ "karma-jasmine-html-reporter": "~2.1.0",
39
+ "typescript": "~5.2.2"
40
+ }
41
  }
flare-ui/src/app/app.component.scss CHANGED
@@ -1,103 +1,103 @@
1
- // Tab içeriği için padding
2
- .mat-tab-body-content {
3
- padding: 24px;
4
- }
5
-
6
- .app-container {
7
- padding: 20px;
8
- min-height: 100vh;
9
- background-color: #f5f5f5;
10
- }
11
-
12
- .header {
13
- display: flex;
14
- justify-content: space-between;
15
- align-items: center;
16
- margin-bottom: 20px;
17
- padding: 15px 20px;
18
- background: white;
19
- border-radius: 8px;
20
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
21
-
22
- h1 {
23
- margin: 0;
24
- color: #333;
25
- font-size: 24px;
26
- }
27
-
28
- .header-actions {
29
- display: flex;
30
- align-items: center;
31
- gap: 20px;
32
-
33
- .user-info {
34
- display: flex;
35
- align-items: center;
36
- gap: 10px;
37
- color: #666;
38
-
39
- mat-icon {
40
- font-size: 20px;
41
- }
42
- }
43
-
44
- button {
45
- mat-icon {
46
- margin-right: 4px;
47
- }
48
- }
49
- }
50
- }
51
-
52
- .main-content {
53
- background: white;
54
- border-radius: 8px;
55
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
56
- overflow: hidden;
57
-
58
- mat-tab-group {
59
- ::ng-deep {
60
- .mat-tab-header {
61
- background-color: #fafafa;
62
- border-bottom: 1px solid #e0e0e0;
63
- }
64
-
65
- .mat-tab-label {
66
- min-width: 120px;
67
- opacity: 0.8;
68
-
69
- &.mat-tab-label-active {
70
- opacity: 1;
71
- font-weight: 500;
72
- }
73
- }
74
-
75
- .mat-tab-body-wrapper {
76
- padding: 24px;
77
- }
78
- }
79
- }
80
- }
81
-
82
- // Responsive adjustments
83
- @media (max-width: 768px) {
84
- .app-container {
85
- padding: 10px;
86
- }
87
-
88
- .header {
89
- flex-direction: column;
90
- gap: 15px;
91
- text-align: center;
92
-
93
- h1 {
94
- font-size: 20px;
95
- }
96
- }
97
-
98
- .main-content {
99
- mat-tab-group ::ng-deep .mat-tab-body-wrapper {
100
- padding: 16px;
101
- }
102
- }
103
  }
 
1
+ // Tab içeriği için padding
2
+ .mat-tab-body-content {
3
+ padding: 24px;
4
+ }
5
+
6
+ .app-container {
7
+ padding: 20px;
8
+ min-height: 100vh;
9
+ background-color: #f5f5f5;
10
+ }
11
+
12
+ .header {
13
+ display: flex;
14
+ justify-content: space-between;
15
+ align-items: center;
16
+ margin-bottom: 20px;
17
+ padding: 15px 20px;
18
+ background: white;
19
+ border-radius: 8px;
20
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
21
+
22
+ h1 {
23
+ margin: 0;
24
+ color: #333;
25
+ font-size: 24px;
26
+ }
27
+
28
+ .header-actions {
29
+ display: flex;
30
+ align-items: center;
31
+ gap: 20px;
32
+
33
+ .user-info {
34
+ display: flex;
35
+ align-items: center;
36
+ gap: 10px;
37
+ color: #666;
38
+
39
+ mat-icon {
40
+ font-size: 20px;
41
+ }
42
+ }
43
+
44
+ button {
45
+ mat-icon {
46
+ margin-right: 4px;
47
+ }
48
+ }
49
+ }
50
+ }
51
+
52
+ .main-content {
53
+ background: white;
54
+ border-radius: 8px;
55
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
56
+ overflow: hidden;
57
+
58
+ mat-tab-group {
59
+ ::ng-deep {
60
+ .mat-tab-header {
61
+ background-color: #fafafa;
62
+ border-bottom: 1px solid #e0e0e0;
63
+ }
64
+
65
+ .mat-tab-label {
66
+ min-width: 120px;
67
+ opacity: 0.8;
68
+
69
+ &.mat-tab-label-active {
70
+ opacity: 1;
71
+ font-weight: 500;
72
+ }
73
+ }
74
+
75
+ .mat-tab-body-wrapper {
76
+ padding: 24px;
77
+ }
78
+ }
79
+ }
80
+ }
81
+
82
+ // Responsive adjustments
83
+ @media (max-width: 768px) {
84
+ .app-container {
85
+ padding: 10px;
86
+ }
87
+
88
+ .header {
89
+ flex-direction: column;
90
+ gap: 15px;
91
+ text-align: center;
92
+
93
+ h1 {
94
+ font-size: 20px;
95
+ }
96
+ }
97
+
98
+ .main-content {
99
+ mat-tab-group ::ng-deep .mat-tab-body-wrapper {
100
+ padding: 16px;
101
+ }
102
+ }
103
  }
flare-ui/src/app/app.module.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { NgModule } from '@angular/core';
2
- import { BrowserModule } from '@angular/platform-browser';
3
- import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
4
- import { HttpClientModule, provideHttpClient, withInterceptors } from '@angular/common/http';
5
- import { FormsModule, ReactiveFormsModule } from '@angular/forms';
6
- import { RouterModule, Routes } from '@angular/router';
7
  import { authInterceptor } from './interceptors/auth.interceptor';
 
1
+ import { NgModule } from '@angular/core';
2
+ import { BrowserModule } from '@angular/platform-browser';
3
+ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
4
+ import { HttpClientModule, provideHttpClient, withInterceptors } from '@angular/common/http';
5
+ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
6
+ import { RouterModule, Routes } from '@angular/router';
7
  import { authInterceptor } from './interceptors/auth.interceptor';
flare-ui/src/app/services/api.service.ts CHANGED
@@ -1,259 +1,259 @@
1
- import { Injectable } from '@angular/core';
2
- import { HttpClient, HttpHeaders } from '@angular/common/http';
3
- import { Observable, throwError } from 'rxjs';
4
- import { catchError, tap } from 'rxjs/operators';
5
- import { Router } from '@angular/router';
6
-
7
- @Injectable({
8
- providedIn: 'root'
9
- })
10
- export class ApiService {
11
- private apiUrl = '/api';
12
- private tokenKey = 'flare_admin_token';
13
- private usernameKey = 'flare_admin_username';
14
-
15
- constructor(
16
- private http: HttpClient,
17
- private router: Router
18
- ) {}
19
-
20
- // ===================== Auth =====================
21
- login(username: string, password: string): Observable<any> {
22
- return this.http.post(`${this.apiUrl}/login`, { username, password }).pipe(
23
- tap((response: any) => {
24
- localStorage.setItem(this.tokenKey, response.token);
25
- localStorage.setItem(this.usernameKey, response.username);
26
- })
27
- );
28
- }
29
-
30
- logout(): void {
31
- localStorage.removeItem(this.tokenKey);
32
- localStorage.removeItem(this.usernameKey);
33
- this.router.navigate(['/login']);
34
- }
35
-
36
- getToken(): string | null {
37
- return localStorage.getItem(this.tokenKey);
38
- }
39
-
40
- getUsername(): string | null {
41
- return localStorage.getItem(this.usernameKey);
42
- }
43
-
44
- isAuthenticated(): boolean {
45
- return !!this.getToken();
46
- }
47
-
48
- private getAuthHeaders(): HttpHeaders {
49
- const token = this.getToken();
50
- return new HttpHeaders({
51
- 'Authorization': `Bearer ${token}`,
52
- 'Content-Type': 'application/json'
53
- });
54
- }
55
-
56
- // ===================== User =====================
57
- changePassword(currentPassword: string, newPassword: string): Observable<any> {
58
- return this.http.post(
59
- `${this.apiUrl}/change-password`,
60
- { current_password: currentPassword, new_password: newPassword },
61
- { headers: this.getAuthHeaders() }
62
- ).pipe(
63
- catchError(this.handleError)
64
- );
65
- }
66
-
67
- // ===================== Environment =====================
68
- getEnvironment(): Observable<any> {
69
- return this.http.get(`${this.apiUrl}/environment`, {
70
- headers: this.getAuthHeaders()
71
- }).pipe(
72
- catchError(this.handleError)
73
- );
74
- }
75
-
76
- updateEnvironment(data: any): Observable<any> {
77
- return this.http.put(`${this.apiUrl}/environment`, data, {
78
- headers: this.getAuthHeaders()
79
- }).pipe(
80
- catchError(this.handleError)
81
- );
82
- }
83
-
84
- // ===================== Projects =====================
85
- getProjects(includeDeleted = false): Observable<any[]> {
86
- return this.http.get<any[]>(`${this.apiUrl}/projects`, {
87
- headers: this.getAuthHeaders(),
88
- params: { include_deleted: includeDeleted.toString() }
89
- }).pipe(
90
- catchError(this.handleError)
91
- );
92
- }
93
-
94
- createProject(data: any): Observable<any> {
95
- return this.http.post(`${this.apiUrl}/projects`, data, {
96
- headers: this.getAuthHeaders()
97
- }).pipe(
98
- catchError(this.handleError)
99
- );
100
- }
101
-
102
- updateProject(id: number, data: any): Observable<any> {
103
- return this.http.put(`${this.apiUrl}/projects/${id}`, data, {
104
- headers: this.getAuthHeaders()
105
- }).pipe(
106
- catchError(this.handleError)
107
- );
108
- }
109
-
110
- deleteProject(id: number): Observable<any> {
111
- return this.http.delete(`${this.apiUrl}/projects/${id}`, {
112
- headers: this.getAuthHeaders()
113
- }).pipe(
114
- catchError(this.handleError)
115
- );
116
- }
117
-
118
- toggleProject(id: number): Observable<any> {
119
- return this.http.patch(`${this.apiUrl}/projects/${id}/toggle`, {}, {
120
- headers: this.getAuthHeaders()
121
- }).pipe(
122
- catchError(this.handleError)
123
- );
124
- }
125
-
126
- exportProject(id: number): Observable<any> {
127
- return this.http.get(`${this.apiUrl}/projects/${id}/export`, {
128
- headers: this.getAuthHeaders()
129
- }).pipe(
130
- catchError(this.handleError)
131
- );
132
- }
133
-
134
- importProject(data: any): Observable<any> {
135
- return this.http.post(`${this.apiUrl}/projects/import`, data, {
136
- headers: this.getAuthHeaders()
137
- }).pipe(
138
- catchError(this.handleError)
139
- );
140
- }
141
-
142
- // ===================== Versions =====================
143
- createVersion(projectId: number, data: any): Observable<any> {
144
- return this.http.post(`${this.apiUrl}/projects/${projectId}/versions`, data, {
145
- headers: this.getAuthHeaders()
146
- }).pipe(
147
- catchError(this.handleError)
148
- );
149
- }
150
-
151
- updateVersion(projectId: number, versionId: number, data: any): Observable<any> {
152
- return this.http.put(`${this.apiUrl}/projects/${projectId}/versions/${versionId}`, data, {
153
- headers: this.getAuthHeaders()
154
- }).pipe(
155
- catchError(this.handleError)
156
- );
157
- }
158
-
159
- deleteVersion(projectId: number, versionId: number): Observable<any> {
160
- return this.http.delete(`${this.apiUrl}/projects/${projectId}/versions/${versionId}`, {
161
- headers: this.getAuthHeaders()
162
- }).pipe(
163
- catchError(this.handleError)
164
- );
165
- }
166
-
167
- publishVersion(projectId: number, versionId: number): Observable<any> {
168
- return this.http.post(`${this.apiUrl}/projects/${projectId}/versions/${versionId}/publish`, {}, {
169
- headers: this.getAuthHeaders()
170
- }).pipe(
171
- catchError(this.handleError)
172
- );
173
- }
174
-
175
- // ===================== APIs =====================
176
- getAPIs(includeDeleted = false): Observable<any[]> {
177
- return this.http.get<any[]>(`${this.apiUrl}/apis`, {
178
- headers: this.getAuthHeaders(),
179
- params: { include_deleted: includeDeleted.toString() }
180
- }).pipe(
181
- catchError(this.handleError)
182
- );
183
- }
184
-
185
- createAPI(data: any): Observable<any> {
186
- return this.http.post(`${this.apiUrl}/apis`, data, {
187
- headers: this.getAuthHeaders()
188
- }).pipe(
189
- catchError(this.handleError)
190
- );
191
- }
192
-
193
- updateAPI(name: string, data: any): Observable<any> {
194
- return this.http.put(`${this.apiUrl}/apis/${name}`, data, {
195
- headers: this.getAuthHeaders()
196
- }).pipe(
197
- catchError(this.handleError)
198
- );
199
- }
200
-
201
- deleteAPI(name: string): Observable<any> {
202
- return this.http.delete(`${this.apiUrl}/apis/${name}`, {
203
- headers: this.getAuthHeaders()
204
- }).pipe(
205
- catchError(this.handleError)
206
- );
207
- }
208
-
209
- testAPI(data: any): Observable<any> {
210
- return this.http.post(`${this.apiUrl}/apis/test`, data, {
211
- headers: this.getAuthHeaders()
212
- }).pipe(
213
- catchError(this.handleError)
214
- );
215
- }
216
-
217
- // ===================== Tests =====================
218
- runTests(testType: string): Observable<any> {
219
- return this.http.post(`${this.apiUrl}/test/run-all`, { test_type: testType }, {
220
- headers: this.getAuthHeaders()
221
- }).pipe(
222
- catchError(this.handleError)
223
- );
224
- }
225
-
226
- // ===================== Activity Log =====================
227
- getActivityLog(limit = 50): Observable<any[]> {
228
- return this.http.get<any[]>(`${this.apiUrl}/activity-log`, {
229
- headers: this.getAuthHeaders(),
230
- params: { limit: limit.toString() }
231
- }).pipe(
232
- catchError(this.handleError)
233
- );
234
- }
235
-
236
- // ===================== Validation =====================
237
- validateRegex(pattern: string, testValue: string): Observable<any> {
238
- return this.http.post(`${this.apiUrl}/validate/regex`,
239
- { pattern, test_value: testValue },
240
- { headers: this.getAuthHeaders() }
241
- ).pipe(
242
- catchError(this.handleError)
243
- );
244
- }
245
-
246
- // ===================== Error Handler =====================
247
- private handleError(error: any) {
248
- console.error('API Error:', error);
249
-
250
- if (error.status === 401) {
251
- // Token expired or invalid
252
- localStorage.removeItem(this.tokenKey);
253
- localStorage.removeItem(this.usernameKey);
254
- this.router.navigate(['/login']);
255
- }
256
-
257
- return throwError(() => error.error || error);
258
- }
259
  }
 
1
+ import { Injectable } from '@angular/core';
2
+ import { HttpClient, HttpHeaders } from '@angular/common/http';
3
+ import { Observable, throwError } from 'rxjs';
4
+ import { catchError, tap } from 'rxjs/operators';
5
+ import { Router } from '@angular/router';
6
+
7
+ @Injectable({
8
+ providedIn: 'root'
9
+ })
10
+ export class ApiService {
11
+ private apiUrl = '/api';
12
+ private tokenKey = 'flare_admin_token';
13
+ private usernameKey = 'flare_admin_username';
14
+
15
+ constructor(
16
+ private http: HttpClient,
17
+ private router: Router
18
+ ) {}
19
+
20
+ // ===================== Auth =====================
21
+ login(username: string, password: string): Observable<any> {
22
+ return this.http.post(`${this.apiUrl}/login`, { username, password }).pipe(
23
+ tap((response: any) => {
24
+ localStorage.setItem(this.tokenKey, response.token);
25
+ localStorage.setItem(this.usernameKey, response.username);
26
+ })
27
+ );
28
+ }
29
+
30
+ logout(): void {
31
+ localStorage.removeItem(this.tokenKey);
32
+ localStorage.removeItem(this.usernameKey);
33
+ this.router.navigate(['/login']);
34
+ }
35
+
36
+ getToken(): string | null {
37
+ return localStorage.getItem(this.tokenKey);
38
+ }
39
+
40
+ getUsername(): string | null {
41
+ return localStorage.getItem(this.usernameKey);
42
+ }
43
+
44
+ isAuthenticated(): boolean {
45
+ return !!this.getToken();
46
+ }
47
+
48
+ private getAuthHeaders(): HttpHeaders {
49
+ const token = this.getToken();
50
+ return new HttpHeaders({
51
+ 'Authorization': `Bearer ${token}`,
52
+ 'Content-Type': 'application/json'
53
+ });
54
+ }
55
+
56
+ // ===================== User =====================
57
+ changePassword(currentPassword: string, newPassword: string): Observable<any> {
58
+ return this.http.post(
59
+ `${this.apiUrl}/change-password`,
60
+ { current_password: currentPassword, new_password: newPassword },
61
+ { headers: this.getAuthHeaders() }
62
+ ).pipe(
63
+ catchError(this.handleError)
64
+ );
65
+ }
66
+
67
+ // ===================== Environment =====================
68
+ getEnvironment(): Observable<any> {
69
+ return this.http.get(`${this.apiUrl}/environment`, {
70
+ headers: this.getAuthHeaders()
71
+ }).pipe(
72
+ catchError(this.handleError)
73
+ );
74
+ }
75
+
76
+ updateEnvironment(data: any): Observable<any> {
77
+ return this.http.put(`${this.apiUrl}/environment`, data, {
78
+ headers: this.getAuthHeaders()
79
+ }).pipe(
80
+ catchError(this.handleError)
81
+ );
82
+ }
83
+
84
+ // ===================== Projects =====================
85
+ getProjects(includeDeleted = false): Observable<any[]> {
86
+ return this.http.get<any[]>(`${this.apiUrl}/projects`, {
87
+ headers: this.getAuthHeaders(),
88
+ params: { include_deleted: includeDeleted.toString() }
89
+ }).pipe(
90
+ catchError(this.handleError)
91
+ );
92
+ }
93
+
94
+ createProject(data: any): Observable<any> {
95
+ return this.http.post(`${this.apiUrl}/projects`, data, {
96
+ headers: this.getAuthHeaders()
97
+ }).pipe(
98
+ catchError(this.handleError)
99
+ );
100
+ }
101
+
102
+ updateProject(id: number, data: any): Observable<any> {
103
+ return this.http.put(`${this.apiUrl}/projects/${id}`, data, {
104
+ headers: this.getAuthHeaders()
105
+ }).pipe(
106
+ catchError(this.handleError)
107
+ );
108
+ }
109
+
110
+ deleteProject(id: number): Observable<any> {
111
+ return this.http.delete(`${this.apiUrl}/projects/${id}`, {
112
+ headers: this.getAuthHeaders()
113
+ }).pipe(
114
+ catchError(this.handleError)
115
+ );
116
+ }
117
+
118
+ toggleProject(id: number): Observable<any> {
119
+ return this.http.patch(`${this.apiUrl}/projects/${id}/toggle`, {}, {
120
+ headers: this.getAuthHeaders()
121
+ }).pipe(
122
+ catchError(this.handleError)
123
+ );
124
+ }
125
+
126
+ exportProject(id: number): Observable<any> {
127
+ return this.http.get(`${this.apiUrl}/projects/${id}/export`, {
128
+ headers: this.getAuthHeaders()
129
+ }).pipe(
130
+ catchError(this.handleError)
131
+ );
132
+ }
133
+
134
+ importProject(data: any): Observable<any> {
135
+ return this.http.post(`${this.apiUrl}/projects/import`, data, {
136
+ headers: this.getAuthHeaders()
137
+ }).pipe(
138
+ catchError(this.handleError)
139
+ );
140
+ }
141
+
142
+ // ===================== Versions =====================
143
+ createVersion(projectId: number, data: any): Observable<any> {
144
+ return this.http.post(`${this.apiUrl}/projects/${projectId}/versions`, data, {
145
+ headers: this.getAuthHeaders()
146
+ }).pipe(
147
+ catchError(this.handleError)
148
+ );
149
+ }
150
+
151
+ updateVersion(projectId: number, versionId: number, data: any): Observable<any> {
152
+ return this.http.put(`${this.apiUrl}/projects/${projectId}/versions/${versionId}`, data, {
153
+ headers: this.getAuthHeaders()
154
+ }).pipe(
155
+ catchError(this.handleError)
156
+ );
157
+ }
158
+
159
+ deleteVersion(projectId: number, versionId: number): Observable<any> {
160
+ return this.http.delete(`${this.apiUrl}/projects/${projectId}/versions/${versionId}`, {
161
+ headers: this.getAuthHeaders()
162
+ }).pipe(
163
+ catchError(this.handleError)
164
+ );
165
+ }
166
+
167
+ publishVersion(projectId: number, versionId: number): Observable<any> {
168
+ return this.http.post(`${this.apiUrl}/projects/${projectId}/versions/${versionId}/publish`, {}, {
169
+ headers: this.getAuthHeaders()
170
+ }).pipe(
171
+ catchError(this.handleError)
172
+ );
173
+ }
174
+
175
+ // ===================== APIs =====================
176
+ getAPIs(includeDeleted = false): Observable<any[]> {
177
+ return this.http.get<any[]>(`${this.apiUrl}/apis`, {
178
+ headers: this.getAuthHeaders(),
179
+ params: { include_deleted: includeDeleted.toString() }
180
+ }).pipe(
181
+ catchError(this.handleError)
182
+ );
183
+ }
184
+
185
+ createAPI(data: any): Observable<any> {
186
+ return this.http.post(`${this.apiUrl}/apis`, data, {
187
+ headers: this.getAuthHeaders()
188
+ }).pipe(
189
+ catchError(this.handleError)
190
+ );
191
+ }
192
+
193
+ updateAPI(name: string, data: any): Observable<any> {
194
+ return this.http.put(`${this.apiUrl}/apis/${name}`, data, {
195
+ headers: this.getAuthHeaders()
196
+ }).pipe(
197
+ catchError(this.handleError)
198
+ );
199
+ }
200
+
201
+ deleteAPI(name: string): Observable<any> {
202
+ return this.http.delete(`${this.apiUrl}/apis/${name}`, {
203
+ headers: this.getAuthHeaders()
204
+ }).pipe(
205
+ catchError(this.handleError)
206
+ );
207
+ }
208
+
209
+ testAPI(data: any): Observable<any> {
210
+ return this.http.post(`${this.apiUrl}/apis/test`, data, {
211
+ headers: this.getAuthHeaders()
212
+ }).pipe(
213
+ catchError(this.handleError)
214
+ );
215
+ }
216
+
217
+ // ===================== Tests =====================
218
+ runTests(testType: string): Observable<any> {
219
+ return this.http.post(`${this.apiUrl}/test/run-all`, { test_type: testType }, {
220
+ headers: this.getAuthHeaders()
221
+ }).pipe(
222
+ catchError(this.handleError)
223
+ );
224
+ }
225
+
226
+ // ===================== Activity Log =====================
227
+ getActivityLog(limit = 50): Observable<any[]> {
228
+ return this.http.get<any[]>(`${this.apiUrl}/activity-log`, {
229
+ headers: this.getAuthHeaders(),
230
+ params: { limit: limit.toString() }
231
+ }).pipe(
232
+ catchError(this.handleError)
233
+ );
234
+ }
235
+
236
+ // ===================== Validation =====================
237
+ validateRegex(pattern: string, testValue: string): Observable<any> {
238
+ return this.http.post(`${this.apiUrl}/validate/regex`,
239
+ { pattern, test_value: testValue },
240
+ { headers: this.getAuthHeaders() }
241
+ ).pipe(
242
+ catchError(this.handleError)
243
+ );
244
+ }
245
+
246
+ // ===================== Error Handler =====================
247
+ private handleError(error: any) {
248
+ console.error('API Error:', error);
249
+
250
+ if (error.status === 401) {
251
+ // Token expired or invalid
252
+ localStorage.removeItem(this.tokenKey);
253
+ localStorage.removeItem(this.usernameKey);
254
+ this.router.navigate(['/login']);
255
+ }
256
+
257
+ return throwError(() => error.error || error);
258
+ }
259
  }
flare-ui/src/main.ts CHANGED
@@ -1,6 +1,7 @@
1
  import { bootstrapApplication } from '@angular/platform-browser';
2
  import { provideRouter } from '@angular/router';
3
  import { provideHttpClient, withInterceptors } from '@angular/common/http';
 
4
  import { AppComponent } from './app/app.component';
5
  import { routes } from './app/app.routes';
6
  import { authInterceptor } from './app/interceptors/auth.interceptor';
@@ -10,6 +11,7 @@ bootstrapApplication(AppComponent, {
10
  provideRouter(routes),
11
  provideHttpClient(
12
  withInterceptors([authInterceptor])
13
- )
 
14
  ]
15
  });
 
1
  import { bootstrapApplication } from '@angular/platform-browser';
2
  import { provideRouter } from '@angular/router';
3
  import { provideHttpClient, withInterceptors } from '@angular/common/http';
4
+ import { provideAnimations } from '@angular/platform-browser/animations';
5
  import { AppComponent } from './app/app.component';
6
  import { routes } from './app/app.routes';
7
  import { authInterceptor } from './app/interceptors/auth.interceptor';
 
11
  provideRouter(routes),
12
  provideHttpClient(
13
  withInterceptors([authInterceptor])
14
+ ),
15
+ provideAnimations()
16
  ]
17
  });
flare-ui/src/styles.scss CHANGED
@@ -1,262 +1,262 @@
1
- @import '@angular/material/prebuilt-themes/indigo-pink.css';
2
-
3
- /* Global Styles */
4
- * {
5
- box-sizing: border-box;
6
- margin: 0;
7
- padding: 0;
8
- }
9
-
10
- body {
11
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
12
- font-size: 14px;
13
- line-height: 1.5;
14
- color: #333;
15
- background-color: #f5f5f5;
16
- }
17
-
18
- /* Utility Classes */
19
- .container {
20
- max-width: 1200px;
21
- margin: 0 auto;
22
- padding: 0 20px;
23
- }
24
-
25
- .btn {
26
- display: inline-block;
27
- padding: 8px 16px;
28
- font-size: 14px;
29
- font-weight: 500;
30
- text-align: center;
31
- text-decoration: none;
32
- border: none;
33
- border-radius: 4px;
34
- cursor: pointer;
35
- transition: all 0.3s ease;
36
-
37
- &.btn-primary {
38
- background-color: #007bff;
39
- color: white;
40
-
41
- &:hover {
42
- background-color: #0056b3;
43
- }
44
- }
45
-
46
- &.btn-secondary {
47
- background-color: #6c757d;
48
- color: white;
49
-
50
- &:hover {
51
- background-color: #545b62;
52
- }
53
- }
54
-
55
- &.btn-danger {
56
- background-color: #dc3545;
57
- color: white;
58
-
59
- &:hover {
60
- background-color: #c82333;
61
- }
62
- }
63
-
64
- &:disabled {
65
- opacity: 0.6;
66
- cursor: not-allowed;
67
- }
68
- }
69
-
70
- /* Form Styles */
71
- .form-group {
72
- margin-bottom: 1rem;
73
-
74
- label {
75
- display: block;
76
- margin-bottom: 0.5rem;
77
- font-weight: 500;
78
- color: #495057;
79
- }
80
-
81
- input,
82
- select,
83
- textarea {
84
- display: block;
85
- width: 100%;
86
- padding: 0.375rem 0.75rem;
87
- font-size: 1rem;
88
- line-height: 1.5;
89
- color: #495057;
90
- background-color: #fff;
91
- background-clip: padding-box;
92
- border: 1px solid #ced4da;
93
- border-radius: 0.25rem;
94
- transition: border-color 0.15s ease-in-out;
95
-
96
- &:focus {
97
- color: #495057;
98
- background-color: #fff;
99
- border-color: #80bdff;
100
- outline: 0;
101
- }
102
- }
103
-
104
- textarea {
105
- min-height: 100px;
106
- resize: vertical;
107
- }
108
- }
109
-
110
- /* Table Styles */
111
- .table {
112
- width: 100%;
113
- margin-bottom: 1rem;
114
- background-color: white;
115
- border-collapse: collapse;
116
-
117
- th,
118
- td {
119
- padding: 0.75rem;
120
- text-align: left;
121
- border-bottom: 1px solid #dee2e6;
122
- }
123
-
124
- th {
125
- background-color: #f8f9fa;
126
- font-weight: 600;
127
- color: #495057;
128
- }
129
-
130
- tbody tr:hover {
131
- background-color: #f8f9fa;
132
- }
133
- }
134
-
135
- /* Card Styles */
136
- .card {
137
- background-color: white;
138
- border: 1px solid #dee2e6;
139
- border-radius: 0.25rem;
140
- box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
141
- margin-bottom: 1rem;
142
-
143
- .card-header {
144
- padding: 0.75rem 1.25rem;
145
- background-color: #f8f9fa;
146
- border-bottom: 1px solid #dee2e6;
147
- font-weight: 600;
148
- }
149
-
150
- .card-body {
151
- padding: 1.25rem;
152
- }
153
- }
154
-
155
- /* Tab Styles */
156
- .tabs {
157
- display: flex;
158
- border-bottom: 2px solid #dee2e6;
159
- margin-bottom: 1rem;
160
-
161
- .tab {
162
- padding: 0.5rem 1rem;
163
- cursor: pointer;
164
- border: none;
165
- background: none;
166
- font-weight: 500;
167
- color: #6c757d;
168
- transition: all 0.3s ease;
169
-
170
- &.active {
171
- color: #007bff;
172
- border-bottom: 2px solid #007bff;
173
- margin-bottom: -2px;
174
- }
175
-
176
- &:hover {
177
- color: #007bff;
178
- }
179
- }
180
- }
181
-
182
- /* Alert Styles */
183
- .alert {
184
- padding: 0.75rem 1.25rem;
185
- margin-bottom: 1rem;
186
- border: 1px solid transparent;
187
- border-radius: 0.25rem;
188
-
189
- &.alert-danger {
190
- color: #721c24;
191
- background-color: #f8d7da;
192
- border-color: #f5c6cb;
193
- }
194
-
195
- &.alert-success {
196
- color: #155724;
197
- background-color: #d4edda;
198
- border-color: #c3e6cb;
199
- }
200
- }
201
-
202
- /* Loading Spinner */
203
- .spinner {
204
- display: inline-block;
205
- width: 20px;
206
- height: 20px;
207
- border: 3px solid rgba(0, 0, 0, 0.1);
208
- border-radius: 50%;
209
- border-top-color: #007bff;
210
- animation: spin 1s ease-in-out infinite;
211
- }
212
-
213
- @keyframes spin {
214
- to { transform: rotate(360deg); }
215
- }
216
-
217
- /* Dialog/Modal Styles */
218
- .dialog-backdrop {
219
- position: fixed;
220
- top: 0;
221
- left: 0;
222
- width: 100%;
223
- height: 100%;
224
- background-color: rgba(0, 0, 0, 0.5);
225
- display: flex;
226
- align-items: center;
227
- justify-content: center;
228
- z-index: 1000;
229
- }
230
-
231
- .dialog {
232
- background-color: white;
233
- border-radius: 8px;
234
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
235
- max-width: 600px;
236
- width: 90%;
237
- max-height: 90vh;
238
- overflow: hidden;
239
- display: flex;
240
- flex-direction: column;
241
-
242
- .dialog-header {
243
- padding: 1rem 1.5rem;
244
- border-bottom: 1px solid #dee2e6;
245
- font-size: 1.25rem;
246
- font-weight: 600;
247
- }
248
-
249
- .dialog-body {
250
- padding: 1.5rem;
251
- overflow-y: auto;
252
- flex: 1;
253
- }
254
-
255
- .dialog-footer {
256
- padding: 1rem 1.5rem;
257
- border-top: 1px solid #dee2e6;
258
- display: flex;
259
- justify-content: flex-end;
260
- gap: 0.5rem;
261
- }
262
  }
 
1
+ @import '@angular/material/prebuilt-themes/indigo-pink.css';
2
+
3
+ /* Global Styles */
4
+ * {
5
+ box-sizing: border-box;
6
+ margin: 0;
7
+ padding: 0;
8
+ }
9
+
10
+ body {
11
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
12
+ font-size: 14px;
13
+ line-height: 1.5;
14
+ color: #333;
15
+ background-color: #f5f5f5;
16
+ }
17
+
18
+ /* Utility Classes */
19
+ .container {
20
+ max-width: 1200px;
21
+ margin: 0 auto;
22
+ padding: 0 20px;
23
+ }
24
+
25
+ .btn {
26
+ display: inline-block;
27
+ padding: 8px 16px;
28
+ font-size: 14px;
29
+ font-weight: 500;
30
+ text-align: center;
31
+ text-decoration: none;
32
+ border: none;
33
+ border-radius: 4px;
34
+ cursor: pointer;
35
+ transition: all 0.3s ease;
36
+
37
+ &.btn-primary {
38
+ background-color: #007bff;
39
+ color: white;
40
+
41
+ &:hover {
42
+ background-color: #0056b3;
43
+ }
44
+ }
45
+
46
+ &.btn-secondary {
47
+ background-color: #6c757d;
48
+ color: white;
49
+
50
+ &:hover {
51
+ background-color: #545b62;
52
+ }
53
+ }
54
+
55
+ &.btn-danger {
56
+ background-color: #dc3545;
57
+ color: white;
58
+
59
+ &:hover {
60
+ background-color: #c82333;
61
+ }
62
+ }
63
+
64
+ &:disabled {
65
+ opacity: 0.6;
66
+ cursor: not-allowed;
67
+ }
68
+ }
69
+
70
+ /* Form Styles */
71
+ .form-group {
72
+ margin-bottom: 1rem;
73
+
74
+ label {
75
+ display: block;
76
+ margin-bottom: 0.5rem;
77
+ font-weight: 500;
78
+ color: #495057;
79
+ }
80
+
81
+ input,
82
+ select,
83
+ textarea {
84
+ display: block;
85
+ width: 100%;
86
+ padding: 0.375rem 0.75rem;
87
+ font-size: 1rem;
88
+ line-height: 1.5;
89
+ color: #495057;
90
+ background-color: #fff;
91
+ background-clip: padding-box;
92
+ border: 1px solid #ced4da;
93
+ border-radius: 0.25rem;
94
+ transition: border-color 0.15s ease-in-out;
95
+
96
+ &:focus {
97
+ color: #495057;
98
+ background-color: #fff;
99
+ border-color: #80bdff;
100
+ outline: 0;
101
+ }
102
+ }
103
+
104
+ textarea {
105
+ min-height: 100px;
106
+ resize: vertical;
107
+ }
108
+ }
109
+
110
+ /* Table Styles */
111
+ .table {
112
+ width: 100%;
113
+ margin-bottom: 1rem;
114
+ background-color: white;
115
+ border-collapse: collapse;
116
+
117
+ th,
118
+ td {
119
+ padding: 0.75rem;
120
+ text-align: left;
121
+ border-bottom: 1px solid #dee2e6;
122
+ }
123
+
124
+ th {
125
+ background-color: #f8f9fa;
126
+ font-weight: 600;
127
+ color: #495057;
128
+ }
129
+
130
+ tbody tr:hover {
131
+ background-color: #f8f9fa;
132
+ }
133
+ }
134
+
135
+ /* Card Styles */
136
+ .card {
137
+ background-color: white;
138
+ border: 1px solid #dee2e6;
139
+ border-radius: 0.25rem;
140
+ box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
141
+ margin-bottom: 1rem;
142
+
143
+ .card-header {
144
+ padding: 0.75rem 1.25rem;
145
+ background-color: #f8f9fa;
146
+ border-bottom: 1px solid #dee2e6;
147
+ font-weight: 600;
148
+ }
149
+
150
+ .card-body {
151
+ padding: 1.25rem;
152
+ }
153
+ }
154
+
155
+ /* Tab Styles */
156
+ .tabs {
157
+ display: flex;
158
+ border-bottom: 2px solid #dee2e6;
159
+ margin-bottom: 1rem;
160
+
161
+ .tab {
162
+ padding: 0.5rem 1rem;
163
+ cursor: pointer;
164
+ border: none;
165
+ background: none;
166
+ font-weight: 500;
167
+ color: #6c757d;
168
+ transition: all 0.3s ease;
169
+
170
+ &.active {
171
+ color: #007bff;
172
+ border-bottom: 2px solid #007bff;
173
+ margin-bottom: -2px;
174
+ }
175
+
176
+ &:hover {
177
+ color: #007bff;
178
+ }
179
+ }
180
+ }
181
+
182
+ /* Alert Styles */
183
+ .alert {
184
+ padding: 0.75rem 1.25rem;
185
+ margin-bottom: 1rem;
186
+ border: 1px solid transparent;
187
+ border-radius: 0.25rem;
188
+
189
+ &.alert-danger {
190
+ color: #721c24;
191
+ background-color: #f8d7da;
192
+ border-color: #f5c6cb;
193
+ }
194
+
195
+ &.alert-success {
196
+ color: #155724;
197
+ background-color: #d4edda;
198
+ border-color: #c3e6cb;
199
+ }
200
+ }
201
+
202
+ /* Loading Spinner */
203
+ .spinner {
204
+ display: inline-block;
205
+ width: 20px;
206
+ height: 20px;
207
+ border: 3px solid rgba(0, 0, 0, 0.1);
208
+ border-radius: 50%;
209
+ border-top-color: #007bff;
210
+ animation: spin 1s ease-in-out infinite;
211
+ }
212
+
213
+ @keyframes spin {
214
+ to { transform: rotate(360deg); }
215
+ }
216
+
217
+ /* Dialog/Modal Styles */
218
+ .dialog-backdrop {
219
+ position: fixed;
220
+ top: 0;
221
+ left: 0;
222
+ width: 100%;
223
+ height: 100%;
224
+ background-color: rgba(0, 0, 0, 0.5);
225
+ display: flex;
226
+ align-items: center;
227
+ justify-content: center;
228
+ z-index: 1000;
229
+ }
230
+
231
+ .dialog {
232
+ background-color: white;
233
+ border-radius: 8px;
234
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
235
+ max-width: 600px;
236
+ width: 90%;
237
+ max-height: 90vh;
238
+ overflow: hidden;
239
+ display: flex;
240
+ flex-direction: column;
241
+
242
+ .dialog-header {
243
+ padding: 1rem 1.5rem;
244
+ border-bottom: 1px solid #dee2e6;
245
+ font-size: 1.25rem;
246
+ font-weight: 600;
247
+ }
248
+
249
+ .dialog-body {
250
+ padding: 1.5rem;
251
+ overflow-y: auto;
252
+ flex: 1;
253
+ }
254
+
255
+ .dialog-footer {
256
+ padding: 1rem 1.5rem;
257
+ border-top: 1px solid #dee2e6;
258
+ display: flex;
259
+ justify-content: flex-end;
260
+ gap: 0.5rem;
261
+ }
262
  }