Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve --proxy-config proxy.conf.json",
"start": "ng serve --host 0.0.0.0 --proxy-config proxy.conf.json",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "jest --verbose",
Expand Down Expand Up @@ -34,6 +34,7 @@
"@ngneat/transloco": "^5.0.7",
"@ngneat/until-destroy": "^10.0.0",
"ace-builds": "^1.24.2",
"chart.js": "^4.4.1",
"rxjs": "~7.8.0",
"source-map-support": "^0.5.21",
"swagger-ui": "^5.6.1",
Expand Down
108 changes: 106 additions & 2 deletions src/app/adf-apps/df-manage-apps/df-manage-apps-table.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import { generateApiKey } from 'src/app/shared/utilities/hash';
import { DfSystemConfigDataService } from 'src/app/shared/services/df-system-config-data.service';
import { AdditonalAction } from 'src/app/shared/types/table';
import { DfSnackbarService } from 'src/app/shared/services/df-snackbar.service';
import { DfDuplicateDialogComponent } from 'src/app/shared/components/df-duplicate-dialog/df-duplicate-dialog.component';
import { faCopy } from '@fortawesome/free-solid-svg-icons';
import { catchError, throwError } from 'rxjs';

@UntilDestroy({ checkProperties: true })
@Component({
Expand Down Expand Up @@ -72,7 +75,7 @@ export class DfManageAppsTableComponent extends DfManageTableComponent<AppRow> {
row.name
);
this.appsService
.update(row.id, { apiKey: newKey })
.update(row.id, { api_key: newKey })
.subscribe(() => this.refreshTable());
},
ariaLabel: {
Expand All @@ -81,10 +84,34 @@ export class DfManageAppsTableComponent extends DfManageTableComponent<AppRow> {
disabled: row => row.createdById === null,
},
];

// Add duplicate action before delete action
const duplicateAction = {
label: 'duplicate',
function: (row: AppRow) => this.duplicateApp(row),
ariaLabel: {
key: 'duplicateApp',
param: 'name',
},
icon: faCopy,
};

if (this.actions.additional) {
// Find the delete action index
const deleteIndex = this.actions.additional.findIndex(
action => action.label === 'delete'
);
if (deleteIndex !== -1) {
// Insert duplicate before delete
this.actions.additional.splice(deleteIndex, 0, duplicateAction);
} else {
// Add at the beginning if no delete found
this.actions.additional.unshift(duplicateAction);
}
// Add the extra actions at the end
this.actions.additional.push(...extraActions);
} else {
this.actions.additional = extraActions;
this.actions.additional = [duplicateAction, ...extraActions];
}
}
override columns = [
Expand Down Expand Up @@ -149,4 +176,81 @@ export class DfManageAppsTableComponent extends DfManageTableComponent<AppRow> {
this.tableLength = data.meta.count;
});
}

