import * as plugins from '../plugins.js'; import * as appstate from '../appstate.js'; import * as shared from './shared/index.js'; import * as interfaces from '../../ts_interfaces/index.js'; import { DeesElement, customElement, html, state, css, cssManager, type TemplateResult, } from '@design.estate/dees-element'; import { DeesModal } from '@design.estate/dees-catalog'; @customElement('objst-view-policies') export class ObjstViewPolicies extends DeesElement { @state() accessor policiesState: appstate.IPoliciesState = { policies: [] }; constructor() { super(); const sub = appstate.policiesStatePart .select((s) => s) .subscribe((policiesState) => { this.policiesState = policiesState; }); this.rxSubscriptions.push(sub); } async connectedCallback() { super.connectedCallback(); appstate.policiesStatePart.dispatchAction(appstate.fetchPoliciesAction, null); } public static styles = [ cssManager.defaultStyles, shared.viewHostCss, ]; public render(): TemplateResult { return html` Policies ({ Name: item.name, Description: item.description, Statements: String(item.statements.length), Updated: new Date(item.updatedAt).toLocaleDateString(), })} .dataActions=${[ { name: 'Create Policy', iconName: 'lucide:plus', type: ['header'] as any[], actionFunc: async () => { await this.showCreatePolicyModal(); }, }, { name: 'Edit', iconName: 'lucide:pencil', type: ['inRow'] as any[], actionFunc: async (args: any) => { await this.showEditPolicyModal(args.item); }, }, { name: 'Buckets', iconName: 'lucide:database', type: ['inRow'] as any[], actionFunc: async (args: any) => { await this.showPolicyBucketsModal(args.item); }, }, { name: 'Delete', iconName: 'lucide:trash2', type: ['inRow', 'contextmenu'] as any[], actionFunc: async (args: any) => { await this.showDeletePolicyModal(args.item); }, }, ]} > `; } private getDefaultStatements(): interfaces.data.IObjstStatement[] { return [ { Sid: 'PublicRead', Effect: 'Allow', Principal: '*', Action: 's3:GetObject', Resource: 'arn:aws:s3:::${bucket}/*', }, ]; } private async showCreatePolicyModal(): Promise { const defaultJson = JSON.stringify(this.getDefaultStatements(), null, 2); await DeesModal.createAndShow({ heading: 'Create Policy', content: html`

Use \${bucket} in Resource ARNs — it will be replaced with the actual bucket name when applied.

`, menuOptions: [ { name: 'Cancel', action: async (modal: any) => { modal.destroy(); } }, { name: 'Create', action: async (modal: any) => { const root = modal.shadowRoot; const name = (root.querySelector('.policy-name') as HTMLInputElement)?.value?.trim(); const description = (root.querySelector('.policy-description') as HTMLInputElement)?.value?.trim(); const statementsText = (root.querySelector('.policy-statements') as HTMLTextAreaElement)?.value?.trim(); const errorDiv = root.querySelector('.policy-error') as HTMLElement; if (!name) { errorDiv.textContent = 'Name is required.'; return; } if (!statementsText) { errorDiv.textContent = 'Statements are required.'; return; } let statements: interfaces.data.IObjstStatement[]; try { statements = JSON.parse(statementsText); if (!Array.isArray(statements)) throw new Error('Must be an array'); } catch (err: any) { errorDiv.textContent = `Invalid JSON: ${err.message}`; return; } await appstate.policiesStatePart.dispatchAction(appstate.createPolicyAction, { name, description: description || '', statements, }); modal.destroy(); }, }, ], }); } private async showEditPolicyModal(policy: interfaces.data.INamedPolicy): Promise { const statementsJson = JSON.stringify(policy.statements, null, 2); await DeesModal.createAndShow({ heading: `Edit Policy: ${policy.name}`, content: html`

Use \${bucket} in Resource ARNs — it will be replaced with the actual bucket name when applied.

`, menuOptions: [ { name: 'Cancel', action: async (modal: any) => { modal.destroy(); } }, { name: 'Save', action: async (modal: any) => { const root = modal.shadowRoot; const name = (root.querySelector('.policy-name') as HTMLInputElement)?.value?.trim(); const description = (root.querySelector('.policy-description') as HTMLInputElement)?.value?.trim(); const statementsText = (root.querySelector('.policy-statements') as HTMLTextAreaElement)?.value?.trim(); const errorDiv = root.querySelector('.policy-error') as HTMLElement; if (!name) { errorDiv.textContent = 'Name is required.'; return; } if (!statementsText) { errorDiv.textContent = 'Statements are required.'; return; } let statements: interfaces.data.IObjstStatement[]; try { statements = JSON.parse(statementsText); if (!Array.isArray(statements)) throw new Error('Must be an array'); } catch (err: any) { errorDiv.textContent = `Invalid JSON: ${err.message}`; return; } await appstate.policiesStatePart.dispatchAction(appstate.updatePolicyAction, { policyId: policy.id, name, description: description || '', statements, }); modal.destroy(); }, }, ], }); } private async showDeletePolicyModal(policy: interfaces.data.INamedPolicy): Promise { await DeesModal.createAndShow({ heading: 'Delete Policy', content: html`

Are you sure you want to delete policy ${policy.name}?

This will also detach the policy from all buckets it is currently applied to.

`, menuOptions: [ { name: 'Cancel', action: async (modal: any) => { modal.destroy(); } }, { name: 'Delete', action: async (modal: any) => { await appstate.policiesStatePart.dispatchAction(appstate.deletePolicyAction, { policyId: policy.id, }); modal.destroy(); }, }, ], }); } private async showPolicyBucketsModal(policy: interfaces.data.INamedPolicy): Promise { const data = await appstate.getPolicyBuckets(policy.id); let modalRef: any = null; const renderContent = (attached: string[], available: string[]) => html`

Attached Buckets (${attached.length})

${attached.length > 0 ? attached.map((bucket) => html`
${bucket}
`) : html`

No buckets attached to this policy.

`}

Available Buckets (${available.length})

${available.length > 0 ? available.map((bucket) => html`
${bucket}
`) : html`

All buckets already have this policy.

`}
`; modalRef = await DeesModal.createAndShow({ heading: `Buckets for: ${policy.name}`, content: renderContent(data.attachedBuckets, data.availableBuckets), menuOptions: [ { name: 'Done', action: async (modal: any) => { modal.destroy(); } }, ], }); } }