From c925e3ecc2b7b1f617f87dd22113e731c8823455 Mon Sep 17 00:00:00 2001 From: Philip Ellis Date: Mon, 16 Mar 2026 09:53:02 -0400 Subject: [PATCH 01/20] initial creation of cicd naviation --- package-lock.json | 32 ++++++-- package.json | 2 + .../src/lib/services/config.service.ts | 8 ++ .../src/lib/services/web-api.service.ts | 73 +++++++++++++++++++ .../sailpoint-components/src/public-api.ts | 3 +- src/app/app.component.html | 5 ++ src/app/app.routes.ts | 7 +- 7 files changed, 122 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0aee63c1..1b099193 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sailpoint-ui-development-kit", - "version": "1.0.9", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sailpoint-ui-development-kit", - "version": "1.0.9", + "version": "1.2.0", "workspaces": [ "app" ], @@ -26,10 +26,12 @@ "@codemirror/legacy-modes": "6.5.1", "@codemirror/theme-one-dark": "6.1.3", "@types/codemirror": "5.60.16", + "@types/diff": "8.0.0", "axios": "1.12.1", "codemirror": "6.0.2", "cronstrue": "3.3.0", "d3": "7.9.0", + "diff": "8.0.3", "electron-context-menu": "4.1.0", "electron-is-dev": "3.0.1", "js-yaml": "4.1.0", @@ -110,7 +112,7 @@ }, "app": { "name": "sailpoint-ui-development-kit", - "version": "1.0.9", + "version": "1.2.0", "dependencies": { "js-yaml": "^4.1.0", "sailpoint-api-client": "1.6.9" @@ -6851,6 +6853,15 @@ "@types/ms": "*" } }, + "node_modules/@types/diff": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@types/diff/-/diff-8.0.0.tgz", + "integrity": "sha512-o7jqJM04gfaYrdCecCVMbZhNdG6T1MHg/oQoRFdERLV+4d+V7FijhiEAbFu0Usww84Yijk9yH58U4Jk4HbtzZw==", + "deprecated": "This is a stub types definition. diff provides its own type definitions, so you do not need this installed.", + "dependencies": { + "diff": "*" + } + }, "node_modules/@types/eslint": { "version": "9.6.1", "dev": true, @@ -10981,9 +10992,9 @@ "license": "MIT" }, "node_modules/diff": { - "version": "4.0.2", - "dev": true, - "license": "BSD-3-Clause", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz", + "integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==", "engines": { "node": ">=0.3.1" } @@ -24370,6 +24381,15 @@ } } }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/tsconfig-paths": { "version": "4.2.0", "dev": true, diff --git a/package.json b/package.json index a5624e84..3275d876 100644 --- a/package.json +++ b/package.json @@ -72,10 +72,12 @@ "@codemirror/legacy-modes": "6.5.1", "@codemirror/theme-one-dark": "6.1.3", "@types/codemirror": "5.60.16", + "@types/diff": "8.0.0", "axios": "1.12.1", "codemirror": "6.0.2", "cronstrue": "3.3.0", "d3": "7.9.0", + "diff": "8.0.3", "electron-context-menu": "4.1.0", "electron-is-dev": "3.0.1", "js-yaml": "4.1.0", diff --git a/projects/sailpoint-components/src/lib/services/config.service.ts b/projects/sailpoint-components/src/lib/services/config.service.ts index 4edfb08b..a6c808fd 100644 --- a/projects/sailpoint-components/src/lib/services/config.service.ts +++ b/projects/sailpoint-components/src/lib/services/config.service.ts @@ -134,6 +134,14 @@ export class ConfigService { icon: 'dashboard', description: 'Manage colab in SailPoint.', enabled: false + }, + { + name: 'config-hub', + displayName: 'Config Hub', + route: '/config-hub', + icon: 'dashboard', + description: 'Manage config hub in SailPoint.', + enabled: false } ]; diff --git a/projects/sailpoint-components/src/lib/services/web-api.service.ts b/projects/sailpoint-components/src/lib/services/web-api.service.ts index f22a2268..86c88744 100644 --- a/projects/sailpoint-components/src/lib/services/web-api.service.ts +++ b/projects/sailpoint-components/src/lib/services/web-api.service.ts @@ -42,6 +42,12 @@ export interface ElectronAPIInterface { listGitHubJsonFiles: (githubRepoUrl: string) => Promise; getGitHubFileContent: (downloadUrl: string, filename: string) => Promise; + // Config Hub git operations + getGitRepoSettings: () => Promise; + saveGitRepoSettings: (settings: GitRepoSettings) => Promise<{ success: boolean; error?: string }>; + getFileCommitHistory: (owner: string, repo: string, path: string, branch?: string, limit?: number) => Promise; + getFileAtRef: (owner: string, repo: string, path: string, ref: string) => Promise; + // Connector deployment uploadConnector: (githubRepoUrl: string, connectorAlias?: string) => Promise; @@ -219,6 +225,25 @@ export type CustomizerDeploymentResponse = { error?: string; }; +// Config Hub types +export type AuthMethod = 'pat' | 'ssh'; + +export type GitRepoSettings = { + repoUrl: string; + authMethod: AuthMethod; + pat?: string; + sshKeyPath?: string; + defaultBranch: string; + backupsPath: string; +}; + +export type GitCommit = { + sha: string; + message: string; + author: string; + timestamp: string; +}; + @Injectable({ providedIn: 'root' }) @@ -699,6 +724,54 @@ export class WebApiService implements ElectronAPIInterface, OnDestroy { return avatarTemplate.replace('{size}', '120'); } + // Config Hub git operations + async getGitRepoSettings(): Promise { + try { + return await this.apiCall('config-hub/git-settings', 'GET'); + } catch { + return null; + } + } + + async saveGitRepoSettings(settings: GitRepoSettings): Promise<{ success: boolean; error?: string }> { + try { + return await this.apiCall<{ success: boolean; error?: string }>('config-hub/git-settings', 'POST', settings); + } catch (error) { + return { success: false, error: error instanceof Error ? error.message : 'Failed to save settings' }; + } + } + + async getFileCommitHistory(owner: string, repo: string, path: string, branch?: string, limit = 30): Promise { + try { + const params = new URLSearchParams({ path, per_page: String(limit) }); + if (branch) params.set('sha', branch); + const url = `https://api.github.com/repos/${owner}/${repo}/commits?${params}`; + const response = await firstValueFrom(this.http.get(url, { + headers: { Accept: 'application/vnd.github.v3+json', 'User-Agent': 'SailPoint-UI-Development-Kit' } + })); + return (response || []).map((c: any) => ({ + sha: c.sha as string, + message: (c.commit?.message as string || '').split('\n')[0], + author: (c.commit?.author?.name || c.author?.login || 'Unknown') as string, + timestamp: (c.commit?.author?.date || '') as string, + })); + } catch { + return []; + } + } + + async getFileAtRef(owner: string, repo: string, path: string, ref: string): Promise { + try { + const url = `https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=${encodeURIComponent(ref)}`; + const response = await firstValueFrom(this.http.get(url, { + headers: { Accept: 'application/vnd.github.v3+json', 'User-Agent': 'SailPoint-UI-Development-Kit' } + })); + return atob((response.content as string).replace(/\n/g, '')); + } catch { + return ''; + } + } + // Connector deployment uploadConnector(): Promise { // Note: This would need to be proxied through the backend server diff --git a/projects/sailpoint-components/src/public-api.ts b/projects/sailpoint-components/src/public-api.ts index e8a295fa..037c9d50 100644 --- a/projects/sailpoint-components/src/public-api.ts +++ b/projects/sailpoint-components/src/public-api.ts @@ -30,4 +30,5 @@ export * from './lib/owner-graph/owner-graph.component'; export * from './lib/colab/colab.component'; export * from './lib/colab/services/discourse.service'; export * from './lib/colab/components/colab-card/colab-card.component'; -export * from './lib/colab/components/colab-section/colab-section.component'; \ No newline at end of file +export * from './lib/colab/components/colab-section/colab-section.component'; +export * from './lib/config-hub/config-hub.component'; \ No newline at end of file diff --git a/src/app/app.component.html b/src/app/app.component.html index fbccdc47..c4490ee1 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -170,6 +170,11 @@ dashboard Colab + + dashboard + Config Hub + diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 7f80070d..4242afda 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -1,5 +1,5 @@ import { Routes } from '@angular/router'; -import { AttachRuleComponent, IdentitiesComponent, REPORT_EXAMPLE_ROUTES, ThemePickerComponent, TransformBuilderComponent, TransformsComponent , AccountsComponent , CronicleComponent, CertificationManagementComponent, OwnerGraphComponent , ColabComponent } from 'sailpoint-components'; +import { AttachRuleComponent, IdentitiesComponent, REPORT_EXAMPLE_ROUTES, ThemePickerComponent, TransformBuilderComponent, TransformsComponent , AccountsComponent , CronicleComponent, CertificationManagementComponent, OwnerGraphComponent , ColabComponent , ConfigHubComponent } from 'sailpoint-components'; import { HomeComponent } from './home/home.component'; import { PageNotFoundComponent } from './shared/components'; @@ -71,6 +71,11 @@ export const appRoutes: Routes = [ component: ColabComponent }, + { + path: 'config-hub', + component: ConfigHubComponent + }, + { path: '**', component: PageNotFoundComponent From cccb5ed38de24837c6986e2e7daeba2f2f4fd842 Mon Sep 17 00:00:00 2001 From: Philip Ellis Date: Mon, 16 Mar 2026 09:53:14 -0400 Subject: [PATCH 02/20] initial creation of cicd naviation --- .../diff-viewer/diff-viewer.component.html | 197 +++++++++++ .../diff-viewer/diff-viewer.component.scss | 328 ++++++++++++++++++ .../diff-viewer/diff-viewer.component.ts | 210 +++++++++++ .../object-browser.component.html | 86 +++++ .../object-browser.component.scss | 113 ++++++ .../object-browser.component.ts | 137 ++++++++ .../repo-settings.component.html | 77 ++++ .../repo-settings.component.scss | 56 +++ .../repo-settings/repo-settings.component.ts | 78 +++++ .../restore-dialog.component.html | 91 +++++ .../restore-dialog.component.scss | 119 +++++++ .../restore-dialog.component.ts | 57 +++ .../lib/config-hub/config-hub.component.html | 42 +++ .../lib/config-hub/config-hub.component.scss | 66 ++++ .../config-hub/config-hub.component.spec.ts | 26 ++ .../lib/config-hub/config-hub.component.ts | 84 +++++ .../config-hub/models/config-hub.models.ts | 48 +++ .../services/config-hub-api.service.ts | 45 +++ .../services/config-hub-git.service.ts | 156 +++++++++ 19 files changed, 2016 insertions(+) create mode 100644 projects/sailpoint-components/src/lib/config-hub/components/diff-viewer/diff-viewer.component.html create mode 100644 projects/sailpoint-components/src/lib/config-hub/components/diff-viewer/diff-viewer.component.scss create mode 100644 projects/sailpoint-components/src/lib/config-hub/components/diff-viewer/diff-viewer.component.ts create mode 100644 projects/sailpoint-components/src/lib/config-hub/components/object-browser/object-browser.component.html create mode 100644 projects/sailpoint-components/src/lib/config-hub/components/object-browser/object-browser.component.scss create mode 100644 projects/sailpoint-components/src/lib/config-hub/components/object-browser/object-browser.component.ts create mode 100644 projects/sailpoint-components/src/lib/config-hub/components/repo-settings/repo-settings.component.html create mode 100644 projects/sailpoint-components/src/lib/config-hub/components/repo-settings/repo-settings.component.scss create mode 100644 projects/sailpoint-components/src/lib/config-hub/components/repo-settings/repo-settings.component.ts create mode 100644 projects/sailpoint-components/src/lib/config-hub/components/restore-dialog/restore-dialog.component.html create mode 100644 projects/sailpoint-components/src/lib/config-hub/components/restore-dialog/restore-dialog.component.scss create mode 100644 projects/sailpoint-components/src/lib/config-hub/components/restore-dialog/restore-dialog.component.ts create mode 100644 projects/sailpoint-components/src/lib/config-hub/config-hub.component.html create mode 100644 projects/sailpoint-components/src/lib/config-hub/config-hub.component.scss create mode 100644 projects/sailpoint-components/src/lib/config-hub/config-hub.component.spec.ts create mode 100644 projects/sailpoint-components/src/lib/config-hub/config-hub.component.ts create mode 100644 projects/sailpoint-components/src/lib/config-hub/models/config-hub.models.ts create mode 100644 projects/sailpoint-components/src/lib/config-hub/services/config-hub-api.service.ts create mode 100644 projects/sailpoint-components/src/lib/config-hub/services/config-hub-git.service.ts diff --git a/projects/sailpoint-components/src/lib/config-hub/components/diff-viewer/diff-viewer.component.html b/projects/sailpoint-components/src/lib/config-hub/components/diff-viewer/diff-viewer.component.html new file mode 100644 index 00000000..13aa6502 --- /dev/null +++ b/projects/sailpoint-components/src/lib/config-hub/components/diff-viewer/diff-viewer.component.html @@ -0,0 +1,197 @@ +@if (!backupObject()) { +
+ compare +

Select an object from the browser to view its change history