duplicateApp(row: AppRow): void {
// First, get the full app details
this.appsService
.get<AppType>(row.id)
.pipe(
catchError(error => {
console.error('Failed to fetch app details:', error);
return throwError(() => error);
})
)
.subscribe(app => {
// Get all existing app names for validation
this.appsService
.getAll<GenericListResponse<AppType>>({ limit: 1000 })
.subscribe(allApps => {
const existingNames = allApps.resource.map(a => a.name);

const dialogRef = this.dialog.open(DfDuplicateDialogComponent, {
width: '400px',
data: {
title: 'apps.duplicate.title',
message: 'apps.duplicate.message',
label: 'apps.duplicate.nameLabel',
originalName: app.name,
existingNames: existingNames,
},
});

dialogRef.afterClosed().subscribe(newName => {
if (newName) {
// Create a copy of the app with the new name
// Using snake_case as expected by the API
// Note: API key is generated server-side, not sent in payload
const duplicatedApp = {
name: newName,
description: `${app.description || ''} (copy)`,
is_active: app.isActive,
type: app.type,
role_id: app.roleId || null,
// Copy app location specific fields
url: app.url || null,
storage_service_id: app.storageServiceId || null,
storage_container: app.storageContainer || null,
path: app.path || null,
// Copy additional settings
requires_fullscreen: app.requiresFullscreen,
allow_fullscreen_toggle: app.allowFullscreenToggle,
toggle_location: app.toggleLocation,
};

// Wrap in resource array as expected by the API
const payload = {
resource: [duplicatedApp],
};

// Create the new app
this.appsService
.create(payload, {
snackbarSuccess: 'apps.alerts.duplicateSuccess',
fields: '*',
related: 'role_by_role_id',
})
.pipe(
catchError(error => {
console.error('Failed to duplicate app:', error);
return throwError(() => error);
})
)
.subscribe(() => {
this.refreshTable();
});
}
});
});
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<mat-card class="dashboard-card" [class]="'card-' + color">
<mat-card-content>
<div class="card-header">
<div class="icon-container" [class]="'icon-' + color">
<fa-icon [icon]="icon"></fa-icon>
</div>
<div class="header-stats" *ngIf="trend !== undefined">
<span class="trend" [class]="trendClass">
<fa-icon *ngIf="trendIcon" [icon]="trendIcon"></fa-icon>
{{ trend }}%
</span>
</div>
</div>

<div class="card-body">
<h3 class="card-title">{{ title }}</h3>
<div class="card-value">{{ value }}</div>
<p class="card-subtitle" *ngIf="subtitle">{{ subtitle }}</p>
<ng-content></ng-content>
</div>

<div class="card-footer" *ngIf="footerText || showPrompt">
<span class="footer-text" *ngIf="footerText && !showPrompt">{{
footerText
}}</span>
<ng-content select="[prompt]"></ng-content>
</div>
</mat-card-content>
</mat-card>
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
.dashboard-card {
height: 100%;
transition:
transform 0.2s ease-in-out,
box-shadow 0.2s ease-in-out;
cursor: default;

&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
}

.mat-card-content {
padding: 20px;
}
}

.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
}

.icon-container {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;

fa-icon {
font-size: 24px;
color: white;
}

&.icon-primary {
background: linear-gradient(135deg, #7f11e0 0%, #5c239a 100%);
}

&.icon-accent {
background: linear-gradient(135deg, #ff4081 0%, #e91e63 100%);
}

&.icon-success {
background: linear-gradient(135deg, #4caf50 0%, #388e3c 100%);
}

&.icon-info {
background: linear-gradient(135deg, #2196f3 0%, #1976d2 100%);
}

&.icon-warn {
background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%);
}
}

.header-stats {
.trend {
font-size: 14px;
font-weight: 500;
display: flex;
align-items: center;
gap: 4px;

&.trend-up {
color: #4caf50;
}

&.trend-down {
color: #f44336;
}

fa-icon {
font-size: 12px;
}
}
}

.card-body {
.card-title {
font-size: 14px;
font-weight: 400;
color: #666;
margin: 0 0 8px 0;
}

.card-value {
font-size: 32px;
font-weight: 600;
color: #333;
line-height: 1.2;
}

.card-subtitle {
font-size: 12px;
color: #999;
margin: 4px 0 0 0;
}
}

.card-footer {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid rgba(0, 0, 0, 0.1);

.footer-text {
font-size: 12px;
color: #666;
}
}

// Prompt styling
::ng-deep [prompt] {
font-size: 13px;
color: #7f11e0;
font-weight: 500;
display: flex;
align-items: center;
gap: 4px;
animation: pulse 2s infinite;

&:before {
content: '👆';
font-size: 16px;
}
}

@keyframes pulse {
0% {
opacity: 0.8;
}
50% {
opacity: 1;
}
100% {
opacity: 0.8;
}
}

// Dark theme
:host-context(.dark-theme) {
.dashboard-card {
background-color: #424242;

.card-body {
.card-title {
color: #bbb;
}

.card-value {
color: #fff;
}

.card-subtitle {
color: #999;
}
}

.card-footer {
border-top-color: rgba(255, 255, 255, 0.1);

.footer-text {
color: #bbb;
}
}
}

::ng-deep [prompt] {
color: #bb86fc;
}
}
Loading