feat(core): rebrand to @lossless.zone/objectstorage
- Rename from @lossless.zone/s3container to @lossless.zone/objectstorage - Replace @push.rocks/smarts3 with @push.rocks/smartstorage - Change env var prefix from S3_ to OBJST_ - Rename S3Container class to ObjectStorageContainer - Update web component prefix from s3c- to objst- - Update UI labels, CLI flags, documentation, and Docker config
This commit is contained in:
8
ts_web/elements/index.ts
Normal file
8
ts_web/elements/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export * from './shared/index.js';
|
||||
export * from './objst-app-shell.js';
|
||||
export * from './objst-view-overview.js';
|
||||
export * from './objst-view-buckets.js';
|
||||
export * from './objst-view-objects.js';
|
||||
export * from './objst-view-policies.js';
|
||||
export * from './objst-view-config.js';
|
||||
export * from './objst-view-credentials.js';
|
||||
200
ts_web/elements/objst-app-shell.ts
Normal file
200
ts_web/elements/objst-app-shell.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as appstate from '../appstate.js';
|
||||
import * as interfaces from '../../ts_interfaces/index.js';
|
||||
import { appRouter } from '../router.js';
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
html,
|
||||
state,
|
||||
css,
|
||||
cssManager,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import type { ObjstViewOverview } from './objst-view-overview.js';
|
||||
import type { ObjstViewBuckets } from './objst-view-buckets.js';
|
||||
import type { ObjstViewObjects } from './objst-view-objects.js';
|
||||
import type { ObjstViewPolicies } from './objst-view-policies.js';
|
||||
import type { ObjstViewConfig } from './objst-view-config.js';
|
||||
import type { ObjstViewCredentials } from './objst-view-credentials.js';
|
||||
|
||||
@customElement('objst-app-shell')
|
||||
export class ObjstAppShell extends DeesElement {
|
||||
@state()
|
||||
accessor loginState: appstate.ILoginState = { identity: null, isLoggedIn: false };
|
||||
|
||||
@state()
|
||||
accessor uiState: appstate.IUiState = {
|
||||
activeView: 'overview',
|
||||
autoRefresh: true,
|
||||
refreshInterval: 30000,
|
||||
};
|
||||
|
||||
private viewTabs = [
|
||||
{ name: 'Overview', iconName: 'lucide:layoutDashboard', element: (async () => (await import('./objst-view-overview.js')).ObjstViewOverview)() },
|
||||
{ name: 'Buckets', iconName: 'lucide:database', element: (async () => (await import('./objst-view-buckets.js')).ObjstViewBuckets)() },
|
||||
{ name: 'Browser', iconName: 'lucide:folderOpen', element: (async () => (await import('./objst-view-objects.js')).ObjstViewObjects)() },
|
||||
{ name: 'Policies', iconName: 'lucide:shield', element: (async () => (await import('./objst-view-policies.js')).ObjstViewPolicies)() },
|
||||
{ name: 'Config', iconName: 'lucide:settings', element: (async () => (await import('./objst-view-config.js')).ObjstViewConfig)() },
|
||||
{ name: 'Access Keys', iconName: 'lucide:key', element: (async () => (await import('./objst-view-credentials.js')).ObjstViewCredentials)() },
|
||||
];
|
||||
|
||||
private resolvedViewTabs: Array<{ name: string; iconName: string; element: any }> = [];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
document.title = 'ObjectStorage';
|
||||
|
||||
const loginSubscription = appstate.loginStatePart
|
||||
.select((stateArg) => stateArg)
|
||||
.subscribe((loginState) => {
|
||||
this.loginState = loginState;
|
||||
if (loginState.isLoggedIn) {
|
||||
appstate.serverStatePart.dispatchAction(appstate.fetchServerStatusAction, null);
|
||||
}
|
||||
});
|
||||
this.rxSubscriptions.push(loginSubscription);
|
||||
|
||||
const uiSubscription = appstate.uiStatePart
|
||||
.select((stateArg) => stateArg)
|
||||
.subscribe((uiState) => {
|
||||
this.uiState = uiState;
|
||||
this.syncAppdashView(uiState.activeView);
|
||||
});
|
||||
this.rxSubscriptions.push(uiSubscription);
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.maincontainer {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<div class="maincontainer">
|
||||
<dees-simple-login name="ObjectStorage">
|
||||
<dees-simple-appdash
|
||||
name="ObjectStorage"
|
||||
.viewTabs=${this.resolvedViewTabs}
|
||||
>
|
||||
</dees-simple-appdash>
|
||||
</dees-simple-login>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
public async firstUpdated() {
|
||||
// Resolve async view tab imports
|
||||
this.resolvedViewTabs = await Promise.all(
|
||||
this.viewTabs.map(async (tab) => ({
|
||||
name: tab.name,
|
||||
iconName: tab.iconName,
|
||||
element: await tab.element,
|
||||
})),
|
||||
);
|
||||
this.requestUpdate();
|
||||
await this.updateComplete;
|
||||
|
||||
const simpleLogin = this.shadowRoot!.querySelector('dees-simple-login') as any;
|
||||
if (simpleLogin) {
|
||||
simpleLogin.addEventListener('login', (e: CustomEvent) => {
|
||||
this.login(e.detail.data.username, e.detail.data.password);
|
||||
});
|
||||
}
|
||||
|
||||
const appDash = this.shadowRoot!.querySelector('dees-simple-appdash') as any;
|
||||
if (appDash) {
|
||||
appDash.addEventListener('view-select', (e: CustomEvent) => {
|
||||
const viewName = e.detail.view.name.toLowerCase();
|
||||
appRouter.navigateToView(viewName);
|
||||
});
|
||||
appDash.addEventListener('logout', async () => {
|
||||
await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null);
|
||||
});
|
||||
}
|
||||
|
||||
// Load the initial view on the appdash now that tabs are resolved
|
||||
if (appDash && this.resolvedViewTabs.length > 0) {
|
||||
const initialView = this.resolvedViewTabs.find(
|
||||
(t) => t.name.toLowerCase() === this.uiState.activeView,
|
||||
) || this.resolvedViewTabs[0];
|
||||
await appDash.loadView(initialView);
|
||||
}
|
||||
|
||||
// Check for stored session (persistent login state)
|
||||
const loginState = appstate.loginStatePart.getState();
|
||||
if (loginState.identity?.jwt) {
|
||||
if (loginState.identity.expiresAt > Date.now()) {
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetServerStatus
|
||||
>('/typedrequest', 'getServerStatus');
|
||||
const response = await typedRequest.fire({ identity: loginState.identity });
|
||||
appstate.serverStatePart.setState({
|
||||
status: response.status,
|
||||
connectionInfo: response.connectionInfo,
|
||||
});
|
||||
this.loginState = loginState;
|
||||
if (simpleLogin) {
|
||||
await simpleLogin.switchToSlottedContent();
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Stored session invalid, returning to login:', err);
|
||||
await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null);
|
||||
}
|
||||
} else {
|
||||
await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async login(username: string, password: string) {
|
||||
const domtools = await this.domtoolsPromise;
|
||||
const simpleLogin = this.shadowRoot!.querySelector('dees-simple-login') as any;
|
||||
const form = simpleLogin?.shadowRoot?.querySelector('dees-form') as any;
|
||||
|
||||
if (form) {
|
||||
form.setStatus('pending', 'Logging in...');
|
||||
}
|
||||
|
||||
const newState = await appstate.loginStatePart.dispatchAction(appstate.loginAction, {
|
||||
username,
|
||||
password,
|
||||
});
|
||||
|
||||
if (newState.identity) {
|
||||
if (form) {
|
||||
form.setStatus('success', 'Logged in!');
|
||||
}
|
||||
if (simpleLogin) {
|
||||
await simpleLogin.switchToSlottedContent();
|
||||
}
|
||||
await appstate.serverStatePart.dispatchAction(appstate.fetchServerStatusAction, null);
|
||||
} else {
|
||||
if (form) {
|
||||
form.setStatus('error', 'Login failed!');
|
||||
await domtools.convenience.smartdelay.delayFor(2000);
|
||||
form.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private syncAppdashView(viewName: string): void {
|
||||
const appDash = this.shadowRoot?.querySelector('dees-simple-appdash') as any;
|
||||
if (!appDash || this.resolvedViewTabs.length === 0) return;
|
||||
const targetTab = this.resolvedViewTabs.find((t) => t.name.toLowerCase() === viewName);
|
||||
if (!targetTab) return;
|
||||
appDash.loadView(targetTab);
|
||||
}
|
||||
}
|
||||
271
ts_web/elements/objst-view-buckets.ts
Normal file
271
ts_web/elements/objst-view-buckets.ts
Normal file
@@ -0,0 +1,271 @@
|
||||
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]}`;
|
||||
}
|
||||
}
|
||||
111
ts_web/elements/objst-view-config.ts
Normal file
111
ts_web/elements/objst-view-config.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as appstate from '../appstate.js';
|
||||
import * as shared from './shared/index.js';
|
||||
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
html,
|
||||
state,
|
||||
css,
|
||||
cssManager,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
import { type IStatsTile } from '@design.estate/dees-catalog';
|
||||
|
||||
@customElement('objst-view-config')
|
||||
export class ObjstViewConfig extends DeesElement {
|
||||
@state()
|
||||
accessor configState: appstate.IConfigState = { config: null };
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const sub = appstate.configStatePart
|
||||
.select((s) => s)
|
||||
.subscribe((configState) => {
|
||||
this.configState = configState;
|
||||
});
|
||||
this.rxSubscriptions.push(sub);
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
appstate.configStatePart.dispatchAction(appstate.fetchConfigAction, null);
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
shared.viewHostCss,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
const config = this.configState.config;
|
||||
|
||||
const tiles: IStatsTile[] = [
|
||||
{
|
||||
id: 'objstPort',
|
||||
title: 'Storage API Port',
|
||||
value: config?.objstPort ?? '--',
|
||||
type: 'number',
|
||||
icon: 'lucide:network',
|
||||
color: '#2196f3',
|
||||
},
|
||||
{
|
||||
id: 'uiPort',
|
||||
title: 'UI Port',
|
||||
value: config?.uiPort ?? '--',
|
||||
type: 'number',
|
||||
icon: 'lucide:monitor',
|
||||
color: '#00bcd4',
|
||||
},
|
||||
{
|
||||
id: 'region',
|
||||
title: 'Region',
|
||||
value: config?.region ?? '--',
|
||||
type: 'text',
|
||||
icon: 'lucide:globe',
|
||||
color: '#607d8b',
|
||||
},
|
||||
{
|
||||
id: 'storageDir',
|
||||
title: 'Storage Directory',
|
||||
value: config?.storageDirectory ?? '--',
|
||||
type: 'text',
|
||||
icon: 'lucide:hardDrive',
|
||||
color: '#9c27b0',
|
||||
},
|
||||
{
|
||||
id: 'auth',
|
||||
title: 'Authentication',
|
||||
value: config?.authEnabled ? 'Enabled' : 'Disabled',
|
||||
type: 'text',
|
||||
icon: 'lucide:shield',
|
||||
color: config?.authEnabled ? '#4caf50' : '#f44336',
|
||||
},
|
||||
{
|
||||
id: 'cors',
|
||||
title: 'CORS',
|
||||
value: config?.corsEnabled ? 'Enabled' : 'Disabled',
|
||||
type: 'text',
|
||||
icon: 'lucide:globe2',
|
||||
color: config?.corsEnabled ? '#4caf50' : '#ff9800',
|
||||
},
|
||||
];
|
||||
|
||||
return html`
|
||||
<objst-sectionheading>Configuration</objst-sectionheading>
|
||||
<dees-statsgrid
|
||||
.tiles=${tiles}
|
||||
.gridActions=${[
|
||||
{
|
||||
name: 'Refresh',
|
||||
iconName: 'lucide:refreshCw',
|
||||
action: async () => {
|
||||
await appstate.configStatePart.dispatchAction(appstate.fetchConfigAction, null);
|
||||
},
|
||||
},
|
||||
]}
|
||||
></dees-statsgrid>
|
||||
`;
|
||||
}
|
||||
}
|
||||
128
ts_web/elements/objst-view-credentials.ts
Normal file
128
ts_web/elements/objst-view-credentials.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as appstate from '../appstate.js';
|
||||
import * as shared from './shared/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-credentials')
|
||||
export class ObjstViewCredentials extends DeesElement {
|
||||
@state()
|
||||
accessor credentialsState: appstate.ICredentialsState = { credentials: [] };
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const sub = appstate.credentialsStatePart
|
||||
.select((s) => s)
|
||||
.subscribe((credentialsState) => {
|
||||
this.credentialsState = credentialsState;
|
||||
});
|
||||
this.rxSubscriptions.push(sub);
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
appstate.credentialsStatePart.dispatchAction(appstate.fetchCredentialsAction, null);
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
shared.viewHostCss,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<objst-sectionheading>Access Keys</objst-sectionheading>
|
||||
<dees-table
|
||||
heading1="Access Credentials"
|
||||
heading2="Manage access keys for API authentication"
|
||||
.data=${this.credentialsState.credentials}
|
||||
.dataName=${'credential'}
|
||||
.displayFunction=${(item: any) => ({
|
||||
'Access Key ID': item.accessKeyId,
|
||||
'Secret Access Key': item.secretAccessKey,
|
||||
})}
|
||||
.dataActions=${[
|
||||
{
|
||||
name: 'Add Key',
|
||||
iconName: 'lucide:plus',
|
||||
type: ['header'] as any[],
|
||||
actionFunc: async () => {
|
||||
await this.showAddKeyModal();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Remove',
|
||||
iconName: 'lucide:trash2',
|
||||
type: ['inRow', 'contextmenu'] as any[],
|
||||
actionFunc: async (args: any) => {
|
||||
await this.showRemoveKeyModal(args.item.accessKeyId);
|
||||
},
|
||||
},
|
||||
]}
|
||||
></dees-table>
|
||||
`;
|
||||
}
|
||||
|
||||
private async showAddKeyModal(): Promise<void> {
|
||||
await DeesModal.createAndShow({
|
||||
heading: 'Add Access Key',
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-text .key=${'accessKeyId'} .label=${'Access Key ID'} .required=${true}></dees-input-text>
|
||||
<dees-input-text .key=${'secretAccessKey'} .label=${'Secret Access Key'} .required=${true}></dees-input-text>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'Cancel',
|
||||
action: async (modal: any) => { modal.destroy(); },
|
||||
},
|
||||
{
|
||||
name: 'Add',
|
||||
action: async (modal: any) => {
|
||||
const form = modal.shadowRoot.querySelector('dees-form') as any;
|
||||
const data = await form.collectFormData();
|
||||
if (data.accessKeyId && data.secretAccessKey) {
|
||||
await appstate.credentialsStatePart.dispatchAction(appstate.addCredentialAction, {
|
||||
accessKeyId: data.accessKeyId,
|
||||
secretAccessKey: data.secretAccessKey,
|
||||
});
|
||||
}
|
||||
modal.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
private async showRemoveKeyModal(accessKeyId: string): Promise<void> {
|
||||
await DeesModal.createAndShow({
|
||||
heading: 'Remove Access Key',
|
||||
content: html`<p>Are you sure you want to remove access key <strong>${accessKeyId}</strong>?</p>`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'Cancel',
|
||||
action: async (modal: any) => { modal.destroy(); },
|
||||
},
|
||||
{
|
||||
name: 'Remove',
|
||||
action: async (modal: any) => {
|
||||
await appstate.credentialsStatePart.dispatchAction(appstate.removeCredentialAction, {
|
||||
accessKeyId,
|
||||
});
|
||||
modal.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
149
ts_web/elements/objst-view-objects.ts
Normal file
149
ts_web/elements/objst-view-objects.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as appstate from '../appstate.js';
|
||||
import { createDataProvider } from '../dataprovider.js';
|
||||
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
html,
|
||||
state,
|
||||
css,
|
||||
cssManager,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
@customElement('objst-view-objects')
|
||||
export class ObjstViewObjects extends DeesElement {
|
||||
@state()
|
||||
accessor bucketsState: appstate.IBucketsState = { buckets: [] };
|
||||
|
||||
@state()
|
||||
accessor selectedBucket: string = '';
|
||||
|
||||
private dataProvider = createDataProvider();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const bucketsSub = appstate.bucketsStatePart
|
||||
.select((s) => s)
|
||||
.subscribe((bucketsState) => {
|
||||
this.bucketsState = bucketsState;
|
||||
});
|
||||
this.rxSubscriptions.push(bucketsSub);
|
||||
|
||||
// Track current bucket from objects state
|
||||
const objSub = appstate.objectsStatePart
|
||||
.select((s) => s.currentBucket)
|
||||
.subscribe((currentBucket) => {
|
||||
if (currentBucket) {
|
||||
this.selectedBucket = currentBucket;
|
||||
}
|
||||
});
|
||||
this.rxSubscriptions.push(objSub);
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this.bucketsState.buckets.length === 0) {
|
||||
appstate.bucketsStatePart.dispatchAction(appstate.fetchBucketsAction, null);
|
||||
}
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.bucket-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.bucket-bar label {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||
}
|
||||
.bucket-chip {
|
||||
padding: 6px 16px;
|
||||
border-radius: 20px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
border: 1px solid ${cssManager.bdTheme('#ddd', '#444')};
|
||||
background: ${cssManager.bdTheme('#f5f5f5', '#1a1a2e')};
|
||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
.bucket-chip:hover {
|
||||
border-color: ${cssManager.bdTheme('#2196f3', '#64b5f6')};
|
||||
color: ${cssManager.bdTheme('#2196f3', '#64b5f6')};
|
||||
}
|
||||
.bucket-chip.active {
|
||||
background: ${cssManager.bdTheme('#2196f3', '#1565c0')};
|
||||
color: white;
|
||||
border-color: ${cssManager.bdTheme('#2196f3', '#1565c0')};
|
||||
}
|
||||
.browser-container {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.noBucket {
|
||||
text-align: center;
|
||||
padding: 64px 0;
|
||||
color: ${cssManager.bdTheme('#666', '#999')};
|
||||
font-size: 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<div class="bucket-bar">
|
||||
<label>Bucket:</label>
|
||||
${this.bucketsState.buckets.map(
|
||||
(bucket) => html`
|
||||
<span
|
||||
class="bucket-chip ${this.selectedBucket === bucket.name ? 'active' : ''}"
|
||||
@click=${() => this.selectBucket(bucket.name)}
|
||||
>${bucket.name}</span>
|
||||
`,
|
||||
)}
|
||||
${this.bucketsState.buckets.length === 0
|
||||
? html`<span style="color: #999; font-size: 13px;">No buckets found</span>`
|
||||
: ''}
|
||||
</div>
|
||||
${this.selectedBucket
|
||||
? html`
|
||||
<div class="browser-container">
|
||||
<dees-s3-browser
|
||||
.dataProvider=${this.dataProvider}
|
||||
.bucketName=${this.selectedBucket}
|
||||
></dees-s3-browser>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="noBucket">
|
||||
<p>Select a bucket above to browse objects.</p>
|
||||
</div>
|
||||
`}
|
||||
`;
|
||||
}
|
||||
|
||||
private selectBucket(bucketName: string): void {
|
||||
this.selectedBucket = bucketName;
|
||||
// Update objects state for tracking
|
||||
appstate.objectsStatePart.dispatchAction(appstate.fetchObjectsAction, {
|
||||
bucketName,
|
||||
prefix: '',
|
||||
delimiter: '/',
|
||||
});
|
||||
}
|
||||
}
|
||||
201
ts_web/elements/objst-view-overview.ts
Normal file
201
ts_web/elements/objst-view-overview.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as appstate from '../appstate.js';
|
||||
import * as shared from './shared/index.js';
|
||||
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
html,
|
||||
state,
|
||||
css,
|
||||
cssManager,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
import { type IStatsTile } from '@design.estate/dees-catalog';
|
||||
|
||||
@customElement('objst-view-overview')
|
||||
export class ObjstViewOverview extends DeesElement {
|
||||
@state()
|
||||
accessor serverState: appstate.IServerState = { status: null, connectionInfo: null };
|
||||
|
||||
@state()
|
||||
accessor bucketsState: appstate.IBucketsState = { buckets: [] };
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const serverSub = appstate.serverStatePart
|
||||
.select((s) => s)
|
||||
.subscribe((serverState) => {
|
||||
this.serverState = serverState;
|
||||
});
|
||||
this.rxSubscriptions.push(serverSub);
|
||||
|
||||
const bucketsSub = appstate.bucketsStatePart
|
||||
.select((s) => s)
|
||||
.subscribe((bucketsState) => {
|
||||
this.bucketsState = bucketsState;
|
||||
});
|
||||
this.rxSubscriptions.push(bucketsSub);
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
appstate.serverStatePart.dispatchAction(appstate.fetchServerStatusAction, null);
|
||||
appstate.bucketsStatePart.dispatchAction(appstate.fetchBucketsAction, null);
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
shared.viewHostCss,
|
||||
css`
|
||||
.connectionInfo {
|
||||
margin-top: 32px;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
background: ${cssManager.bdTheme('#f5f5f5', '#1a1a2e')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#2a2a4a')};
|
||||
}
|
||||
.connectionInfo h2 {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||
}
|
||||
.connectionInfo .row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.connectionInfo .label {
|
||||
width: 120px;
|
||||
font-weight: 500;
|
||||
color: ${cssManager.bdTheme('#666', '#999')};
|
||||
}
|
||||
.connectionInfo .value {
|
||||
font-family: monospace;
|
||||
color: ${cssManager.bdTheme('#333', '#e0e0e0')};
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
background: ${cssManager.bdTheme('#e8e8e8', '#252540')};
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
const status = this.serverState.status;
|
||||
const connInfo = this.serverState.connectionInfo;
|
||||
|
||||
const statsTiles: IStatsTile[] = [
|
||||
{
|
||||
id: 'status',
|
||||
title: 'Server Status',
|
||||
value: status?.running ? 'Online' : 'Offline',
|
||||
type: 'text',
|
||||
icon: 'lucide:server',
|
||||
color: status?.running ? '#4caf50' : '#f44336',
|
||||
description: status ? `Uptime: ${this.formatUptime(status.uptime)}` : 'Loading...',
|
||||
},
|
||||
{
|
||||
id: 'buckets',
|
||||
title: 'Buckets',
|
||||
value: status?.bucketCount ?? 0,
|
||||
type: 'number',
|
||||
icon: 'lucide:database',
|
||||
color: '#2196f3',
|
||||
},
|
||||
{
|
||||
id: 'objects',
|
||||
title: 'Total Objects',
|
||||
value: status?.totalObjectCount ?? 0,
|
||||
type: 'number',
|
||||
icon: 'lucide:file',
|
||||
color: '#ff9800',
|
||||
},
|
||||
{
|
||||
id: 'storage',
|
||||
title: 'Storage Used',
|
||||
value: status ? this.formatBytes(status.totalStorageBytes) : '0 B',
|
||||
type: 'text',
|
||||
icon: 'lucide:hardDrive',
|
||||
color: '#9c27b0',
|
||||
},
|
||||
{
|
||||
id: 'storagePort',
|
||||
title: 'Storage Port',
|
||||
value: status?.objstPort ?? 9000,
|
||||
type: 'number',
|
||||
icon: 'lucide:network',
|
||||
color: '#00bcd4',
|
||||
},
|
||||
{
|
||||
id: 'region',
|
||||
title: 'Region',
|
||||
value: status?.region ?? 'us-east-1',
|
||||
type: 'text',
|
||||
icon: 'lucide:globe',
|
||||
color: '#607d8b',
|
||||
},
|
||||
];
|
||||
|
||||
return html`
|
||||
<objst-sectionheading>Overview</objst-sectionheading>
|
||||
<dees-statsgrid
|
||||
.tiles=${statsTiles}
|
||||
.gridActions=${[
|
||||
{
|
||||
name: 'Refresh',
|
||||
iconName: 'lucide:refreshCw',
|
||||
action: async () => {
|
||||
await appstate.serverStatePart.dispatchAction(
|
||||
appstate.fetchServerStatusAction,
|
||||
null,
|
||||
);
|
||||
await appstate.bucketsStatePart.dispatchAction(appstate.fetchBucketsAction, null);
|
||||
},
|
||||
},
|
||||
]}
|
||||
></dees-statsgrid>
|
||||
|
||||
${connInfo
|
||||
? html`
|
||||
<div class="connectionInfo">
|
||||
<h2>Connection Info</h2>
|
||||
<div class="row">
|
||||
<span class="label">Endpoint</span>
|
||||
<span class="value">${connInfo.endpoint}:${connInfo.port}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="label">Protocol</span>
|
||||
<span class="value">${connInfo.useSsl ? 'HTTPS' : 'HTTP'}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="label">Access Key</span>
|
||||
<span class="value">${connInfo.accessKey}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="label">Region</span>
|
||||
<span class="value">${connInfo.region}</span>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
`;
|
||||
}
|
||||
|
||||
private formatUptime(seconds: number): string {
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
if (days > 0) return `${days}d ${hours}h ${minutes}m`;
|
||||
if (hours > 0) return `${hours}h ${minutes}m`;
|
||||
return `${minutes}m`;
|
||||
}
|
||||
|
||||
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]}`;
|
||||
}
|
||||
}
|
||||
394
ts_web/elements/objst-view-policies.ts
Normal file
394
ts_web/elements/objst-view-policies.ts
Normal file
@@ -0,0 +1,394 @@
|
||||
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`
|
||||
<objst-sectionheading>Policies</objst-sectionheading>
|
||||
<dees-table
|
||||
heading1="Named Policies"
|
||||
heading2="Create reusable policies and attach them to buckets"
|
||||
.data=${this.policiesState.policies}
|
||||
.dataName=${'policy'}
|
||||
.displayFunction=${(item: any) => ({
|
||||
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);
|
||||
},
|
||||
},
|
||||
]}
|
||||
></dees-table>
|
||||
`;
|
||||
}
|
||||
|
||||
private getDefaultStatements(): interfaces.data.IObjstStatement[] {
|
||||
return [
|
||||
{
|
||||
Sid: 'PublicRead',
|
||||
Effect: 'Allow',
|
||||
Principal: '*',
|
||||
Action: 's3:GetObject',
|
||||
Resource: 'arn:aws:s3:::${bucket}/*',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private async showCreatePolicyModal(): Promise<void> {
|
||||
const defaultJson = JSON.stringify(this.getDefaultStatements(), null, 2);
|
||||
await DeesModal.createAndShow({
|
||||
heading: 'Create Policy',
|
||||
content: html`
|
||||
<style>
|
||||
.policy-form { display: flex; flex-direction: column; gap: 12px; }
|
||||
.policy-form label { font-size: 13px; font-weight: 600; color: ${cssManager.bdTheme('#333', '#ccc')}; }
|
||||
.policy-form input, .policy-form textarea {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid ${cssManager.bdTheme('#ddd', '#444')};
|
||||
border-radius: 6px;
|
||||
background: ${cssManager.bdTheme('#fff', '#111')};
|
||||
color: ${cssManager.bdTheme('#333', '#eee')};
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.policy-form textarea {
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
min-height: 200px;
|
||||
resize: vertical;
|
||||
}
|
||||
.policy-form input:focus, .policy-form textarea:focus {
|
||||
outline: none;
|
||||
border-color: ${cssManager.bdTheme('#2196f3', '#64b5f6')};
|
||||
}
|
||||
.policy-error { color: #ef5350; font-size: 13px; min-height: 20px; }
|
||||
.policy-hint { color: ${cssManager.bdTheme('#666', '#999')}; font-size: 12px; }
|
||||
</style>
|
||||
<div class="policy-form">
|
||||
<div>
|
||||
<label>Name</label>
|
||||
<input type="text" class="policy-name" placeholder="e.g. Public Read Access" />
|
||||
</div>
|
||||
<div>
|
||||
<label>Description</label>
|
||||
<input type="text" class="policy-description" placeholder="e.g. Allow anonymous read access to objects" />
|
||||
</div>
|
||||
<div>
|
||||
<label>Statements (JSON array)</label>
|
||||
<p class="policy-hint">Use \${bucket} in Resource ARNs — it will be replaced with the actual bucket name when applied.</p>
|
||||
<textarea class="policy-statements">${defaultJson}</textarea>
|
||||
</div>
|
||||
<div class="policy-error"></div>
|
||||
</div>
|
||||
`,
|
||||
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<void> {
|
||||
const statementsJson = JSON.stringify(policy.statements, null, 2);
|
||||
await DeesModal.createAndShow({
|
||||
heading: `Edit Policy: ${policy.name}`,
|
||||
content: html`
|
||||
<style>
|
||||
.policy-form { display: flex; flex-direction: column; gap: 12px; }
|
||||
.policy-form label { font-size: 13px; font-weight: 600; color: ${cssManager.bdTheme('#333', '#ccc')}; }
|
||||
.policy-form input, .policy-form textarea {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid ${cssManager.bdTheme('#ddd', '#444')};
|
||||
border-radius: 6px;
|
||||
background: ${cssManager.bdTheme('#fff', '#111')};
|
||||
color: ${cssManager.bdTheme('#333', '#eee')};
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.policy-form textarea {
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
min-height: 200px;
|
||||
resize: vertical;
|
||||
}
|
||||
.policy-form input:focus, .policy-form textarea:focus {
|
||||
outline: none;
|
||||
border-color: ${cssManager.bdTheme('#2196f3', '#64b5f6')};
|
||||
}
|
||||
.policy-error { color: #ef5350; font-size: 13px; min-height: 20px; }
|
||||
.policy-hint { color: ${cssManager.bdTheme('#666', '#999')}; font-size: 12px; }
|
||||
</style>
|
||||
<div class="policy-form">
|
||||
<div>
|
||||
<label>Name</label>
|
||||
<input type="text" class="policy-name" .value=${policy.name} />
|
||||
</div>
|
||||
<div>
|
||||
<label>Description</label>
|
||||
<input type="text" class="policy-description" .value=${policy.description} />
|
||||
</div>
|
||||
<div>
|
||||
<label>Statements (JSON array)</label>
|
||||
<p class="policy-hint">Use \${bucket} in Resource ARNs — it will be replaced with the actual bucket name when applied.</p>
|
||||
<textarea class="policy-statements">${statementsJson}</textarea>
|
||||
</div>
|
||||
<div class="policy-error"></div>
|
||||
</div>
|
||||
`,
|
||||
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<void> {
|
||||
await DeesModal.createAndShow({
|
||||
heading: 'Delete Policy',
|
||||
content: html`
|
||||
<p>Are you sure you want to delete policy <strong>${policy.name}</strong>?</p>
|
||||
<p style="color: ${cssManager.bdTheme('#666', '#999')}; font-size: 13px;">
|
||||
This will also detach the policy from all buckets it is currently applied to.
|
||||
</p>
|
||||
`,
|
||||
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<void> {
|
||||
const data = await appstate.getPolicyBuckets(policy.id);
|
||||
let modalRef: any = null;
|
||||
|
||||
const renderContent = (attached: string[], available: string[]) => html`
|
||||
<style>
|
||||
.bucket-lists { display: flex; flex-direction: column; gap: 16px; }
|
||||
.bucket-section h4 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||
}
|
||||
.bucket-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
background: ${cssManager.bdTheme('#f5f5f5', '#1a1a2e')};
|
||||
margin-bottom: 4px;
|
||||
font-size: 14px;
|
||||
color: ${cssManager.bdTheme('#333', '#eee')};
|
||||
}
|
||||
.bucket-item button {
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.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="bucket-lists">
|
||||
<div class="bucket-section">
|
||||
<h4>Attached Buckets (${attached.length})</h4>
|
||||
${attached.length > 0
|
||||
? attached.map((bucket) => html`
|
||||
<div class="bucket-item">
|
||||
<span>${bucket}</span>
|
||||
<button class="btn-detach" @click=${async (e: Event) => {
|
||||
(e.target as HTMLButtonElement).disabled = true;
|
||||
await appstate.detachPolicyFromBucket(policy.id, bucket);
|
||||
const fresh = await appstate.getPolicyBuckets(policy.id);
|
||||
if (modalRef) {
|
||||
modalRef.content = renderContent(fresh.attachedBuckets, fresh.availableBuckets);
|
||||
}
|
||||
}}>Detach</button>
|
||||
</div>
|
||||
`)
|
||||
: html`<p class="empty-note">No buckets attached to this policy.</p>`}
|
||||
</div>
|
||||
<div class="bucket-section">
|
||||
<h4>Available Buckets (${available.length})</h4>
|
||||
${available.length > 0
|
||||
? available.map((bucket) => html`
|
||||
<div class="bucket-item">
|
||||
<span>${bucket}</span>
|
||||
<button class="btn-attach" @click=${async (e: Event) => {
|
||||
(e.target as HTMLButtonElement).disabled = true;
|
||||
await appstate.attachPolicyToBucket(policy.id, bucket);
|
||||
const fresh = await appstate.getPolicyBuckets(policy.id);
|
||||
if (modalRef) {
|
||||
modalRef.content = renderContent(fresh.attachedBuckets, fresh.availableBuckets);
|
||||
}
|
||||
}}>Attach</button>
|
||||
</div>
|
||||
`)
|
||||
: html`<p class="empty-note">All buckets already have this policy.</p>`}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
modalRef = await DeesModal.createAndShow({
|
||||
heading: `Buckets for: ${policy.name}`,
|
||||
content: renderContent(data.attachedBuckets, data.availableBuckets),
|
||||
menuOptions: [
|
||||
{ name: 'Done', action: async (modal: any) => { modal.destroy(); } },
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
10
ts_web/elements/shared/css.ts
Normal file
10
ts_web/elements/shared/css.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { css } from '@design.estate/dees-element';
|
||||
|
||||
export const viewHostCss = css`
|
||||
:host {
|
||||
display: block;
|
||||
margin: auto;
|
||||
max-width: 1280px;
|
||||
padding: 16px 16px;
|
||||
}
|
||||
`;
|
||||
2
ts_web/elements/shared/index.ts
Normal file
2
ts_web/elements/shared/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './css.js';
|
||||
export * from './objst-sectionheading.js';
|
||||
31
ts_web/elements/shared/objst-sectionheading.ts
Normal file
31
ts_web/elements/shared/objst-sectionheading.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
html,
|
||||
css,
|
||||
cssManager,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
@customElement('objst-sectionheading')
|
||||
export class ObjstSectionHeading extends DeesElement {
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: ${cssManager.bdTheme('#1a1a2e', '#e0e0e0')};
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`<h1><slot></slot></h1>`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user