@syndew/oluso-admin-ui 0.0.0-stage → 1.30.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +76 -0
- package/README.md +29 -2
- package/fesm2022/syndew-oluso-admin-ui-entitlements-page.component-BeZtIUnk.mjs +149 -0
- package/fesm2022/syndew-oluso-admin-ui-entitlements-page.component-BeZtIUnk.mjs.map +1 -0
- package/fesm2022/syndew-oluso-admin-ui-license-status-page.component-JQmj_qhz.mjs +205 -0
- package/fesm2022/syndew-oluso-admin-ui-license-status-page.component-JQmj_qhz.mjs.map +1 -0
- package/fesm2022/syndew-oluso-admin-ui-platform-clients-page.component-ab3WDRsI.mjs +80 -0
- package/fesm2022/syndew-oluso-admin-ui-platform-clients-page.component-ab3WDRsI.mjs.map +1 -0
- package/fesm2022/syndew-oluso-admin-ui-search-results-page.component-EH82yuW3.mjs +543 -0
- package/fesm2022/syndew-oluso-admin-ui-search-results-page.component-EH82yuW3.mjs.map +1 -0
- package/fesm2022/syndew-oluso-admin-ui-syndew-oluso-admin-ui-BVs_bAxm.mjs +22987 -0
- package/fesm2022/syndew-oluso-admin-ui-syndew-oluso-admin-ui-BVs_bAxm.mjs.map +1 -0
- package/fesm2022/syndew-oluso-admin-ui-team-detail-page.component-CjrQ89qU.mjs +566 -0
- package/fesm2022/syndew-oluso-admin-ui-team-detail-page.component-CjrQ89qU.mjs.map +1 -0
- package/fesm2022/syndew-oluso-admin-ui-unauthorized-page.component-CWYFN6mp.mjs +73 -0
- package/fesm2022/syndew-oluso-admin-ui-unauthorized-page.component-CWYFN6mp.mjs.map +1 -0
- package/fesm2022/syndew-oluso-admin-ui-webhook-detail-page.component-Bh5dZwt-.mjs +728 -0
- package/fesm2022/syndew-oluso-admin-ui-webhook-detail-page.component-Bh5dZwt-.mjs.map +1 -0
- package/fesm2022/syndew-oluso-admin-ui.mjs +3 -0
- package/fesm2022/syndew-oluso-admin-ui.mjs.map +1 -0
- package/help/en/admin.api-scopes.detail.md +43 -0
- package/help/en/admin.api-scopes.md +37 -0
- package/help/en/admin.audit-logs.md +44 -0
- package/help/en/admin.clients.create.md +60 -0
- package/help/en/admin.clients.detail.md +61 -0
- package/help/en/admin.clients.md +48 -0
- package/help/en/admin.dashboard.md +28 -0
- package/help/en/admin.experience.journeys.builder.md +115 -0
- package/help/en/admin.experience.journeys.md +54 -0
- package/help/en/admin.experience.plugins.md +83 -0
- package/help/en/admin.experience.submissions.md +71 -0
- package/help/en/admin.feedback.detail.md +36 -0
- package/help/en/admin.feedback.md +42 -0
- package/help/en/admin.grants.md +42 -0
- package/help/en/admin.identity-providers.detail.md +46 -0
- package/help/en/admin.identity-providers.md +39 -0
- package/help/en/admin.identity-resources.md +47 -0
- package/help/en/admin.module-seats.md +45 -0
- package/help/en/admin.organizations.detail.md +51 -0
- package/help/en/admin.organizations.md +38 -0
- package/help/en/admin.platform.clients.create.md +34 -0
- package/help/en/admin.platform.clients.detail.md +28 -0
- package/help/en/admin.platform.clients.md +38 -0
- package/help/en/admin.resources.detail.md +44 -0
- package/help/en/admin.resources.md +43 -0
- package/help/en/admin.roles.md +33 -0
- package/help/en/admin.settings.license.md +39 -0
- package/help/en/admin.settings.md +70 -0
- package/help/en/admin.signing-keys.md +47 -0
- package/help/en/admin.tenants.detail.md +58 -0
- package/help/en/admin.tenants.md +46 -0
- package/help/en/admin.users.detail.md +41 -0
- package/help/en/admin.users.md +56 -0
- package/help/en/admin.webhooks.detail.md +41 -0
- package/help/en/admin.webhooks.md +37 -0
- package/package.json +51 -3
- package/types/syndew-oluso-admin-ui.d.ts +5786 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
Oluso Community License v1.0
|
|
2
|
+
Copyright (c) 2025-2026 Syndew Technology Ltd. All rights reserved.
|
|
3
|
+
|
|
4
|
+
TERMS AND CONDITIONS
|
|
5
|
+
|
|
6
|
+
1. GRANT OF LICENSE
|
|
7
|
+
|
|
8
|
+
Subject to the terms of this license, Syndew Technology Ltd ("Licensor")
|
|
9
|
+
grants you a non-exclusive, worldwide, royalty-free license to use, copy,
|
|
10
|
+
modify, and distribute this software, provided that:
|
|
11
|
+
|
|
12
|
+
a) You include this license and copyright notice in all copies
|
|
13
|
+
b) You comply with the Commercial Use restrictions below
|
|
14
|
+
|
|
15
|
+
2. COMMUNITY USE (FREE)
|
|
16
|
+
|
|
17
|
+
The core identity server is FREE for:
|
|
18
|
+
|
|
19
|
+
- Personal projects and learning
|
|
20
|
+
- Open source projects
|
|
21
|
+
- Non-profit organizations
|
|
22
|
+
- Educational and academic use
|
|
23
|
+
- Evaluation and testing
|
|
24
|
+
- Small businesses and startups (see eligibility at https://oluso.io/license)
|
|
25
|
+
|
|
26
|
+
3. ENTERPRISE ADD-ONS (LICENSED)
|
|
27
|
+
|
|
28
|
+
The following features require a separate license:
|
|
29
|
+
|
|
30
|
+
- SAML 2.0 (Identity Provider and Service Provider)
|
|
31
|
+
- SCIM 2.0 (User and group provisioning)
|
|
32
|
+
- LDAP Server (Legacy application support)
|
|
33
|
+
|
|
34
|
+
Enterprise add-on pricing: https://oluso.io/pricing
|
|
35
|
+
|
|
36
|
+
4. COMMERCIAL LICENSE REQUIRED
|
|
37
|
+
|
|
38
|
+
A commercial license is REQUIRED if:
|
|
39
|
+
|
|
40
|
+
- Your organization exceeds the community use thresholds (varies by region)
|
|
41
|
+
- You offer this software as a hosted/managed service to third parties
|
|
42
|
+
- You want to remove attribution requirements
|
|
43
|
+
|
|
44
|
+
To check eligibility or obtain a commercial license:
|
|
45
|
+
Visit: https://oluso.io/license
|
|
46
|
+
Contact: license@oluso.io
|
|
47
|
+
|
|
48
|
+
5. CONTRIBUTIONS
|
|
49
|
+
|
|
50
|
+
We welcome community contributions! By submitting a pull request or issue,
|
|
51
|
+
you agree that your contribution will be licensed under the same terms as
|
|
52
|
+
this license, and you grant Syndew Technology Ltd the right to use your
|
|
53
|
+
contribution in both open source and commercial versions.
|
|
54
|
+
|
|
55
|
+
6. ATTRIBUTION
|
|
56
|
+
|
|
57
|
+
If you use this software, you must:
|
|
58
|
+
|
|
59
|
+
- Retain all copyright notices
|
|
60
|
+
- Include a visible attribution to "Powered by Oluso" in your application
|
|
61
|
+
(this requirement is waived for commercial license holders)
|
|
62
|
+
|
|
63
|
+
7. NO WARRANTY
|
|
64
|
+
|
|
65
|
+
THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
66
|
+
IMPLIED. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES, OR
|
|
67
|
+
OTHER LIABILITY ARISING FROM THE USE OF THIS SOFTWARE.
|
|
68
|
+
|
|
69
|
+
8. TERMINATION
|
|
70
|
+
|
|
71
|
+
This license terminates automatically if you violate any of its terms.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
Full license terms and FAQs: https://oluso.io/license
|
|
76
|
+
Questions: license@oluso.io
|
package/README.md
CHANGED
|
@@ -1,3 +1,30 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @syndew/oluso-admin-ui
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The Oluso admin portal as an Angular library: the routes, pages and services for administering an Oluso server. That covers tenants, users and roles, clients, user journeys, signing keys and security settings.
|
|
4
|
+
|
|
5
|
+
Mount it in your own Angular app to host the admin portal yourself, rebrand it, or extend it with admin plugins such as `@syndew/oluso-plugin-saml`.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @syndew/oluso-admin-ui
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Peer dependencies (npm installs them with the package): `@angular/common`, `@angular/core`, `@angular/forms`, `@angular/platform-browser`, `@angular/router`, `angular-auth-oidc-client`, `rxjs`, `@syndew/oluso-auth`, `@syndew/oluso-ui-core`.
|
|
14
|
+
|
|
15
|
+
## Use
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { provideRouter } from '@angular/router';
|
|
19
|
+
import { ADMIN_ROUTES, provideAdminAuth } from '@syndew/oluso-admin-ui';
|
|
20
|
+
|
|
21
|
+
providers: [provideRouter(ADMIN_ROUTES), provideAdminAuth()]
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
At runtime the portal reads `window.__OLUSO_CONFIG__` (from a `config.js` next to `index.html`) for the server URL. The `admin-standalone` app in the repository is a complete example.
|
|
25
|
+
|
|
26
|
+
## Links
|
|
27
|
+
|
|
28
|
+
- [Oluso on GitHub](https://github.com/SyndewTech/Oluso)
|
|
29
|
+
- [Hosting the portals](https://github.com/SyndewTech/Oluso/blob/main/docs/docker.md)
|
|
30
|
+
- [Getting started](https://github.com/SyndewTech/Oluso/blob/main/docs/v2-quickstart.md)
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { inject, signal, Component } from '@angular/core';
|
|
3
|
+
import { CommonModule } from '@angular/common';
|
|
4
|
+
import * as i1 from '@angular/forms';
|
|
5
|
+
import { FormsModule } from '@angular/forms';
|
|
6
|
+
import { CardComponent, ButtonComponent, BadgeComponent, ModalComponent, IconComponent } from '@syndew/oluso-ui-core';
|
|
7
|
+
import { E as EntitlementService } from './syndew-oluso-admin-ui-syndew-oluso-admin-ui-BVs_bAxm.mjs';
|
|
8
|
+
|
|
9
|
+
class EntitlementsPageComponent {
|
|
10
|
+
entitlementService = inject(EntitlementService);
|
|
11
|
+
loading = signal(true, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
12
|
+
saving = signal(false, ...(ngDevMode ? [{ debugName: "saving" }] : /* istanbul ignore next */ []));
|
|
13
|
+
deleting = signal(false, ...(ngDevMode ? [{ debugName: "deleting" }] : /* istanbul ignore next */ []));
|
|
14
|
+
entitlements = signal([], ...(ngDevMode ? [{ debugName: "entitlements" }] : /* istanbul ignore next */ []));
|
|
15
|
+
error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
16
|
+
showCreateEditModal = signal(false, ...(ngDevMode ? [{ debugName: "showCreateEditModal" }] : /* istanbul ignore next */ []));
|
|
17
|
+
showDeleteModal = signal(false, ...(ngDevMode ? [{ debugName: "showDeleteModal" }] : /* istanbul ignore next */ []));
|
|
18
|
+
selected = signal(null, ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
|
|
19
|
+
formData = this.emptyForm();
|
|
20
|
+
constructor() {
|
|
21
|
+
this.load();
|
|
22
|
+
}
|
|
23
|
+
emptyForm() {
|
|
24
|
+
return { name: '', displayName: '', description: '', group: '', enabled: true };
|
|
25
|
+
}
|
|
26
|
+
load() {
|
|
27
|
+
this.loading.set(true);
|
|
28
|
+
this.entitlementService.getAll().subscribe({
|
|
29
|
+
next: (entitlements) => {
|
|
30
|
+
this.entitlements.set(entitlements);
|
|
31
|
+
this.loading.set(false);
|
|
32
|
+
},
|
|
33
|
+
error: (err) => {
|
|
34
|
+
console.error('Failed to load entitlements:', err);
|
|
35
|
+
this.loading.set(false);
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/** Grouped for display; ungrouped entries fall under "General". */
|
|
40
|
+
groups() {
|
|
41
|
+
const groups = [];
|
|
42
|
+
for (const entitlement of this.entitlements()) {
|
|
43
|
+
const groupName = entitlement.group || 'General';
|
|
44
|
+
const group = groups.find((g) => g.name === groupName);
|
|
45
|
+
if (group) {
|
|
46
|
+
group.entitlements.push(entitlement);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
groups.push({ name: groupName, entitlements: [entitlement] });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return groups;
|
|
53
|
+
}
|
|
54
|
+
openCreateModal() {
|
|
55
|
+
this.selected.set(null);
|
|
56
|
+
this.formData = this.emptyForm();
|
|
57
|
+
this.error.set(null);
|
|
58
|
+
this.showCreateEditModal.set(true);
|
|
59
|
+
}
|
|
60
|
+
openEditModal(entitlement) {
|
|
61
|
+
this.selected.set(entitlement);
|
|
62
|
+
this.formData = {
|
|
63
|
+
name: entitlement.name,
|
|
64
|
+
displayName: entitlement.displayName,
|
|
65
|
+
description: entitlement.description || '',
|
|
66
|
+
group: entitlement.group || '',
|
|
67
|
+
enabled: entitlement.enabled,
|
|
68
|
+
};
|
|
69
|
+
this.error.set(null);
|
|
70
|
+
this.showCreateEditModal.set(true);
|
|
71
|
+
}
|
|
72
|
+
closeCreateEditModal() {
|
|
73
|
+
this.showCreateEditModal.set(false);
|
|
74
|
+
this.selected.set(null);
|
|
75
|
+
this.error.set(null);
|
|
76
|
+
}
|
|
77
|
+
onSave() {
|
|
78
|
+
const existing = this.selected();
|
|
79
|
+
this.saving.set(true);
|
|
80
|
+
this.error.set(null);
|
|
81
|
+
const done = {
|
|
82
|
+
next: () => {
|
|
83
|
+
this.saving.set(false);
|
|
84
|
+
this.closeCreateEditModal();
|
|
85
|
+
this.load();
|
|
86
|
+
},
|
|
87
|
+
error: (err) => {
|
|
88
|
+
this.error.set(err?.error?.error || 'Save failed');
|
|
89
|
+
this.saving.set(false);
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
if (existing) {
|
|
93
|
+
// Name is immutable — it's already in issued tokens and on roles.
|
|
94
|
+
this.entitlementService
|
|
95
|
+
.update(existing.id, {
|
|
96
|
+
displayName: this.formData.displayName || this.formData.name,
|
|
97
|
+
description: this.formData.description,
|
|
98
|
+
group: this.formData.group,
|
|
99
|
+
enabled: this.formData.enabled,
|
|
100
|
+
})
|
|
101
|
+
.subscribe(done);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
this.entitlementService
|
|
105
|
+
.create({
|
|
106
|
+
name: this.formData.name.trim(),
|
|
107
|
+
displayName: this.formData.displayName || undefined,
|
|
108
|
+
description: this.formData.description || undefined,
|
|
109
|
+
group: this.formData.group || undefined,
|
|
110
|
+
enabled: this.formData.enabled,
|
|
111
|
+
})
|
|
112
|
+
.subscribe(done);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
openDeleteModal(entitlement) {
|
|
116
|
+
this.selected.set(entitlement);
|
|
117
|
+
this.showDeleteModal.set(true);
|
|
118
|
+
}
|
|
119
|
+
closeDeleteModal() {
|
|
120
|
+
this.showDeleteModal.set(false);
|
|
121
|
+
this.selected.set(null);
|
|
122
|
+
}
|
|
123
|
+
confirmDelete() {
|
|
124
|
+
const entitlement = this.selected();
|
|
125
|
+
if (!entitlement)
|
|
126
|
+
return;
|
|
127
|
+
this.deleting.set(true);
|
|
128
|
+
this.entitlementService.delete(entitlement.id).subscribe({
|
|
129
|
+
next: () => {
|
|
130
|
+
this.deleting.set(false);
|
|
131
|
+
this.closeDeleteModal();
|
|
132
|
+
this.load();
|
|
133
|
+
},
|
|
134
|
+
error: (err) => {
|
|
135
|
+
console.error('Failed to delete entitlement:', err);
|
|
136
|
+
this.deleting.set(false);
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: EntitlementsPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
141
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.25", type: EntitlementsPageComponent, isStandalone: true, selector: "admin-entitlements-page", ngImport: i0, template: "<div class=\"space-y-6\">\n <div class=\"flex items-start justify-between\">\n <div>\n <h1 class=\"text-2xl font-semibold text-gray-900\">Entitlements</h1>\n <p class=\"mt-1 text-sm text-gray-500\">\n Permissions your own applications enforce. Assign them to roles, and they arrive in the\n <code class=\"font-mono\">entitlements</code> claim for clients granted the\n <code class=\"font-mono\">entitlements</code> scope. They grant nothing inside Oluso.\n </p>\n </div>\n <ui-button (clicked)=\"openCreateModal()\">\n <ui-icon name=\"plus\" class=\"h-4 w-4 mr-1\" />\n New Entitlement\n </ui-button>\n </div>\n\n @if (loading()) {\n <div class=\"text-sm text-gray-500 p-4\">Loading entitlements...</div>\n } @else if (entitlements().length === 0) {\n <ui-card>\n <div class=\"text-center py-10\">\n <ui-icon name=\"key\" class=\"mx-auto h-8 w-8 text-gray-300\" />\n <p class=\"mt-2 text-sm text-gray-500\">\n No entitlements yet. Define one to make it selectable when editing a role.\n </p>\n </div>\n </ui-card>\n } @else {\n @for (group of groups(); track group.name) {\n <ui-card [title]=\"group.name\">\n <div class=\"divide-y\">\n @for (entitlement of group.entitlements; track entitlement.id) {\n <div class=\"flex items-start justify-between py-3\">\n <div>\n <div class=\"flex items-center gap-2\">\n <span class=\"text-sm font-medium text-gray-900\">{{ entitlement.displayName }}</span>\n <span class=\"font-mono text-xs text-gray-500\">{{ entitlement.name }}</span>\n @if (!entitlement.enabled) {\n <ui-badge variant=\"warning\">Disabled</ui-badge>\n }\n </div>\n @if (entitlement.description) {\n <p class=\"mt-1 text-sm text-gray-500\">{{ entitlement.description }}</p>\n }\n </div>\n <div class=\"flex items-center gap-2\">\n <button\n type=\"button\"\n class=\"text-gray-500 hover:text-gray-700\"\n title=\"Edit\"\n (click)=\"openEditModal(entitlement)\"\n >\n <ui-icon name=\"pencil\" class=\"h-4 w-4\" />\n </button>\n <button\n type=\"button\"\n class=\"text-red-500 hover:text-red-700\"\n title=\"Delete\"\n (click)=\"openDeleteModal(entitlement)\"\n >\n <ui-icon name=\"trash\" class=\"h-4 w-4\" />\n </button>\n </div>\n </div>\n }\n </div>\n </ui-card>\n }\n }\n</div>\n\n<ui-modal\n [open]=\"showCreateEditModal()\"\n [title]=\"selected() ? 'Edit Entitlement' : 'New Entitlement'\"\n (closed)=\"closeCreateEditModal()\"\n>\n <div class=\"space-y-4\">\n @if (error()) {\n <div class=\"rounded-md bg-red-50 p-3 text-sm text-red-700\">{{ error() }}</div>\n }\n\n <div>\n <label class=\"block text-sm font-medium text-gray-700 mb-1\">Name</label>\n <input\n type=\"text\"\n [(ngModel)]=\"formData.name\"\n [disabled]=\"!!selected()\"\n placeholder=\"invoices.approve\"\n class=\"w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm disabled:bg-gray-100\"\n />\n <p class=\"mt-1 text-xs text-gray-500\">\n @if (selected()) {\n The name is what your app checks and is already in issued tokens, so it can't be changed.\n Delete and recreate to rename.\n } @else {\n The exact value your application will check. No spaces.\n }\n </p>\n </div>\n\n <div>\n <label class=\"block text-sm font-medium text-gray-700 mb-1\">Display name</label>\n <input\n type=\"text\"\n [(ngModel)]=\"formData.displayName\"\n placeholder=\"Approve invoices\"\n class=\"w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm\"\n />\n </div>\n\n <div>\n <label class=\"block text-sm font-medium text-gray-700 mb-1\">Description</label>\n <textarea\n [(ngModel)]=\"formData.description\"\n rows=\"2\"\n class=\"w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm\"\n ></textarea>\n </div>\n\n <div>\n <label class=\"block text-sm font-medium text-gray-700 mb-1\">Group</label>\n <input\n type=\"text\"\n [(ngModel)]=\"formData.group\"\n placeholder=\"Invoices\"\n class=\"w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm\"\n />\n <p class=\"mt-1 text-xs text-gray-500\">Groups the entitlement in the role editor.</p>\n </div>\n\n <label class=\"flex items-start gap-2 text-sm\">\n <input\n type=\"checkbox\"\n class=\"h-4 w-4 mt-0.5 text-primary-600 focus:ring-primary-500 border-gray-300 rounded\"\n [(ngModel)]=\"formData.enabled\"\n />\n <span>\n Enabled\n <span class=\"block text-gray-500\">\n Turning this off withholds it from new tokens without unassigning it from roles.\n </span>\n </span>\n </label>\n </div>\n\n <div modal-footer class=\"px-6 py-4 border-t border-gray-200 flex justify-end gap-2\">\n <ui-button variant=\"secondary\" (clicked)=\"closeCreateEditModal()\">Cancel</ui-button>\n <ui-button [loading]=\"saving()\" [disabled]=\"!formData.name.trim()\" (clicked)=\"onSave()\">\n {{ selected() ? 'Save' : 'Create' }}\n </ui-button>\n </div>\n</ui-modal>\n\n<ui-modal [open]=\"showDeleteModal()\" title=\"Delete Entitlement\" (closed)=\"closeDeleteModal()\">\n <p class=\"text-sm text-gray-500\">\n Delete <span class=\"font-mono\">{{ selected()?.name }}</span>? It will also be removed from every\n role that grants it, and will stop appearing in tokens.\n </p>\n <div modal-footer class=\"px-6 py-4 border-t border-gray-200 flex justify-end gap-2\">\n <ui-button variant=\"secondary\" (clicked)=\"closeDeleteModal()\">Cancel</ui-button>\n <ui-button variant=\"danger\" [loading]=\"deleting()\" (clicked)=\"confirmDelete()\">Delete</ui-button>\n </div>\n</ui-modal>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: CardComponent, selector: "ui-card", inputs: ["title", "subtitle", "padding", "shadow"] }, { kind: "component", type: ButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "loading", "type", "fullWidth"], outputs: ["clicked"] }, { kind: "component", type: BadgeComponent, selector: "ui-badge", inputs: ["variant", "size"] }, { kind: "component", type: ModalComponent, selector: "ui-modal", inputs: ["open", "title", "size", "showCloseButton", "closeOnBackdrop", "closeOnEscape", "scrollable"], outputs: ["closed"] }, { kind: "component", type: IconComponent, selector: "ui-icon", inputs: ["name", "strokeWidth"] }] });
|
|
142
|
+
}
|
|
143
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: EntitlementsPageComponent, decorators: [{
|
|
144
|
+
type: Component,
|
|
145
|
+
args: [{ selector: 'admin-entitlements-page', standalone: true, imports: [CommonModule, FormsModule, CardComponent, ButtonComponent, BadgeComponent, ModalComponent, IconComponent], template: "<div class=\"space-y-6\">\n <div class=\"flex items-start justify-between\">\n <div>\n <h1 class=\"text-2xl font-semibold text-gray-900\">Entitlements</h1>\n <p class=\"mt-1 text-sm text-gray-500\">\n Permissions your own applications enforce. Assign them to roles, and they arrive in the\n <code class=\"font-mono\">entitlements</code> claim for clients granted the\n <code class=\"font-mono\">entitlements</code> scope. They grant nothing inside Oluso.\n </p>\n </div>\n <ui-button (clicked)=\"openCreateModal()\">\n <ui-icon name=\"plus\" class=\"h-4 w-4 mr-1\" />\n New Entitlement\n </ui-button>\n </div>\n\n @if (loading()) {\n <div class=\"text-sm text-gray-500 p-4\">Loading entitlements...</div>\n } @else if (entitlements().length === 0) {\n <ui-card>\n <div class=\"text-center py-10\">\n <ui-icon name=\"key\" class=\"mx-auto h-8 w-8 text-gray-300\" />\n <p class=\"mt-2 text-sm text-gray-500\">\n No entitlements yet. Define one to make it selectable when editing a role.\n </p>\n </div>\n </ui-card>\n } @else {\n @for (group of groups(); track group.name) {\n <ui-card [title]=\"group.name\">\n <div class=\"divide-y\">\n @for (entitlement of group.entitlements; track entitlement.id) {\n <div class=\"flex items-start justify-between py-3\">\n <div>\n <div class=\"flex items-center gap-2\">\n <span class=\"text-sm font-medium text-gray-900\">{{ entitlement.displayName }}</span>\n <span class=\"font-mono text-xs text-gray-500\">{{ entitlement.name }}</span>\n @if (!entitlement.enabled) {\n <ui-badge variant=\"warning\">Disabled</ui-badge>\n }\n </div>\n @if (entitlement.description) {\n <p class=\"mt-1 text-sm text-gray-500\">{{ entitlement.description }}</p>\n }\n </div>\n <div class=\"flex items-center gap-2\">\n <button\n type=\"button\"\n class=\"text-gray-500 hover:text-gray-700\"\n title=\"Edit\"\n (click)=\"openEditModal(entitlement)\"\n >\n <ui-icon name=\"pencil\" class=\"h-4 w-4\" />\n </button>\n <button\n type=\"button\"\n class=\"text-red-500 hover:text-red-700\"\n title=\"Delete\"\n (click)=\"openDeleteModal(entitlement)\"\n >\n <ui-icon name=\"trash\" class=\"h-4 w-4\" />\n </button>\n </div>\n </div>\n }\n </div>\n </ui-card>\n }\n }\n</div>\n\n<ui-modal\n [open]=\"showCreateEditModal()\"\n [title]=\"selected() ? 'Edit Entitlement' : 'New Entitlement'\"\n (closed)=\"closeCreateEditModal()\"\n>\n <div class=\"space-y-4\">\n @if (error()) {\n <div class=\"rounded-md bg-red-50 p-3 text-sm text-red-700\">{{ error() }}</div>\n }\n\n <div>\n <label class=\"block text-sm font-medium text-gray-700 mb-1\">Name</label>\n <input\n type=\"text\"\n [(ngModel)]=\"formData.name\"\n [disabled]=\"!!selected()\"\n placeholder=\"invoices.approve\"\n class=\"w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm disabled:bg-gray-100\"\n />\n <p class=\"mt-1 text-xs text-gray-500\">\n @if (selected()) {\n The name is what your app checks and is already in issued tokens, so it can't be changed.\n Delete and recreate to rename.\n } @else {\n The exact value your application will check. No spaces.\n }\n </p>\n </div>\n\n <div>\n <label class=\"block text-sm font-medium text-gray-700 mb-1\">Display name</label>\n <input\n type=\"text\"\n [(ngModel)]=\"formData.displayName\"\n placeholder=\"Approve invoices\"\n class=\"w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm\"\n />\n </div>\n\n <div>\n <label class=\"block text-sm font-medium text-gray-700 mb-1\">Description</label>\n <textarea\n [(ngModel)]=\"formData.description\"\n rows=\"2\"\n class=\"w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm\"\n ></textarea>\n </div>\n\n <div>\n <label class=\"block text-sm font-medium text-gray-700 mb-1\">Group</label>\n <input\n type=\"text\"\n [(ngModel)]=\"formData.group\"\n placeholder=\"Invoices\"\n class=\"w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm\"\n />\n <p class=\"mt-1 text-xs text-gray-500\">Groups the entitlement in the role editor.</p>\n </div>\n\n <label class=\"flex items-start gap-2 text-sm\">\n <input\n type=\"checkbox\"\n class=\"h-4 w-4 mt-0.5 text-primary-600 focus:ring-primary-500 border-gray-300 rounded\"\n [(ngModel)]=\"formData.enabled\"\n />\n <span>\n Enabled\n <span class=\"block text-gray-500\">\n Turning this off withholds it from new tokens without unassigning it from roles.\n </span>\n </span>\n </label>\n </div>\n\n <div modal-footer class=\"px-6 py-4 border-t border-gray-200 flex justify-end gap-2\">\n <ui-button variant=\"secondary\" (clicked)=\"closeCreateEditModal()\">Cancel</ui-button>\n <ui-button [loading]=\"saving()\" [disabled]=\"!formData.name.trim()\" (clicked)=\"onSave()\">\n {{ selected() ? 'Save' : 'Create' }}\n </ui-button>\n </div>\n</ui-modal>\n\n<ui-modal [open]=\"showDeleteModal()\" title=\"Delete Entitlement\" (closed)=\"closeDeleteModal()\">\n <p class=\"text-sm text-gray-500\">\n Delete <span class=\"font-mono\">{{ selected()?.name }}</span>? It will also be removed from every\n role that grants it, and will stop appearing in tokens.\n </p>\n <div modal-footer class=\"px-6 py-4 border-t border-gray-200 flex justify-end gap-2\">\n <ui-button variant=\"secondary\" (clicked)=\"closeDeleteModal()\">Cancel</ui-button>\n <ui-button variant=\"danger\" [loading]=\"deleting()\" (clicked)=\"confirmDelete()\">Delete</ui-button>\n </div>\n</ui-modal>\n" }]
|
|
146
|
+
}], ctorParameters: () => [] });
|
|
147
|
+
|
|
148
|
+
export { EntitlementsPageComponent };
|
|
149
|
+
//# sourceMappingURL=syndew-oluso-admin-ui-entitlements-page.component-BeZtIUnk.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"syndew-oluso-admin-ui-entitlements-page.component-BeZtIUnk.mjs","sources":["../../../../libs/admin-ui/src/lib/pages/entitlements/entitlements-page.component.ts","../../../../libs/admin-ui/src/lib/pages/entitlements/entitlements-page.component.html"],"sourcesContent":["import { Component, inject, signal } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { FormsModule } from '@angular/forms';\nimport {\n CardComponent,\n ButtonComponent,\n BadgeComponent,\n ModalComponent,\n IconComponent,\n} from '@syndew/oluso-ui-core';\nimport { EntitlementService } from '../../services/entitlement.service';\nimport { EntitlementDto } from '../../models/entitlement.model';\n\ninterface EntitlementFormData {\n name: string;\n displayName: string;\n description: string;\n group: string;\n enabled: boolean;\n}\n\n@Component({\n selector: 'admin-entitlements-page',\n standalone: true,\n imports: [CommonModule, FormsModule, CardComponent, ButtonComponent, BadgeComponent, ModalComponent, IconComponent],\n templateUrl: './entitlements-page.component.html',\n})\nexport class EntitlementsPageComponent {\n private readonly entitlementService = inject(EntitlementService);\n\n readonly loading = signal(true);\n readonly saving = signal(false);\n readonly deleting = signal(false);\n readonly entitlements = signal<EntitlementDto[]>([]);\n readonly error = signal<string | null>(null);\n\n readonly showCreateEditModal = signal(false);\n readonly showDeleteModal = signal(false);\n readonly selected = signal<EntitlementDto | null>(null);\n\n formData: EntitlementFormData = this.emptyForm();\n\n constructor() {\n this.load();\n }\n\n private emptyForm(): EntitlementFormData {\n return { name: '', displayName: '', description: '', group: '', enabled: true };\n }\n\n load(): void {\n this.loading.set(true);\n this.entitlementService.getAll().subscribe({\n next: (entitlements) => {\n this.entitlements.set(entitlements);\n this.loading.set(false);\n },\n error: (err) => {\n console.error('Failed to load entitlements:', err);\n this.loading.set(false);\n },\n });\n }\n\n /** Grouped for display; ungrouped entries fall under \"General\". */\n groups(): { name: string; entitlements: EntitlementDto[] }[] {\n const groups: { name: string; entitlements: EntitlementDto[] }[] = [];\n for (const entitlement of this.entitlements()) {\n const groupName = entitlement.group || 'General';\n const group = groups.find((g) => g.name === groupName);\n if (group) {\n group.entitlements.push(entitlement);\n } else {\n groups.push({ name: groupName, entitlements: [entitlement] });\n }\n }\n return groups;\n }\n\n openCreateModal(): void {\n this.selected.set(null);\n this.formData = this.emptyForm();\n this.error.set(null);\n this.showCreateEditModal.set(true);\n }\n\n openEditModal(entitlement: EntitlementDto): void {\n this.selected.set(entitlement);\n this.formData = {\n name: entitlement.name,\n displayName: entitlement.displayName,\n description: entitlement.description || '',\n group: entitlement.group || '',\n enabled: entitlement.enabled,\n };\n this.error.set(null);\n this.showCreateEditModal.set(true);\n }\n\n closeCreateEditModal(): void {\n this.showCreateEditModal.set(false);\n this.selected.set(null);\n this.error.set(null);\n }\n\n onSave(): void {\n const existing = this.selected();\n this.saving.set(true);\n this.error.set(null);\n\n const done = {\n next: () => {\n this.saving.set(false);\n this.closeCreateEditModal();\n this.load();\n },\n error: (err: { error?: { error?: string } }) => {\n this.error.set(err?.error?.error || 'Save failed');\n this.saving.set(false);\n },\n };\n\n if (existing) {\n // Name is immutable — it's already in issued tokens and on roles.\n this.entitlementService\n .update(existing.id, {\n displayName: this.formData.displayName || this.formData.name,\n description: this.formData.description,\n group: this.formData.group,\n enabled: this.formData.enabled,\n })\n .subscribe(done);\n } else {\n this.entitlementService\n .create({\n name: this.formData.name.trim(),\n displayName: this.formData.displayName || undefined,\n description: this.formData.description || undefined,\n group: this.formData.group || undefined,\n enabled: this.formData.enabled,\n })\n .subscribe(done);\n }\n }\n\n openDeleteModal(entitlement: EntitlementDto): void {\n this.selected.set(entitlement);\n this.showDeleteModal.set(true);\n }\n\n closeDeleteModal(): void {\n this.showDeleteModal.set(false);\n this.selected.set(null);\n }\n\n confirmDelete(): void {\n const entitlement = this.selected();\n if (!entitlement) return;\n\n this.deleting.set(true);\n this.entitlementService.delete(entitlement.id).subscribe({\n next: () => {\n this.deleting.set(false);\n this.closeDeleteModal();\n this.load();\n },\n error: (err) => {\n console.error('Failed to delete entitlement:', err);\n this.deleting.set(false);\n },\n });\n }\n}\n","<div class=\"space-y-6\">\n <div class=\"flex items-start justify-between\">\n <div>\n <h1 class=\"text-2xl font-semibold text-gray-900\">Entitlements</h1>\n <p class=\"mt-1 text-sm text-gray-500\">\n Permissions your own applications enforce. Assign them to roles, and they arrive in the\n <code class=\"font-mono\">entitlements</code> claim for clients granted the\n <code class=\"font-mono\">entitlements</code> scope. They grant nothing inside Oluso.\n </p>\n </div>\n <ui-button (clicked)=\"openCreateModal()\">\n <ui-icon name=\"plus\" class=\"h-4 w-4 mr-1\" />\n New Entitlement\n </ui-button>\n </div>\n\n @if (loading()) {\n <div class=\"text-sm text-gray-500 p-4\">Loading entitlements...</div>\n } @else if (entitlements().length === 0) {\n <ui-card>\n <div class=\"text-center py-10\">\n <ui-icon name=\"key\" class=\"mx-auto h-8 w-8 text-gray-300\" />\n <p class=\"mt-2 text-sm text-gray-500\">\n No entitlements yet. Define one to make it selectable when editing a role.\n </p>\n </div>\n </ui-card>\n } @else {\n @for (group of groups(); track group.name) {\n <ui-card [title]=\"group.name\">\n <div class=\"divide-y\">\n @for (entitlement of group.entitlements; track entitlement.id) {\n <div class=\"flex items-start justify-between py-3\">\n <div>\n <div class=\"flex items-center gap-2\">\n <span class=\"text-sm font-medium text-gray-900\">{{ entitlement.displayName }}</span>\n <span class=\"font-mono text-xs text-gray-500\">{{ entitlement.name }}</span>\n @if (!entitlement.enabled) {\n <ui-badge variant=\"warning\">Disabled</ui-badge>\n }\n </div>\n @if (entitlement.description) {\n <p class=\"mt-1 text-sm text-gray-500\">{{ entitlement.description }}</p>\n }\n </div>\n <div class=\"flex items-center gap-2\">\n <button\n type=\"button\"\n class=\"text-gray-500 hover:text-gray-700\"\n title=\"Edit\"\n (click)=\"openEditModal(entitlement)\"\n >\n <ui-icon name=\"pencil\" class=\"h-4 w-4\" />\n </button>\n <button\n type=\"button\"\n class=\"text-red-500 hover:text-red-700\"\n title=\"Delete\"\n (click)=\"openDeleteModal(entitlement)\"\n >\n <ui-icon name=\"trash\" class=\"h-4 w-4\" />\n </button>\n </div>\n </div>\n }\n </div>\n </ui-card>\n }\n }\n</div>\n\n<ui-modal\n [open]=\"showCreateEditModal()\"\n [title]=\"selected() ? 'Edit Entitlement' : 'New Entitlement'\"\n (closed)=\"closeCreateEditModal()\"\n>\n <div class=\"space-y-4\">\n @if (error()) {\n <div class=\"rounded-md bg-red-50 p-3 text-sm text-red-700\">{{ error() }}</div>\n }\n\n <div>\n <label class=\"block text-sm font-medium text-gray-700 mb-1\">Name</label>\n <input\n type=\"text\"\n [(ngModel)]=\"formData.name\"\n [disabled]=\"!!selected()\"\n placeholder=\"invoices.approve\"\n class=\"w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm disabled:bg-gray-100\"\n />\n <p class=\"mt-1 text-xs text-gray-500\">\n @if (selected()) {\n The name is what your app checks and is already in issued tokens, so it can't be changed.\n Delete and recreate to rename.\n } @else {\n The exact value your application will check. No spaces.\n }\n </p>\n </div>\n\n <div>\n <label class=\"block text-sm font-medium text-gray-700 mb-1\">Display name</label>\n <input\n type=\"text\"\n [(ngModel)]=\"formData.displayName\"\n placeholder=\"Approve invoices\"\n class=\"w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm\"\n />\n </div>\n\n <div>\n <label class=\"block text-sm font-medium text-gray-700 mb-1\">Description</label>\n <textarea\n [(ngModel)]=\"formData.description\"\n rows=\"2\"\n class=\"w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm\"\n ></textarea>\n </div>\n\n <div>\n <label class=\"block text-sm font-medium text-gray-700 mb-1\">Group</label>\n <input\n type=\"text\"\n [(ngModel)]=\"formData.group\"\n placeholder=\"Invoices\"\n class=\"w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm\"\n />\n <p class=\"mt-1 text-xs text-gray-500\">Groups the entitlement in the role editor.</p>\n </div>\n\n <label class=\"flex items-start gap-2 text-sm\">\n <input\n type=\"checkbox\"\n class=\"h-4 w-4 mt-0.5 text-primary-600 focus:ring-primary-500 border-gray-300 rounded\"\n [(ngModel)]=\"formData.enabled\"\n />\n <span>\n Enabled\n <span class=\"block text-gray-500\">\n Turning this off withholds it from new tokens without unassigning it from roles.\n </span>\n </span>\n </label>\n </div>\n\n <div modal-footer class=\"px-6 py-4 border-t border-gray-200 flex justify-end gap-2\">\n <ui-button variant=\"secondary\" (clicked)=\"closeCreateEditModal()\">Cancel</ui-button>\n <ui-button [loading]=\"saving()\" [disabled]=\"!formData.name.trim()\" (clicked)=\"onSave()\">\n {{ selected() ? 'Save' : 'Create' }}\n </ui-button>\n </div>\n</ui-modal>\n\n<ui-modal [open]=\"showDeleteModal()\" title=\"Delete Entitlement\" (closed)=\"closeDeleteModal()\">\n <p class=\"text-sm text-gray-500\">\n Delete <span class=\"font-mono\">{{ selected()?.name }}</span>? It will also be removed from every\n role that grants it, and will stop appearing in tokens.\n </p>\n <div modal-footer class=\"px-6 py-4 border-t border-gray-200 flex justify-end gap-2\">\n <ui-button variant=\"secondary\" (clicked)=\"closeDeleteModal()\">Cancel</ui-button>\n <ui-button variant=\"danger\" [loading]=\"deleting()\" (clicked)=\"confirmDelete()\">Delete</ui-button>\n </div>\n</ui-modal>\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { inject, signal, Component } from '@angular/core';
|
|
3
|
+
import * as i1 from '@angular/common';
|
|
4
|
+
import { CommonModule } from '@angular/common';
|
|
5
|
+
import { CardComponent, BadgeComponent, IconComponent, LocaleDatePipe } from '@syndew/oluso-ui-core';
|
|
6
|
+
import { f as ApiService } from './syndew-oluso-admin-ui-syndew-oluso-admin-ui-BVs_bAxm.mjs';
|
|
7
|
+
|
|
8
|
+
class LicenseStatusPageComponent {
|
|
9
|
+
api = inject(ApiService);
|
|
10
|
+
loading = signal(true, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
11
|
+
license = signal(null, ...(ngDevMode ? [{ debugName: "license" }] : /* istanbul ignore next */ []));
|
|
12
|
+
ngOnInit() {
|
|
13
|
+
this.api.get('/settings/license').subscribe({
|
|
14
|
+
next: (status) => {
|
|
15
|
+
this.license.set(status);
|
|
16
|
+
this.loading.set(false);
|
|
17
|
+
},
|
|
18
|
+
error: (err) => {
|
|
19
|
+
console.error('Failed to load license status:', err);
|
|
20
|
+
this.loading.set(false);
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: LicenseStatusPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
25
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.25", type: LicenseStatusPageComponent, isStandalone: true, selector: "admin-license-status-page", ngImport: i0, template: `
|
|
26
|
+
<div class="page-header">
|
|
27
|
+
<h1>License Status</h1>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
@if (loading()) {
|
|
31
|
+
<ui-card>
|
|
32
|
+
<div class="loading-placeholder">Loading license information...</div>
|
|
33
|
+
</ui-card>
|
|
34
|
+
} @else if (license(); as lic) {
|
|
35
|
+
<!-- License Overview -->
|
|
36
|
+
<ui-card>
|
|
37
|
+
<div class="license-overview">
|
|
38
|
+
<div class="license-tier">
|
|
39
|
+
<span class="tier-label">License Tier</span>
|
|
40
|
+
<span class="tier-value">{{ lic.tier | titlecase }}</span>
|
|
41
|
+
@if (lic.isTrial) {
|
|
42
|
+
<ui-badge variant="warning">Trial</ui-badge>
|
|
43
|
+
}
|
|
44
|
+
@if (lic.isExpired) {
|
|
45
|
+
<ui-badge variant="danger">Expired</ui-badge>
|
|
46
|
+
} @else if (lic.isValid) {
|
|
47
|
+
<ui-badge variant="success">Active</ui-badge>
|
|
48
|
+
}
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<div class="license-details">
|
|
52
|
+
@if (lic.expiresAt) {
|
|
53
|
+
<div class="detail-item">
|
|
54
|
+
<span class="detail-label">Expires</span>
|
|
55
|
+
<span class="detail-value">
|
|
56
|
+
{{ lic.expiresAt | localeDate:'mediumDate' }}
|
|
57
|
+
@if (!lic.isExpired && lic.daysUntilExpiration <= 30) {
|
|
58
|
+
<span class="expiry-warning">({{ lic.daysUntilExpiration }} days remaining)</span>
|
|
59
|
+
}
|
|
60
|
+
</span>
|
|
61
|
+
</div>
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
<div class="detail-item">
|
|
65
|
+
<span class="detail-label">Max Tenants</span>
|
|
66
|
+
<span class="detail-value">{{ lic.maxTenants === -1 ? 'Unlimited' : lic.maxTenants }}</span>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</ui-card>
|
|
71
|
+
|
|
72
|
+
<!-- Licensed Features -->
|
|
73
|
+
<ui-card>
|
|
74
|
+
<h2 class="section-title">Licensed Features</h2>
|
|
75
|
+
@if (lic.features.length === 0) {
|
|
76
|
+
<p class="empty-state">No features licensed</p>
|
|
77
|
+
} @else {
|
|
78
|
+
<div class="feature-grid">
|
|
79
|
+
@for (feature of lic.features; track feature) {
|
|
80
|
+
<div class="feature-item">
|
|
81
|
+
<ui-icon name="check-circle" class="feature-icon licensed"></ui-icon>
|
|
82
|
+
<span>{{ feature }}</span>
|
|
83
|
+
</div>
|
|
84
|
+
}
|
|
85
|
+
</div>
|
|
86
|
+
}
|
|
87
|
+
</ui-card>
|
|
88
|
+
|
|
89
|
+
<!-- Add-Ons -->
|
|
90
|
+
@if (lic.addOns.length > 0) {
|
|
91
|
+
<ui-card>
|
|
92
|
+
<h2 class="section-title">Add-Ons</h2>
|
|
93
|
+
<div class="feature-grid">
|
|
94
|
+
@for (addon of lic.addOns; track addon) {
|
|
95
|
+
<div class="feature-item">
|
|
96
|
+
<ui-icon name="puzzle-piece" class="feature-icon addon"></ui-icon>
|
|
97
|
+
<span>{{ addon }}</span>
|
|
98
|
+
</div>
|
|
99
|
+
}
|
|
100
|
+
</div>
|
|
101
|
+
</ui-card>
|
|
102
|
+
}
|
|
103
|
+
} @else {
|
|
104
|
+
<ui-card>
|
|
105
|
+
<div class="empty-state">
|
|
106
|
+
<p>No license information available.</p>
|
|
107
|
+
<p class="empty-hint">Contact support to obtain a license key.</p>
|
|
108
|
+
</div>
|
|
109
|
+
</ui-card>
|
|
110
|
+
}
|
|
111
|
+
`, isInline: true, styles: [".page-header{margin-bottom:1.5rem}.page-header h1{font-size:1.5rem;font-weight:600}.license-overview{display:flex;flex-direction:column;gap:1.5rem}.license-tier{display:flex;align-items:center;gap:.75rem}.tier-label{font-size:.875rem;color:var(--text-secondary)}.tier-value{font-size:1.5rem;font-weight:700}.license-details{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:1rem}.detail-item{display:flex;flex-direction:column;gap:.25rem}.detail-label{font-size:.75rem;color:var(--text-secondary);text-transform:uppercase;letter-spacing:.05em}.detail-value{font-size:.875rem;font-weight:500}.expiry-warning{color:var(--color-warning);font-size:.75rem}.section-title{font-size:1rem;font-weight:600;margin-bottom:1rem}.feature-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:.5rem}.feature-item{display:flex;align-items:center;gap:.5rem;padding:.5rem;border-radius:.375rem;background:var(--bg-secondary);font-size:.875rem}.feature-icon{width:1rem;height:1rem;flex-shrink:0}.feature-icon.licensed{color:var(--color-success)}.feature-icon.addon{color:var(--color-primary)}.loading-placeholder{padding:2rem;text-align:center;color:var(--text-secondary)}.empty-state{text-align:center;padding:2rem;color:var(--text-secondary)}.empty-hint{font-size:.875rem;margin-top:.5rem}ui-card{display:block;margin-bottom:1rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: CardComponent, selector: "ui-card", inputs: ["title", "subtitle", "padding", "shadow"] }, { kind: "component", type: BadgeComponent, selector: "ui-badge", inputs: ["variant", "size"] }, { kind: "component", type: IconComponent, selector: "ui-icon", inputs: ["name", "strokeWidth"] }, { kind: "pipe", type: i1.TitleCasePipe, name: "titlecase" }, { kind: "pipe", type: LocaleDatePipe, name: "localeDate" }] });
|
|
112
|
+
}
|
|
113
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: LicenseStatusPageComponent, decorators: [{
|
|
114
|
+
type: Component,
|
|
115
|
+
args: [{ selector: 'admin-license-status-page', standalone: true, imports: [CommonModule, CardComponent, BadgeComponent, IconComponent, LocaleDatePipe], template: `
|
|
116
|
+
<div class="page-header">
|
|
117
|
+
<h1>License Status</h1>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
@if (loading()) {
|
|
121
|
+
<ui-card>
|
|
122
|
+
<div class="loading-placeholder">Loading license information...</div>
|
|
123
|
+
</ui-card>
|
|
124
|
+
} @else if (license(); as lic) {
|
|
125
|
+
<!-- License Overview -->
|
|
126
|
+
<ui-card>
|
|
127
|
+
<div class="license-overview">
|
|
128
|
+
<div class="license-tier">
|
|
129
|
+
<span class="tier-label">License Tier</span>
|
|
130
|
+
<span class="tier-value">{{ lic.tier | titlecase }}</span>
|
|
131
|
+
@if (lic.isTrial) {
|
|
132
|
+
<ui-badge variant="warning">Trial</ui-badge>
|
|
133
|
+
}
|
|
134
|
+
@if (lic.isExpired) {
|
|
135
|
+
<ui-badge variant="danger">Expired</ui-badge>
|
|
136
|
+
} @else if (lic.isValid) {
|
|
137
|
+
<ui-badge variant="success">Active</ui-badge>
|
|
138
|
+
}
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<div class="license-details">
|
|
142
|
+
@if (lic.expiresAt) {
|
|
143
|
+
<div class="detail-item">
|
|
144
|
+
<span class="detail-label">Expires</span>
|
|
145
|
+
<span class="detail-value">
|
|
146
|
+
{{ lic.expiresAt | localeDate:'mediumDate' }}
|
|
147
|
+
@if (!lic.isExpired && lic.daysUntilExpiration <= 30) {
|
|
148
|
+
<span class="expiry-warning">({{ lic.daysUntilExpiration }} days remaining)</span>
|
|
149
|
+
}
|
|
150
|
+
</span>
|
|
151
|
+
</div>
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
<div class="detail-item">
|
|
155
|
+
<span class="detail-label">Max Tenants</span>
|
|
156
|
+
<span class="detail-value">{{ lic.maxTenants === -1 ? 'Unlimited' : lic.maxTenants }}</span>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</ui-card>
|
|
161
|
+
|
|
162
|
+
<!-- Licensed Features -->
|
|
163
|
+
<ui-card>
|
|
164
|
+
<h2 class="section-title">Licensed Features</h2>
|
|
165
|
+
@if (lic.features.length === 0) {
|
|
166
|
+
<p class="empty-state">No features licensed</p>
|
|
167
|
+
} @else {
|
|
168
|
+
<div class="feature-grid">
|
|
169
|
+
@for (feature of lic.features; track feature) {
|
|
170
|
+
<div class="feature-item">
|
|
171
|
+
<ui-icon name="check-circle" class="feature-icon licensed"></ui-icon>
|
|
172
|
+
<span>{{ feature }}</span>
|
|
173
|
+
</div>
|
|
174
|
+
}
|
|
175
|
+
</div>
|
|
176
|
+
}
|
|
177
|
+
</ui-card>
|
|
178
|
+
|
|
179
|
+
<!-- Add-Ons -->
|
|
180
|
+
@if (lic.addOns.length > 0) {
|
|
181
|
+
<ui-card>
|
|
182
|
+
<h2 class="section-title">Add-Ons</h2>
|
|
183
|
+
<div class="feature-grid">
|
|
184
|
+
@for (addon of lic.addOns; track addon) {
|
|
185
|
+
<div class="feature-item">
|
|
186
|
+
<ui-icon name="puzzle-piece" class="feature-icon addon"></ui-icon>
|
|
187
|
+
<span>{{ addon }}</span>
|
|
188
|
+
</div>
|
|
189
|
+
}
|
|
190
|
+
</div>
|
|
191
|
+
</ui-card>
|
|
192
|
+
}
|
|
193
|
+
} @else {
|
|
194
|
+
<ui-card>
|
|
195
|
+
<div class="empty-state">
|
|
196
|
+
<p>No license information available.</p>
|
|
197
|
+
<p class="empty-hint">Contact support to obtain a license key.</p>
|
|
198
|
+
</div>
|
|
199
|
+
</ui-card>
|
|
200
|
+
}
|
|
201
|
+
`, styles: [".page-header{margin-bottom:1.5rem}.page-header h1{font-size:1.5rem;font-weight:600}.license-overview{display:flex;flex-direction:column;gap:1.5rem}.license-tier{display:flex;align-items:center;gap:.75rem}.tier-label{font-size:.875rem;color:var(--text-secondary)}.tier-value{font-size:1.5rem;font-weight:700}.license-details{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:1rem}.detail-item{display:flex;flex-direction:column;gap:.25rem}.detail-label{font-size:.75rem;color:var(--text-secondary);text-transform:uppercase;letter-spacing:.05em}.detail-value{font-size:.875rem;font-weight:500}.expiry-warning{color:var(--color-warning);font-size:.75rem}.section-title{font-size:1rem;font-weight:600;margin-bottom:1rem}.feature-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:.5rem}.feature-item{display:flex;align-items:center;gap:.5rem;padding:.5rem;border-radius:.375rem;background:var(--bg-secondary);font-size:.875rem}.feature-icon{width:1rem;height:1rem;flex-shrink:0}.feature-icon.licensed{color:var(--color-success)}.feature-icon.addon{color:var(--color-primary)}.loading-placeholder{padding:2rem;text-align:center;color:var(--text-secondary)}.empty-state{text-align:center;padding:2rem;color:var(--text-secondary)}.empty-hint{font-size:.875rem;margin-top:.5rem}ui-card{display:block;margin-bottom:1rem}\n"] }]
|
|
202
|
+
}] });
|
|
203
|
+
|
|
204
|
+
export { LicenseStatusPageComponent };
|
|
205
|
+
//# sourceMappingURL=syndew-oluso-admin-ui-license-status-page.component-JQmj_qhz.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"syndew-oluso-admin-ui-license-status-page.component-JQmj_qhz.mjs","sources":["../../../../libs/admin-ui/src/lib/pages/settings/license-status-page.component.ts"],"sourcesContent":["import { Component, inject, signal, OnInit } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { CardComponent, BadgeComponent, IconComponent, LocaleDatePipe } from '@syndew/oluso-ui-core';\nimport { ApiService } from '../../services/api.service';\n\ninterface LicenseStatus {\n tier: string;\n isValid: boolean;\n expiresAt: string | null;\n isExpired: boolean;\n isTrial: boolean;\n daysUntilExpiration: number;\n maxTenants: number;\n features: string[];\n addOns: string[];\n message: string | null;\n}\n\n@Component({\n selector: 'admin-license-status-page',\n standalone: true,\n imports: [CommonModule, CardComponent, BadgeComponent, IconComponent, LocaleDatePipe],\n template: `\n <div class=\"page-header\">\n <h1>License Status</h1>\n </div>\n\n @if (loading()) {\n <ui-card>\n <div class=\"loading-placeholder\">Loading license information...</div>\n </ui-card>\n } @else if (license(); as lic) {\n <!-- License Overview -->\n <ui-card>\n <div class=\"license-overview\">\n <div class=\"license-tier\">\n <span class=\"tier-label\">License Tier</span>\n <span class=\"tier-value\">{{ lic.tier | titlecase }}</span>\n @if (lic.isTrial) {\n <ui-badge variant=\"warning\">Trial</ui-badge>\n }\n @if (lic.isExpired) {\n <ui-badge variant=\"danger\">Expired</ui-badge>\n } @else if (lic.isValid) {\n <ui-badge variant=\"success\">Active</ui-badge>\n }\n </div>\n\n <div class=\"license-details\">\n @if (lic.expiresAt) {\n <div class=\"detail-item\">\n <span class=\"detail-label\">Expires</span>\n <span class=\"detail-value\">\n {{ lic.expiresAt | localeDate:'mediumDate' }}\n @if (!lic.isExpired && lic.daysUntilExpiration <= 30) {\n <span class=\"expiry-warning\">({{ lic.daysUntilExpiration }} days remaining)</span>\n }\n </span>\n </div>\n }\n\n <div class=\"detail-item\">\n <span class=\"detail-label\">Max Tenants</span>\n <span class=\"detail-value\">{{ lic.maxTenants === -1 ? 'Unlimited' : lic.maxTenants }}</span>\n </div>\n </div>\n </div>\n </ui-card>\n\n <!-- Licensed Features -->\n <ui-card>\n <h2 class=\"section-title\">Licensed Features</h2>\n @if (lic.features.length === 0) {\n <p class=\"empty-state\">No features licensed</p>\n } @else {\n <div class=\"feature-grid\">\n @for (feature of lic.features; track feature) {\n <div class=\"feature-item\">\n <ui-icon name=\"check-circle\" class=\"feature-icon licensed\"></ui-icon>\n <span>{{ feature }}</span>\n </div>\n }\n </div>\n }\n </ui-card>\n\n <!-- Add-Ons -->\n @if (lic.addOns.length > 0) {\n <ui-card>\n <h2 class=\"section-title\">Add-Ons</h2>\n <div class=\"feature-grid\">\n @for (addon of lic.addOns; track addon) {\n <div class=\"feature-item\">\n <ui-icon name=\"puzzle-piece\" class=\"feature-icon addon\"></ui-icon>\n <span>{{ addon }}</span>\n </div>\n }\n </div>\n </ui-card>\n }\n } @else {\n <ui-card>\n <div class=\"empty-state\">\n <p>No license information available.</p>\n <p class=\"empty-hint\">Contact support to obtain a license key.</p>\n </div>\n </ui-card>\n }\n `,\n styles: [`\n .page-header { margin-bottom: 1.5rem; }\n .page-header h1 { font-size: 1.5rem; font-weight: 600; }\n\n .license-overview { display: flex; flex-direction: column; gap: 1.5rem; }\n .license-tier { display: flex; align-items: center; gap: 0.75rem; }\n .tier-label { font-size: 0.875rem; color: var(--text-secondary); }\n .tier-value { font-size: 1.5rem; font-weight: 700; }\n\n .license-details { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 1rem; }\n .detail-item { display: flex; flex-direction: column; gap: 0.25rem; }\n .detail-label { font-size: 0.75rem; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em; }\n .detail-value { font-size: 0.875rem; font-weight: 500; }\n .expiry-warning { color: var(--color-warning); font-size: 0.75rem; }\n\n .section-title { font-size: 1rem; font-weight: 600; margin-bottom: 1rem; }\n .feature-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 0.5rem; }\n .feature-item { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem; border-radius: 0.375rem; background: var(--bg-secondary); font-size: 0.875rem; }\n .feature-icon { width: 1rem; height: 1rem; flex-shrink: 0; }\n .feature-icon.licensed { color: var(--color-success); }\n .feature-icon.addon { color: var(--color-primary); }\n\n .loading-placeholder { padding: 2rem; text-align: center; color: var(--text-secondary); }\n .empty-state { text-align: center; padding: 2rem; color: var(--text-secondary); }\n .empty-hint { font-size: 0.875rem; margin-top: 0.5rem; }\n\n ui-card { display: block; margin-bottom: 1rem; }\n `],\n})\nexport class LicenseStatusPageComponent implements OnInit {\n private readonly api = inject(ApiService);\n\n readonly loading = signal(true);\n readonly license = signal<LicenseStatus | null>(null);\n\n ngOnInit(): void {\n this.api.get<LicenseStatus>('/settings/license').subscribe({\n next: (status) => {\n this.license.set(status);\n this.loading.set(false);\n },\n error: (err) => {\n console.error('Failed to load license status:', err);\n this.loading.set(false);\n },\n });\n }\n}\n"],"names":[],"mappings":";;;;;;;MA0Ia,0BAA0B,CAAA;AACpB,IAAA,GAAG,GAAG,MAAM,CAAC,UAAU,CAAC;AAEhC,IAAA,OAAO,GAAG,MAAM,CAAC,IAAI,8EAAC;AACtB,IAAA,OAAO,GAAG,MAAM,CAAuB,IAAI,8EAAC;IAErD,QAAQ,GAAA;QACN,IAAI,CAAC,GAAG,CAAC,GAAG,CAAgB,mBAAmB,CAAC,CAAC,SAAS,CAAC;AACzD,YAAA,IAAI,EAAE,CAAC,MAAM,KAAI;AACf,gBAAA,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC;AACxB,gBAAA,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC;YACzB,CAAC;AACD,YAAA,KAAK,EAAE,CAAC,GAAG,KAAI;AACb,gBAAA,OAAO,CAAC,KAAK,CAAC,gCAAgC,EAAE,GAAG,CAAC;AACpD,gBAAA,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC;YACzB,CAAC;AACF,SAAA,CAAC;IACJ;wGAjBW,0BAA0B,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;AAA1B,IAAA,OAAA,IAAA,GAAA,EAAA,CAAA,oBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,SAAA,EAAA,IAAA,EAAA,0BAA0B,EAAA,YAAA,EAAA,IAAA,EAAA,QAAA,EAAA,2BAAA,EAAA,QAAA,EAAA,EAAA,EAAA,QAAA,EApH3B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsFT,EAAA,QAAA,EAAA,IAAA,EAAA,MAAA,EAAA,CAAA,y1CAAA,CAAA,EAAA,YAAA,EAAA,CAAA,EAAA,IAAA,EAAA,UAAA,EAAA,IAAA,EAvFS,YAAY,+BAAE,aAAa,EAAA,QAAA,EAAA,SAAA,EAAA,MAAA,EAAA,CAAA,OAAA,EAAA,UAAA,EAAA,SAAA,EAAA,QAAA,CAAA,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAAE,cAAc,EAAA,QAAA,EAAA,UAAA,EAAA,MAAA,EAAA,CAAA,SAAA,EAAA,MAAA,CAAA,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAAE,aAAa,6IAAE,cAAc,EAAA,IAAA,EAAA,YAAA,EAAA,CAAA,EAAA,CAAA;;4FAqHzE,0BAA0B,EAAA,UAAA,EAAA,CAAA;kBAxHtC,SAAS;AACE,YAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,2BAA2B,EAAA,UAAA,EACzB,IAAI,EAAA,OAAA,EACP,CAAC,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,aAAa,EAAE,cAAc,CAAC,EAAA,QAAA,EAC3E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsFT,EAAA,CAAA,EAAA,MAAA,EAAA,CAAA,y1CAAA,CAAA,EAAA;;;;;"}
|