272 lines
9.2 KiB
TypeScript
272 lines
9.2 KiB
TypeScript
|
|
import * as plugins from '../plugins.js';
|
||
|
|
import * as appstate from '../appstate.js';
|
||
|
|
import * as shared from './shared/index.js';
|
||
|
|
import { appRouter } from '../router.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-buckets')
|
||
|
|
export class ObjstViewBuckets extends DeesElement {
|
||
|
|
@state()
|
||
|
|
accessor bucketsState: appstate.IBucketsState = { buckets: [] };
|
||
|
|
|
||
|
|
constructor() {
|
||
|
|
super();
|
||
|
|
const sub = appstate.bucketsStatePart
|
||
|
|
.select((s) => s)
|
||
|
|
.subscribe((bucketsState) => {
|
||
|
|
this.bucketsState = bucketsState;
|
||
|
|
});
|
||
|
|
this.rxSubscriptions.push(sub);
|
||
|
|
}
|
||
|
|
|
||
|
|
async connectedCallback() {
|
||
|
|
super.connectedCallback();
|
||
|
|
appstate.bucketsStatePart.dispatchAction(appstate.fetchBucketsAction, null);
|
||
|
|
}
|
||
|
|
|
||
|
|
public static styles = [
|
||
|
|
cssManager.defaultStyles,
|
||
|
|
shared.viewHostCss,
|
||
|
|
];
|
||
|
|
|
||
|
|
public render(): TemplateResult {
|
||
|
|
return html`
|
||
|
|
<objst-sectionheading>Buckets</objst-sectionheading>
|
||
|
|
<dees-table
|
||
|
|
heading1="Buckets"
|
||
|
|
heading2="Manage your storage buckets"
|
||
|
|
.data=${this.bucketsState.buckets}
|
||
|
|
.dataName=${'bucket'}
|
||
|
|
.searchable=${true}
|
||
|
|
.displayFunction=${(item: any) => ({
|
||
|
|
name: item.name,
|
||
|
|
objects: String(item.objectCount),
|
||
|
|
size: this.formatBytes(item.totalSizeBytes),
|
||
|
|
created: new Date(item.creationDate).toLocaleDateString(),
|
||
|
|
})}
|
||
|
|
.dataActions=${[
|
||
|
|
{
|
||
|
|
name: 'Create Bucket',
|
||
|
|
iconName: 'lucide:plus',
|
||
|
|
type: ['header'] as any[],
|
||
|
|
actionFunc: async () => {
|
||
|
|
await this.showCreateBucketModal();
|
||
|
|
},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: 'Browse',
|
||
|
|
iconName: 'lucide:folderOpen',
|
||
|
|
type: ['inRow'] as any[],
|
||
|
|
actionFunc: async (args: any) => {
|
||
|
|
await appstate.objectsStatePart.dispatchAction(appstate.fetchObjectsAction, {
|
||
|
|
bucketName: args.item.name,
|
||
|
|
prefix: '',
|
||
|
|
delimiter: '/',
|
||
|
|
});
|
||
|
|
appRouter.navigateToView('browser');
|
||
|
|
},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: 'Policy',
|
||
|
|
iconName: 'lucide:shield',
|
||
|
|
type: ['inRow'] as any[],
|
||
|
|
actionFunc: async (args: any) => {
|
||
|
|
await this.showPolicyModal(args.item.name);
|
||
|
|
},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: 'Delete',
|
||
|
|
iconName: 'lucide:trash2',
|
||
|
|
type: ['inRow', 'contextmenu'] as any[],
|
||
|
|
actionFunc: async (args: any) => {
|
||
|
|
await this.showDeleteBucketModal(args.item.name);
|
||
|
|
},
|
||
|
|
},
|
||
|
|
]}
|
||
|
|
></dees-table>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
|
||
|
|
private async showCreateBucketModal(): Promise<void> {
|
||
|
|
await DeesModal.createAndShow({
|
||
|
|
heading: 'Create Bucket',
|
||
|
|
content: html`
|
||
|
|
<dees-form>
|
||
|
|
<dees-input-text .key=${'bucketName'} .label=${'Bucket Name'} .required=${true}></dees-input-text>
|
||
|
|
</dees-form>
|
||
|
|
`,
|
||
|
|
menuOptions: [
|
||
|
|
{
|
||
|
|
name: 'Cancel',
|
||
|
|
action: async (modal: any) => { modal.destroy(); },
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: 'Create',
|
||
|
|
action: async (modal: any) => {
|
||
|
|
const form = modal.shadowRoot.querySelector('dees-form') as any;
|
||
|
|
const data = await form.collectFormData();
|
||
|
|
if (data.bucketName) {
|
||
|
|
await appstate.bucketsStatePart.dispatchAction(appstate.createBucketAction, {
|
||
|
|
bucketName: data.bucketName,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
modal.destroy();
|
||
|
|
},
|
||
|
|
},
|
||
|
|
],
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
private async showDeleteBucketModal(bucketName: string): Promise<void> {
|
||
|
|
await DeesModal.createAndShow({
|
||
|
|
heading: 'Delete Bucket',
|
||
|
|
content: html`<p>Are you sure you want to delete bucket <strong>${bucketName}</strong>? This action cannot be undone.</p>`,
|
||
|
|
menuOptions: [
|
||
|
|
{
|
||
|
|
name: 'Cancel',
|
||
|
|
action: async (modal: any) => { modal.destroy(); },
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: 'Delete',
|
||
|
|
action: async (modal: any) => {
|
||
|
|
await appstate.bucketsStatePart.dispatchAction(appstate.deleteBucketAction, {
|
||
|
|
bucketName,
|
||
|
|
});
|
||
|
|
modal.destroy();
|
||
|
|
},
|
||
|
|
},
|
||
|
|
],
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
private async showPolicyModal(bucketName: string): Promise<void> {
|
||
|
|
const data = await appstate.getBucketNamedPolicies(bucketName);
|
||
|
|
let modalRef: any = null;
|
||
|
|
|
||
|
|
const renderContent = (
|
||
|
|
attached: typeof data.attachedPolicies,
|
||
|
|
available: typeof data.availablePolicies,
|
||
|
|
) => html`
|
||
|
|
<style>
|
||
|
|
.policy-lists { display: flex; flex-direction: column; gap: 16px; }
|
||
|
|
.policy-section h4 {
|
||
|
|
margin: 0 0 8px 0;
|
||
|
|
font-size: 13px;
|
||
|
|
font-weight: 600;
|
||
|
|
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||
|
|
}
|
||
|
|
.policy-item {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: space-between;
|
||
|
|
padding: 8px 12px;
|
||
|
|
border-radius: 6px;
|
||
|
|
background: ${cssManager.bdTheme('#f5f5f5', '#1a1a2e')};
|
||
|
|
margin-bottom: 4px;
|
||
|
|
color: ${cssManager.bdTheme('#333', '#eee')};
|
||
|
|
}
|
||
|
|
.policy-item-info { display: flex; flex-direction: column; gap: 2px; }
|
||
|
|
.policy-item-name { font-size: 14px; font-weight: 500; }
|
||
|
|
.policy-item-desc { font-size: 12px; color: ${cssManager.bdTheme('#666', '#999')}; }
|
||
|
|
.policy-item button {
|
||
|
|
padding: 4px 12px;
|
||
|
|
border-radius: 4px;
|
||
|
|
border: none;
|
||
|
|
cursor: pointer;
|
||
|
|
font-size: 12px;
|
||
|
|
font-weight: 500;
|
||
|
|
flex-shrink: 0;
|
||
|
|
}
|
||
|
|
.btn-detach {
|
||
|
|
background: ${cssManager.bdTheme('#ffebee', '#3e1a1a')};
|
||
|
|
color: ${cssManager.bdTheme('#c62828', '#ef5350')};
|
||
|
|
}
|
||
|
|
.btn-detach:hover { opacity: 0.8; }
|
||
|
|
.btn-attach {
|
||
|
|
background: ${cssManager.bdTheme('#e3f2fd', '#1a2a3e')};
|
||
|
|
color: ${cssManager.bdTheme('#1565c0', '#64b5f6')};
|
||
|
|
}
|
||
|
|
.btn-attach:hover { opacity: 0.8; }
|
||
|
|
.empty-note {
|
||
|
|
color: ${cssManager.bdTheme('#999', '#666')};
|
||
|
|
font-size: 13px;
|
||
|
|
font-style: italic;
|
||
|
|
padding: 8px 0;
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
<div class="policy-lists">
|
||
|
|
<div class="policy-section">
|
||
|
|
<h4>Attached Policies (${attached.length})</h4>
|
||
|
|
${attached.length > 0
|
||
|
|
? attached.map((policy) => html`
|
||
|
|
<div class="policy-item">
|
||
|
|
<div class="policy-item-info">
|
||
|
|
<span class="policy-item-name">${policy.name}</span>
|
||
|
|
${policy.description ? html`<span class="policy-item-desc">${policy.description}</span>` : ''}
|
||
|
|
</div>
|
||
|
|
<button class="btn-detach" @click=${async (e: Event) => {
|
||
|
|
(e.target as HTMLButtonElement).disabled = true;
|
||
|
|
await appstate.detachPolicyFromBucket(policy.id, bucketName);
|
||
|
|
const fresh = await appstate.getBucketNamedPolicies(bucketName);
|
||
|
|
if (modalRef) {
|
||
|
|
modalRef.content = renderContent(fresh.attachedPolicies, fresh.availablePolicies);
|
||
|
|
}
|
||
|
|
}}>Detach</button>
|
||
|
|
</div>
|
||
|
|
`)
|
||
|
|
: html`<p class="empty-note">No policies attached to this bucket.</p>`}
|
||
|
|
</div>
|
||
|
|
<div class="policy-section">
|
||
|
|
<h4>Available Policies (${available.length})</h4>
|
||
|
|
${available.length > 0
|
||
|
|
? available.map((policy) => html`
|
||
|
|
<div class="policy-item">
|
||
|
|
<div class="policy-item-info">
|
||
|
|
<span class="policy-item-name">${policy.name}</span>
|
||
|
|
${policy.description ? html`<span class="policy-item-desc">${policy.description}</span>` : ''}
|
||
|
|
</div>
|
||
|
|
<button class="btn-attach" @click=${async (e: Event) => {
|
||
|
|
(e.target as HTMLButtonElement).disabled = true;
|
||
|
|
await appstate.attachPolicyToBucket(policy.id, bucketName);
|
||
|
|
const fresh = await appstate.getBucketNamedPolicies(bucketName);
|
||
|
|
if (modalRef) {
|
||
|
|
modalRef.content = renderContent(fresh.attachedPolicies, fresh.availablePolicies);
|
||
|
|
}
|
||
|
|
}}>Attach</button>
|
||
|
|
</div>
|
||
|
|
`)
|
||
|
|
: html`<p class="empty-note">${attached.length > 0
|
||
|
|
? 'All policies are already attached.'
|
||
|
|
: 'No policies defined yet. Create policies in the Policies view.'
|
||
|
|
}</p>`}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
|
||
|
|
modalRef = await DeesModal.createAndShow({
|
||
|
|
heading: `Policies for: ${bucketName}`,
|
||
|
|
content: renderContent(data.attachedPolicies, data.availablePolicies),
|
||
|
|
menuOptions: [
|
||
|
|
{ name: 'Done', action: async (modal: any) => { modal.destroy(); } },
|
||
|
|
],
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
private formatBytes(bytes: number): string {
|
||
|
|
if (bytes === 0) return '0 B';
|
||
|
|
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||
|
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||
|
|
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`;
|
||
|
|
}
|
||
|
|
}
|