+
+} @else { +
+ + +
+
+ {{ backupObject()!.objectType }} + {{ backupObject()!.name }} +
+ +
+ @if (hasChanges) { + + +{{ addedCount }} + -{{ removedCount }} + + } + + + + view_stream + + + view_column + + + + +
+
+ + + + @if (loading()) { +
+ + Loading commit history… +
+ } @else if (errorMessage()) { +
+ error_outline + {{ errorMessage() }} +
+ } @else { + + +
+ + Compare from (base) + + @for (entry of commits(); track entry.commit.sha) { + + {{ entry.commit.sha.slice(0, 7) }} + {{ entry.commit.message | slice:0:60 }} + {{ formatTimestamp(entry.commit.timestamp) }} + + } + + + + arrow_forward + + + Compare to (target) + + @for (entry of commits(); track entry.commit.sha) { + + {{ entry.commit.sha.slice(0, 7) }} + {{ entry.commit.message | slice:0:60 }} + {{ formatTimestamp(entry.commit.timestamp) }} + + } + + +
+ + +
+ @for (entry of commits(); track entry.commit.sha) { +
+
+
+
{{ entry.commit.message | slice:0:80 }}
+
+ {{ entry.commit.sha.slice(0, 7) }} + {{ entry.commit.author }} + {{ formatTimestamp(entry.commit.timestamp) }} +
+
+ @if (entry.commit.sha === selectedCommitSha()) { + + } +
+ } +
+ + + + + @if (loadingDiff()) { +
+ + Computing diff… +
+ } @else if (diffLines().length === 0) { +
+ check_circle +

No changes between the selected commits

+
+ } @else if (!hasChanges) { +
+ check_circle +

Files are identical

+
+ } @else { + + @if (viewMode() === 'unified') { +
+ + + @for (line of diffLines(); track $index) { + + + + + + + } + +
{{ line.lineNumberLeft ?? '' }}{{ line.lineNumberRight ?? '' }} + {{ line.type === 'added' ? '+' : line.type === 'removed' ? '-' : ' ' }} +
{{ line.content }}
+
+ } + + @if (viewMode() === 'split') { +
+
+
+ Base — {{ compareCommitSha()?.slice(0, 7) }} +
+ + + @for (line of splitLeftLines(); track $index) { + + + + + + } + +
{{ line.lineNumberLeft ?? '' }}{{ line.type === 'removed' ? '-' : ' ' }}
{{ line.content }}
+
+
+
+ Target — {{ selectedCommitSha()?.slice(0, 7) }} +
+ + + @for (line of splitRightLines(); track $index) { + + + + + + } + +
{{ line.lineNumberRight ?? '' }}{{ line.type === 'added' ? '+' : ' ' }}
{{ line.content }}
+
+
+ } + + } + } + +
+} diff --git a/projects/sailpoint-components/src/lib/config-hub/components/diff-viewer/diff-viewer.component.scss b/projects/sailpoint-components/src/lib/config-hub/components/diff-viewer/diff-viewer.component.scss new file mode 100644 index 00000000..bb6b2af9 --- /dev/null +++ b/projects/sailpoint-components/src/lib/config-hub/components/diff-viewer/diff-viewer.component.scss @@ -0,0 +1,328 @@ +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + gap: 12px; + color: var(--mat-sys-on-surface-variant); + + mat-icon { + font-size: 48px; + width: 48px; + height: 48px; + opacity: 0.35; + } + + p { + margin: 0; + font-size: 14px; + text-align: center; + } + + &.small { + height: auto; + padding: 32px; + } +} + +.diff-viewer { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; +} + +.diff-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + gap: 16px; + flex-wrap: wrap; +} + +.object-title { + display: flex; + align-items: center; + gap: 10px; +} + +.type-chip { + background: var(--mat-sys-secondary-container); + color: var(--mat-sys-on-secondary-container); + padding: 2px 10px; + border-radius: 12px; + font-size: 12px; + font-weight: 600; + letter-spacing: 0.05em; +} + +.object-name { + font-size: 16px; + font-weight: 500; +} + +.header-actions { + display: flex; + align-items: center; + gap: 12px; +} + +.diff-stats { + display: flex; + gap: 6px; + font-size: 13px; + font-family: monospace; + font-weight: 600; +} + +.added-count { + color: #2e7d32; +} + +.removed-count { + color: #c62828; +} + +.view-toggle { + height: 36px; +} + +.loading-row { + display: flex; + align-items: center; + gap: 12px; + padding: 24px; + color: var(--mat-sys-on-surface-variant); +} + +.error-message { + display: flex; + align-items: center; + gap: 8px; + padding: 16px; + color: var(--mat-sys-error); +} + +.commit-selectors { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; +} + +.commit-select { + flex: 1; +} + +.arrow-icon { + color: var(--mat-sys-on-surface-variant); + flex-shrink: 0; +} + +.sha-label { + font-family: monospace; + font-size: 12px; + margin-right: 8px; + color: var(--mat-sys-on-surface-variant); +} + +.msg-label { + font-size: 13px; +} + +.time-label { + font-size: 11px; + color: var(--mat-sys-on-surface-variant); + margin-left: 8px; +} + +// Commit timeline +.commit-timeline { + max-height: 220px; + overflow-y: auto; + padding: 8px 16px; +} + +.timeline-entry { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 8px 6px; + border-radius: 8px; + cursor: pointer; + position: relative; + + &:hover { + background: var(--mat-sys-surface-variant); + } + + &.selected { + background: var(--mat-sys-secondary-container); + + .timeline-dot { + background: var(--mat-sys-secondary); + width: 12px; + height: 12px; + margin-top: 5px; + } + } +} + +.timeline-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--mat-sys-outline); + flex-shrink: 0; + margin-top: 7px; +} + +.timeline-content { + flex: 1; + min-width: 0; +} + +.commit-message { + font-size: 13px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.commit-meta { + display: flex; + gap: 8px; + font-size: 11px; + color: var(--mat-sys-on-surface-variant); + margin-top: 2px; +} + +.sha { + font-family: monospace; + background: var(--mat-sys-surface-variant); + padding: 0 4px; + border-radius: 3px; +} + +// Diff table +.diff-table-wrapper { + flex: 1; + overflow: auto; +} + +.diff-table { + width: 100%; + border-collapse: collapse; + font-family: 'Consolas', 'Monaco', 'Courier New', monospace; + font-size: 12px; + line-height: 1.5; +} + +.diff-line { + &.diff-added { + background: #e8f5e9; + + .diff-gutter { + color: #2e7d32; + background: #c8e6c9; + } + } + + &.diff-removed { + background: #ffebee; + + .diff-gutter { + color: #c62828; + background: #ffcdd2; + } + } + + &.diff-unchanged { + .diff-gutter { + color: var(--mat-sys-on-surface-variant); + } + } +} + +.line-num { + width: 48px; + min-width: 48px; + text-align: right; + padding: 0 8px; + color: var(--mat-sys-on-surface-variant); + user-select: none; + border-right: 1px solid var(--mat-sys-outline-variant); +} + +.diff-gutter { + width: 20px; + min-width: 20px; + text-align: center; + padding: 0 2px; + user-select: none; + border-right: 1px solid var(--mat-sys-outline-variant); +} + +.diff-content { + padding: 0 8px; + width: 100%; + + pre { + margin: 0; + white-space: pre-wrap; + word-break: break-all; + font-family: inherit; + font-size: inherit; + } +} + +// Split diff +.split-diff { + flex: 1; + display: flex; + overflow: hidden; +} + +.split-pane { + flex: 1; + overflow: auto; + border-right: 1px solid var(--mat-sys-outline-variant); + + &:last-child { + border-right: none; + } +} + +.split-pane-header { + position: sticky; + top: 0; + z-index: 1; + background: var(--mat-sys-surface-variant); + font-size: 12px; + font-family: monospace; + padding: 4px 8px; + border-bottom: 1px solid var(--mat-sys-outline-variant); +} + +// Dark mode overrides +@media (prefers-color-scheme: dark) { + .diff-line.diff-added { + background: #1b5e20; + + .diff-gutter { + background: #2e7d32; + color: #c8e6c9; + } + } + + .diff-line.diff-removed { + background: #7f0000; + + .diff-gutter { + background: #c62828; + color: #ffcdd2; + } + } +} diff --git a/projects/sailpoint-components/src/lib/config-hub/components/diff-viewer/diff-viewer.component.ts b/projects/sailpoint-components/src/lib/config-hub/components/diff-viewer/diff-viewer.component.ts new file mode 100644 index 00000000..0be8262e --- /dev/null +++ b/projects/sailpoint-components/src/lib/config-hub/components/diff-viewer/diff-viewer.component.ts @@ -0,0 +1,210 @@ +import { Component, input, OnChanges, output, signal, SimpleChanges } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MatButtonModule } from '@angular/material/button'; +import { MatButtonToggleModule } from '@angular/material/button-toggle'; +import { MatIconModule } from '@angular/material/icon'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatSelectModule } from '@angular/material/select'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatDividerModule } from '@angular/material/divider'; +import { MatChipsModule } from '@angular/material/chips'; +import { FormsModule } from '@angular/forms'; +import * as Diff from 'diff'; +import { ConfigHubGitService } from '../../services/config-hub-git.service'; +import { BackupObject, DiffLine, GitCommit } from '../../models/config-hub.models'; + +export type DiffViewMode = 'unified' | 'split'; + +interface CommitEntry { + commit: GitCommit; + content?: string; +} + +@Component({ + selector: 'app-diff-viewer', + standalone: true, + imports: [ + CommonModule, + FormsModule, + MatButtonModule, + MatButtonToggleModule, + MatIconModule, + MatProgressSpinnerModule, + MatSelectModule, + MatTooltipModule, + MatDividerModule, + MatChipsModule, + ], + templateUrl: './diff-viewer.component.html', + styleUrl: './diff-viewer.component.scss', +}) +export class DiffViewerComponent implements OnChanges { + readonly backupObject = input(null); + readonly restoreRequested = output<{ object: BackupObject; content: any; commitSha: string }>(); + + commits = signal([]); + selectedCommitSha = signal(null); + compareCommitSha = signal(null); + viewMode = signal('unified'); + + diffLines = signal([]); + leftContent = signal(''); + rightContent = signal(''); + + loading = signal(false); + loadingDiff = signal(false); + errorMessage = signal(null); + + private branches: string[] = []; + + constructor(private gitService: ConfigHubGitService) {} + + async ngOnChanges(changes: SimpleChanges): Promise { + if (changes['backupObject']) { + const obj = this.backupObject(); + if (obj) { + await this.loadHistory(obj); + } else { + this.reset(); + } + } + } + + private reset(): void { + this.commits.set([]); + this.selectedCommitSha.set(null); + this.compareCommitSha.set(null); + this.diffLines.set([]); + this.leftContent.set(''); + this.rightContent.set(''); + this.errorMessage.set(null); + } + + private async loadHistory(obj: BackupObject): Promise { + this.loading.set(true); + this.errorMessage.set(null); + this.reset(); + + const history = await this.gitService.getCommitHistory(obj.objectType, obj.objectId, undefined, 50); + if (history.length === 0) { + this.loading.set(false); + this.errorMessage.set('No commit history found for this object.'); + return; + } + + this.commits.set(history.map(c => ({ commit: c }))); + this.loading.set(false); + + // Default: show most recent commit diffed against the previous one + this.selectedCommitSha.set(history[0].sha); + if (history.length > 1) { + this.compareCommitSha.set(history[1].sha); + } else { + this.compareCommitSha.set(history[0].sha); + } + + await this.computeDiff(); + } + + async onCommitSelect(sha: string): Promise { + this.selectedCommitSha.set(sha); + await this.computeDiff(); + } + + async onCompareSelect(sha: string): Promise { + this.compareCommitSha.set(sha); + await this.computeDiff(); + } + + private async computeDiff(): Promise { + const obj = this.backupObject(); + const leftSha = this.compareCommitSha(); + const rightSha = this.selectedCommitSha(); + if (!obj || !leftSha || !rightSha) return; + + this.loadingDiff.set(true); + const [leftRaw, rightRaw] = await Promise.all([ + this.gitService.getFileAtCommit(obj.objectType, obj.objectId, leftSha), + this.gitService.getFileAtCommit(obj.objectType, obj.objectId, rightSha), + ]); + + this.leftContent.set(leftRaw); + this.rightContent.set(rightRaw); + this.diffLines.set(this.buildDiffLines(leftRaw, rightRaw)); + this.loadingDiff.set(false); + } + + private buildDiffLines(oldText: string, newText: string): DiffLine[] { + const changes = Diff.diffLines(oldText, newText); + const result: DiffLine[] = []; + let leftLine = 1; + let rightLine = 1; + + for (const part of changes) { + const lines = part.value.split('\n'); + // diffLines includes a trailing empty string when value ends with \n + if (lines[lines.length - 1] === '') lines.pop(); + + for (const line of lines) { + if (part.added) { + result.push({ type: 'added', content: line, lineNumberRight: rightLine++ }); + } else if (part.removed) { + result.push({ type: 'removed', content: line, lineNumberLeft: leftLine++ }); + } else { + result.push({ type: 'unchanged', content: line, lineNumberLeft: leftLine++, lineNumberRight: rightLine++ }); + } + } + } + return result; + } + + get hasChanges(): boolean { + return this.diffLines().some(l => l.type !== 'unchanged'); + } + + get addedCount(): number { + return this.diffLines().filter(l => l.type === 'added').length; + } + + get removedCount(): number { + return this.diffLines().filter(l => l.type === 'removed').length; + } + + getCommitLabel(sha: string): string { + const entry = this.commits().find(c => c.commit.sha === sha); + if (!entry) return sha.slice(0, 7); + return `${sha.slice(0, 7)} – ${entry.commit.message.slice(0, 50)}`; + } + + formatTimestamp(iso: string): string { + if (!iso) return ''; + return new Date(iso).toLocaleString(); + } + + onRestoreClick(): void { + const obj = this.backupObject(); + const sha = this.selectedCommitSha(); + if (!obj || !sha) return; + const raw = this.rightContent(); + if (!raw) return; + try { + const parsed = JSON.parse(raw); + this.restoreRequested.emit({ object: obj, content: parsed, commitSha: sha }); + } catch { + this.errorMessage.set('Could not parse object JSON for restore.'); + } + } + + selectedCommit(): GitCommit | undefined { + const sha = this.selectedCommitSha(); + return this.commits().find(c => c.commit.sha === sha)?.commit; + } + + splitLeftLines(): DiffLine[] { + return this.diffLines().filter(l => l.type !== 'added'); + } + + splitRightLines(): DiffLine[] { + return this.diffLines().filter(l => l.type !== 'removed'); + } +} diff --git a/projects/sailpoint-components/src/lib/config-hub/components/object-browser/object-browser.component.html b/projects/sailpoint-components/src/lib/config-hub/components/object-browser/object-browser.component.html new file mode 100644 index 00000000..4e2187b9 --- /dev/null +++ b/projects/sailpoint-components/src/lib/config-hub/components/object-browser/object-browser.component.html @@ -0,0 +1,86 @@ +
+ + +
+
+ Object Types + +
+ + @if (loadingTypes()) { +
+ } @else if (objectTypes().length === 0) { +
+ folder_open + No object types found.
Configure the repository first.
+
+ } @else { + + @for (type of objectTypes(); track type.name) { + + {{ typeIcon(type.name) }} + {{ type.name }} + + } + + } +
+ + +
+ @if (selectedType()) { +
+ {{ selectedType() }} + @if (loadingObjects()) { + + } +
+ + @if (!loadingObjects() && objects().length === 0) { +
+ inbox + No objects found +
+ } @else { + + @for (obj of objects(); track obj.objectId) { + + {{ typeIcon(obj.objectType) }} + + {{ obj.name }} + + @if (obj.lastCommit) { +
+ history + + {{ formatRelativeTime(obj.lastCommit.timestamp) }} + + · {{ obj.lastCommit.author }} +
+ } @else { +
+ schedule + Loading history… +
+ } +
+ } +
+ } + } @else { +
+ arrow_back + Select an object type +
+ } +
+ +
diff --git a/projects/sailpoint-components/src/lib/config-hub/components/object-browser/object-browser.component.scss b/projects/sailpoint-components/src/lib/config-hub/components/object-browser/object-browser.component.scss new file mode 100644 index 00000000..3509a5d8 --- /dev/null +++ b/projects/sailpoint-components/src/lib/config-hub/components/object-browser/object-browser.component.scss @@ -0,0 +1,113 @@ +.browser-container { + display: flex; + height: 100%; + overflow: hidden; +} + +.type-column { + width: 220px; + min-width: 180px; + border-right: 1px solid var(--mat-sys-outline-variant); + display: flex; + flex-direction: column; + overflow: hidden; +} + +.object-column { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.column-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 16px; + font-weight: 500; + font-size: 13px; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--mat-sys-on-surface-variant); + border-bottom: 1px solid var(--mat-sys-outline-variant); + min-height: 44px; +} + +mat-nav-list { + flex: 1; + overflow-y: auto; + padding: 4px 0; +} + +mat-list-item { + border-radius: 8px; + margin: 2px 6px; + cursor: pointer; + + &.selected { + background: var(--mat-sys-secondary-container); + color: var(--mat-sys-on-secondary-container); + } +} + +.object-name { + font-size: 14px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.commit-meta { + display: flex; + align-items: center; + gap: 4px; + font-size: 11px; + color: var(--mat-sys-on-surface-variant); + overflow: hidden; + + &.loading { + opacity: 0.5; + } +} + +.meta-icon { + font-size: 13px; + width: 13px; + height: 13px; +} + +.relative-time { + white-space: nowrap; + flex-shrink: 0; +} + +.author { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.spinner-row { + display: flex; + justify-content: center; + padding: 24px; +} + +.empty-message { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + padding: 32px 16px; + text-align: center; + color: var(--mat-sys-on-surface-variant); + font-size: 13px; + + mat-icon { + font-size: 36px; + width: 36px; + height: 36px; + opacity: 0.4; + } +} diff --git a/projects/sailpoint-components/src/lib/config-hub/components/object-browser/object-browser.component.ts b/projects/sailpoint-components/src/lib/config-hub/components/object-browser/object-browser.component.ts new file mode 100644 index 00000000..812c9e74 --- /dev/null +++ b/projects/sailpoint-components/src/lib/config-hub/components/object-browser/object-browser.component.ts @@ -0,0 +1,137 @@ +import { Component, OnInit, output, signal } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { MatListModule } from '@angular/material/list'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatBadgeModule } from '@angular/material/badge'; +import { MatChipsModule } from '@angular/material/chips'; +import { ConfigHubGitService } from '../../services/config-hub-git.service'; +import { BackupObject, BackupObjectType, GitCommit } from '../../models/config-hub.models'; + +@Component({ + selector: 'app-object-browser', + standalone: true, + imports: [ + CommonModule, + MatButtonModule, + MatIconModule, + MatListModule, + MatProgressSpinnerModule, + MatTooltipModule, + MatBadgeModule, + MatChipsModule, + ], + templateUrl: './object-browser.component.html', + styleUrl: './object-browser.component.scss', +}) +export class ObjectBrowserComponent implements OnInit { + readonly objectSelected = output(); + + objectTypes = signal([]); + objects = signal([]); + selectedType = signal(null); + selectedObject = signal(null); + loadingTypes = signal(false); + loadingObjects = signal(false); + historyMap = signal>(new Map()); + + constructor(private gitService: ConfigHubGitService) {} + + ngOnInit(): void { + this.refresh(); + } + + async refresh(): Promise { + if (!this.gitService.settings()) return; + this.loadingTypes.set(true); + const types = await this.gitService.getObjectTypes(); + this.objectTypes.set(types); + this.loadingTypes.set(false); + if (this.selectedType()) { + await this.loadObjectsForType(this.selectedType()!); + } + } + + async selectType(typeName: string): Promise { + this.selectedType.set(typeName); + this.selectedObject.set(null); + await this.loadObjectsForType(typeName); + } + + private async loadObjectsForType(typeName: string): Promise { + this.loadingObjects.set(true); + this.objects.set([]); + this.historyMap.set(new Map()); + const objs = await this.gitService.getObjectsForType(typeName); + this.objects.set(objs); + this.loadingObjects.set(false); + + // Load last-commit info for each object concurrently (batched) + const BATCH = 5; + for (let i = 0; i < objs.length; i += BATCH) { + const batch = objs.slice(i, i + BATCH); + await Promise.all(batch.map(async (obj) => { + const history = await this.gitService.getCommitHistory(obj.objectType, obj.objectId, undefined, 1); + if (history.length > 0) { + const map = new Map(this.historyMap()); + map.set(obj.objectId, history[0]); + this.historyMap.set(map); + // Update object name from commit if possible + const idx = this.objects().findIndex(o => o.objectId === obj.objectId); + if (idx >= 0) { + const updated = [...this.objects()]; + updated[idx] = { ...updated[idx], lastCommit: history[0] }; + this.objects.set(updated); + } + } + })); + } + } + + selectObject(obj: BackupObject): void { + this.selectedObject.set(obj); + this.objectSelected.emit(obj); + } + + getLastCommit(objectId: string): GitCommit | undefined { + return this.historyMap().get(objectId); + } + + formatTimestamp(iso: string): string { + if (!iso) return ''; + const d = new Date(iso); + return d.toLocaleDateString(undefined, { + month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit', + }); + } + + formatRelativeTime(iso: string): string { + if (!iso) return ''; + const diff = Date.now() - new Date(iso).getTime(); + const minutes = Math.floor(diff / 60_000); + if (minutes < 1) return 'just now'; + if (minutes < 60) return `${minutes}m ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ago`; + const days = Math.floor(hours / 24); + if (days < 30) return `${days}d ago`; + return Math.floor(days / 30) + 'mo ago'; + } + + typeIcon(typeName: string): string { + const icons: Record = { + ROLE: 'manage_accounts', + RULE: 'code', + SOURCE: 'device_hub', + WORKFLOW: 'account_tree', + TRIGGER_SUBSCRIPTION: 'notifications', + IDENTITY_PROFILE: 'person', + ACCESS_PROFILE: 'badge', + TRANSFORM: 'transform', + CONNECTOR_RULE: 'build', + }; + return icons[typeName] ?? 'description'; + } +} diff --git a/projects/sailpoint-components/src/lib/config-hub/components/repo-settings/repo-settings.component.html b/projects/sailpoint-components/src/lib/config-hub/components/repo-settings/repo-settings.component.html new file mode 100644 index 00000000..39d6c1f4 --- /dev/null +++ b/projects/sailpoint-components/src/lib/config-hub/components/repo-settings/repo-settings.component.html @@ -0,0 +1,77 @@ + + + settings + Repository Settings + Connect to the GitHub repository containing your backups + + + +
+ + + Repository URL + source + + HTTPS or SSH clone URL + + + + Backups Path + folder + + Path within the repo where tenant backup folders live (e.g. backups) + + + + Default Branch + call_split + + + +
+ info + A GitHub API Token (PAT) is required for all repository access — + the GitHub REST API does not support SSH key authentication. + SSH keys are only used for local git operations (clone / push / pull). +
+ + + GitHub API Token (PAT) + vpn_key + + + Requires repo scope. Used for all history and file browsing. + + + + SSH Private Key Path (optional — Electron only) + key + + Used for local git clone / pull / push operations when running in Electron + + +
+
+ + + @if (dialogRef) { + + } + + +
diff --git a/projects/sailpoint-components/src/lib/config-hub/components/repo-settings/repo-settings.component.scss b/projects/sailpoint-components/src/lib/config-hub/components/repo-settings/repo-settings.component.scss new file mode 100644 index 00000000..2ba3e50a --- /dev/null +++ b/projects/sailpoint-components/src/lib/config-hub/components/repo-settings/repo-settings.component.scss @@ -0,0 +1,56 @@ +.settings-card { + max-width: 640px; +} + +.settings-form { + display: flex; + flex-direction: column; + gap: 16px; + padding-top: 16px; +} + +.full-width { + width: 100%; +} + +.half-width { + width: 50%; +} + +.auth-info-banner { + display: flex; + align-items: flex-start; + gap: 10px; + background: var(--mat-sys-secondary-container); + color: var(--mat-sys-on-secondary-container); + padding: 10px 14px; + border-radius: 8px; + font-size: 13px; + line-height: 1.5; + + mat-icon { + flex-shrink: 0; + font-size: 18px; + width: 18px; + height: 18px; + margin-top: 2px; + } +} + +.optional-label { + font-size: 12px; + font-weight: 400; + opacity: 0.7; +} + +.button-spinner { + display: inline-block; + margin-right: 8px; +} + +code { + font-family: monospace; + background: var(--mat-sys-surface-variant); + padding: 1px 4px; + border-radius: 3px; +} diff --git a/projects/sailpoint-components/src/lib/config-hub/components/repo-settings/repo-settings.component.ts b/projects/sailpoint-components/src/lib/config-hub/components/repo-settings/repo-settings.component.ts new file mode 100644 index 00000000..e8d7ed44 --- /dev/null +++ b/projects/sailpoint-components/src/lib/config-hub/components/repo-settings/repo-settings.component.ts @@ -0,0 +1,78 @@ +import { Component, OnInit, Optional, output } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCardModule } from '@angular/material/card'; +import { MatDialogModule, MatDialogRef } from '@angular/material/dialog'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatIconModule } from '@angular/material/icon'; +import { MatInputModule } from '@angular/material/input'; +import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { ConfigHubGitService } from '../../services/config-hub-git.service'; +import { GitRepoSettings } from '../../models/config-hub.models'; + +@Component({ + selector: 'app-repo-settings', + standalone: true, + imports: [ + CommonModule, + ReactiveFormsModule, + MatButtonModule, + MatCardModule, + MatDialogModule, + MatFormFieldModule, + MatIconModule, + MatInputModule, + MatSnackBarModule, + MatProgressSpinnerModule, + ], + templateUrl: './repo-settings.component.html', + styleUrl: './repo-settings.component.scss', +}) +export class RepoSettingsComponent implements OnInit { + readonly saved = output(); + + form!: FormGroup; + saving = false; + hidePatValue = true; + + constructor( + private fb: FormBuilder, + private gitService: ConfigHubGitService, + private snackBar: MatSnackBar, + @Optional() private dialogRef?: MatDialogRef, + ) {} + + ngOnInit(): void { + this.form = this.fb.group({ + repoUrl: ['', Validators.required], + authMethod: ['pat'], + pat: [''], + sshKeyPath: [''], + defaultBranch: ['main', Validators.required], + backupsPath: ['backups', Validators.required], + }); + + const existing = this.gitService.settings(); + if (existing) { + this.form.patchValue(existing); + } + } + + async onSave(): Promise { + if (this.form.invalid) return; + this.saving = true; + const settings = this.form.value as GitRepoSettings; + const result = await this.gitService.saveSettings(settings); + this.saving = false; + if (result.success) { + this.snackBar.open('Repository settings saved', 'Dismiss', { duration: 3000 }); + this.saved.emit(settings); + await this.gitService.loadBranches(); + this.dialogRef?.close(settings); + } else { + this.snackBar.open(`Failed to save: ${result.error}`, 'Dismiss', { duration: 5000 }); + } + } +} diff --git a/projects/sailpoint-components/src/lib/config-hub/components/restore-dialog/restore-dialog.component.html b/projects/sailpoint-components/src/lib/config-hub/components/restore-dialog/restore-dialog.component.html new file mode 100644 index 00000000..6e7c71b6 --- /dev/null +++ b/projects/sailpoint-components/src/lib/config-hub/components/restore-dialog/restore-dialog.component.html @@ -0,0 +1,91 @@ +

+ restore + Restore Object +

+ + + + @if (!result()) { +

+ You are about to restore the following object to the active SailPoint environment. + This will overwrite the current version. +

+ +
+
+ Type + {{ data.object.objectType }} +
+
+ Name + {{ data.object.name }} +
+
+ Object ID + {{ data.object.objectId }} +
+
+ Commit + {{ data.commitSha.slice(0, 7) }} +
+ @if (data.commitMessage) { +
+ Message + {{ data.commitMessage }} +
+ } + @if (data.commitAuthor) { +
+ Author + {{ data.commitAuthor }} +
+ } + @if (data.commitTimestamp) { +
+ Date + {{ formatTimestamp(data.commitTimestamp) }} +
+ } +
+ +
+ warning + This action cannot be undone automatically. Ensure you have a current backup before proceeding. +
+ } + + @if (result(); as r) { +
+ {{ r.success ? 'check_circle' : 'error' }} +

{{ r.success ? r.message : r.error }}

+
+ } + + @if (restoring()) { +
+ + Sending restore request… +
+ } + +
+ + + + + @if (!result()) { + + + } @else { + + } + diff --git a/projects/sailpoint-components/src/lib/config-hub/components/restore-dialog/restore-dialog.component.scss b/projects/sailpoint-components/src/lib/config-hub/components/restore-dialog/restore-dialog.component.scss new file mode 100644 index 00000000..2200ada3 --- /dev/null +++ b/projects/sailpoint-components/src/lib/config-hub/components/restore-dialog/restore-dialog.component.scss @@ -0,0 +1,119 @@ +h2[mat-dialog-title] { + display: flex; + align-items: center; + gap: 8px; +} + +.title-icon { + color: var(--mat-sys-primary); +} + +mat-dialog-content { + min-width: 480px; + max-width: 600px; +} + +.confirm-intro { + margin: 0 0 20px 0; + color: var(--mat-sys-on-surface-variant); + font-size: 14px; +} + +.detail-grid { + display: flex; + flex-direction: column; + gap: 10px; + margin-bottom: 20px; +} + +.detail-row { + display: flex; + gap: 16px; + align-items: baseline; +} + +.detail-label { + width: 80px; + min-width: 80px; + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--mat-sys-on-surface-variant); +} + +.detail-value { + font-size: 14px; + word-break: break-all; +} + +.mono { + font-family: monospace; + font-size: 13px; +} + +.type-chip { + background: var(--mat-sys-secondary-container); + color: var(--mat-sys-on-secondary-container); + padding: 2px 10px; + border-radius: 12px; + font-size: 12px; + font-weight: 600; +} + +.warning-banner { + display: flex; + align-items: flex-start; + gap: 10px; + background: var(--mat-sys-error-container); + color: var(--mat-sys-on-error-container); + padding: 12px 14px; + border-radius: 8px; + font-size: 13px; + + mat-icon { + flex-shrink: 0; + color: var(--mat-sys-error); + } +} + +.result-state { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + padding: 24px; + text-align: center; + + mat-icon { + font-size: 48px; + width: 48px; + height: 48px; + } + + p { + margin: 0; + font-size: 14px; + } + + &.success mat-icon { + color: #2e7d32; + } + + &.failure mat-icon { + color: var(--mat-sys-error); + } +} + +.restoring-state { + display: flex; + align-items: center; + gap: 16px; + padding: 16px 0; + color: var(--mat-sys-on-surface-variant); +} + +mat-dialog-actions { + padding: 12px 24px; + gap: 8px; +} diff --git a/projects/sailpoint-components/src/lib/config-hub/components/restore-dialog/restore-dialog.component.ts b/projects/sailpoint-components/src/lib/config-hub/components/restore-dialog/restore-dialog.component.ts new file mode 100644 index 00000000..1d2a9528 --- /dev/null +++ b/projects/sailpoint-components/src/lib/config-hub/components/restore-dialog/restore-dialog.component.ts @@ -0,0 +1,57 @@ +import { Component, inject, signal } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatDividerModule } from '@angular/material/divider'; +import { ConfigHubApiService } from '../../services/config-hub-api.service'; +import { BackupObject, RestoreResult } from '../../models/config-hub.models'; + +export interface RestoreDialogData { + object: BackupObject; + content: any; + commitSha: string; + commitMessage?: string; + commitAuthor?: string; + commitTimestamp?: string; +} + +@Component({ + selector: 'app-restore-dialog', + standalone: true, + imports: [ + CommonModule, + MatDialogModule, + MatButtonModule, + MatIconModule, + MatProgressSpinnerModule, + MatDividerModule, + ], + templateUrl: './restore-dialog.component.html', + styleUrl: './restore-dialog.component.scss', +}) +export class RestoreDialogComponent { + readonly data: RestoreDialogData = inject(MAT_DIALOG_DATA); + private dialogRef = inject(MatDialogRef); + private apiService = inject(ConfigHubApiService); + + restoring = signal(false); + result = signal(null); + + async onConfirm(): Promise { + this.restoring.set(true); + const outcome = await this.apiService.restore(this.data.object, this.data.content); + this.result.set(outcome); + this.restoring.set(false); + } + + onClose(): void { + this.dialogRef.close(this.result()); + } + + formatTimestamp(iso: string | undefined): string { + if (!iso) return ''; + return new Date(iso).toLocaleString(); + } +} diff --git a/projects/sailpoint-components/src/lib/config-hub/config-hub.component.html b/projects/sailpoint-components/src/lib/config-hub/config-hub.component.html new file mode 100644 index 00000000..cd363aaa --- /dev/null +++ b/projects/sailpoint-components/src/lib/config-hub/config-hub.component.html @@ -0,0 +1,42 @@ +
+ + + + hub + Config Hub + + + + + @if (!hasSettings()) { +
+ info + No repository configured yet. + +
+ } + +
+ + +
+ + +
+ + +
+ + +
+ +
+
diff --git a/projects/sailpoint-components/src/lib/config-hub/config-hub.component.scss b/projects/sailpoint-components/src/lib/config-hub/config-hub.component.scss new file mode 100644 index 00000000..19b24209 --- /dev/null +++ b/projects/sailpoint-components/src/lib/config-hub/config-hub.component.scss @@ -0,0 +1,66 @@ +.config-hub-shell { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; +} + +.hub-toolbar { + flex-shrink: 0; + gap: 12px; + + .toolbar-title { + font-size: 18px; + font-weight: 500; + } + + .spacer { + flex: 1; + } +} + +.no-settings-banner { + display: flex; + align-items: center; + gap: 12px; + background: var(--mat-sys-tertiary-container); + color: var(--mat-sys-on-tertiary-container); + padding: 10px 20px; + font-size: 14px; + flex-shrink: 0; + + mat-icon { + flex-shrink: 0; + } + + span { + flex: 1; + } +} + +.workspace { + display: flex; + flex: 1; + overflow: hidden; +} + +.workspace-no-settings { + opacity: 0.55; + pointer-events: none; +} + +.browser-pane { + width: 480px; + min-width: 360px; + border-right: 1px solid var(--mat-sys-outline-variant); + overflow: hidden; + display: flex; + flex-direction: column; +} + +.detail-pane { + flex: 1; + overflow: hidden; + display: flex; + flex-direction: column; +} diff --git a/projects/sailpoint-components/src/lib/config-hub/config-hub.component.spec.ts b/projects/sailpoint-components/src/lib/config-hub/config-hub.component.spec.ts new file mode 100644 index 00000000..602d5bf6 --- /dev/null +++ b/projects/sailpoint-components/src/lib/config-hub/config-hub.component.spec.ts @@ -0,0 +1,26 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ConfigHubComponent } from './config-hub.component'; + +describe('ConfigHubComponent', () => { + let component: ConfigHubComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ConfigHubComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ConfigHubComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should have correct title', () => { + expect(component.title).toBe('Config Hub'); + }); +}); diff --git a/projects/sailpoint-components/src/lib/config-hub/config-hub.component.ts b/projects/sailpoint-components/src/lib/config-hub/config-hub.component.ts new file mode 100644 index 00000000..1fb201e7 --- /dev/null +++ b/projects/sailpoint-components/src/lib/config-hub/config-hub.component.ts @@ -0,0 +1,84 @@ +import { Component, OnInit, signal } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MatButtonModule } from '@angular/material/button'; +import { MatDialog, MatDialogModule } from '@angular/material/dialog'; +import { MatIconModule } from '@angular/material/icon'; +import { MatToolbarModule } from '@angular/material/toolbar'; +import { MatTooltipModule } from '@angular/material/tooltip'; + +import { RepoSettingsComponent } from './components/repo-settings/repo-settings.component'; +import { ObjectBrowserComponent } from './components/object-browser/object-browser.component'; +import { DiffViewerComponent } from './components/diff-viewer/diff-viewer.component'; +import { + RestoreDialogComponent, + RestoreDialogData, +} from './components/restore-dialog/restore-dialog.component'; +import { ConfigHubGitService } from './services/config-hub-git.service'; +import { BackupObject, GitRepoSettings } from './models/config-hub.models'; + +@Component({ + selector: 'app-config-hub', + standalone: true, + imports: [ + CommonModule, + MatButtonModule, + MatDialogModule, + MatIconModule, + MatToolbarModule, + MatTooltipModule, + ObjectBrowserComponent, + DiffViewerComponent, + ], + templateUrl: './config-hub.component.html', + styleUrl: './config-hub.component.scss', +}) +export class ConfigHubComponent implements OnInit { + selectedObject = signal(null); + hasSettings = signal(false); + + constructor( + private gitService: ConfigHubGitService, + private dialog: MatDialog, + ) {} + + async ngOnInit(): Promise { + await this.gitService.loadSettings(); + const s = this.gitService.settings(); + this.hasSettings.set(!!s?.repoUrl); + if (s?.repoUrl) { + await this.gitService.loadBranches(); + } else { + this.openSettings(); + } + } + + onObjectSelected(obj: BackupObject): void { + this.selectedObject.set(obj); + } + + openSettings(): void { + const ref = this.dialog.open(RepoSettingsComponent, { + width: '680px', + maxHeight: '90vh', + disableClose: false, + }); + ref.afterClosed().subscribe((saved: GitRepoSettings | undefined) => { + if (saved?.repoUrl) { + this.hasSettings.set(true); + } + }); + } + + onRestoreRequested(event: { object: BackupObject; content: any; commitSha: string }): void { + const data: RestoreDialogData = { + object: event.object, + content: event.content, + commitSha: event.commitSha, + }; + this.dialog.open(RestoreDialogComponent, { + data, + width: '560px', + disableClose: true, + }); + } +} diff --git a/projects/sailpoint-components/src/lib/config-hub/models/config-hub.models.ts b/projects/sailpoint-components/src/lib/config-hub/models/config-hub.models.ts new file mode 100644 index 00000000..d432c0e8 --- /dev/null +++ b/projects/sailpoint-components/src/lib/config-hub/models/config-hub.models.ts @@ -0,0 +1,48 @@ +export type AuthMethod = 'pat' | 'ssh'; + +export interface GitRepoSettings { + repoUrl: string; + authMethod: AuthMethod; + pat?: string; + sshKeyPath?: string; + defaultBranch: string; + backupsPath: string; +} + +export interface GitCommit { + sha: string; + message: string; + author: string; + timestamp: string; +} + +export interface BackupObjectType { + name: string; + objectCount: number; +} + +export interface BackupObject { + objectType: string; + objectId: string; + name: string; + lastCommit?: GitCommit; +} + +export interface ObjectContent { + object: BackupObject; + content: any; + rawJson: string; +} + +export interface DiffLine { + type: 'added' | 'removed' | 'unchanged'; + content: string; + lineNumberLeft?: number; + lineNumberRight?: number; +} + +export interface RestoreResult { + success: boolean; + message?: string; + error?: string; +} diff --git a/projects/sailpoint-components/src/lib/config-hub/services/config-hub-api.service.ts b/projects/sailpoint-components/src/lib/config-hub/services/config-hub-api.service.ts new file mode 100644 index 00000000..3c0a3b3c --- /dev/null +++ b/projects/sailpoint-components/src/lib/config-hub/services/config-hub-api.service.ts @@ -0,0 +1,45 @@ +import { Injectable, signal } from '@angular/core'; +import { ElectronApiFactoryService } from '../../services/electron-api-factory.service'; +import { BackupObject, RestoreResult } from '../models/config-hub.models'; + +@Injectable({ providedIn: 'root' }) +export class ConfigHubApiService { + readonly restoring = signal(false); + + constructor(private factory: ElectronApiFactoryService) {} + + /** + * Restore a single config object to the active SailPoint tenant. + * Calls POST /v2025/sp-config/import with the object wrapped in the + * sp-config export format expected by the API. + */ + async restore(backupObject: BackupObject, objectContent: any): Promise { + this.restoring.set(true); + try { + const api = this.factory.getApi(); + const payload = { + description: `Restore ${backupObject.objectType} ${backupObject.name} via Config Hub`, + includeTypes: [backupObject.objectType], + objects: [ + { + version: objectContent.version ?? 1, + self: objectContent.self, + object: objectContent.object, + }, + ], + }; + + // The sp-config import endpoint expects a JSON body with the export format + const result = await api.callSdkMethod('importConfig', payload); + return { + success: true, + message: `Successfully queued restore for ${backupObject.objectType} "${backupObject.name}"`, + }; + } catch (error) { + const msg = error instanceof Error ? error.message : 'Unknown error during restore'; + return { success: false, error: msg }; + } finally { + this.restoring.set(false); + } + } +} diff --git a/projects/sailpoint-components/src/lib/config-hub/services/config-hub-git.service.ts b/projects/sailpoint-components/src/lib/config-hub/services/config-hub-git.service.ts new file mode 100644 index 00000000..11c3ea1e --- /dev/null +++ b/projects/sailpoint-components/src/lib/config-hub/services/config-hub-git.service.ts @@ -0,0 +1,156 @@ +import { Injectable, signal } from '@angular/core'; +import { GitRepoSettings, GitCommit, BackupObject, BackupObjectType } from '../models/config-hub.models'; + +const SETTINGS_KEY = 'config-hub-git-settings'; + +@Injectable({ providedIn: 'root' }) +export class ConfigHubGitService { + readonly settings = signal(null); + readonly branches = signal([]); + readonly loading = signal(false); + + async loadSettings(): Promise { + try { + const raw = localStorage.getItem(SETTINGS_KEY); + this.settings.set(raw ? (JSON.parse(raw) as GitRepoSettings) : null); + } catch { + this.settings.set(null); + } + } + + async saveSettings(settings: GitRepoSettings): Promise<{ success: boolean; error?: string }> { + try { + localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings)); + this.settings.set(settings); + return { success: true }; + } catch (error) { + const msg = error instanceof Error ? error.message : 'Failed to save settings'; + return { success: false, error: msg }; + } + } + + async loadBranches(): Promise { + const s = this.settings(); + if (!s) return; + const { owner, repo } = this.parseRepoUrl(s.repoUrl); + if (!owner || !repo) return; + try { + const url = `https://api.github.com/repos/${owner}/${repo}/branches?per_page=100`; + const res = await fetch(url, { headers: this.githubHeaders(s) }); + if (!res.ok) return; + const data = await res.json() as any[]; + this.branches.set(data.map((b: any) => b.name as string)); + } catch { + this.branches.set([]); + } + } + + async getObjectTypes(): Promise { + const s = this.settings(); + if (!s) return []; + const { owner, repo } = this.parseRepoUrl(s.repoUrl); + if (!owner || !repo) return []; + try { + const basePath = s.backupsPath.replace(/^\/|\/$/g, ''); + const url = `https://api.github.com/repos/${owner}/${repo}/contents/${basePath}?ref=${encodeURIComponent(s.defaultBranch)}`; + const res = await fetch(url, { headers: this.githubHeaders(s) }); + if (!res.ok) return []; + const items = await res.json() as any[]; + return items + .filter((i: any) => i.type === 'dir') + .map((i: any) => ({ name: i.name as string, objectCount: 0 })); + } catch { + return []; + } + } + + async getObjectsForType(objectType: string): Promise { + const s = this.settings(); + if (!s) return []; + const { owner, repo } = this.parseRepoUrl(s.repoUrl); + if (!owner || !repo) return []; + try { + const basePath = s.backupsPath.replace(/^\/|\/$/g, ''); + const dirPath = `${basePath}/${objectType}`; + const url = `https://api.github.com/repos/${owner}/${repo}/contents/${dirPath}?ref=${encodeURIComponent(s.defaultBranch)}`; + const res = await fetch(url, { headers: this.githubHeaders(s) }); + if (!res.ok) return []; + const items = await res.json() as any[]; + return items + .filter((i: any) => i.type === 'file' && (i.name as string).endsWith('.json')) + .map((i: any) => ({ + objectType, + objectId: (i.name as string).replace('.json', ''), + name: (i.name as string).replace('.json', ''), + })); + } catch { + return []; + } + } + + async getCommitHistory(objectType: string, objectId: string, branch?: string, limit = 30): Promise { + const s = this.settings(); + if (!s) return []; + const { owner, repo } = this.parseRepoUrl(s.repoUrl); + if (!owner || !repo) return []; + try { + const basePath = s.backupsPath.replace(/^\/|\/$/g, ''); + const filePath = `${basePath}/${objectType}/${objectId}.json`; + const params = new URLSearchParams({ path: filePath, per_page: String(limit) }); + params.set('sha', branch ?? s.defaultBranch); + const url = `https://api.github.com/repos/${owner}/${repo}/commits?${params}`; + const res = await fetch(url, { headers: this.githubHeaders(s) }); + if (!res.ok) return []; + const data = await res.json() as any[]; + return (data || []).map((c: any) => ({ + sha: c.sha as string, + message: ((c.commit?.message as string) || '').split('\n')[0], + author: (c.commit?.author?.name || c.author?.login || 'Unknown') as string, + timestamp: (c.commit?.author?.date || '') as string, + })); + } catch { + return []; + } + } + + async getFileAtCommit(objectType: string, objectId: string, ref: string): Promise { + const s = this.settings(); + if (!s) return ''; + const { owner, repo } = this.parseRepoUrl(s.repoUrl); + if (!owner || !repo) return ''; + try { + const basePath = s.backupsPath.replace(/^\/|\/$/g, ''); + const filePath = `${basePath}/${objectType}/${objectId}.json`; + const url = `https://api.github.com/repos/${owner}/${repo}/contents/${filePath}?ref=${encodeURIComponent(ref)}`; + const res = await fetch(url, { headers: this.githubHeaders(s) }); + if (!res.ok) return ''; + const data = await res.json() as any; + return atob((data.content as string).replace(/\n/g, '')); + } catch { + return ''; + } + } + + parseRepoUrl(repoUrl: string): { owner: string; repo: string } { + const clean = repoUrl.trim().replace(/\.git$/, '').replace(/\/$/, ''); + const httpsMatch = clean.match(/github\.com\/([^/]+)\/([^/]+)/i); + if (httpsMatch) return { owner: httpsMatch[1], repo: httpsMatch[2] }; + const sshMatch = clean.match(/git@github\.com:([^/]+)\/([^/]+)/i); + if (sshMatch) return { owner: sshMatch[1], repo: sshMatch[2] }; + return { owner: '', repo: '' }; + } + + private githubHeaders(s: GitRepoSettings): Record { + const headers: Record = { + Accept: 'application/vnd.github.v3+json', + 'User-Agent': 'SailPoint-UI-Development-Kit', + }; + // Always use the PAT for REST API calls when one is available. + // SSH keys are for local git operations only and cannot authenticate + // the GitHub REST API — a PAT is required for private repo access. + if (s.pat) { + headers['Authorization'] = `Bearer ${s.pat}`; + } + return headers; + } +} From fc886893e19d60c4799166f7125b066f3f0fe5aa Mon Sep 17 00:00:00 2001 From: Philip Ellis Date: Mon, 16 Mar 2026 11:09:51 -0400 Subject: [PATCH 03/20] updated for better dark/light mode support --- .../diff-viewer/diff-viewer.component.html | 105 +++---- .../diff-viewer/diff-viewer.component.scss | 258 ++++++++---------- .../diff-viewer/diff-viewer.component.ts | 17 ++ .../object-browser.component.html | 28 +- .../object-browser.component.scss | 54 ++-- .../repo-settings.component.scss | 9 +- .../restore-dialog.component.scss | 28 +- .../lib/config-hub/config-hub.component.scss | 8 +- 8 files changed, 247 insertions(+), 260 deletions(-) diff --git a/projects/sailpoint-components/src/lib/config-hub/components/diff-viewer/diff-viewer.component.html b/projects/sailpoint-components/src/lib/config-hub/components/diff-viewer/diff-viewer.component.html index 13aa6502..3ae91128 100644 --- a/projects/sailpoint-components/src/lib/config-hub/components/diff-viewer/diff-viewer.component.html +++ b/projects/sailpoint-components/src/lib/config-hub/components/diff-viewer/diff-viewer.component.html @@ -6,23 +6,31 @@ } @else {
- +
- {{ backupObject()!.objectType }} + + + {{ typeIcon(backupObject()!.objectType) }} + {{ backupObject()!.objectType }} + + {{ backupObject()!.name }}
@if (hasChanges) { - - +{{ addedCount }} - -{{ removedCount }} - + + + +{{ addedCount }} + + + -{{ removedCount }} + + } - + view_stream @@ -31,7 +39,7 @@ - } -
+ } -
+ @if (loadingDiff()) { -
+
- Computing diff… + Computing diff…
- } @else if (diffLines().length === 0) { + } @else if (!hasChanges && diffLines().length > 0) {
check_circle -

No changes between the selected commits

+

Files are identical

- } @else if (!hasChanges) { + } @else if (diffLines().length === 0) {
- check_circle -

Files are identical

+ hourglass_empty +

Select two commits above to compare

} @else { @@ -140,8 +143,8 @@ @for (line of diffLines(); track $index) { - {{ line.lineNumberLeft ?? '' }} - {{ line.lineNumberRight ?? '' }} + {{ line.lineNumberLeft ?? '' }} + {{ line.lineNumberRight ?? '' }} {{ line.type === 'added' ? '+' : line.type === 'removed' ? '-' : ' ' }} @@ -157,7 +160,8 @@
- Base — {{ compareCommitSha()?.slice(0, 7) }} + arrow_back + Base — {{ compareCommitSha()?.slice(0, 7) }}
@@ -173,7 +177,8 @@
- Target — {{ selectedCommitSha()?.slice(0, 7) }} + arrow_forward + Target — {{ selectedCommitSha()?.slice(0, 7) }}
diff --git a/projects/sailpoint-components/src/lib/config-hub/components/diff-viewer/diff-viewer.component.scss b/projects/sailpoint-components/src/lib/config-hub/components/diff-viewer/diff-viewer.component.scss index bb6b2af9..3e4c1a24 100644 --- a/projects/sailpoint-components/src/lib/config-hub/components/diff-viewer/diff-viewer.component.scss +++ b/projects/sailpoint-components/src/lib/config-hub/components/diff-viewer/diff-viewer.component.scss @@ -5,19 +5,20 @@ justify-content: center; height: 100%; gap: 12px; - color: var(--mat-sys-on-surface-variant); + color: var(--theme-primary-text); + opacity: 0.55; mat-icon { font-size: 48px; width: 48px; height: 48px; - opacity: 0.35; } p { margin: 0; font-size: 14px; text-align: center; + opacity: 1; // reset parent opacity for text } &.small { @@ -33,83 +34,98 @@ overflow: hidden; } +// ── Header ─────────────────────────────────────────────────────────────────── + .diff-header { display: flex; align-items: center; justify-content: space-between; - padding: 12px 16px; - gap: 16px; + padding: 10px 16px; + gap: 12px; flex-wrap: wrap; + // Use a subtle header tint rather than a full surface colour so it stays + // visible in both light and dark without depending on --mat-sys-* tokens + background: color-mix(in srgb, var(--theme-primary-text) 6%, var(--theme-background)); + border-bottom: 1px solid color-mix(in srgb, var(--theme-primary-text) 15%, transparent); + flex-shrink: 0; } .object-title { display: flex; align-items: center; gap: 10px; -} - -.type-chip { - background: var(--mat-sys-secondary-container); - color: var(--mat-sys-on-secondary-container); - padding: 2px 10px; - border-radius: 12px; - font-size: 12px; - font-weight: 600; - letter-spacing: 0.05em; + min-width: 0; } .object-name { - font-size: 16px; + font-size: 15px; font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: var(--theme-primary-text); +} + +// Type chip — tinted with the primary brand colour +.type-chip { + --mdc-chip-elevated-container-color: color-mix(in srgb, var(--theme-primary) 15%, var(--theme-background)); + --mdc-chip-label-text-color: var(--theme-primary); + font-weight: 600; + font-size: 11px; + letter-spacing: 0.06em; + text-transform: uppercase; } .header-actions { display: flex; align-items: center; - gap: 12px; + gap: 10px; + flex-shrink: 0; } -.diff-stats { - display: flex; - gap: 6px; - font-size: 13px; +// Diff-stat chips — green / red tinted against the current background so they +// automatically invert in dark mode without needing a media query +.stat-chip { font-family: monospace; - font-weight: 600; + font-weight: 700; + font-size: 13px; } -.added-count { - color: #2e7d32; +.added-chip { + --mdc-chip-elevated-container-color: color-mix(in srgb, #4caf50 18%, var(--theme-background)); + --mdc-chip-label-text-color: color-mix(in srgb, #4caf50 70%, var(--theme-primary-text)); } -.removed-count { - color: #c62828; +.removed-chip { + --mdc-chip-elevated-container-color: color-mix(in srgb, #f44336 18%, var(--theme-background)); + --mdc-chip-label-text-color: color-mix(in srgb, #f44336 70%, var(--theme-primary-text)); } -.view-toggle { - height: 36px; -} +// ── Status rows ─────────────────────────────────────────────────────────────── -.loading-row { +.status-row { display: flex; align-items: center; gap: 12px; - padding: 24px; - color: var(--mat-sys-on-surface-variant); + padding: 20px 16px; + color: var(--theme-primary-text); + font-size: 14px; + flex-shrink: 0; } -.error-message { - display: flex; - align-items: center; - gap: 8px; - padding: 16px; - color: var(--mat-sys-error); +.error-row { + // Warn colour handled by mat-icon color="warn"; keep text readable + color: var(--theme-primary-text); } +// ── Commit selectors ────────────────────────────────────────────────────────── + .commit-selectors { display: flex; align-items: center; - gap: 12px; - padding: 12px 16px; + gap: 10px; + padding: 10px 16px 4px; + flex-shrink: 0; } .commit-select { @@ -117,96 +133,49 @@ } .arrow-icon { - color: var(--mat-sys-on-surface-variant); + color: var(--theme-primary-text); + opacity: 0.5; flex-shrink: 0; } -.sha-label { +.opt-sha { font-family: monospace; - font-size: 12px; + font-size: 11px; margin-right: 8px; - color: var(--mat-sys-on-surface-variant); + opacity: 0.65; } -.msg-label { +.opt-msg { font-size: 13px; } -.time-label { - font-size: 11px; - color: var(--mat-sys-on-surface-variant); - margin-left: 8px; -} +// ── Commit timeline ─────────────────────────────────────────────────────────── -// Commit timeline .commit-timeline { - max-height: 220px; + max-height: 200px; overflow-y: auto; - padding: 8px 16px; -} - -.timeline-entry { - display: flex; - align-items: flex-start; - gap: 12px; - padding: 8px 6px; - border-radius: 8px; - cursor: pointer; - position: relative; - - &:hover { - background: var(--mat-sys-surface-variant); - } - - &.selected { - background: var(--mat-sys-secondary-container); - - .timeline-dot { - background: var(--mat-sys-secondary); - width: 12px; - height: 12px; - margin-top: 5px; - } - } -} - -.timeline-dot { - width: 8px; - height: 8px; - border-radius: 50%; - background: var(--mat-sys-outline); flex-shrink: 0; - margin-top: 7px; -} - -.timeline-content { - flex: 1; - min-width: 0; -} - -.commit-message { - font-size: 13px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; } -.commit-meta { - display: flex; - gap: 8px; +.commit-meta-line { font-size: 11px; - color: var(--mat-sys-on-surface-variant); - margin-top: 2px; + color: var(--theme-primary-text); + opacity: 0.7; } -.sha { +.sha-badge { + display: inline-block; font-family: monospace; - background: var(--mat-sys-surface-variant); - padding: 0 4px; - border-radius: 3px; + font-size: 11px; + background: color-mix(in srgb, var(--theme-primary-text) 12%, var(--theme-background)); + color: var(--theme-primary-text); + padding: 0 5px; + border-radius: 4px; + margin-right: 4px; } -// Diff table +// ── Diff table ──────────────────────────────────────────────────────────────── + .diff-table-wrapper { flex: 1; overflow: auto; @@ -217,43 +186,44 @@ border-collapse: collapse; font-family: 'Consolas', 'Monaco', 'Courier New', monospace; font-size: 12px; - line-height: 1.5; + line-height: 1.6; + background: var(--theme-background); } +// Diff row colours use color-mix against --theme-background so they +// automatically invert in dark mode without a media query. .diff-line { &.diff-added { - background: #e8f5e9; + background-color: color-mix(in srgb, #4caf50 13%, var(--theme-background)); .diff-gutter { - color: #2e7d32; - background: #c8e6c9; + background-color: color-mix(in srgb, #4caf50 28%, var(--theme-background)); + color: color-mix(in srgb, #4caf50 70%, var(--theme-primary-text)); } } &.diff-removed { - background: #ffebee; + background-color: color-mix(in srgb, #f44336 13%, var(--theme-background)); .diff-gutter { - color: #c62828; - background: #ffcdd2; + background-color: color-mix(in srgb, #f44336 28%, var(--theme-background)); + color: color-mix(in srgb, #f44336 70%, var(--theme-primary-text)); } } - &.diff-unchanged { - .diff-gutter { - color: var(--mat-sys-on-surface-variant); - } + &.diff-unchanged .diff-gutter { + color: color-mix(in srgb, var(--theme-primary-text) 45%, transparent); } } .line-num { - width: 48px; - min-width: 48px; + width: 44px; + min-width: 44px; text-align: right; - padding: 0 8px; - color: var(--mat-sys-on-surface-variant); + padding: 0 6px; + color: color-mix(in srgb, var(--theme-primary-text) 45%, transparent); user-select: none; - border-right: 1px solid var(--mat-sys-outline-variant); + border-right: 1px solid color-mix(in srgb, var(--theme-primary-text) 15%, transparent); } .diff-gutter { @@ -262,12 +232,13 @@ text-align: center; padding: 0 2px; user-select: none; - border-right: 1px solid var(--mat-sys-outline-variant); + border-right: 1px solid color-mix(in srgb, var(--theme-primary-text) 15%, transparent); } .diff-content { padding: 0 8px; width: 100%; + color: var(--theme-primary-text); pre { margin: 0; @@ -278,7 +249,8 @@ } } -// Split diff +// ── Split diff ──────────────────────────────────────────────────────────────── + .split-diff { flex: 1; display: flex; @@ -288,7 +260,7 @@ .split-pane { flex: 1; overflow: auto; - border-right: 1px solid var(--mat-sys-outline-variant); + border-right: 1px solid color-mix(in srgb, var(--theme-primary-text) 15%, transparent); &:last-child { border-right: none; @@ -299,30 +271,22 @@ position: sticky; top: 0; z-index: 1; - background: var(--mat-sys-surface-variant); + display: flex; + align-items: center; + gap: 6px; + background: color-mix(in srgb, var(--theme-primary-text) 8%, var(--theme-background)); + color: var(--theme-primary-text); font-size: 12px; - font-family: monospace; - padding: 4px 8px; - border-bottom: 1px solid var(--mat-sys-outline-variant); -} - -// Dark mode overrides -@media (prefers-color-scheme: dark) { - .diff-line.diff-added { - background: #1b5e20; + padding: 6px 10px; + border-bottom: 1px solid color-mix(in srgb, var(--theme-primary-text) 15%, transparent); - .diff-gutter { - background: #2e7d32; - color: #c8e6c9; - } + mat-icon { + font-size: 16px; + width: 16px; + height: 16px; } - .diff-line.diff-removed { - background: #7f0000; - - .diff-gutter { - background: #c62828; - color: #ffcdd2; - } + code { + font-family: monospace; } } diff --git a/projects/sailpoint-components/src/lib/config-hub/components/diff-viewer/diff-viewer.component.ts b/projects/sailpoint-components/src/lib/config-hub/components/diff-viewer/diff-viewer.component.ts index 0be8262e..dbaee760 100644 --- a/projects/sailpoint-components/src/lib/config-hub/components/diff-viewer/diff-viewer.component.ts +++ b/projects/sailpoint-components/src/lib/config-hub/components/diff-viewer/diff-viewer.component.ts @@ -8,6 +8,7 @@ import { MatSelectModule } from '@angular/material/select'; import { MatTooltipModule } from '@angular/material/tooltip'; import { MatDividerModule } from '@angular/material/divider'; import { MatChipsModule } from '@angular/material/chips'; +import { MatListModule } from '@angular/material/list'; import { FormsModule } from '@angular/forms'; import * as Diff from 'diff'; import { ConfigHubGitService } from '../../services/config-hub-git.service'; @@ -29,6 +30,7 @@ interface CommitEntry { MatButtonModule, MatButtonToggleModule, MatIconModule, + MatListModule, MatProgressSpinnerModule, MatSelectModule, MatTooltipModule, @@ -207,4 +209,19 @@ export class DiffViewerComponent implements OnChanges { splitRightLines(): DiffLine[] { return this.diffLines().filter(l => l.type !== 'removed'); } + + typeIcon(typeName: string): string { + const icons: Record = { + ROLE: 'manage_accounts', + RULE: 'code', + SOURCE: 'device_hub', + WORKFLOW: 'account_tree', + TRIGGER_SUBSCRIPTION: 'notifications', + IDENTITY_PROFILE: 'person', + ACCESS_PROFILE: 'badge', + TRANSFORM: 'transform', + CONNECTOR_RULE: 'build', + }; + return icons[typeName] ?? 'description'; + } } diff --git a/projects/sailpoint-components/src/lib/config-hub/components/object-browser/object-browser.component.html b/projects/sailpoint-components/src/lib/config-hub/components/object-browser/object-browser.component.html index 4e2187b9..9d6be94f 100644 --- a/projects/sailpoint-components/src/lib/config-hub/components/object-browser/object-browser.component.html +++ b/projects/sailpoint-components/src/lib/config-hub/components/object-browser/object-browser.component.html @@ -3,7 +3,7 @@
- Object Types + Object Types
} @else { - + @for (type of objectTypes(); track type.name) { {{ typeIcon(type.name) }} {{ type.name }} @@ -35,7 +35,7 @@
@if (selectedType()) {
- {{ selectedType() }} + {{ selectedType() }} @if (loadingObjects()) { } @@ -47,29 +47,27 @@ No objects found
} @else { - + @for (obj of objects(); track obj.objectId) { {{ typeIcon(obj.objectType) }} - - {{ obj.name }} - + {{ obj.name }} @if (obj.lastCommit) { -
+ history {{ formatRelativeTime(obj.lastCommit.timestamp) }} - · {{ obj.lastCommit.author }} -
+ · {{ obj.lastCommit.author }} + } @else { -
+ schedule - Loading history… -
+ Loading… + }
} diff --git a/projects/sailpoint-components/src/lib/config-hub/components/object-browser/object-browser.component.scss b/projects/sailpoint-components/src/lib/config-hub/components/object-browser/object-browser.component.scss index 3509a5d8..05c8b2f4 100644 --- a/projects/sailpoint-components/src/lib/config-hub/components/object-browser/object-browser.component.scss +++ b/projects/sailpoint-components/src/lib/config-hub/components/object-browser/object-browser.component.scss @@ -7,7 +7,9 @@ .type-column { width: 220px; min-width: 180px; - border-right: 1px solid var(--mat-sys-outline-variant); + // Subtle tint so the column reads as a separate panel in both light and dark + background: color-mix(in srgb, var(--theme-primary-text) 5%, var(--theme-background)); + border-right: 1px solid color-mix(in srgb, var(--theme-primary-text) 15%, transparent); display: flex; flex-direction: column; overflow: hidden; @@ -24,50 +26,39 @@ display: flex; align-items: center; justify-content: space-between; - padding: 8px 16px; - font-weight: 500; - font-size: 13px; + padding: 0 8px 0 16px; + min-height: 48px; + background: color-mix(in srgb, var(--theme-primary-text) 8%, var(--theme-background)); + border-bottom: 1px solid color-mix(in srgb, var(--theme-primary-text) 15%, transparent); + flex-shrink: 0; +} + +.column-label { + font-size: 12px; + font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; - color: var(--mat-sys-on-surface-variant); - border-bottom: 1px solid var(--mat-sys-outline-variant); - min-height: 44px; + color: var(--theme-primary-text); } mat-nav-list { flex: 1; overflow-y: auto; - padding: 4px 0; } -mat-list-item { - border-radius: 8px; - margin: 2px 6px; - cursor: pointer; - - &.selected { - background: var(--mat-sys-secondary-container); - color: var(--mat-sys-on-secondary-container); - } -} - -.object-name { - font-size: 14px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} +// Angular Material's [activated] binding handles selection highlight. +// No manual .selected override needed. .commit-meta { display: flex; align-items: center; gap: 4px; - font-size: 11px; - color: var(--mat-sys-on-surface-variant); overflow: hidden; + color: var(--theme-primary-text); + opacity: 0.65; &.loading { - opacity: 0.5; + opacity: 0.4; } } @@ -75,6 +66,7 @@ mat-list-item { font-size: 13px; width: 13px; height: 13px; + flex-shrink: 0; } .relative-time { @@ -82,7 +74,7 @@ mat-list-item { flex-shrink: 0; } -.author { +.commit-author { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -101,13 +93,13 @@ mat-list-item { gap: 8px; padding: 32px 16px; text-align: center; - color: var(--mat-sys-on-surface-variant); + color: var(--theme-primary-text); + opacity: 0.55; font-size: 13px; mat-icon { font-size: 36px; width: 36px; height: 36px; - opacity: 0.4; } } diff --git a/projects/sailpoint-components/src/lib/config-hub/components/repo-settings/repo-settings.component.scss b/projects/sailpoint-components/src/lib/config-hub/components/repo-settings/repo-settings.component.scss index 2ba3e50a..534dc392 100644 --- a/projects/sailpoint-components/src/lib/config-hub/components/repo-settings/repo-settings.component.scss +++ b/projects/sailpoint-components/src/lib/config-hub/components/repo-settings/repo-settings.component.scss @@ -21,8 +21,9 @@ display: flex; align-items: flex-start; gap: 10px; - background: var(--mat-sys-secondary-container); - color: var(--mat-sys-on-secondary-container); + background: color-mix(in srgb, var(--theme-primary) 10%, var(--theme-background)); + color: var(--theme-primary-text); + border: 1px solid color-mix(in srgb, var(--theme-primary) 30%, transparent); padding: 10px 14px; border-radius: 8px; font-size: 13px; @@ -34,6 +35,7 @@ width: 18px; height: 18px; margin-top: 2px; + color: var(--theme-primary); } } @@ -50,7 +52,8 @@ code { font-family: monospace; - background: var(--mat-sys-surface-variant); + background: color-mix(in srgb, var(--theme-primary-text) 10%, var(--theme-background)); + color: var(--theme-primary-text); padding: 1px 4px; border-radius: 3px; } diff --git a/projects/sailpoint-components/src/lib/config-hub/components/restore-dialog/restore-dialog.component.scss b/projects/sailpoint-components/src/lib/config-hub/components/restore-dialog/restore-dialog.component.scss index 2200ada3..0368d862 100644 --- a/projects/sailpoint-components/src/lib/config-hub/components/restore-dialog/restore-dialog.component.scss +++ b/projects/sailpoint-components/src/lib/config-hub/components/restore-dialog/restore-dialog.component.scss @@ -5,7 +5,7 @@ h2[mat-dialog-title] { } .title-icon { - color: var(--mat-sys-primary); + color: var(--theme-primary); } mat-dialog-content { @@ -15,7 +15,8 @@ mat-dialog-content { .confirm-intro { margin: 0 0 20px 0; - color: var(--mat-sys-on-surface-variant); + color: var(--theme-primary-text); + opacity: 0.75; font-size: 14px; } @@ -39,12 +40,14 @@ mat-dialog-content { font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; - color: var(--mat-sys-on-surface-variant); + color: var(--theme-primary-text); + opacity: 0.6; } .detail-value { font-size: 14px; word-break: break-all; + color: var(--theme-primary-text); } .mono { @@ -53,8 +56,8 @@ mat-dialog-content { } .type-chip { - background: var(--mat-sys-secondary-container); - color: var(--mat-sys-on-secondary-container); + background: color-mix(in srgb, var(--theme-primary) 15%, var(--theme-background)); + color: var(--theme-primary); padding: 2px 10px; border-radius: 12px; font-size: 12px; @@ -65,15 +68,16 @@ mat-dialog-content { display: flex; align-items: flex-start; gap: 10px; - background: var(--mat-sys-error-container); - color: var(--mat-sys-on-error-container); + background: color-mix(in srgb, #f44336 12%, var(--theme-background)); + color: var(--theme-primary-text); + border: 1px solid color-mix(in srgb, #f44336 35%, transparent); padding: 12px 14px; border-radius: 8px; font-size: 13px; mat-icon { flex-shrink: 0; - color: var(--mat-sys-error); + color: #f44336; } } @@ -84,6 +88,7 @@ mat-dialog-content { gap: 12px; padding: 24px; text-align: center; + color: var(--theme-primary-text); mat-icon { font-size: 48px; @@ -97,11 +102,11 @@ mat-dialog-content { } &.success mat-icon { - color: #2e7d32; + color: #4caf50; } &.failure mat-icon { - color: var(--mat-sys-error); + color: #f44336; } } @@ -110,7 +115,8 @@ mat-dialog-content { align-items: center; gap: 16px; padding: 16px 0; - color: var(--mat-sys-on-surface-variant); + color: var(--theme-primary-text); + opacity: 0.75; } mat-dialog-actions { diff --git a/projects/sailpoint-components/src/lib/config-hub/config-hub.component.scss b/projects/sailpoint-components/src/lib/config-hub/config-hub.component.scss index 19b24209..cd8ddfbc 100644 --- a/projects/sailpoint-components/src/lib/config-hub/config-hub.component.scss +++ b/projects/sailpoint-components/src/lib/config-hub/config-hub.component.scss @@ -23,14 +23,16 @@ display: flex; align-items: center; gap: 12px; - background: var(--mat-sys-tertiary-container); - color: var(--mat-sys-on-tertiary-container); + background: color-mix(in srgb, var(--theme-primary) 12%, var(--theme-background)); + color: var(--theme-primary-text); + border-bottom: 1px solid color-mix(in srgb, var(--theme-primary) 30%, transparent); padding: 10px 20px; font-size: 14px; flex-shrink: 0; mat-icon { flex-shrink: 0; + color: var(--theme-primary); } span { @@ -52,7 +54,7 @@ .browser-pane { width: 480px; min-width: 360px; - border-right: 1px solid var(--mat-sys-outline-variant); + border-right: 1px solid color-mix(in srgb, var(--theme-primary-text) 15%, transparent); overflow: hidden; display: flex; flex-direction: column; From 853794624fcaeaabbc1983826a42242c2378b667 Mon Sep 17 00:00:00 2001 From: Philip Ellis Date: Mon, 16 Mar 2026 11:52:31 -0400 Subject: [PATCH 04/20] fixed css for button --- .../diff-viewer/diff-viewer.component.html | 3 +-- .../repo-settings/repo-settings.component.html | 2 +- src/styles.scss | 17 +++++++++++++++++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/projects/sailpoint-components/src/lib/config-hub/components/diff-viewer/diff-viewer.component.html b/projects/sailpoint-components/src/lib/config-hub/components/diff-viewer/diff-viewer.component.html index 3ae91128..c32a9b6e 100644 --- a/projects/sailpoint-components/src/lib/config-hub/components/diff-viewer/diff-viewer.component.html +++ b/projects/sailpoint-components/src/lib/config-hub/components/diff-viewer/diff-viewer.component.html @@ -43,8 +43,7 @@ [disabled]="!selectedCommitSha() || loadingDiff()" (click)="onRestoreClick()" matTooltip="Restore this version to the active SailPoint tenant"> - restore - Restore This Version + restore Restore This Version
diff --git a/projects/sailpoint-components/src/lib/config-hub/components/repo-settings/repo-settings.component.html b/projects/sailpoint-components/src/lib/config-hub/components/repo-settings/repo-settings.component.html index 39d6c1f4..b15a0520 100644 --- a/projects/sailpoint-components/src/lib/config-hub/components/repo-settings/repo-settings.component.html +++ b/projects/sailpoint-components/src/lib/config-hub/components/repo-settings/repo-settings.component.html @@ -67,7 +67,7 @@ }
@@ -176,8 +150,8 @@
- arrow_forward - Target — {{ selectedCommitSha()?.slice(0, 7) }} + commit + This version — {{ selectedCommitSha()?.slice(0, 7) }}
diff --git a/projects/sailpoint-components/src/lib/config-hub/components/diff-viewer/diff-viewer.component.scss b/projects/sailpoint-components/src/lib/config-hub/components/diff-viewer/diff-viewer.component.scss index 389b8528..edebee3f 100644 --- a/projects/sailpoint-components/src/lib/config-hub/components/diff-viewer/diff-viewer.component.scss +++ b/projects/sailpoint-components/src/lib/config-hub/components/diff-viewer/diff-viewer.component.scss @@ -118,37 +118,6 @@ color: var(--theme-primary-text); } -// ── Commit selectors ────────────────────────────────────────────────────────── - -.commit-selectors { - display: flex; - align-items: center; - gap: 10px; - padding: 10px 16px 4px; - flex-shrink: 0; -} - -.commit-select { - flex: 1; -} - -.arrow-icon { - color: var(--theme-primary-text); - opacity: 0.5; - flex-shrink: 0; -} - -.opt-sha { - font-family: monospace; - font-size: 11px; - margin-right: 8px; - opacity: 0.65; -} - -.opt-msg { - font-size: 13px; -} - // ── Commit timeline ─────────────────────────────────────────────────────────── .commit-timeline { @@ -169,6 +138,20 @@ opacity: 0.7; } +.head-badge { + display: inline-block; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.05em; + text-transform: uppercase; + background: color-mix(in srgb, var(--theme-primary) 20%, var(--theme-background)); + color: var(--theme-primary); + padding: 1px 6px; + border-radius: 4px; + margin-left: 8px; + vertical-align: middle; +} + .sha-badge { display: inline-block; font-family: monospace; diff --git a/projects/sailpoint-components/src/lib/config-hub/components/diff-viewer/diff-viewer.component.ts b/projects/sailpoint-components/src/lib/config-hub/components/diff-viewer/diff-viewer.component.ts index dbaee760..ce134cfb 100644 --- a/projects/sailpoint-components/src/lib/config-hub/components/diff-viewer/diff-viewer.component.ts +++ b/projects/sailpoint-components/src/lib/config-hub/components/diff-viewer/diff-viewer.component.ts @@ -4,12 +4,10 @@ import { MatButtonModule } from '@angular/material/button'; import { MatButtonToggleModule } from '@angular/material/button-toggle'; import { MatIconModule } from '@angular/material/icon'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; -import { MatSelectModule } from '@angular/material/select'; import { MatTooltipModule } from '@angular/material/tooltip'; import { MatDividerModule } from '@angular/material/divider'; import { MatChipsModule } from '@angular/material/chips'; import { MatListModule } from '@angular/material/list'; -import { FormsModule } from '@angular/forms'; import * as Diff from 'diff'; import { ConfigHubGitService } from '../../services/config-hub-git.service'; import { BackupObject, DiffLine, GitCommit } from '../../models/config-hub.models'; @@ -18,7 +16,6 @@ export type DiffViewMode = 'unified' | 'split'; interface CommitEntry { commit: GitCommit; - content?: string; } @Component({ @@ -26,13 +23,11 @@ interface CommitEntry { standalone: true, imports: [ CommonModule, - FormsModule, MatButtonModule, MatButtonToggleModule, MatIconModule, MatListModule, MatProgressSpinnerModule, - MatSelectModule, MatTooltipModule, MatDividerModule, MatChipsModule, @@ -46,19 +41,18 @@ export class DiffViewerComponent implements OnChanges { commits = signal([]); selectedCommitSha = signal(null); - compareCommitSha = signal(null); viewMode = signal('unified'); diffLines = signal([]); + /** Left pane = HEAD (current deployed state). */ leftContent = signal(''); + /** Right pane = selected historical commit. */ rightContent = signal(''); loading = signal(false); loadingDiff = signal(false); errorMessage = signal(null); - private branches: string[] = []; - constructor(private gitService: ConfigHubGitService) {} async ngOnChanges(changes: SimpleChanges): Promise { @@ -75,7 +69,6 @@ export class DiffViewerComponent implements OnChanges { private reset(): void { this.commits.set([]); this.selectedCommitSha.set(null); - this.compareCommitSha.set(null); this.diffLines.set([]); this.leftContent.set(''); this.rightContent.set(''); @@ -97,14 +90,10 @@ export class DiffViewerComponent implements OnChanges { this.commits.set(history.map(c => ({ commit: c }))); this.loading.set(false); - // Default: show most recent commit diffed against the previous one - this.selectedCommitSha.set(history[0].sha); - if (history.length > 1) { - this.compareCommitSha.set(history[1].sha); - } else { - this.compareCommitSha.set(history[0].sha); - } - + // Default: select the most recent (non-HEAD) commit so the diff is visible immediately. + // If there is only one commit the diff will show "identical" which is correct. + const defaultSha = history.length > 1 ? history[1].sha : history[0].sha; + this.selectedCommitSha.set(defaultSha); await this.computeDiff(); } @@ -113,20 +102,18 @@ export class DiffViewerComponent implements OnChanges { await this.computeDiff(); } - async onCompareSelect(sha: string): Promise { - this.compareCommitSha.set(sha); - await this.computeDiff(); - } - + /** Always diffs HEAD (left) against the selected commit (right). */ private async computeDiff(): Promise { const obj = this.backupObject(); - const leftSha = this.compareCommitSha(); + const entries = this.commits(); const rightSha = this.selectedCommitSha(); - if (!obj || !leftSha || !rightSha) return; + if (!obj || entries.length === 0 || !rightSha) return; + + const headSha = entries[0].commit.sha; this.loadingDiff.set(true); const [leftRaw, rightRaw] = await Promise.all([ - this.gitService.getFileAtCommit(obj.objectType, obj.objectId, leftSha), + this.gitService.getFileAtCommit(obj.objectType, obj.objectId, headSha), this.gitService.getFileAtCommit(obj.objectType, obj.objectId, rightSha), ]); @@ -172,10 +159,8 @@ export class DiffViewerComponent implements OnChanges { return this.diffLines().filter(l => l.type === 'removed').length; } - getCommitLabel(sha: string): string { - const entry = this.commits().find(c => c.commit.sha === sha); - if (!entry) return sha.slice(0, 7); - return `${sha.slice(0, 7)} – ${entry.commit.message.slice(0, 50)}`; + get headSha(): string | null { + return this.commits()[0]?.commit.sha ?? null; } formatTimestamp(iso: string): string { From 2300c1955e00115959b9ae2b3a5d6ce599b91d5b Mon Sep 17 00:00:00 2001 From: Philip Ellis Date: Wed, 18 Mar 2026 11:44:20 -0400 Subject: [PATCH 12/20] built view for navigating by commit instead of by object --- .../restore-dialog.component.html | 51 ++++++++---- .../restore-dialog.component.scss | 24 ++++++ .../restore-dialog.component.ts | 33 ++++++-- .../lib/config-hub/config-hub.component.html | 53 +++++++++--- .../lib/config-hub/config-hub.component.scss | 20 ++++- .../lib/config-hub/config-hub.component.ts | 28 +++++-- .../config-hub/models/config-hub.models.ts | 8 ++ .../services/config-hub-api.service.ts | 83 ++++++++----------- .../services/config-hub-git.service.ts | 64 +++++++++++++- 9 files changed, 273 insertions(+), 91 deletions(-) diff --git a/projects/sailpoint-components/src/lib/config-hub/components/restore-dialog/restore-dialog.component.html b/projects/sailpoint-components/src/lib/config-hub/components/restore-dialog/restore-dialog.component.html index b19ea9e3..2af762c9 100644 --- a/projects/sailpoint-components/src/lib/config-hub/components/restore-dialog/restore-dialog.component.html +++ b/projects/sailpoint-components/src/lib/config-hub/components/restore-dialog/restore-dialog.component.html @@ -1,28 +1,50 @@

restore - Restore Object + Restore {{ isBundle ? 'Objects' : 'Object' }}

@if (!result()) {

- This will upload the selected version to Config Hub and wait for processing to complete. + @if (isBundle) { + This will upload {{ data.bundle!.length }} object(s) to Config Hub and wait for processing to complete. + } @else { + This will upload the selected version to Config Hub and wait for processing to complete. + }

-
- Type - {{ data.object.objectType }} -
-
- Name - {{ data.object.name }} -
-
- Object ID - {{ data.object.objectId }} -
+ + @if (!isBundle) { + +
+ Type + {{ data.object!.objectType }} +
+
+ Name + {{ data.object!.name }} +
+
+ Object ID + {{ data.object!.objectId }} +
+ } @else { + +
+ Objects +
+ @for (o of data.affectedObjects; track $index) { +
+ {{ o.objectType }} + {{ o.name }} +
+ } +
+
+ } +
Commit {{ data.commitSha.slice(0, 7) }} @@ -46,7 +68,6 @@

}
- } @if (restoring()) { diff --git a/projects/sailpoint-components/src/lib/config-hub/components/restore-dialog/restore-dialog.component.scss b/projects/sailpoint-components/src/lib/config-hub/components/restore-dialog/restore-dialog.component.scss index 0368d862..a69819e4 100644 --- a/projects/sailpoint-components/src/lib/config-hub/components/restore-dialog/restore-dialog.component.scss +++ b/projects/sailpoint-components/src/lib/config-hub/components/restore-dialog/restore-dialog.component.scss @@ -64,6 +64,30 @@ mat-dialog-content { font-weight: 600; } +.affected-list { + display: flex; + flex-direction: column; + gap: 6px; + max-height: 200px; + overflow-y: auto; +} + +.affected-row { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + color: var(--theme-primary-text); +} + +.type-chip-sm { + --mdc-chip-elevated-container-color: color-mix(in srgb, var(--theme-primary) 15%, var(--theme-background)); + --mdc-chip-label-text-color: var(--theme-primary); + font-size: 10px; + font-weight: 600; + height: 20px; +} + .warning-banner { display: flex; align-items: flex-start; diff --git a/projects/sailpoint-components/src/lib/config-hub/components/restore-dialog/restore-dialog.component.ts b/projects/sailpoint-components/src/lib/config-hub/components/restore-dialog/restore-dialog.component.ts index 895b3f21..5b1f1974 100644 --- a/projects/sailpoint-components/src/lib/config-hub/components/restore-dialog/restore-dialog.component.ts +++ b/projects/sailpoint-components/src/lib/config-hub/components/restore-dialog/restore-dialog.component.ts @@ -5,12 +5,24 @@ import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatDividerModule } from '@angular/material/divider'; +import { MatChipsModule } from '@angular/material/chips'; import { ConfigHubApiService } from '../../services/config-hub-api.service'; import { BackupObject, RestoreResult } from '../../models/config-hub.models'; export interface RestoreDialogData { - object: BackupObject; - content: any; + // ── Single-object mode ────────────────────────────────────────────────── + object?: BackupObject; + content?: any; + + // ── Multi-object / commit-bundle mode ────────────────────────────────── + /** Pre-fetched array of parsed config objects to restore together. */ + bundle?: any[]; + /** Human-readable name for the bundle upload. */ + bundleName?: string; + /** Summary rows shown in the confirmation — one per affected object. */ + affectedObjects?: Array<{ objectType: string; name: string }>; + + // ── Shared commit metadata ────────────────────────────────────────────── commitSha: string; commitMessage?: string; commitAuthor?: string; @@ -27,6 +39,7 @@ export interface RestoreDialogData { MatIconModule, MatProgressSpinnerModule, MatDividerModule, + MatChipsModule, ], templateUrl: './restore-dialog.component.html', styleUrl: './restore-dialog.component.scss', @@ -36,15 +49,25 @@ export class RestoreDialogComponent { private dialogRef = inject(MatDialogRef); private apiService = inject(ConfigHubApiService); - /** Delegate restoring + status message directly to the service signals so - * polling phase updates are reflected in real time without extra wiring. */ readonly restoring = this.apiService.restoring; readonly statusMessage = this.apiService.restoreStatusMessage; result = signal(null); + get isBundle(): boolean { + return Array.isArray(this.data.bundle); + } + async onConfirm(): Promise { - const outcome = await this.apiService.restore(this.data.object, this.data.content); + let outcome: RestoreResult; + if (this.isBundle) { + outcome = await this.apiService.restoreBundle( + this.data.bundle!, + this.data.bundleName ?? `Restore ${this.data.bundle!.length} objects`, + ); + } else { + outcome = await this.apiService.restore(this.data.object!, this.data.content); + } this.result.set(outcome); } diff --git a/projects/sailpoint-components/src/lib/config-hub/config-hub.component.html b/projects/sailpoint-components/src/lib/config-hub/config-hub.component.html index cd363aaa..28ce17a5 100644 --- a/projects/sailpoint-components/src/lib/config-hub/config-hub.component.html +++ b/projects/sailpoint-components/src/lib/config-hub/config-hub.component.html @@ -4,7 +4,27 @@ hub Config Hub + + + + + + folder_open + By Object + + + history + By Commit + + + + + @@ -23,20 +43,27 @@
- -
- - -
+ @if (viewMode() === 'object') { + +
+ + +
+
+ + +
+ } - -
- - -
+ @if (viewMode() === 'commit') { + + + + }
diff --git a/projects/sailpoint-components/src/lib/config-hub/config-hub.component.scss b/projects/sailpoint-components/src/lib/config-hub/config-hub.component.scss index cd8ddfbc..6e364a9c 100644 --- a/projects/sailpoint-components/src/lib/config-hub/config-hub.component.scss +++ b/projects/sailpoint-components/src/lib/config-hub/config-hub.component.scss @@ -14,8 +14,17 @@ font-weight: 500; } - .spacer { - flex: 1; + .spacer { flex: 1; } + .spacer-sm { width: 8px; } +} + +.view-toggle { + // Slightly reduce height to fit neatly in the toolbar + --mat-standard-button-toggle-height: 34px; + + .toggle-label { + font-size: 13px; + margin-left: 4px; } } @@ -66,3 +75,10 @@ display: flex; flex-direction: column; } + +.commit-browser-host { + flex: 1; + overflow: hidden; + display: flex; + flex-direction: column; +} diff --git a/projects/sailpoint-components/src/lib/config-hub/config-hub.component.ts b/projects/sailpoint-components/src/lib/config-hub/config-hub.component.ts index 1fb201e7..e1468b00 100644 --- a/projects/sailpoint-components/src/lib/config-hub/config-hub.component.ts +++ b/projects/sailpoint-components/src/lib/config-hub/config-hub.component.ts @@ -1,6 +1,7 @@ import { Component, OnInit, signal } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MatButtonModule } from '@angular/material/button'; +import { MatButtonToggleModule } from '@angular/material/button-toggle'; import { MatDialog, MatDialogModule } from '@angular/material/dialog'; import { MatIconModule } from '@angular/material/icon'; import { MatToolbarModule } from '@angular/material/toolbar'; @@ -9,6 +10,7 @@ import { MatTooltipModule } from '@angular/material/tooltip'; import { RepoSettingsComponent } from './components/repo-settings/repo-settings.component'; import { ObjectBrowserComponent } from './components/object-browser/object-browser.component'; import { DiffViewerComponent } from './components/diff-viewer/diff-viewer.component'; +import { CommitBrowserComponent, CommitRestoreEvent } from './components/commit-browser/commit-browser.component'; import { RestoreDialogComponent, RestoreDialogData, @@ -16,18 +18,22 @@ import { import { ConfigHubGitService } from './services/config-hub-git.service'; import { BackupObject, GitRepoSettings } from './models/config-hub.models'; +export type ViewMode = 'object' | 'commit'; + @Component({ selector: 'app-config-hub', standalone: true, imports: [ CommonModule, MatButtonModule, + MatButtonToggleModule, MatDialogModule, MatIconModule, MatToolbarModule, MatTooltipModule, ObjectBrowserComponent, DiffViewerComponent, + CommitBrowserComponent, ], templateUrl: './config-hub.component.html', styleUrl: './config-hub.component.scss', @@ -35,6 +41,7 @@ import { BackupObject, GitRepoSettings } from './models/config-hub.models'; export class ConfigHubComponent implements OnInit { selectedObject = signal(null); hasSettings = signal(false); + viewMode = signal('object'); constructor( private gitService: ConfigHubGitService, @@ -69,16 +76,27 @@ export class ConfigHubComponent implements OnInit { }); } + /** Fired by DiffViewerComponent when the user clicks "Restore This Version" (single object). */ onRestoreRequested(event: { object: BackupObject; content: any; commitSha: string }): void { const data: RestoreDialogData = { object: event.object, content: event.content, commitSha: event.commitSha, }; - this.dialog.open(RestoreDialogComponent, { - data, - width: '560px', - disableClose: true, - }); + this.dialog.open(RestoreDialogComponent, { data, width: '560px', disableClose: true }); + } + + /** Fired by CommitBrowserComponent when the user clicks "Restore Selected". */ + onCommitRestoreRequested(event: CommitRestoreEvent): void { + const data: RestoreDialogData = { + bundle: event.bundle, + bundleName: event.bundleName, + affectedObjects: event.affectedObjects, + commitSha: event.commitSha, + commitMessage: event.commitMessage, + commitAuthor: event.commitAuthor, + commitTimestamp: event.commitTimestamp, + }; + this.dialog.open(RestoreDialogComponent, { data, width: '600px', disableClose: true }); } } diff --git a/projects/sailpoint-components/src/lib/config-hub/models/config-hub.models.ts b/projects/sailpoint-components/src/lib/config-hub/models/config-hub.models.ts index d432c0e8..797712c6 100644 --- a/projects/sailpoint-components/src/lib/config-hub/models/config-hub.models.ts +++ b/projects/sailpoint-components/src/lib/config-hub/models/config-hub.models.ts @@ -46,3 +46,11 @@ export interface RestoreResult { message?: string; error?: string; } + +export interface CommitFile { + objectType: string; + objectId: string; + filePath: string; + /** GitHub change status for this file in the commit. */ + status: 'added' | 'modified' | 'removed' | 'renamed'; +} diff --git a/projects/sailpoint-components/src/lib/config-hub/services/config-hub-api.service.ts b/projects/sailpoint-components/src/lib/config-hub/services/config-hub-api.service.ts index f83392d3..701da87b 100644 --- a/projects/sailpoint-components/src/lib/config-hub/services/config-hub-api.service.ts +++ b/projects/sailpoint-components/src/lib/config-hub/services/config-hub-api.service.ts @@ -8,42 +8,47 @@ const TERMINAL_STATUSES = new Set(['COMPLETE', 'CANCELLED', 'FAILED']); @Injectable({ providedIn: 'root' }) export class ConfigHubApiService { - /** True while the upload or polling is in flight. */ + /** True while an upload or poll is in flight. */ readonly restoring = signal(false); /** Human-readable phase message shown in the restore dialog. */ readonly restoreStatusMessage = signal(''); constructor(private sdkService: SailPointSDKService) {} + // ── Public API ──────────────────────────────────────────────────────────── + /** - * Upload a single config object to Config Hub and poll until the job - * reaches a terminal state (COMPLETE / CANCELLED / FAILED). - * - * Flow: - * 1. POST /v2025/configuration-hub/backups/uploads → BackupResponseV2025 with jobId - * 2. Poll GET /v2025/configuration-hub/backups/uploads/{id} every 2 s - * 3. Resolve with success/failure once the job finishes (or times out). + * Restore a single config object — wraps the bundle upload with a + * descriptive name derived from the object metadata. */ async restore(backupObject: BackupObject, objectContent: any): Promise { + const name = `Restore ${backupObject.objectType} - ${backupObject.name}`; + console.log('[ConfigHubApiService] restore() single object:', backupObject.objectType, backupObject.objectId); + return this.runRestore([objectContent], name); + } + + /** + * Restore a pre-built bundle of config objects (e.g. all objects changed in + * a commit). The caller is responsible for assembling the array. + */ + async restoreBundle(bundle: any[], name: string): Promise { + console.log('[ConfigHubApiService] restoreBundle():', bundle.length, 'objects, name:', name); + return this.runRestore(bundle, name); + } + + // ── Private ─────────────────────────────────────────────────────────────── + + private async runRestore(bundle: any[], name: string): Promise { this.restoring.set(true); this.restoreStatusMessage.set('Uploading configuration…'); - console.log('[ConfigHubApiService] restore() called for:', - backupObject.objectType, backupObject.objectId, backupObject.name); try { - const name = `Restore ${backupObject.objectType} - ${backupObject.name}`; - - const bundle = [objectContent]; const jsonStr = JSON.stringify(bundle, null, 2); - console.log('[ConfigHubApiService] Payload size:', jsonStr.length, 'chars'); + console.log('[ConfigHubApiService] Payload size:', jsonStr.length, 'chars,', bundle.length, 'object(s)'); - // File/Blob objects lose their prototype methods over Electron IPC structured - // clone. Pass a plain object; the SDK wrapper reconstructs a Blob from it. - const fileProxy: any = { - content: jsonStr, - name: `${name}.json`, - type: 'application/json', - }; + // File/Blob objects lose prototype methods over Electron IPC structured + // clone — pass a plain object; the SDK wrapper reconstructs a Blob from it. + const fileProxy: any = { content: jsonStr, name: `${name}.json`, type: 'application/json' }; console.log('[ConfigHubApiService] Calling sdkService.createUploadedConfiguration…'); const response = await this.sdkService.createUploadedConfiguration({ data: fileProxy as File, name }); @@ -54,7 +59,7 @@ export class ConfigHubApiService { ?? (response?.data as any)?.detailCode ?? response?.statusText ?? 'Unknown error'; - console.error('[ConfigHubApiService] Upload failed:', response?.status, errDetail, response?.data); + console.error('[ConfigHubApiService] Upload failed:', response?.status, errDetail); return { success: false, error: `Upload failed (HTTP ${response?.status}): ${errDetail}` }; } @@ -62,36 +67,24 @@ export class ConfigHubApiService { console.log('[ConfigHubApiService] Upload accepted, jobId:', jobId); if (!jobId) { - // No jobId — upload was accepted but polling isn't possible. return { success: true, - message: `"${backupObject.name}" has been uploaded to Config Hub as "${name}". ` - + `Open the SailPoint UI to review and deploy the configuration.`, + message: `Uploaded to Config Hub as "${name}". Open the SailPoint UI to review and deploy.`, }; } - // ── Polling phase ──────────────────────────────────────────────────── const finalStatus = await this.pollUploadStatus(jobId); if (finalStatus === 'COMPLETE') { - return { - success: true, - message: `"${backupObject.name}" has been successfully processed by Config Hub as "${name}".`, - }; + return { success: true, message: `Successfully processed by Config Hub as "${name}".` }; } - if (finalStatus === 'TIMEOUT') { return { success: false, - error: `Upload was accepted (job ${jobId}) but did not complete within the timeout window. ` - + `Check Config Hub for the current status.`, + error: `Upload accepted (job ${jobId}) but did not complete within the timeout. Check Config Hub.`, }; } - - return { - success: false, - error: `Upload processing ended with status: ${finalStatus}. Check Config Hub for details.`, - }; + return { success: false, error: `Upload ended with status: ${finalStatus}. Check Config Hub for details.` }; } catch (error) { console.error('[ConfigHubApiService] Caught exception:', error); @@ -102,30 +95,20 @@ export class ConfigHubApiService { } } - // ── Private ─────────────────────────────────────────────────────────────── - private async pollUploadStatus(jobId: string): Promise { for (let attempt = 1; attempt <= MAX_POLLS; attempt++) { await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL_MS)); - try { const res = await this.sdkService.getUploadedConfiguration({ id: jobId }); const status = (res?.data as any)?.status as string | undefined; console.log(`[ConfigHubApiService] Poll ${attempt}/${MAX_POLLS}: status =`, status); - - this.restoreStatusMessage.set( - `Processing… ${status ?? 'checking'} (${attempt}/${MAX_POLLS})` - ); - - if (status && TERMINAL_STATUSES.has(status)) { - return status; - } + this.restoreStatusMessage.set(`Processing… ${status ?? 'checking'} (${attempt}/${MAX_POLLS})`); + if (status && TERMINAL_STATUSES.has(status)) return status; } catch (err) { console.warn('[ConfigHubApiService] Poll error (will retry):', err); this.restoreStatusMessage.set(`Polling… (attempt ${attempt}/${MAX_POLLS})`); } } - return 'TIMEOUT'; } } diff --git a/projects/sailpoint-components/src/lib/config-hub/services/config-hub-git.service.ts b/projects/sailpoint-components/src/lib/config-hub/services/config-hub-git.service.ts index 11c3ea1e..9a4b4d13 100644 --- a/projects/sailpoint-components/src/lib/config-hub/services/config-hub-git.service.ts +++ b/projects/sailpoint-components/src/lib/config-hub/services/config-hub-git.service.ts @@ -1,5 +1,5 @@ import { Injectable, signal } from '@angular/core'; -import { GitRepoSettings, GitCommit, BackupObject, BackupObjectType } from '../models/config-hub.models'; +import { GitRepoSettings, GitCommit, BackupObject, BackupObjectType, CommitFile } from '../models/config-hub.models'; const SETTINGS_KEY = 'config-hub-git-settings'; @@ -131,6 +131,68 @@ export class ConfigHubGitService { } } + /** + * Return the most recent commits that touch the configured backups path, + * sorted newest-first. Used by the "By Commit" view. + */ + async getRecentCommits(limit = 50): Promise { + const s = this.settings(); + if (!s) return []; + const { owner, repo } = this.parseRepoUrl(s.repoUrl); + if (!owner || !repo) return []; + try { + const basePath = s.backupsPath.replace(/^\/|\/$/g, ''); + const params = new URLSearchParams({ sha: s.defaultBranch, path: basePath, per_page: String(limit) }); + const url = `https://api.github.com/repos/${owner}/${repo}/commits?${params}`; + const res = await fetch(url, { headers: this.githubHeaders(s) }); + if (!res.ok) return []; + const data = await res.json() as any[]; + return (data || []).map((c: any) => ({ + sha: c.sha as string, + message: ((c.commit?.message as string) || '').split('\n')[0], + author: (c.commit?.author?.name || c.author?.login || 'Unknown') as string, + timestamp: (c.commit?.author?.date || '') as string, + })); + } catch { + return []; + } + } + + /** + * Return the list of backup objects changed by a specific commit. + * Parses file paths of the form `{backupsPath}/{OBJECT_TYPE}/{objectId}.json`. + */ + async getCommitFiles(sha: string): Promise { + const s = this.settings(); + if (!s) return []; + const { owner, repo } = this.parseRepoUrl(s.repoUrl); + if (!owner || !repo) return []; + try { + const basePath = s.backupsPath.replace(/^\/|\/$/g, ''); + const url = `https://api.github.com/repos/${owner}/${repo}/commits/${sha}`; + const res = await fetch(url, { headers: this.githubHeaders(s) }); + if (!res.ok) return []; + const data = await res.json() as any; + const escapedBase = basePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const pattern = new RegExp(`^${escapedBase}/([^/]+)/([^/]+)\\.json$`); + const files: CommitFile[] = []; + for (const f of (data.files ?? [])) { + const match = (f.filename as string).match(pattern); + if (match) { + files.push({ + objectType: match[1], + objectId: match[2], + filePath: f.filename as string, + status: f.status as CommitFile['status'], + }); + } + } + return files; + } catch { + return []; + } + } + parseRepoUrl(repoUrl: string): { owner: string; repo: string } { const clean = repoUrl.trim().replace(/\.git$/, '').replace(/\/$/, ''); const httpsMatch = clean.match(/github\.com\/([^/]+)\/([^/]+)/i); From e6d90990a0107f60555980799c63fa11ca85f35b Mon Sep 17 00:00:00 2001 From: Philip Ellis Date: Thu, 19 Mar 2026 09:04:24 -0400 Subject: [PATCH 13/20] added commit browser --- .../commit-browser.component.html | 179 ++++++++++ .../commit-browser.component.scss | 334 ++++++++++++++++++ .../commit-browser.component.ts | 294 +++++++++++++++ 3 files changed, 807 insertions(+) create mode 100644 projects/sailpoint-components/src/lib/config-hub/components/commit-browser/commit-browser.component.html create mode 100644 projects/sailpoint-components/src/lib/config-hub/components/commit-browser/commit-browser.component.scss create mode 100644 projects/sailpoint-components/src/lib/config-hub/components/commit-browser/commit-browser.component.ts diff --git a/projects/sailpoint-components/src/lib/config-hub/components/commit-browser/commit-browser.component.html b/projects/sailpoint-components/src/lib/config-hub/components/commit-browser/commit-browser.component.html new file mode 100644 index 00000000..ef664ee6 --- /dev/null +++ b/projects/sailpoint-components/src/lib/config-hub/components/commit-browser/commit-browser.component.html @@ -0,0 +1,179 @@ +
+ + +
+
+ history + Commits + +
+ + + @if (loading()) { +
+ + Loading commits… +
+ } @else if (commits().length === 0) { +
+ commit +

No commits found

+
+ } @else { + + @for (c of commits(); track c.sha) { + + commit + {{ c.message | slice:0:60 }} + + {{ c.sha.slice(0, 7) }} + {{ c.author }} · {{ formatRelativeTime(c.timestamp) }} + + + } + + } +
+ + +
+ + @if (!selectedSha()) { +
+ event_note +

Select a commit to see changed objects

+
+ } @else { + + +
+
+
+ {{ selectedSha()!.slice(0, 7) }} + {{ selectedCommit()?.message | slice:0:70 }} + + {{ selectedCommit()?.author }} · {{ formatTimestamp(selectedCommit()?.timestamp || '') }} + +
+ +
+ @if (loadingFiles()) { + + } @else { + + + } +
+
+ + @if (loadingFiles()) { +
+ + Loading changed objects… +
+ } @else if (commitFiles().length === 0) { +
+ folder_open +

No backup objects changed in this commit

+
+ } @else { + + @for (f of commitFiles(); track f.filePath) { + + + + + + {{ fileNames().get(f.filePath) || f.objectId }} + + + + {{ f.objectType }} + + {{ f.status }} + + + + + } + + } +
+ + + + +
+ @if (!focusedFile()) { +
+ difference +

Click an object above to view its diff vs HEAD

+
+ } @else if (loadingDiff()) { +
+ + Computing diff… +
+ } @else { +
+ + {{ fileNames().get(focusedFile()!.filePath) || focusedFile()!.objectId }} + + HEAD → this commit + @if (hasChanges) { + + +{{ addedCount }} + -{{ removedCount }} + + } @else { + No differences from HEAD + } +
+ +
+
+ + @for (line of diffLines(); track $index) { + + + + + + + } + +
{{ line.lineNumberLeft ?? '' }}{{ line.lineNumberRight ?? '' }} + {{ line.type === 'added' ? '+' : line.type === 'removed' ? '-' : ' ' }} +
{{ line.content }}
+
+ } +
+ + } +
+ +
diff --git a/projects/sailpoint-components/src/lib/config-hub/components/commit-browser/commit-browser.component.scss b/projects/sailpoint-components/src/lib/config-hub/components/commit-browser/commit-browser.component.scss new file mode 100644 index 00000000..a2fe6ca2 --- /dev/null +++ b/projects/sailpoint-components/src/lib/config-hub/components/commit-browser/commit-browser.component.scss @@ -0,0 +1,334 @@ +.commit-browser { + display: flex; + height: 100%; + overflow: hidden; +} + +// ── Shared empty / status states ────────────────────────────────────────────── + +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + gap: 12px; + color: var(--theme-primary-text); + opacity: 0.55; + + mat-icon { + font-size: 36px; + width: 36px; + height: 36px; + } + + p { margin: 0; font-size: 14px; } + + &.small { + height: auto; + padding: 24px; + } +} + +.status-row { + display: flex; + align-items: center; + gap: 12px; + padding: 16px; + color: var(--theme-primary-text); + font-size: 14px; +} + +// ── Left: commit list ───────────────────────────────────────────────────────── + +.commits-pane { + width: 300px; + min-width: 260px; + border-right: 1px solid color-mix(in srgb, var(--theme-primary-text) 15%, transparent); + display: flex; + flex-direction: column; + overflow: hidden; + flex-shrink: 0; +} + +.pane-header { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 12px 10px 16px; + font-size: 13px; + font-weight: 600; + color: var(--theme-primary-text); + flex-shrink: 0; + + span { flex: 1; } + + mat-icon:first-child { + font-size: 18px; + width: 18px; + height: 18px; + color: var(--theme-primary); + } +} + +.commit-list { + flex: 1; + overflow-y: auto; +} + +.commit-msg-title { + font-size: 13px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.commit-meta-line { + font-size: 11px; + color: var(--theme-primary-text); + opacity: 0.7; +} + +.sha-badge { + display: inline-block; + font-family: monospace; + font-size: 11px; + background: color-mix(in srgb, var(--theme-primary-text) 12%, var(--theme-background)); + color: var(--theme-primary-text); + padding: 0 5px; + border-radius: 4px; + margin-right: 4px; +} + +// ── Right: detail (objects + diff) ──────────────────────────────────────────── + +.commit-detail { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; +} + +// ── Objects section ─────────────────────────────────────────────────────────── + +.objects-section { + flex-shrink: 0; + max-height: 45%; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.objects-header { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 12px 16px; + background: color-mix(in srgb, var(--theme-primary-text) 5%, var(--theme-background)); + border-bottom: 1px solid color-mix(in srgb, var(--theme-primary-text) 10%, transparent); + flex-shrink: 0; + flex-wrap: wrap; +} + +.commit-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 3px; + min-width: 0; +} + +.sha-badge-lg { + font-family: monospace; + font-size: 12px; + background: color-mix(in srgb, var(--theme-primary) 15%, var(--theme-background)); + color: var(--theme-primary); + padding: 1px 7px; + border-radius: 4px; + align-self: flex-start; +} + +.commit-title { + font-size: 14px; + font-weight: 500; + color: var(--theme-primary-text); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.commit-meta { + font-size: 11px; + color: var(--theme-primary-text); + opacity: 0.65; +} + +.objects-actions { + display: flex; + align-items: center; + gap: 8px; + flex-shrink: 0; +} + +.objects-list { + flex: 1; + overflow-y: auto; +} + +.file-meta-line { + display: flex; + align-items: center; + gap: 6px; +} + +.type-chip-sm { + --mdc-chip-elevated-container-color: color-mix(in srgb, var(--theme-primary) 14%, var(--theme-background)); + --mdc-chip-label-text-color: var(--theme-primary); + font-size: 10px; + font-weight: 600; + height: 20px; +} + +.status-chip { + font-size: 10px; + height: 20px; + + &.status-added { + --mdc-chip-elevated-container-color: color-mix(in srgb, #4caf50 16%, var(--theme-background)); + --mdc-chip-label-text-color: color-mix(in srgb, #4caf50 80%, var(--theme-primary-text)); + } + &.status-modified { + --mdc-chip-elevated-container-color: color-mix(in srgb, #ff9800 16%, var(--theme-background)); + --mdc-chip-label-text-color: color-mix(in srgb, #ff9800 80%, var(--theme-primary-text)); + } + &.status-removed { + --mdc-chip-elevated-container-color: color-mix(in srgb, #f44336 16%, var(--theme-background)); + --mdc-chip-label-text-color: color-mix(in srgb, #f44336 80%, var(--theme-primary-text)); + } + &.status-renamed { + --mdc-chip-elevated-container-color: color-mix(in srgb, #9c27b0 16%, var(--theme-background)); + --mdc-chip-label-text-color: color-mix(in srgb, #9c27b0 80%, var(--theme-primary-text)); + } +} + +// ── Diff section ────────────────────────────────────────────────────────────── + +.diff-section { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.diff-header { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 16px; + background: color-mix(in srgb, var(--theme-primary-text) 5%, var(--theme-background)); + border-bottom: 1px solid color-mix(in srgb, var(--theme-primary-text) 10%, transparent); + flex-shrink: 0; + flex-wrap: wrap; +} + +.diff-obj-name { + font-size: 14px; + font-weight: 500; + color: var(--theme-primary-text); +} + +.diff-subtitle { + font-size: 12px; + color: var(--theme-primary-text); + opacity: 0.6; +} + +.no-diff-label { + font-size: 12px; + color: var(--theme-primary-text); + opacity: 0.55; +} + +.stat-chip { + font-family: monospace; + font-weight: 700; + font-size: 12px; + height: 22px; +} + +.added-chip { + --mdc-chip-elevated-container-color: color-mix(in srgb, #4caf50 18%, var(--theme-background)); + --mdc-chip-label-text-color: color-mix(in srgb, #4caf50 70%, var(--theme-primary-text)); +} + +.removed-chip { + --mdc-chip-elevated-container-color: color-mix(in srgb, #f44336 18%, var(--theme-background)); + --mdc-chip-label-text-color: color-mix(in srgb, #f44336 70%, var(--theme-primary-text)); +} + +.diff-table-wrapper { + flex: 1; + overflow: auto; +} + +.diff-table { + width: 100%; + border-collapse: collapse; + font-family: 'Consolas', 'Monaco', 'Courier New', monospace; + font-size: 12px; + line-height: 1.6; + background: var(--theme-background); +} + +.diff-line { + &.diff-added { + background-color: color-mix(in srgb, #4caf50 13%, var(--theme-background)); + .diff-gutter { + background-color: color-mix(in srgb, #4caf50 28%, var(--theme-background)); + color: color-mix(in srgb, #4caf50 70%, var(--theme-primary-text)); + } + } + &.diff-removed { + background-color: color-mix(in srgb, #f44336 13%, var(--theme-background)); + .diff-gutter { + background-color: color-mix(in srgb, #f44336 28%, var(--theme-background)); + color: color-mix(in srgb, #f44336 70%, var(--theme-primary-text)); + } + } + &.diff-unchanged .diff-gutter { + color: color-mix(in srgb, var(--theme-primary-text) 45%, transparent); + } +} + +.line-num { + width: 44px; + min-width: 44px; + text-align: right; + padding: 0 6px; + color: color-mix(in srgb, var(--theme-primary-text) 45%, transparent); + user-select: none; + border-right: 1px solid color-mix(in srgb, var(--theme-primary-text) 15%, transparent); +} + +.diff-gutter { + width: 20px; + min-width: 20px; + text-align: center; + padding: 0 2px; + user-select: none; + border-right: 1px solid color-mix(in srgb, var(--theme-primary-text) 15%, transparent); +} + +.diff-content { + padding: 0 8px; + width: 100%; + color: var(--theme-primary-text); + + pre { + margin: 0; + white-space: pre-wrap; + word-break: break-all; + font-family: inherit; + font-size: inherit; + } +} diff --git a/projects/sailpoint-components/src/lib/config-hub/components/commit-browser/commit-browser.component.ts b/projects/sailpoint-components/src/lib/config-hub/components/commit-browser/commit-browser.component.ts new file mode 100644 index 00000000..5af528a0 --- /dev/null +++ b/projects/sailpoint-components/src/lib/config-hub/components/commit-browser/commit-browser.component.ts @@ -0,0 +1,294 @@ +import { Component, OnInit, output, signal } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatChipsModule } from '@angular/material/chips'; +import { MatDividerModule } from '@angular/material/divider'; +import { MatIconModule } from '@angular/material/icon'; +import { MatListModule } from '@angular/material/list'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import * as Diff from 'diff'; +import { ConfigHubGitService } from '../../services/config-hub-git.service'; +import { CommitFile, DiffLine, GitCommit } from '../../models/config-hub.models'; + +export interface CommitRestoreEvent { + bundle: any[]; + bundleName: string; + affectedObjects: Array<{ objectType: string; name: string }>; + commitSha: string; + commitMessage?: string; + commitAuthor?: string; + commitTimestamp?: string; +} + +@Component({ + selector: 'app-commit-browser', + standalone: true, + imports: [ + CommonModule, + MatButtonModule, + MatCheckboxModule, + MatChipsModule, + MatDividerModule, + MatIconModule, + MatListModule, + MatProgressSpinnerModule, + MatTooltipModule, + ], + templateUrl: './commit-browser.component.html', + styleUrl: './commit-browser.component.scss', +}) +export class CommitBrowserComponent implements OnInit { + readonly restoreRequested = output(); + + // ── State ───────────────────────────────────────────────────────────────── + + commits = signal([]); + selectedSha = signal(null); + + commitFiles = signal([]); + /** Set of filePaths that have their checkbox checked. */ + selectedFiles = signal>(new Set()); + /** The file currently shown in the inline diff panel. */ + focusedFile = signal(null); + /** Maps filePath → human-readable object name (resolved lazily). */ + fileNames = signal>(new Map()); + + diffLines = signal([]); + + loading = signal(false); + loadingFiles = signal(false); + loadingDiff = signal(false); + buildingBundle = signal(false); + errorMessage = signal(null); + + constructor(private gitService: ConfigHubGitService) {} + + async ngOnInit(): Promise { + await this.loadCommits(); + } + + // ── Loaders ─────────────────────────────────────────────────────────────── + + async loadCommits(): Promise { + this.loading.set(true); + this.errorMessage.set(null); + const commits = await this.gitService.getRecentCommits(50); + this.commits.set(commits); + this.loading.set(false); + if (commits.length > 0) { + await this.onCommitSelect(commits[0].sha); + } + } + + async onCommitSelect(sha: string): Promise { + this.selectedSha.set(sha); + this.focusedFile.set(null); + this.diffLines.set([]); + this.commitFiles.set([]); + this.selectedFiles.set(new Set()); + this.fileNames.set(new Map()); + + this.loadingFiles.set(true); + const files = await this.gitService.getCommitFiles(sha); + this.commitFiles.set(files); + // Pre-select all files + this.selectedFiles.set(new Set(files.map(f => f.filePath))); + this.loadingFiles.set(false); + + // Auto-focus the first file so the diff is visible immediately + if (files.length > 0) { + await this.onFileFocus(files[0]); + } + } + + async onFileFocus(file: CommitFile): Promise { + this.focusedFile.set(file); + this.diffLines.set([]); + + const sha = this.selectedSha(); + const branch = this.gitService.settings()?.defaultBranch ?? 'main'; + if (!sha) return; + + this.loadingDiff.set(true); + const [headRaw, commitRaw] = await Promise.all([ + this.gitService.getFileAtCommit(file.objectType, file.objectId, branch), + file.status === 'removed' + ? Promise.resolve('') + : this.gitService.getFileAtCommit(file.objectType, file.objectId, sha), + ]); + + // Resolve the object name from JSON content + const nameSource = commitRaw || headRaw; + if (nameSource) { + try { + const parsed = JSON.parse(nameSource); + const name = parsed?.object?.name ?? parsed?.self?.name ?? parsed?.name ?? file.objectId; + const map = new Map(this.fileNames()); + map.set(file.filePath, name); + this.fileNames.set(map); + } catch { /* keep objectId as fallback */ } + } + + this.diffLines.set(this.buildDiffLines(headRaw, commitRaw)); + this.loadingDiff.set(false); + } + + // ── Selection ───────────────────────────────────────────────────────────── + + onFileToggle(filePath: string, checked: boolean): void { + const next = new Set(this.selectedFiles()); + if (checked) { + next.add(filePath); + } else { + next.delete(filePath); + } + this.selectedFiles.set(next); + } + + toggleSelectAll(): void { + if (this.allSelected) { + this.selectedFiles.set(new Set()); + } else { + this.selectedFiles.set(new Set(this.commitFiles().map(f => f.filePath))); + } + } + + get allSelected(): boolean { + const all = this.commitFiles(); + return all.length > 0 && all.every(f => this.selectedFiles().has(f.filePath)); + } + + get noneSelected(): boolean { + return this.selectedFiles().size === 0; + } + + // ── Restore ─────────────────────────────────────────────────────────────── + + async onRestoreSelected(): Promise { + const sha = this.selectedSha(); + if (!sha) return; + + const chosen = this.commitFiles().filter(f => this.selectedFiles().has(f.filePath)); + if (chosen.length === 0) return; + + this.buildingBundle.set(true); + + // Fetch content for each selected file at the commit SHA + const rawContents = await Promise.all( + chosen.map(f => + f.status === 'removed' + ? Promise.resolve('') + : this.gitService.getFileAtCommit(f.objectType, f.objectId, sha), + ), + ); + + const bundle: any[] = []; + const affectedObjects: Array<{ objectType: string; name: string }> = []; + + for (let i = 0; i < chosen.length; i++) { + const raw = rawContents[i]; + if (!raw) continue; + try { + const parsed = JSON.parse(raw); + bundle.push(parsed); + const name = this.fileNames().get(chosen[i].filePath) ?? chosen[i].objectId; + affectedObjects.push({ objectType: chosen[i].objectType, name }); + } catch { + console.warn('[CommitBrowser] Could not parse file:', chosen[i].filePath); + } + } + + this.buildingBundle.set(false); + + if (bundle.length === 0) return; + + const commit = this.selectedCommit(); + const bundleName = commit?.message + ? `Restore: ${commit.message.slice(0, 50)}` + : `Restore ${bundle.length} objects from ${sha.slice(0, 7)}`; + + this.restoreRequested.emit({ + bundle, + bundleName, + affectedObjects, + commitSha: sha, + commitMessage: commit?.message, + commitAuthor: commit?.author, + commitTimestamp: commit?.timestamp, + }); + } + + // ── Helpers ─────────────────────────────────────────────────────────────── + + selectedCommit(): GitCommit | undefined { + return this.commits().find(c => c.sha === this.selectedSha()); + } + + private buildDiffLines(oldText: string, newText: string): DiffLine[] { + const changes = Diff.diffLines(oldText, newText); + const result: DiffLine[] = []; + let leftLine = 1; + let rightLine = 1; + for (const part of changes) { + const lines = part.value.split('\n'); + if (lines[lines.length - 1] === '') lines.pop(); + for (const line of lines) { + if (part.added) { + result.push({ type: 'added', content: line, lineNumberRight: rightLine++ }); + } else if (part.removed) { + result.push({ type: 'removed', content: line, lineNumberLeft: leftLine++ }); + } else { + result.push({ type: 'unchanged', content: line, lineNumberLeft: leftLine++, lineNumberRight: rightLine++ }); + } + } + } + return result; + } + + get hasChanges(): boolean { + return this.diffLines().some(l => l.type !== 'unchanged'); + } + + get addedCount(): number { + return this.diffLines().filter(l => l.type === 'added').length; + } + + get removedCount(): number { + return this.diffLines().filter(l => l.type === 'removed').length; + } + + formatTimestamp(iso: string): string { + if (!iso) return ''; + return new Date(iso).toLocaleString(); + } + + formatRelativeTime(iso: string): string { + if (!iso) return ''; + const diff = Date.now() - new Date(iso).getTime(); + const minutes = Math.floor(diff / 60_000); + if (minutes < 1) return 'just now'; + if (minutes < 60) return `${minutes}m ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ago`; + const days = Math.floor(hours / 24); + if (days < 30) return `${days}d ago`; + return `${Math.floor(days / 30)}mo ago`; + } + + typeIcon(typeName: string): string { + const icons: Record = { + ROLE: 'manage_accounts', + RULE: 'code', + SOURCE: 'device_hub', + WORKFLOW: 'account_tree', + TRIGGER_SUBSCRIPTION: 'notifications', + IDENTITY_PROFILE: 'person', + ACCESS_PROFILE: 'badge', + TRANSFORM: 'transform', + CONNECTOR_RULE: 'build', + }; + return icons[typeName] ?? 'description'; + } +} From 098c9cfe305c510d7929b5c74993d91976e91a5b Mon Sep 17 00:00:00 2001 From: Philip Ellis Date: Thu, 19 Mar 2026 09:38:58 -0400 Subject: [PATCH 14/20] fixed linter errors --- .../commit-browser/commit-browser.component.ts | 6 +++--- .../components/diff-viewer/diff-viewer.component.ts | 4 ++-- .../object-browser/object-browser.component.ts | 2 +- .../repo-settings/repo-settings.component.ts | 9 +++++---- .../src/lib/config-hub/config-hub.component.ts | 6 +++++- .../config-hub/services/config-hub-git.service.ts | 13 +++++++------ 6 files changed, 23 insertions(+), 17 deletions(-) diff --git a/projects/sailpoint-components/src/lib/config-hub/components/commit-browser/commit-browser.component.ts b/projects/sailpoint-components/src/lib/config-hub/components/commit-browser/commit-browser.component.ts index 5af528a0..41153d9f 100644 --- a/projects/sailpoint-components/src/lib/config-hub/components/commit-browser/commit-browser.component.ts +++ b/projects/sailpoint-components/src/lib/config-hub/components/commit-browser/commit-browser.component.ts @@ -65,8 +65,8 @@ export class CommitBrowserComponent implements OnInit { constructor(private gitService: ConfigHubGitService) {} - async ngOnInit(): Promise { - await this.loadCommits(); + ngOnInit(): void { + void this.loadCommits(); } // ── Loaders ─────────────────────────────────────────────────────────────── @@ -124,7 +124,7 @@ export class CommitBrowserComponent implements OnInit { if (nameSource) { try { const parsed = JSON.parse(nameSource); - const name = parsed?.object?.name ?? parsed?.self?.name ?? parsed?.name ?? file.objectId; + const name = (parsed?.object?.name ?? parsed?.self?.name ?? parsed?.name ?? file.objectId) as string; const map = new Map(this.fileNames()); map.set(file.filePath, name); this.fileNames.set(map); diff --git a/projects/sailpoint-components/src/lib/config-hub/components/diff-viewer/diff-viewer.component.ts b/projects/sailpoint-components/src/lib/config-hub/components/diff-viewer/diff-viewer.component.ts index ce134cfb..f09369fa 100644 --- a/projects/sailpoint-components/src/lib/config-hub/components/diff-viewer/diff-viewer.component.ts +++ b/projects/sailpoint-components/src/lib/config-hub/components/diff-viewer/diff-viewer.component.ts @@ -55,11 +55,11 @@ export class DiffViewerComponent implements OnChanges { constructor(private gitService: ConfigHubGitService) {} - async ngOnChanges(changes: SimpleChanges): Promise { + ngOnChanges(changes: SimpleChanges): void { if (changes['backupObject']) { const obj = this.backupObject(); if (obj) { - await this.loadHistory(obj); + void this.loadHistory(obj); } else { this.reset(); } diff --git a/projects/sailpoint-components/src/lib/config-hub/components/object-browser/object-browser.component.ts b/projects/sailpoint-components/src/lib/config-hub/components/object-browser/object-browser.component.ts index 890c1855..a48270b8 100644 --- a/projects/sailpoint-components/src/lib/config-hub/components/object-browser/object-browser.component.ts +++ b/projects/sailpoint-components/src/lib/config-hub/components/object-browser/object-browser.component.ts @@ -40,7 +40,7 @@ export class ObjectBrowserComponent implements OnInit { constructor(private gitService: ConfigHubGitService) {} ngOnInit(): void { - this.refresh(); + void this.refresh(); } async refresh(): Promise { diff --git a/projects/sailpoint-components/src/lib/config-hub/components/repo-settings/repo-settings.component.ts b/projects/sailpoint-components/src/lib/config-hub/components/repo-settings/repo-settings.component.ts index e8d7ed44..87e3faf2 100644 --- a/projects/sailpoint-components/src/lib/config-hub/components/repo-settings/repo-settings.component.ts +++ b/projects/sailpoint-components/src/lib/config-hub/components/repo-settings/repo-settings.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit, Optional, output } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +import { AbstractControl, FormBuilder, FormGroup, ReactiveFormsModule, ValidationErrors, Validators } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatDialogModule, MatDialogRef } from '@angular/material/dialog'; @@ -45,13 +45,14 @@ export class RepoSettingsComponent implements OnInit { ) {} ngOnInit(): void { + const required = (ctrl: AbstractControl): ValidationErrors | null => Validators.required(ctrl); this.form = this.fb.group({ - repoUrl: ['', Validators.required], + repoUrl: ['', required], authMethod: ['pat'], pat: [''], sshKeyPath: [''], - defaultBranch: ['main', Validators.required], - backupsPath: ['backups', Validators.required], + defaultBranch: ['main', required], + backupsPath: ['backups', required], }); const existing = this.gitService.settings(); diff --git a/projects/sailpoint-components/src/lib/config-hub/config-hub.component.ts b/projects/sailpoint-components/src/lib/config-hub/config-hub.component.ts index e1468b00..2726659a 100644 --- a/projects/sailpoint-components/src/lib/config-hub/config-hub.component.ts +++ b/projects/sailpoint-components/src/lib/config-hub/config-hub.component.ts @@ -48,7 +48,11 @@ export class ConfigHubComponent implements OnInit { private dialog: MatDialog, ) {} - async ngOnInit(): Promise { + ngOnInit(): void { + void this.initialize(); + } + + private async initialize(): Promise { await this.gitService.loadSettings(); const s = this.gitService.settings(); this.hasSettings.set(!!s?.repoUrl); diff --git a/projects/sailpoint-components/src/lib/config-hub/services/config-hub-git.service.ts b/projects/sailpoint-components/src/lib/config-hub/services/config-hub-git.service.ts index 9a4b4d13..56167897 100644 --- a/projects/sailpoint-components/src/lib/config-hub/services/config-hub-git.service.ts +++ b/projects/sailpoint-components/src/lib/config-hub/services/config-hub-git.service.ts @@ -9,23 +9,24 @@ export class ConfigHubGitService { readonly branches = signal([]); readonly loading = signal(false); - async loadSettings(): Promise { + loadSettings(): Promise { try { const raw = localStorage.getItem(SETTINGS_KEY); this.settings.set(raw ? (JSON.parse(raw) as GitRepoSettings) : null); } catch { this.settings.set(null); } + return Promise.resolve(); } - async saveSettings(settings: GitRepoSettings): Promise<{ success: boolean; error?: string }> { + saveSettings(settings: GitRepoSettings): Promise<{ success: boolean; error?: string }> { try { localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings)); this.settings.set(settings); - return { success: true }; + return Promise.resolve({ success: true }); } catch (error) { const msg = error instanceof Error ? error.message : 'Failed to save settings'; - return { success: false, error: msg }; + return Promise.resolve({ success: false, error: msg }); } } @@ -124,7 +125,7 @@ export class ConfigHubGitService { const url = `https://api.github.com/repos/${owner}/${repo}/contents/${filePath}?ref=${encodeURIComponent(ref)}`; const res = await fetch(url, { headers: this.githubHeaders(s) }); if (!res.ok) return ''; - const data = await res.json() as any; + const data = await res.json(); return atob((data.content as string).replace(/\n/g, '')); } catch { return ''; @@ -172,7 +173,7 @@ export class ConfigHubGitService { const url = `https://api.github.com/repos/${owner}/${repo}/commits/${sha}`; const res = await fetch(url, { headers: this.githubHeaders(s) }); if (!res.ok) return []; - const data = await res.json() as any; + const data = await res.json(); const escapedBase = basePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const pattern = new RegExp(`^${escapedBase}/([^/]+)/([^/]+)\\.json$`); const files: CommitFile[] = []; From d666ee282a87f8b4606abc47a32c0d804739a4b9 Mon Sep 17 00:00:00 2001 From: Philip Ellis Date: Thu, 19 Mar 2026 09:46:30 -0400 Subject: [PATCH 15/20] fixed button group formatting error --- src/styles.scss | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/styles.scss b/src/styles.scss index bf8145f4..ac7fbf31 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -585,6 +585,16 @@ button.mat-mdc-mini-fab { padding-bottom: 0 !important; } +// The global `button { border, border-radius }` rule causes doubled/overlapping +// Reset those properties so Material's own group border is the only one present. +.mat-button-toggle-button { + border: none !important; + border-radius: 0 !important; + background-color: transparent !important; + padding-top: 0 !important; + padding-bottom: 0 !important; +} + .mat-mdc-paginator-range-actions .mat-mdc-icon-button, .mat-mdc-paginator-range-actions .mdc-icon-button { border: none !important; From f15faa16fcb291631877557f6149447027068ca0 Mon Sep 17 00:00:00 2001 From: Philip Ellis Date: Thu, 19 Mar 2026 09:47:27 -0400 Subject: [PATCH 16/20] made component protected --- .../components/repo-settings/repo-settings.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/sailpoint-components/src/lib/config-hub/components/repo-settings/repo-settings.component.ts b/projects/sailpoint-components/src/lib/config-hub/components/repo-settings/repo-settings.component.ts index 87e3faf2..4b04066f 100644 --- a/projects/sailpoint-components/src/lib/config-hub/components/repo-settings/repo-settings.component.ts +++ b/projects/sailpoint-components/src/lib/config-hub/components/repo-settings/repo-settings.component.ts @@ -41,7 +41,7 @@ export class RepoSettingsComponent implements OnInit { private fb: FormBuilder, private gitService: ConfigHubGitService, private snackBar: MatSnackBar, - @Optional() private dialogRef?: MatDialogRef, + @Optional() protected dialogRef?: MatDialogRef, ) {} ngOnInit(): void { From 9f959e19d5e82948326d784538e44ef0369954a3 Mon Sep 17 00:00:00 2001 From: Philip Ellis Date: Thu, 19 Mar 2026 09:56:54 -0400 Subject: [PATCH 17/20] added missing component --- src/tsconfig.spec.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tsconfig.spec.json b/src/tsconfig.spec.json index 1ca25bf1..088239f8 100644 --- a/src/tsconfig.spec.json +++ b/src/tsconfig.spec.json @@ -16,6 +16,7 @@ "@angular/cdk/layout": ["node_modules/@angular/cdk/types/layout.d.ts"], "@angular/cdk/drag-drop": ["node_modules/@angular/cdk/types/drag-drop.d.ts"], "@angular/material/button": ["node_modules/@angular/material/types/button.d.ts"], + "@angular/material/button-toggle": ["node_modules/@angular/material/types/button-toggle.d.ts"], "@angular/material/icon": ["node_modules/@angular/material/types/icon.d.ts"], "@angular/material/list": ["node_modules/@angular/material/types/list.d.ts"], "@angular/material/sidenav": ["node_modules/@angular/material/types/sidenav.d.ts"], From 5c887ded1333d6f435f862e3d13a456c405f9ba4 Mon Sep 17 00:00:00 2001 From: Philip Ellis Date: Thu, 19 Mar 2026 10:29:39 -0400 Subject: [PATCH 18/20] updated build command for windows --- .github/workflows/windows.yml | 9 +++++++-- package-lock.json | 10 ---------- package.json | 1 - 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 272b47b5..0919da79 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -33,9 +33,14 @@ jobs: node-version: '24' cache: 'npm' - - name: Install Dependencies + - name: Exclude workspace from Windows Defender + shell: powershell run: | - npm install + Add-MpPreference -ExclusionPath $env:GITHUB_WORKSPACE + Add-MpPreference -ExclusionPath "$env:APPDATA\npm-cache" + + - name: Install Dependencies + run: npm ci - name: Install Angular CLI Globally run: npm install -g @angular/cli diff --git a/package-lock.json b/package-lock.json index b635d39e..560ec69d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,7 +33,6 @@ "@codemirror/view": "^6.0.1", "@lezer/highlight": "^1.2.0", "@types/codemirror": "5.60.16", - "@types/diff": "8.0.0", "axios": "1.12.1", "codemirror": "6.0.2", "cronstrue": "3.3.0", @@ -28510,15 +28509,6 @@ "@types/d3-selection": "*" } }, - "node_modules/@types/diff": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@types/diff/-/diff-8.0.0.tgz", - "integrity": "sha512-o7jqJM04gfaYrdCecCVMbZhNdG6T1MHg/oQoRFdERLV+4d+V7FijhiEAbFu0Usww84Yijk9yH58U4Jk4HbtzZw==", - "deprecated": "This is a stub types definition. diff provides its own type definitions, so you do not need this installed.", - "dependencies": { - "diff": "*" - } - }, "node_modules/@types/jasmine": { "version": "5.1.8", "dev": true, diff --git a/package.json b/package.json index 50d7e6af..5e2b15db 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,6 @@ "@codemirror/view": "^6.0.1", "@lezer/highlight": "^1.2.0", "@types/codemirror": "5.60.16", - "@types/diff": "8.0.0", "axios": "1.12.1", "codemirror": "6.0.2", "cronstrue": "3.3.0", From 5efc793b08b79a02ad0054a17a1ea492cc86b8de Mon Sep 17 00:00:00 2001 From: Philip Ellis Date: Thu, 19 Mar 2026 11:39:31 -0400 Subject: [PATCH 19/20] updated package-lock --- app/package-lock.json | 681 ------------------------------------------ package-lock.json | 3 +- 2 files changed, 1 insertion(+), 683 deletions(-) delete mode 100644 app/package-lock.json diff --git a/app/package-lock.json b/app/package-lock.json deleted file mode 100644 index fadccb85..00000000 --- a/app/package-lock.json +++ /dev/null @@ -1,681 +0,0 @@ -{ - "name": "sailpoint-ui-development-kit", - "version": "1.0.9", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "sailpoint-ui-development-kit", - "version": "1.0.9", - "dependencies": { - "js-yaml": "^4.1.0", - "sailpoint-api-client": "1.6.9" - } - }, - "node_modules/@babel/runtime": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", - "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "license": "MIT" - }, - "node_modules/agent-base": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", - "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, - "node_modules/ast-types": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", - "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/axios-retry": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-3.9.1.tgz", - "integrity": "sha512-8PJDLJv7qTTMMwdnbMvrLYuvB47M81wRtxQmEdV5w4rgbTXTt+vtPkXwajOfOdSyv/wZICJOC+/UhXH4aQ/R+w==", - "dependencies": { - "@babel/runtime": "^7.15.4", - "is-retry-allowed": "^2.2.0" - } - }, - "node_modules/basic-ftp": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", - "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", - "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/degenerator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", - "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", - "license": "MIT", - "dependencies": { - "ast-types": "^0.13.4", - "escodegen": "^2.1.0", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "license": "BSD-2-Clause", - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-uri": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.4.tgz", - "integrity": "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==", - "license": "MIT", - "dependencies": { - "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.2", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/ip-address": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", - "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", - "license": "MIT", - "dependencies": { - "jsbn": "1.1.0", - "sprintf-js": "^1.1.3" - }, - "engines": { - "node": ">= 12" - } - }, - "node_modules/is-retry-allowed": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", - "integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsbn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", - "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", - "license": "MIT" - }, - "node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/netmask": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", - "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/pac-proxy-agent": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", - "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", - "license": "MIT", - "dependencies": { - "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "get-uri": "^6.0.1", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.6", - "pac-resolver": "^7.0.1", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-resolver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", - "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", - "license": "MIT", - "dependencies": { - "degenerator": "^5.0.0", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-agent": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", - "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.6", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.1.0", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" - }, - "node_modules/sailpoint-api-client": { - "version": "1.6.9", - "resolved": "https://registry.npmjs.org/sailpoint-api-client/-/sailpoint-api-client-1.6.9.tgz", - "integrity": "sha512-gjZzCuRbgY7+21PZwR0zHYtEPrj2JyC08t1EK1F2h8c5EQhEdo3MFCmyXx0gYdBnHto4qeu/GbowyQ8okf0teg==", - "license": "MIT", - "dependencies": { - "axios": "^1.8.3", - "axios-retry": "^3.4.0", - "js-yaml": "^4.1.0", - "proxy-agent": "^6.4.0" - } - }, - "node_modules/sailpoint-api-client/node_modules/axios": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", - "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "license": "MIT", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.4", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", - "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==", - "license": "MIT", - "dependencies": { - "ip-address": "^9.0.5", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "license": "BSD-3-Clause" - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - } - } -} diff --git a/package-lock.json b/package-lock.json index 560ec69d..5ab38ceb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33379,8 +33379,7 @@ }, "node_modules/diff": { "version": "8.0.3", - "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz", - "integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } From 9e407822b8b272ddc4ec665055efbc1ef1238204 Mon Sep 17 00:00:00 2001 From: Philip Ellis Date: Thu, 19 Mar 2026 11:56:28 -0400 Subject: [PATCH 20/20] reverted to npm i --- .github/workflows/windows.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 0919da79..1d967db9 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -40,7 +40,7 @@ jobs: Add-MpPreference -ExclusionPath "$env:APPDATA\npm-cache" - name: Install Dependencies - run: npm ci + run: npm install - name: Install Angular CLI Globally run: npm install -g @angular/cli