import { LitElement, html, css, unsafeCSS } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import {
getTools,
getApprovalWorkflows,
deleteApprovalWorkflow,
createToolConfiguration,
updateToolConfiguration,
getFeatures,
fetchWithAuth,
} from '../../api';
import type { Tool, ApprovalWorkflow } from '../../components/tool-card';
import '../../components/view-header';
import '../../components/approval-workflow-dialog';
import '@shoelace-style/shoelace/dist/components/alert/alert.js';
import '@shoelace-style/shoelace/dist/components/spinner/spinner.js';
import '@shoelace-style/shoelace/dist/components/icon/icon.js';
import '@shoelace-style/shoelace/dist/components/button/button.js';
import '@shoelace-style/shoelace/dist/components/card/card.js';
import '@shoelace-style/shoelace/dist/components/badge/badge.js';
import '@shoelace-style/shoelace/dist/components/divider/divider.js';
import '@shoelace-style/shoelace/dist/components/icon-button/icon-button.js';
import '@shoelace-style/shoelace/dist/components/dialog/dialog.js';
import '@shoelace-style/shoelace/dist/components/input/input.js';
import '@shoelace-style/shoelace/dist/components/textarea/textarea.js';
import '@shoelace-style/shoelace/dist/components/select/select.js';
import '@shoelace-style/shoelace/dist/components/option/option.js';
import '@shoelace-style/shoelace/dist/components/switch/switch.js';
import '@shoelace-style/shoelace/dist/components/tooltip/tooltip.js';
import '@shoelace-style/shoelace/dist/components/radio-group/radio-group.js';
import '@shoelace-style/shoelace/dist/components/radio/radio.js';
import '@shoelace-style/shoelace/dist/components/range/range.js';
import '@shoelace-style/shoelace/dist/components/tab-group/tab-group.js';
import '@shoelace-style/shoelace/dist/components/tab/tab.js';
import '@shoelace-style/shoelace/dist/components/tab-panel/tab-panel.js';
import '@shoelace-style/shoelace/dist/components/details/details.js';
import '@shoelace-style/shoelace/dist/components/copy-button/copy-button.js';
import consoleStyles from '../../styles/console-styles.css?inline';
// Types for tool access rules
interface ToolAccessRule {
toolName: string;
source: string;
sourceId: string | null;
sourceName: string;
action: 'allow' | 'deny' | 'require_approval';
workflowId: string | null;
condition: string | null;
isEnabled: boolean;
configId: string | null;
}
// Types for policy file history
interface PolicyFileHistory {
id: string;
filename: string;
appliedAt: string;
summary: string;
status: 'applied' | 'pending' | 'failed';
}
// Types for diff result
interface DiffChange {
type: 'added' | 'removed' | 'modified';
category: 'mcp_servers' | 'approval_workflows' | 'tools';
name: string;
details?: string;
}
interface PolicyDiffResult {
summary: string;
has_changes: boolean;
changes: {
added: DiffChange[];
removed: DiffChange[];
modified: DiffChange[];
};
}
// Types for policy versions
interface PolicyVersion {
id: string;
version_number: number;
tag: string | null;
description: string | null;
created_at: string;
created_by_username: string | null;
is_active: boolean;
snapshot_summary: {
mcp_servers_count: number;
tools_count: number;
policies_count: number;
};
}
interface CreateVersionRequest {
description?: string;
tag?: string;
}
interface RollbackResponse {
success: boolean;
message: string;
preview_only: boolean;
changes?: PolicyDiffResult;
rolled_back_to_version?: number;
}
interface PruneOptions {
keep_days?: number;
keep_tagged?: boolean;
min_versions_to_keep?: number;
}
interface PruneResponse {
deleted_count: number;
remaining_count: number;
}
@customElement('policies-view')
export class PoliciesView extends LitElement {
@state() private _activeTab = 'access';
@state() private _tools: Tool[] = [];
@state() private _approvalPolicies: ApprovalWorkflow[] = [];
@state() private _loading = false;
@state() private _error: string | null = null;
@state() private _features: { [key: string]: boolean | string[] } = {};
// Access policies state
@state() private _toolAccessRules: ToolAccessRule[] = [];
@state() private _expandedTools: Set<string> = new Set();
// Approval workflows state
@state() private _showPolicyDialog = false;
@state() private _editingPolicy: ApprovalWorkflow | null = null;
// Policy files state
@state() private _policyFileHistory: PolicyFileHistory[] = [];
@state() private _showDiffDialog = false;
@state() private _diffResult: PolicyDiffResult | null = null;
@state() private _pendingFile: File | null = null;
@state() private _isUploading = false;
@state() private _isExporting = false;
// Version management state
@state() private _versions: PolicyVersion[] = [];
@state() private _loadingVersions = false;
@state() private _selectedVersion: PolicyVersion | null = null;
@state() private _expandedVersions: Set<string> = new Set();
@state() private _showSaveVersionDialog = false;
@state() private _showPruneDialog = false;
@state() private _showTagDialog = false;
@state() private _showRollbackDialog = false;
@state() private _rollbackPreview: RollbackResponse | null = null;
@state() private _savingVersion = false;
@state() private _pruningVersions = false;
@state() private _rollingBack = false;
@state() private _taggingVersion = false;
@state() private _deletingVersion = false;
@state() private _versionForm = {
description: '',
tag: '',
};
@state() private _pruneForm = {
keepDays: 30,
keepTagged: true,
minVersionsToKeep: 5,
};
@state() private _tagForm = {
tag: '',
};
@state() private _versionToTag: PolicyVersion | null = null;
@state() private _versionToRollback: PolicyVersion | null = null;
static styles = [
unsafeCSS(consoleStyles),
css`
:host {
display: block;
}
.tabs-container {
margin-bottom: var(--sl-spacing-large);
}
sl-tab-group {
--indicator-color: var(--sl-color-primary-600);
}
sl-tab::part(base) {
font-size: var(--sl-font-size-medium);
padding: var(--sl-spacing-medium) var(--sl-spacing-large);
}
sl-tab-panel {
padding-top: var(--sl-spacing-large);
}
/* Access Policies Tab */
.access-rules-list {
display: flex;
flex-direction: column;
gap: var(--sl-spacing-medium);
}
.access-rule-card {
border: 1px solid var(--sl-color-neutral-200);
border-radius: var(--sl-border-radius-medium);
overflow: hidden;
}
.access-rule-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--sl-spacing-medium);
background: var(--sl-color-neutral-50);
cursor: pointer;
transition: background 0.2s;
}
.access-rule-header:hover {
background: var(--sl-color-neutral-100);
}
.access-rule-info {
display: flex;
align-items: center;
gap: var(--sl-spacing-medium);
flex: 1;
}
.access-rule-name {
font-weight: var(--sl-font-weight-semibold);
color: var(--sl-color-neutral-900);
}
.access-rule-source {
font-size: var(--sl-font-size-small);
color: var(--sl-color-neutral-600);
}
.access-rule-actions {
display: flex;
align-items: center;
gap: var(--sl-spacing-small);
}
.access-rule-details {
padding: var(--sl-spacing-medium);
border-top: 1px solid var(--sl-color-neutral-200);
background: var(--sl-color-neutral-0);
}
.rule-row {
display: flex;
align-items: center;
gap: var(--sl-spacing-medium);
padding: var(--sl-spacing-small) 0;
}
.rule-label {
font-size: var(--sl-font-size-small);
color: var(--sl-color-neutral-700);
min-width: 120px;
}
.rule-value {
flex: 1;
}
/* Approval Workflows Tab */
.policies-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: var(--sl-spacing-large);
}
.policy-card {
display: flex;
flex-direction: column;
height: 100%;
}
.policy-card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--sl-spacing-small);
}
.policy-name {
font-size: var(--sl-font-size-large);
font-weight: var(--sl-font-weight-semibold);
margin: 0;
}
.policy-description {
font-size: var(--sl-font-size-small);
color: var(--sl-color-neutral-600);
margin: 0 0 var(--sl-spacing-medium) 0;
line-height: 1.5;
}
.policy-meta {
display: flex;
flex-wrap: wrap;
gap: var(--sl-spacing-small);
font-size: var(--sl-font-size-x-small);
color: var(--sl-color-neutral-600);
}
.policy-meta-item {
display: flex;
align-items: center;
gap: var(--sl-spacing-2x-small);
}
sl-card::part(footer) {
display: flex;
justify-content: flex-end;
gap: var(--sl-spacing-small);
padding: var(--sl-spacing-medium);
border-top: 1px solid var(--sl-color-neutral-200);
}
/* Policy Files Tab */
.policy-files-container {
display: flex;
flex-direction: column;
gap: var(--sl-spacing-large);
}
.policy-files-actions {
display: flex;
gap: var(--sl-spacing-medium);
flex-wrap: wrap;
}
.upload-area {
border: 2px dashed var(--sl-color-neutral-300);
border-radius: var(--sl-border-radius-large);
padding: var(--sl-spacing-2x-large);
text-align: center;
background: var(--sl-color-neutral-50);
transition: all 0.2s;
}
.upload-area:hover {
border-color: var(--sl-color-primary-400);
background: var(--sl-color-primary-50);
}
.upload-area.drag-over {
border-color: var(--sl-color-primary-600);
background: var(--sl-color-primary-100);
}
.upload-icon {
font-size: 3rem;
color: var(--sl-color-neutral-400);
margin-bottom: var(--sl-spacing-medium);
}
.upload-text {
font-size: var(--sl-font-size-medium);
color: var(--sl-color-neutral-700);
margin-bottom: var(--sl-spacing-small);
}
.upload-hint {
font-size: var(--sl-font-size-small);
color: var(--sl-color-neutral-500);
}
.history-list {
display: flex;
flex-direction: column;
gap: var(--sl-spacing-small);
}
.history-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--sl-spacing-medium);
background: var(--sl-color-neutral-50);
border-radius: var(--sl-border-radius-medium);
border-left: 3px solid var(--sl-color-primary-600);
}
.history-item.failed {
border-left-color: var(--sl-color-danger-600);
}
.history-info {
display: flex;
flex-direction: column;
gap: var(--sl-spacing-2x-small);
}
.history-filename {
font-weight: var(--sl-font-weight-semibold);
}
.history-meta {
font-size: var(--sl-font-size-small);
color: var(--sl-color-neutral-600);
}
/* Diff view */
.diff-container {
max-height: 400px;
overflow-y: auto;
}
.diff-section {
margin-bottom: var(--sl-spacing-large);
}
.diff-section-title {
font-weight: var(--sl-font-weight-semibold);
margin-bottom: var(--sl-spacing-small);
display: flex;
align-items: center;
gap: var(--sl-spacing-small);
}
.diff-item {
padding: var(--sl-spacing-small) var(--sl-spacing-medium);
margin: var(--sl-spacing-2x-small) 0;
border-radius: var(--sl-border-radius-small);
font-size: var(--sl-font-size-small);
}
.diff-item.added {
background: var(--sl-color-success-100);
border-left: 3px solid var(--sl-color-success-600);
}
.diff-item.removed {
background: var(--sl-color-danger-100);
border-left: 3px solid var(--sl-color-danger-600);
}
.diff-item.modified {
background: var(--sl-color-warning-100);
border-left: 3px solid var(--sl-color-warning-600);
}
/* Loading */
.loading-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 200px;
}
/* Form styles */
.form-field {
display: flex;
flex-direction: column;
gap: var(--sl-spacing-2x-small);
margin-bottom: var(--sl-spacing-medium);
}
.form-label {
font-size: var(--sl-font-size-small);
font-weight: var(--sl-font-weight-semibold);
color: var(--sl-color-neutral-700);
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--sl-spacing-medium);
}
/* Dialog styles */
sl-dialog::part(panel) {
max-width: 600px;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: var(--sl-spacing-small);
}
/* Version management styles */
.versions-section {
margin-top: var(--sl-spacing-large);
}
.versions-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--sl-spacing-medium);
}
.versions-header h3 {
margin: 0;
font-size: var(--sl-font-size-large);
font-weight: var(--sl-font-weight-semibold);
}
.versions-actions {
display: flex;
gap: var(--sl-spacing-small);
}
.version-list {
display: flex;
flex-direction: column;
gap: var(--sl-spacing-small);
}
.version-item {
border: 1px solid var(--sl-color-neutral-200);
border-radius: var(--sl-border-radius-medium);
overflow: hidden;
background: var(--sl-color-neutral-0);
}
.version-item.active {
border-color: var(--sl-color-primary-400);
border-width: 2px;
}
.version-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--sl-spacing-medium);
background: var(--sl-color-neutral-50);
cursor: pointer;
transition: background 0.2s;
}
.version-header:hover {
background: var(--sl-color-neutral-100);
}
.version-info {
display: flex;
align-items: center;
gap: var(--sl-spacing-medium);
flex: 1;
}
.version-number {
font-weight: var(--sl-font-weight-bold);
font-size: var(--sl-font-size-medium);
color: var(--sl-color-neutral-900);
min-width: 60px;
}
.version-meta {
display: flex;
flex-direction: column;
gap: var(--sl-spacing-2x-small);
}
.version-description {
font-size: var(--sl-font-size-small);
color: var(--sl-color-neutral-700);
}
.version-date {
font-size: var(--sl-font-size-x-small);
color: var(--sl-color-neutral-500);
}
.version-badges {
display: flex;
gap: var(--sl-spacing-x-small);
align-items: center;
}
.version-actions {
display: flex;
gap: var(--sl-spacing-2x-small);
align-items: center;
}
.version-details {
padding: var(--sl-spacing-medium);
border-top: 1px solid var(--sl-color-neutral-200);
background: var(--sl-color-neutral-0);
}
.version-stats {
display: flex;
gap: var(--sl-spacing-large);
flex-wrap: wrap;
}
.version-stat {
display: flex;
align-items: center;
gap: var(--sl-spacing-x-small);
font-size: var(--sl-font-size-small);
color: var(--sl-color-neutral-600);
}
.version-stat sl-icon {
color: var(--sl-color-neutral-500);
}
.empty-versions {
text-align: center;
padding: var(--sl-spacing-2x-large);
color: var(--sl-color-neutral-500);
}
.rollback-preview {
margin-top: var(--sl-spacing-medium);
padding: var(--sl-spacing-medium);
background: var(--sl-color-neutral-50);
border-radius: var(--sl-border-radius-medium);
}
.rollback-warning {
display: flex;
align-items: flex-start;
gap: var(--sl-spacing-small);
padding: var(--sl-spacing-medium);
background: var(--sl-color-warning-100);
border-radius: var(--sl-border-radius-medium);
margin-bottom: var(--sl-spacing-medium);
}
.rollback-warning sl-icon {
color: var(--sl-color-warning-700);
flex-shrink: 0;
}
`,
];
connectedCallback() {
super.connectedCallback();
this.loadData();
}
private async loadData() {
this._loading = true;
this._error = null;
try {
const [tools, policies, featuresResponse] = await Promise.all([
getTools(),
getApprovalWorkflows(),
getFeatures(),
]);
this._tools = tools;
this._approvalPolicies = policies;
this._features = featuresResponse.features || {};
// Build tool access rules from tools
this._toolAccessRules = this._tools.map((tool) => ({
toolName: tool.name,
source: tool.source,
sourceId: tool.source_id,
sourceName: tool.source_name,
action: tool.approval_workflow_id
? 'require_approval'
: tool.is_enabled
? 'allow'
: 'deny',
workflowId: tool.approval_workflow_id,
condition: tool.has_approval_condition ? '(condition set)' : null,
isEnabled: tool.is_enabled,
configId: tool.config_id,
}));
} catch (err: any) {
this._error = err.message || 'Failed to load data';
console.error('Error loading policies data:', err);
} finally {
this._loading = false;
}
}
private hasAdvancedApprovals(): boolean {
return this._features['advanced_approvals'] === true;
}
private toggleToolExpanded(toolKey: string) {
const newExpanded = new Set(this._expandedTools);
if (newExpanded.has(toolKey)) {
newExpanded.delete(toolKey);
} else {
newExpanded.add(toolKey);
}
this._expandedTools = newExpanded;
}
private getToolKey(rule: ToolAccessRule): string {
return `${rule.toolName}-${rule.source}-${rule.sourceId || 'null'}`;
}
private async handleAccessActionChange(
rule: ToolAccessRule,
newAction: 'allow' | 'deny' | 'require_approval'
) {
try {
const tool = this._tools.find(
(t) =>
t.name === rule.toolName &&
t.source === rule.source &&
t.source_id === rule.sourceId
);
if (!tool) return;
if (newAction === 'deny') {
// Disable the tool
if (tool.config_id) {
await updateToolConfiguration(tool.config_id, {
is_enabled: false,
approval_workflow_id: null,
});
} else {
await createToolConfiguration({
tool_name: tool.name,
tool_source: tool.source,
mcp_server_id: tool.source_id,
is_enabled: false,
account_id: '',
});
}
} else if (newAction === 'allow') {
// Enable the tool without approval
if (tool.config_id) {
await updateToolConfiguration(tool.config_id, {
is_enabled: true,
approval_workflow_id: null,
});
} else {
await createToolConfiguration({
tool_name: tool.name,
tool_source: tool.source,
mcp_server_id: tool.source_id,
is_enabled: true,
account_id: '',
});
}
} else if (newAction === 'require_approval') {
// Enable with default approval workflow
const defaultPolicy =
this._approvalPolicies.find((p) => p.is_default) ||
this._approvalPolicies[0];
if (tool.config_id) {
await updateToolConfiguration(tool.config_id, {
is_enabled: true,
approval_workflow_id: defaultPolicy?.id || null,
});
} else {
await createToolConfiguration({
tool_name: tool.name,
tool_source: tool.source,
mcp_server_id: tool.source_id,
is_enabled: true,
approval_workflow_id: defaultPolicy?.id || null,
account_id: '',
});
}
}
await this.loadData();
} catch (err: any) {
this._error = err.message || 'Failed to update tool access';
}
}
private openPolicyDialog(policy: ApprovalWorkflow | null = null) {
this._editingPolicy = policy;
this._showPolicyDialog = true;
}
private closePolicyDialog() {
this._showPolicyDialog = false;
this._editingPolicy = null;
}
private async deletePolicy(policy: ApprovalWorkflow) {
if (
!confirm(
`Are you sure you want to delete the policy "${policy.name}"? This cannot be undone.`
)
) {
return;
}
try {
await deleteApprovalWorkflow(policy.id);
await this.loadData();
} catch (err: any) {
this._error = err.message || 'Failed to delete policy';
}
}
private async handleFileUpload(event: Event) {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
if (!file) return;
await this.previewPolicyFile(file);
input.value = '';
}
private async previewPolicyFile(file: File) {
this._pendingFile = file;
this._isUploading = true;
try {
// Get diff preview
const formData = new FormData();
formData.append('file', file);
const response = await fetchWithAuth('/api/v1/policies/diff', {
method: 'POST',
body: formData,
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail?.message || 'Failed to preview policy');
}
this._diffResult = await response.json();
this._showDiffDialog = true;
} catch (err: any) {
this._error = err.message || 'Failed to preview policy file';
} finally {
this._isUploading = false;
}
}
private async applyPolicyFile() {
if (!this._pendingFile) return;
this._isUploading = true;
try {
const formData = new FormData();
formData.append('file', this._pendingFile);
const response = await fetchWithAuth('/api/v1/policies/upload', {
method: 'POST',
body: formData,
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail?.message || 'Failed to apply policy');
}
this._showDiffDialog = false;
this._pendingFile = null;
this._diffResult = null;
await this.loadData();
} catch (err: any) {
this._error = err.message || 'Failed to apply policy file';
} finally {
this._isUploading = false;
}
}
private async exportPolicies() {
this._isExporting = true;
try {
const response = await fetchWithAuth(
'/api/v1/policies/export?format=yaml'
);
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to export policies');
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'policies.yaml';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
} catch (err: any) {
this._error = err.message || 'Failed to export policies';
} finally {
this._isExporting = false;
}
}
// ============================================================================
// Version Management API Methods
// ============================================================================
private async loadVersions() {
this._loadingVersions = true;
try {
const response = await fetchWithAuth(
'/api/v1/policies/versions?limit=50'
);
if (!response.ok) {
throw new Error('Failed to fetch versions');
}
this._versions = await response.json();
} catch (err: any) {
this._error = err.message || 'Failed to load versions';
} finally {
this._loadingVersions = false;
}
}
private async createVersion() {
this._savingVersion = true;
try {
const body: CreateVersionRequest = {};
if (this._versionForm.description.trim()) {
body.description = this._versionForm.description.trim();
}
if (this._versionForm.tag.trim()) {
body.tag = this._versionForm.tag.trim();
}
const response = await fetchWithAuth('/api/v1/policies/versions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!response.ok) {
const error = await response.json();
throw new Error(
error.detail?.message || error.detail || 'Failed to save version'
);
}
this._showSaveVersionDialog = false;
this._versionForm = { description: '', tag: '' };
await this.loadVersions();
} catch (err: any) {
this._error = err.message || 'Failed to save version';
} finally {
this._savingVersion = false;
}
}
private async rollbackToVersion(versionId: string, previewOnly: boolean) {
if (previewOnly) {
this._rollingBack = true;
}
try {
const response = await fetchWithAuth(
`/api/v1/policies/versions/${versionId}/rollback`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ preview_only: previewOnly }),
}
);
if (!response.ok) {
const error = await response.json();
throw new Error(
error.detail?.message || error.detail || 'Failed to rollback'
);
}
const result: RollbackResponse = await response.json();
if (previewOnly) {
this._rollbackPreview = result;
this._showRollbackDialog = true;
} else {
this._showRollbackDialog = false;
this._rollbackPreview = null;
this._versionToRollback = null;
// Refresh everything after successful rollback
await Promise.all([this.loadData(), this.loadVersions()]);
}
} catch (err: any) {
this._error = err.message || 'Failed to rollback to version';
} finally {
this._rollingBack = false;
}
}
private async tagVersion(versionId: string, tag: string) {
this._taggingVersion = true;
try {
const response = await fetchWithAuth(
`/api/v1/policies/versions/${versionId}/tag`,
{
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tag: tag.trim() || null }),
}
);
if (!response.ok) {
const error = await response.json();
throw new Error(
error.detail?.message || error.detail || 'Failed to update tag'
);
}
this._showTagDialog = false;
this._versionToTag = null;
this._tagForm = { tag: '' };
await this.loadVersions();
} catch (err: any) {
this._error = err.message || 'Failed to update tag';
} finally {
this._taggingVersion = false;
}
}
private async deleteVersion(version: PolicyVersion) {
if (
!confirm(
`Are you sure you want to delete version ${version.version_number}${version.tag ? ` (${version.tag})` : ''}? This cannot be undone.`
)
) {
return;
}
this._deletingVersion = true;
try {
const response = await fetchWithAuth(
`/api/v1/policies/versions/${version.id}`,
{ method: 'DELETE' }
);
if (!response.ok) {
const error = await response.json();
throw new Error(
error.detail?.message || error.detail || 'Failed to delete version'
);
}
await this.loadVersions();
} catch (err: any) {
this._error = err.message || 'Failed to delete version';
} finally {
this._deletingVersion = false;
}
}
private async pruneVersions() {
this._pruningVersions = true;
try {
const body: PruneOptions = {
keep_days: this._pruneForm.keepDays,
keep_tagged: this._pruneForm.keepTagged,
min_versions_to_keep: this._pruneForm.minVersionsToKeep,
};
const response = await fetchWithAuth('/api/v1/policies/versions/prune', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!response.ok) {
const error = await response.json();
throw new Error(
error.detail?.message || error.detail || 'Failed to prune versions'
);
}
const result: PruneResponse = await response.json();
this._showPruneDialog = false;
// Show success message
const alertEl = document.createElement('sl-alert');
alertEl.variant = 'success';
alertEl.closable = true;
alertEl.duration = 5000;
alertEl.innerHTML = `
<sl-icon slot="icon" name="check-circle"></sl-icon>
Pruned ${result.deleted_count} old versions. ${result.remaining_count} versions remaining.
`;
document.body.appendChild(alertEl);
alertEl.toast();
await this.loadVersions();
} catch (err: any) {
this._error = err.message || 'Failed to prune versions';
} finally {
this._pruningVersions = false;
}
}
private toggleVersionExpanded(versionId: string) {
const newExpanded = new Set(this._expandedVersions);
if (newExpanded.has(versionId)) {
newExpanded.delete(versionId);
} else {
newExpanded.add(versionId);
}
this._expandedVersions = newExpanded;
}
private openTagDialog(version: PolicyVersion) {
this._versionToTag = version;
this._tagForm = { tag: version.tag || '' };
this._showTagDialog = true;
}
private openRollbackPreview(version: PolicyVersion) {
this._versionToRollback = version;
this.rollbackToVersion(version.id, true);
}
private formatVersionDate(dateStr: string): string {
const date = new Date(dateStr);
return date.toLocaleDateString(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
}
private renderAccessPoliciesTab() {
const sortedRules = [...this._toolAccessRules].sort((a, b) => {
// Sort by source first, then by name
if (a.source !== b.source) {
if (a.source === 'builtin') return -1;
if (b.source === 'builtin') return 1;
}
return a.toolName.localeCompare(b.toolName);
});
return html`
<div class="access-rules-list">
${sortedRules.length === 0
? html`
<div class="empty-state">
<sl-icon name="tools"></sl-icon>
<p>No tools configured. Add an MCP server to get started.</p>
<sl-button href="/console/tools" variant="primary">
Go to Tools
</sl-button>
</div>
`
: repeat(
sortedRules,
(rule) => this.getToolKey(rule),
(rule) => this.renderAccessRuleCard(rule)
)}
</div>
`;
}
private renderAccessRuleCard(rule: ToolAccessRule) {
const toolKey = this.getToolKey(rule);
const isExpanded = this._expandedTools.has(toolKey);
const assignedPolicy = this._approvalPolicies.find(
(p) => p.id === rule.workflowId
);
return html`
<div class="access-rule-card">
<div
class="access-rule-header"
@click=${() => this.toggleToolExpanded(toolKey)}
>
<div class="access-rule-info">
<sl-icon
name=${isExpanded ? 'chevron-down' : 'chevron-right'}
></sl-icon>
<div>
<div class="access-rule-name">${rule.toolName}</div>
<div class="access-rule-source">${rule.sourceName}</div>
</div>
</div>
<div
class="access-rule-actions"
@click=${(e: Event) => e.stopPropagation()}
>
<sl-badge
variant=${rule.action === 'allow'
? 'success'
: rule.action === 'deny'
? 'danger'
: 'warning'}
>
${rule.action === 'allow'
? 'Allowed'
: rule.action === 'deny'
? 'Denied'
: 'Approval Required'}
</sl-badge>
<sl-select
size="small"
value=${rule.action}
@sl-change=${(e: any) =>
this.handleAccessActionChange(rule, e.target.value)}
style="min-width: 160px;"
>
<sl-option value="allow">Allow</sl-option>
<sl-option value="deny">Deny</sl-option>
<sl-option value="require_approval">Require Approval</sl-option>
</sl-select>
</div>
</div>
${isExpanded
? html`
<div class="access-rule-details">
<div class="rule-row">
<span class="rule-label">Source:</span>
<span class="rule-value">
<sl-badge variant="neutral" size="small">
${rule.source}
</sl-badge>
</span>
</div>
<div class="rule-row">
<span class="rule-label">Enabled:</span>
<span class="rule-value">
${rule.isEnabled ? 'Yes' : 'No'}
</span>
</div>
${rule.action === 'require_approval'
? html`
<div class="rule-row">
<span class="rule-label">Policy:</span>
<span class="rule-value">
${assignedPolicy
? assignedPolicy.name
: 'Default Policy'}
</span>
</div>
${rule.condition
? html`
<div class="rule-row">
<span class="rule-label">Condition:</span>
<span class="rule-value">
<code>${rule.condition}</code>
</span>
</div>
`
: ''}
`
: ''}
</div>
`
: ''}
</div>
`;
}
private renderApprovalPoliciesTab() {
return html`
<div style="margin-bottom: var(--sl-spacing-large);">
<sl-button variant="primary" @click=${() => this.openPolicyDialog()}>
<sl-icon slot="prefix" name="plus-lg"></sl-icon>
Create Approval Workflow
</sl-button>
</div>
${this._approvalPolicies.length === 0
? html`
<div class="empty-state">
<sl-icon name="shield-check"></sl-icon>
<p>No approval workflows configured yet.</p>
<p
style="font-size: var(--sl-font-size-small); color: var(--sl-color-neutral-500);"
>
Create an approval workflow to define how tool executions are
approved (human, AI, Slack, etc.).
</p>
</div>
`
: html`
<div class="policies-grid">
${repeat(
this._approvalPolicies,
(policy) => policy.id,
(policy) => this.renderPolicyCard(policy)
)}
</div>
`}
<approval-workflow-dialog
?open=${this._showPolicyDialog}
.policy=${this._editingPolicy}
.existingPolicies=${this._approvalPolicies}
.features=${this._features}
@close=${this.closePolicyDialog}
@saved=${this._handlePolicySaved}
@add-model=${this._handleAddModel}
></approval-workflow-dialog>
`;
}
private async _handlePolicySaved() {
await this.loadData();
}
private _handleAddModel() {
// Navigate to model configuration
window.location.href = '/console/settings/models';
}
private renderPolicyCard(policy: ApprovalWorkflow) {
const isAiDriven = policy.approval_type === 'ai_driven';
return html`
<sl-card class="policy-card">
<div class="card-content">
<div class="policy-card-header">
<h3 class="policy-name">${policy.name}</h3>
<div style="display: flex; gap: var(--sl-spacing-x-small);">
${isAiDriven
? html`<sl-badge variant="warning">
<sl-icon name="robot" style="margin-right: 4px;"></sl-icon>
AI-Driven
</sl-badge>`
: ''}
${policy.is_default
? html`<sl-badge variant="primary">Default</sl-badge>`
: ''}
</div>
</div>
<p class="policy-description">
${policy.description || 'No description'}
</p>
<div class="policy-meta">
${isAiDriven
? html`
<div class="policy-meta-item">
<sl-icon name="cpu"></sl-icon>
<span>${policy.ai_model || 'No model set'}</span>
</div>
<div class="policy-meta-item">
<sl-icon name="speedometer2"></sl-icon>
<span>
${Math.round(
(policy.ai_confidence_threshold || 0.8) * 100
)}%
threshold
</span>
</div>
<div class="policy-meta-item">
<sl-badge variant="neutral" size="small">
${policy.ai_fallback_behavior === 'escalate'
? 'Escalates when uncertain'
: policy.ai_fallback_behavior === 'approve'
? 'Auto-approves when uncertain'
: 'Auto-denies when uncertain'}
</sl-badge>
</div>
`
: html`
<div class="policy-meta-item">
<sl-icon name="clock"></sl-icon>
<span>${policy.timeout_seconds || 300}s timeout</span>
</div>
<div class="policy-meta-item">
<sl-icon name="people"></sl-icon>
<span>${policy.approvals_required || 1} approval(s)</span>
</div>
<div class="policy-meta-item">
<sl-badge variant="neutral" size="small">
${policy.approval_type}
</sl-badge>
</div>
`}
</div>
</div>
<div slot="footer">
<sl-button
size="small"
variant="danger"
outline
@click=${() => this.deletePolicy(policy)}
>
<sl-icon slot="prefix" name="trash"></sl-icon>
Delete
</sl-button>
<sl-button size="small" @click=${() => this.openPolicyDialog(policy)}>
<sl-icon slot="prefix" name="pencil"></sl-icon>
Edit
</sl-button>
</div>
</sl-card>
`;
}
private renderPolicyFilesTab() {
// Load versions when tab is shown if not already loaded
if (this._versions.length === 0 && !this._loadingVersions) {
this.loadVersions();
}
return html`
<div class="policy-files-container">
<div class="policy-files-actions">
<sl-button
variant="primary"
@click=${() =>
this.shadowRoot
?.querySelector<HTMLInputElement>('#policy-file-input')
?.click()}
?loading=${this._isUploading}
>
<sl-icon slot="prefix" name="upload"></sl-icon>
Import YAML
</sl-button>
<sl-button
@click=${this.exportPolicies}
?loading=${this._isExporting}
>
<sl-icon slot="prefix" name="download"></sl-icon>
Export YAML
</sl-button>
<input
type="file"
id="policy-file-input"
accept=".yaml,.yml,.json"
@change=${this.handleFileUpload}
style="display: none"
/>
</div>
<sl-card>
<div slot="header">Policy File Format</div>
<p
style="font-size: var(--sl-font-size-small); color: var(--sl-color-neutral-600); margin: 0 0 var(--sl-spacing-medium) 0;"
>
Preloop supports declarative policy-as-code using YAML files. Define
MCP servers, approval workflows, and tool configurations in a single
file.
</p>
<sl-details summary="Example Policy File">
<pre
style="background: var(--sl-color-neutral-100); padding: var(--sl-spacing-medium); border-radius: var(--sl-border-radius-medium); font-size: var(--sl-font-size-small); overflow-x: auto;"
><code>version: "1.0"
metadata:
name: "Production Safeguards"
description: "Safety policies for production environment"
approval_workflows:
- name: "Critical Operations"
approval_type: standard
approvals_required: 2
timeout_seconds: 600
is_default: true
tools:
- name: "shell"
action: require_approval
policy: "Critical Operations"
condition: 'args.command.contains("rm") || args.command.contains("sudo")'
- name: "file_write"
action: require_approval
policy: "Critical Operations"
defaults:
require_approval: false
enabled: true</code></pre>
</sl-details>
</sl-card>
${this._policyFileHistory.length > 0
? html`
<sl-card>
<div slot="header">Import History</div>
<div class="history-list">
${repeat(
this._policyFileHistory,
(item) => item.id,
(item) => html`
<div
class="history-item ${item.status === 'failed'
? 'failed'
: ''}"
>
<div class="history-info">
<span class="history-filename">${item.filename}</span>
<span class="history-meta">
${item.appliedAt} - ${item.summary}
</span>
</div>
<sl-badge
variant=${item.status === 'applied'
? 'success'
: item.status === 'failed'
? 'danger'
: 'neutral'}
>
${item.status}
</sl-badge>
</div>
`
)}
</div>
</sl-card>
`
: ''}
<!-- Version Management Section -->
${this.renderVersionsSection()}
</div>
${this.renderDiffDialog()} ${this.renderSaveVersionDialog()}
${this.renderPruneVersionsDialog()} ${this.renderTagVersionDialog()}
${this.renderRollbackConfirmDialog()}
`;
}
private renderVersionsSection() {
return html`
<div class="versions-section">
<div class="versions-header">
<h3>Version History</h3>
<div class="versions-actions">
<sl-button
size="small"
variant="primary"
@click=${() => (this._showSaveVersionDialog = true)}
>
<sl-icon slot="prefix" name="save"></sl-icon>
Save Version
</sl-button>
<sl-button
size="small"
@click=${() => (this._showPruneDialog = true)}
?disabled=${this._versions.length === 0}
>
<sl-icon slot="prefix" name="trash"></sl-icon>
Prune Old Versions
</sl-button>
<sl-button
size="small"
@click=${() => this.loadVersions()}
?loading=${this._loadingVersions}
>
<sl-icon slot="prefix" name="arrow-clockwise"></sl-icon>
Refresh
</sl-button>
</div>
</div>
${this._loadingVersions
? html`
<div class="loading-container">
<sl-spinner></sl-spinner>
</div>
`
: this._versions.length === 0
? html`
<div class="empty-versions">
<sl-icon
name="clock-history"
style="font-size: 3rem; margin-bottom: var(--sl-spacing-medium);"
></sl-icon>
<p>No versions saved yet.</p>
<p style="font-size: var(--sl-font-size-small);">
Save a version to create a snapshot of your current policy
configuration.
</p>
</div>
`
: html`
<div class="version-list">
${repeat(
this._versions,
(v) => v.id,
(version) => this.renderVersionItem(version)
)}
</div>
`}
</div>
`;
}
private renderVersionItem(version: PolicyVersion) {
const isExpanded = this._expandedVersions.has(version.id);
return html`
<div class="version-item ${version.is_active ? 'active' : ''}">
<div
class="version-header"
@click=${() => this.toggleVersionExpanded(version.id)}
>
<div class="version-info">
<sl-icon
name=${isExpanded ? 'chevron-down' : 'chevron-right'}
></sl-icon>
<span class="version-number">v${version.version_number}</span>
<div class="version-meta">
<span class="version-description">
${version.description || 'No description'}
</span>
<span class="version-date">
${this.formatVersionDate(version.created_at)}
${version.created_by_username
? ` by ${version.created_by_username}`
: ''}
</span>
</div>
</div>
<div class="version-badges">
${version.is_active
? html`<sl-badge variant="success">Active</sl-badge>`
: ''}
${version.tag
? html`<sl-badge variant="primary">${version.tag}</sl-badge>`
: ''}
</div>
<div
class="version-actions"
@click=${(e: Event) => e.stopPropagation()}
>
<sl-tooltip content="View Diff">
<sl-icon-button
name="file-diff"
@click=${() => this.openRollbackPreview(version)}
?disabled=${version.is_active}
></sl-icon-button>
</sl-tooltip>
<sl-tooltip content="Rollback to this version">
<sl-icon-button
name="arrow-counterclockwise"
@click=${() => this.openRollbackPreview(version)}
?disabled=${version.is_active}
></sl-icon-button>
</sl-tooltip>
<sl-tooltip content="Edit Tag">
<sl-icon-button
name="tag"
@click=${() => this.openTagDialog(version)}
></sl-icon-button>
</sl-tooltip>
<sl-tooltip content="Delete">
<sl-icon-button
name="trash"
@click=${() => this.deleteVersion(version)}
?disabled=${version.is_active || this._deletingVersion}
></sl-icon-button>
</sl-tooltip>
</div>
</div>
${isExpanded
? html`
<div class="version-details">
<div class="version-stats">
<div class="version-stat">
<sl-icon name="hdd-network"></sl-icon>
<span>
${version.snapshot_summary.mcp_servers_count} MCP servers
</span>
</div>
<div class="version-stat">
<sl-icon name="tools"></sl-icon>
<span>${version.snapshot_summary.tools_count} tools</span>
</div>
<div class="version-stat">
<sl-icon name="shield-check"></sl-icon>
<span>
${version.snapshot_summary.policies_count} policies
</span>
</div>
</div>
</div>
`
: ''}
</div>
`;
}
private renderSaveVersionDialog() {
return html`
<sl-dialog
label="Save Version"
?open=${this._showSaveVersionDialog}
@sl-request-close=${() => (this._showSaveVersionDialog = false)}
>
<p style="margin-top: 0;">
Create a snapshot of your current policy configuration. You can
rollback to this version later if needed.
</p>
<div class="form-field">
<label class="form-label">Description</label>
<sl-textarea
placeholder="Optional description of this version"
.value=${this._versionForm.description}
@sl-input=${(e: any) =>
(this._versionForm = {
...this._versionForm,
description: e.target.value,
})}
rows="3"
></sl-textarea>
</div>
<div class="form-field">
<label class="form-label">Tag (optional)</label>
<sl-input
placeholder="e.g., production-v1, stable, release-2024-01"
.value=${this._versionForm.tag}
@sl-input=${(e: any) =>
(this._versionForm = {
...this._versionForm,
tag: e.target.value,
})}
></sl-input>
<small style="color: var(--sl-color-neutral-500);">
Tagged versions can be protected from pruning.
</small>
</div>
<div slot="footer" class="dialog-footer">
<sl-button @click=${() => (this._showSaveVersionDialog = false)}>
Cancel
</sl-button>
<sl-button
variant="primary"
@click=${() => this.createVersion()}
?loading=${this._savingVersion}
>
Save Version
</sl-button>
</div>
</sl-dialog>
`;
}
private renderPruneVersionsDialog() {
return html`
<sl-dialog
label="Prune Old Versions"
?open=${this._showPruneDialog}
@sl-request-close=${() => (this._showPruneDialog = false)}
>
<p style="margin-top: 0;">
Remove old versions to save space. Configure the criteria for which
versions to keep.
</p>
<div class="form-field">
<label class="form-label">Keep versions newer than (days)</label>
<sl-input
type="number"
min="1"
.value=${String(this._pruneForm.keepDays)}
@sl-input=${(e: any) =>
(this._pruneForm = {
...this._pruneForm,
keepDays: parseInt(e.target.value) || 30,
})}
></sl-input>
</div>
<div class="form-field">
<label class="form-label">Minimum versions to keep</label>
<sl-input
type="number"
min="1"
.value=${String(this._pruneForm.minVersionsToKeep)}
@sl-input=${(e: any) =>
(this._pruneForm = {
...this._pruneForm,
minVersionsToKeep: parseInt(e.target.value) || 5,
})}
></sl-input>
</div>
<div class="form-field">
<div
style="display: flex; justify-content: space-between; align-items: center;"
>
<label class="form-label" style="margin-bottom: 0;">
Keep tagged versions
</label>
<sl-switch
?checked=${this._pruneForm.keepTagged}
@sl-change=${(e: any) =>
(this._pruneForm = {
...this._pruneForm,
keepTagged: e.target.checked,
})}
></sl-switch>
</div>
<small style="color: var(--sl-color-neutral-500);">
Tagged versions will not be deleted regardless of age.
</small>
</div>
<div slot="footer" class="dialog-footer">
<sl-button @click=${() => (this._showPruneDialog = false)}>
Cancel
</sl-button>
<sl-button
variant="danger"
@click=${() => this.pruneVersions()}
?loading=${this._pruningVersions}
>
Prune Versions
</sl-button>
</div>
</sl-dialog>
`;
}
private renderTagVersionDialog() {
return html`
<sl-dialog
label="Edit Version Tag"
?open=${this._showTagDialog}
@sl-request-close=${() => {
this._showTagDialog = false;
this._versionToTag = null;
}}
>
${this._versionToTag
? html`
<p style="margin-top: 0;">
Update the tag for version ${this._versionToTag.version_number}.
Leave empty to remove the tag.
</p>
<div class="form-field">
<label class="form-label">Tag</label>
<sl-input
placeholder="e.g., production-v1, stable"
.value=${this._tagForm.tag}
@sl-input=${(e: any) =>
(this._tagForm = { tag: e.target.value })}
></sl-input>
</div>
<div slot="footer" class="dialog-footer">
<sl-button
@click=${() => {
this._showTagDialog = false;
this._versionToTag = null;
}}
>
Cancel
</sl-button>
<sl-button
variant="primary"
@click=${() =>
this.tagVersion(this._versionToTag!.id, this._tagForm.tag)}
?loading=${this._taggingVersion}
>
Save Tag
</sl-button>
</div>
`
: ''}
</sl-dialog>
`;
}
private renderRollbackConfirmDialog() {
return html`
<sl-dialog
label="Rollback to Version"
?open=${this._showRollbackDialog}
@sl-request-close=${() => {
this._showRollbackDialog = false;
this._rollbackPreview = null;
this._versionToRollback = null;
}}
style="--width: 700px;"
>
${this._versionToRollback
? html`
<div class="rollback-warning">
<sl-icon name="exclamation-triangle"></sl-icon>
<div>
<strong>Warning:</strong> Rolling back will replace your
current policy configuration with the snapshot from version
${this._versionToRollback.version_number}. This action cannot
be automatically undone.
</div>
</div>
${this._rollbackPreview
? html`
<p style="margin-top: 0;">
${this._rollbackPreview.changes?.has_changes
? 'The following changes will be made:'
: 'No changes would be made by this rollback.'}
</p>
${this._rollbackPreview.changes?.has_changes
? html`
<div class="diff-container">
${this._rollbackPreview.changes.changes.added
.length > 0
? html`
<div class="diff-section">
<div class="diff-section-title">
<sl-icon
name="plus-circle-fill"
style="color: var(--sl-color-success-600);"
></sl-icon>
Added
(${this._rollbackPreview.changes.changes
.added.length})
</div>
${this._rollbackPreview.changes.changes.added.map(
(change) => html`
<div class="diff-item added">
<strong>${change.category}:</strong>
${change.name}
</div>
`
)}
</div>
`
: ''}
${this._rollbackPreview.changes.changes.modified
.length > 0
? html`
<div class="diff-section">
<div class="diff-section-title">
<sl-icon
name="pencil-fill"
style="color: var(--sl-color-warning-600);"
></sl-icon>
Modified
(${this._rollbackPreview.changes.changes
.modified.length})
</div>
${this._rollbackPreview.changes.changes.modified.map(
(change) => html`
<div class="diff-item modified">
<strong>${change.category}:</strong>
${change.name}
</div>
`
)}
</div>
`
: ''}
${this._rollbackPreview.changes.changes.removed
.length > 0
? html`
<div class="diff-section">
<div class="diff-section-title">
<sl-icon
name="dash-circle-fill"
style="color: var(--sl-color-danger-600);"
></sl-icon>
Removed
(${this._rollbackPreview.changes.changes
.removed.length})
</div>
${this._rollbackPreview.changes.changes.removed.map(
(change) => html`
<div class="diff-item removed">
<strong>${change.category}:</strong>
${change.name}
</div>
`
)}
</div>
`
: ''}
</div>
`
: ''}
`
: html`
<div class="loading-container">
<sl-spinner></sl-spinner>
</div>
`}
<div slot="footer" class="dialog-footer">
<sl-button
@click=${() => {
this._showRollbackDialog = false;
this._rollbackPreview = null;
this._versionToRollback = null;
}}
>
Cancel
</sl-button>
<sl-button
variant="danger"
@click=${() =>
this.rollbackToVersion(this._versionToRollback!.id, false)}
?loading=${this._rollingBack}
?disabled=${!this._rollbackPreview?.changes?.has_changes}
>
Confirm Rollback
</sl-button>
</div>
`
: ''}
</sl-dialog>
`;
}
private renderDiffDialog() {
return html`
<sl-dialog
label="Preview Policy Changes"
?open=${this._showDiffDialog}
@sl-request-close=${() => {
this._showDiffDialog = false;
this._pendingFile = null;
this._diffResult = null;
}}
style="--width: 700px;"
>
${this._diffResult
? html`
<p style="margin-top: 0;">
${this._diffResult.summary ||
(this._diffResult.has_changes
? 'The following changes will be applied:'
: 'No changes detected.')}
</p>
${this._diffResult.has_changes
? html`
<div class="diff-container">
${this._diffResult.changes.added.length > 0
? html`
<div class="diff-section">
<div class="diff-section-title">
<sl-icon
name="plus-circle-fill"
style="color: var(--sl-color-success-600);"
></sl-icon>
Added (${this._diffResult.changes.added.length})
</div>
${this._diffResult.changes.added.map(
(change) => html`
<div class="diff-item added">
<strong>${change.category}:</strong>
${change.name}
${change.details
? html`<br /><small
>${change.details}</small
>`
: ''}
</div>
`
)}
</div>
`
: ''}
${this._diffResult.changes.modified.length > 0
? html`
<div class="diff-section">
<div class="diff-section-title">
<sl-icon
name="pencil-fill"
style="color: var(--sl-color-warning-600);"
></sl-icon>
Modified
(${this._diffResult.changes.modified.length})
</div>
${this._diffResult.changes.modified.map(
(change) => html`
<div class="diff-item modified">
<strong>${change.category}:</strong>
${change.name}
${change.details
? html`<br /><small
>${change.details}</small
>`
: ''}
</div>
`
)}
</div>
`
: ''}
${this._diffResult.changes.removed.length > 0
? html`
<div class="diff-section">
<div class="diff-section-title">
<sl-icon
name="dash-circle-fill"
style="color: var(--sl-color-danger-600);"
></sl-icon>
Removed
(${this._diffResult.changes.removed.length})
</div>
${this._diffResult.changes.removed.map(
(change) => html`
<div class="diff-item removed">
<strong>${change.category}:</strong>
${change.name}
</div>
`
)}
</div>
`
: ''}
</div>
`
: ''}
`
: html`
<div class="loading-container">
<sl-spinner></sl-spinner>
</div>
`}
<div slot="footer" class="dialog-footer">
<sl-button
@click=${() => {
this._showDiffDialog = false;
this._pendingFile = null;
this._diffResult = null;
}}
>
Cancel
</sl-button>
<sl-button
variant="primary"
@click=${this.applyPolicyFile}
?loading=${this._isUploading}
?disabled=${!this._diffResult?.has_changes}
>
Apply Changes
</sl-button>
</div>
</sl-dialog>
`;
}
render() {
return html`
<view-header headerText="Governance" width="extra-wide"></view-header>
<div class="column-layout extra-wide">
<div class="main-column">
${this._error
? html`
<sl-alert variant="danger" open closable>
<sl-icon slot="icon" name="exclamation-octagon"></sl-icon>
<strong>Error:</strong> ${this._error}
</sl-alert>
`
: ''}
${this._loading
? html`
<div class="loading-container">
<sl-spinner style="font-size: 2rem;"></sl-spinner>
</div>
`
: html`
<sl-tab-group
@sl-tab-show=${(e: any) => (this._activeTab = e.detail.name)}
>
<sl-tab
slot="nav"
panel="access"
?active=${this._activeTab === 'access'}
>
<sl-icon
name="shield-lock"
style="margin-right: var(--sl-spacing-x-small);"
></sl-icon>
Access Rules
</sl-tab>
<sl-tab
slot="nav"
panel="approval"
?active=${this._activeTab === 'approval'}
>
<sl-icon
name="person-check"
style="margin-right: var(--sl-spacing-x-small);"
></sl-icon>
Approval Workflows
</sl-tab>
<sl-tab
slot="nav"
panel="files"
?active=${this._activeTab === 'files'}
>
<sl-icon
name="file-earmark-code"
style="margin-right: var(--sl-spacing-x-small);"
></sl-icon>
Import / Export
</sl-tab>
<sl-tab-panel name="access">
${this.renderAccessPoliciesTab()}
</sl-tab-panel>
<sl-tab-panel name="approval">
${this.renderApprovalPoliciesTab()}
</sl-tab-panel>
<sl-tab-panel name="files">
${this.renderPolicyFilesTab()}
</sl-tab-panel>
</sl-tab-group>
`}
</div>
<div class="side-column"></div>
</div>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'policies-view': PoliciesView;
}
}