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/00_commitinfo_data.ts
Normal file
8
ts_web/00_commitinfo_data.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* autocreated commitinfo by @push.rocks/commitinfo
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@lossless.zone/objectstorage',
|
||||
version: '1.4.0',
|
||||
description: 'object storage server with management UI powered by smartstorage'
|
||||
}
|
||||
589
ts_web/appstate.ts
Normal file
589
ts_web/appstate.ts
Normal file
@@ -0,0 +1,589 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as interfaces from '../ts_interfaces/index.js';
|
||||
|
||||
// ============================================================================
|
||||
// Smartstate instance
|
||||
// ============================================================================
|
||||
export const appState = new plugins.domtools.plugins.smartstate.Smartstate();
|
||||
|
||||
// ============================================================================
|
||||
// State Part Interfaces
|
||||
// ============================================================================
|
||||
|
||||
export interface ILoginState {
|
||||
identity: interfaces.data.IIdentity | null;
|
||||
isLoggedIn: boolean;
|
||||
}
|
||||
|
||||
export interface IServerState {
|
||||
status: interfaces.data.IServerStatus | null;
|
||||
connectionInfo: interfaces.data.IConnectionInfo | null;
|
||||
}
|
||||
|
||||
export interface IBucketsState {
|
||||
buckets: interfaces.data.IBucketInfo[];
|
||||
}
|
||||
|
||||
export interface IObjectsState {
|
||||
result: interfaces.data.IObjectListResult | null;
|
||||
currentBucket: string;
|
||||
currentPrefix: string;
|
||||
}
|
||||
|
||||
export interface ICredentialsState {
|
||||
credentials: interfaces.data.IObjstCredential[];
|
||||
}
|
||||
|
||||
export interface IPoliciesState {
|
||||
policies: interfaces.data.INamedPolicy[];
|
||||
}
|
||||
|
||||
export interface IConfigState {
|
||||
config: interfaces.data.IServerConfig | null;
|
||||
}
|
||||
|
||||
export interface IUiState {
|
||||
activeView: string;
|
||||
autoRefresh: boolean;
|
||||
refreshInterval: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// State Parts
|
||||
// ============================================================================
|
||||
|
||||
export const loginStatePart = await appState.getStatePart<ILoginState>(
|
||||
'login',
|
||||
{
|
||||
identity: null,
|
||||
isLoggedIn: false,
|
||||
},
|
||||
'persistent',
|
||||
);
|
||||
|
||||
export const serverStatePart = await appState.getStatePart<IServerState>(
|
||||
'server',
|
||||
{
|
||||
status: null,
|
||||
connectionInfo: null,
|
||||
},
|
||||
'soft',
|
||||
);
|
||||
|
||||
export const bucketsStatePart = await appState.getStatePart<IBucketsState>(
|
||||
'buckets',
|
||||
{
|
||||
buckets: [],
|
||||
},
|
||||
'soft',
|
||||
);
|
||||
|
||||
export const objectsStatePart = await appState.getStatePart<IObjectsState>(
|
||||
'objects',
|
||||
{
|
||||
result: null,
|
||||
currentBucket: '',
|
||||
currentPrefix: '',
|
||||
},
|
||||
'soft',
|
||||
);
|
||||
|
||||
export const credentialsStatePart = await appState.getStatePart<ICredentialsState>(
|
||||
'credentials',
|
||||
{
|
||||
credentials: [],
|
||||
},
|
||||
'soft',
|
||||
);
|
||||
|
||||
export const policiesStatePart = await appState.getStatePart<IPoliciesState>(
|
||||
'policies',
|
||||
{
|
||||
policies: [],
|
||||
},
|
||||
'soft',
|
||||
);
|
||||
|
||||
export const configStatePart = await appState.getStatePart<IConfigState>(
|
||||
'config',
|
||||
{
|
||||
config: null,
|
||||
},
|
||||
'soft',
|
||||
);
|
||||
|
||||
export const uiStatePart = await appState.getStatePart<IUiState>(
|
||||
'ui',
|
||||
{
|
||||
activeView: 'overview',
|
||||
autoRefresh: true,
|
||||
refreshInterval: 30000,
|
||||
},
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// Helpers
|
||||
// ============================================================================
|
||||
|
||||
interface IActionContext {
|
||||
identity: interfaces.data.IIdentity | null;
|
||||
}
|
||||
|
||||
const getActionContext = (): IActionContext => {
|
||||
return { identity: loginStatePart.getState().identity };
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Login Actions
|
||||
// ============================================================================
|
||||
|
||||
export const loginAction = loginStatePart.createAction<{
|
||||
username: string;
|
||||
password: string;
|
||||
}>(async (statePartArg, dataArg) => {
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_AdminLoginWithUsernameAndPassword
|
||||
>('/typedrequest', 'adminLoginWithUsernameAndPassword');
|
||||
|
||||
const response = await typedRequest.fire({
|
||||
username: dataArg.username,
|
||||
password: dataArg.password,
|
||||
});
|
||||
|
||||
return {
|
||||
identity: response.identity || null,
|
||||
isLoggedIn: !!response.identity,
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Login failed:', err);
|
||||
return { identity: null, isLoggedIn: false };
|
||||
}
|
||||
});
|
||||
|
||||
export const logoutAction = loginStatePart.createAction(async (statePartArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
if (context.identity) {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_AdminLogout
|
||||
>('/typedrequest', 'adminLogout');
|
||||
await typedRequest.fire({ identity: context.identity });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Logout error:', err);
|
||||
}
|
||||
return { identity: null, isLoggedIn: false };
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Server Status Actions
|
||||
// ============================================================================
|
||||
|
||||
export const fetchServerStatusAction = serverStatePart.createAction(async (statePartArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetServerStatus
|
||||
>('/typedrequest', 'getServerStatus');
|
||||
const response = await typedRequest.fire({ identity: context.identity! });
|
||||
return { status: response.status, connectionInfo: response.connectionInfo };
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch server status:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Buckets Actions
|
||||
// ============================================================================
|
||||
|
||||
export const fetchBucketsAction = bucketsStatePart.createAction(async (statePartArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_ListBuckets
|
||||
>('/typedrequest', 'listBuckets');
|
||||
const response = await typedRequest.fire({ identity: context.identity! });
|
||||
return { buckets: response.buckets };
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch buckets:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
});
|
||||
|
||||
export const createBucketAction = bucketsStatePart.createAction<{ bucketName: string }>(
|
||||
async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_CreateBucket
|
||||
>('/typedrequest', 'createBucket');
|
||||
await typedRequest.fire({ identity: context.identity!, bucketName: dataArg.bucketName });
|
||||
// Re-fetch buckets list
|
||||
const listReq = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_ListBuckets
|
||||
>('/typedrequest', 'listBuckets');
|
||||
const listResp = await listReq.fire({ identity: context.identity! });
|
||||
return { buckets: listResp.buckets };
|
||||
} catch (err) {
|
||||
console.error('Failed to create bucket:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const deleteBucketAction = bucketsStatePart.createAction<{ bucketName: string }>(
|
||||
async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_DeleteBucket
|
||||
>('/typedrequest', 'deleteBucket');
|
||||
await typedRequest.fire({ identity: context.identity!, bucketName: dataArg.bucketName });
|
||||
const state = statePartArg.getState();
|
||||
return {
|
||||
buckets: state.buckets.filter((b) => b.name !== dataArg.bucketName),
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Failed to delete bucket:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// Named Policies Actions
|
||||
// ============================================================================
|
||||
|
||||
export const fetchPoliciesAction = policiesStatePart.createAction(async (statePartArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_ListNamedPolicies
|
||||
>('/typedrequest', 'listNamedPolicies');
|
||||
const response = await typedRequest.fire({ identity: context.identity! });
|
||||
return { policies: response.policies };
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch policies:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
});
|
||||
|
||||
export const createPolicyAction = policiesStatePart.createAction<{
|
||||
name: string;
|
||||
description: string;
|
||||
statements: interfaces.data.IObjstStatement[];
|
||||
}>(async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_CreateNamedPolicy
|
||||
>('/typedrequest', 'createNamedPolicy');
|
||||
await typedRequest.fire({
|
||||
identity: context.identity!,
|
||||
name: dataArg.name,
|
||||
description: dataArg.description,
|
||||
statements: dataArg.statements,
|
||||
});
|
||||
// Re-fetch
|
||||
const listReq = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_ListNamedPolicies
|
||||
>('/typedrequest', 'listNamedPolicies');
|
||||
const listResp = await listReq.fire({ identity: context.identity! });
|
||||
return { policies: listResp.policies };
|
||||
} catch (err) {
|
||||
console.error('Failed to create policy:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
});
|
||||
|
||||
export const updatePolicyAction = policiesStatePart.createAction<{
|
||||
policyId: string;
|
||||
name: string;
|
||||
description: string;
|
||||
statements: interfaces.data.IObjstStatement[];
|
||||
}>(async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_UpdateNamedPolicy
|
||||
>('/typedrequest', 'updateNamedPolicy');
|
||||
await typedRequest.fire({
|
||||
identity: context.identity!,
|
||||
policyId: dataArg.policyId,
|
||||
name: dataArg.name,
|
||||
description: dataArg.description,
|
||||
statements: dataArg.statements,
|
||||
});
|
||||
// Re-fetch
|
||||
const listReq = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_ListNamedPolicies
|
||||
>('/typedrequest', 'listNamedPolicies');
|
||||
const listResp = await listReq.fire({ identity: context.identity! });
|
||||
return { policies: listResp.policies };
|
||||
} catch (err) {
|
||||
console.error('Failed to update policy:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
});
|
||||
|
||||
export const deletePolicyAction = policiesStatePart.createAction<{ policyId: string }>(
|
||||
async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_DeleteNamedPolicy
|
||||
>('/typedrequest', 'deleteNamedPolicy');
|
||||
await typedRequest.fire({ identity: context.identity!, policyId: dataArg.policyId });
|
||||
const state = statePartArg.getState();
|
||||
return { policies: state.policies.filter((p) => p.id !== dataArg.policyId) };
|
||||
} catch (err) {
|
||||
console.error('Failed to delete policy:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Standalone async functions for policy-bucket management
|
||||
|
||||
export const getBucketNamedPolicies = async (bucketName: string) => {
|
||||
const context = getActionContext();
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetBucketNamedPolicies
|
||||
>('/typedrequest', 'getBucketNamedPolicies');
|
||||
return await typedRequest.fire({ identity: context.identity!, bucketName });
|
||||
};
|
||||
|
||||
export const attachPolicyToBucket = async (policyId: string, bucketName: string) => {
|
||||
const context = getActionContext();
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_AttachPolicyToBucket
|
||||
>('/typedrequest', 'attachPolicyToBucket');
|
||||
return await typedRequest.fire({ identity: context.identity!, policyId, bucketName });
|
||||
};
|
||||
|
||||
export const detachPolicyFromBucket = async (policyId: string, bucketName: string) => {
|
||||
const context = getActionContext();
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_DetachPolicyFromBucket
|
||||
>('/typedrequest', 'detachPolicyFromBucket');
|
||||
return await typedRequest.fire({ identity: context.identity!, policyId, bucketName });
|
||||
};
|
||||
|
||||
export const getPolicyBuckets = async (policyId: string) => {
|
||||
const context = getActionContext();
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetPolicyBuckets
|
||||
>('/typedrequest', 'getPolicyBuckets');
|
||||
return await typedRequest.fire({ identity: context.identity!, policyId });
|
||||
};
|
||||
|
||||
export const setPolicyBuckets = async (policyId: string, bucketNames: string[]) => {
|
||||
const context = getActionContext();
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_SetPolicyBuckets
|
||||
>('/typedrequest', 'setPolicyBuckets');
|
||||
return await typedRequest.fire({ identity: context.identity!, policyId, bucketNames });
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Objects Actions
|
||||
// ============================================================================
|
||||
|
||||
export const fetchObjectsAction = objectsStatePart.createAction<{
|
||||
bucketName: string;
|
||||
prefix?: string;
|
||||
delimiter?: string;
|
||||
}>(async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_ListObjects
|
||||
>('/typedrequest', 'listObjects');
|
||||
const response = await typedRequest.fire({
|
||||
identity: context.identity!,
|
||||
bucketName: dataArg.bucketName,
|
||||
prefix: dataArg.prefix || '',
|
||||
delimiter: dataArg.delimiter || '/',
|
||||
});
|
||||
return {
|
||||
result: response.result,
|
||||
currentBucket: dataArg.bucketName,
|
||||
currentPrefix: dataArg.prefix || '',
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch objects:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
});
|
||||
|
||||
export const deleteObjectAction = objectsStatePart.createAction<{
|
||||
bucketName: string;
|
||||
key: string;
|
||||
}>(async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_DeleteObject
|
||||
>('/typedrequest', 'deleteObject');
|
||||
await typedRequest.fire({
|
||||
identity: context.identity!,
|
||||
bucketName: dataArg.bucketName,
|
||||
key: dataArg.key,
|
||||
});
|
||||
// Re-fetch objects
|
||||
const state = statePartArg.getState();
|
||||
const listReq = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_ListObjects
|
||||
>('/typedrequest', 'listObjects');
|
||||
const listResp = await listReq.fire({
|
||||
identity: context.identity!,
|
||||
bucketName: state.currentBucket,
|
||||
prefix: state.currentPrefix,
|
||||
delimiter: '/',
|
||||
});
|
||||
return {
|
||||
result: listResp.result,
|
||||
currentBucket: state.currentBucket,
|
||||
currentPrefix: state.currentPrefix,
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Failed to delete object:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Credentials Actions
|
||||
// ============================================================================
|
||||
|
||||
export const fetchCredentialsAction = credentialsStatePart.createAction(async (statePartArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetCredentials
|
||||
>('/typedrequest', 'getCredentials');
|
||||
const response = await typedRequest.fire({ identity: context.identity! });
|
||||
return { credentials: response.credentials };
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch credentials:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
});
|
||||
|
||||
export const addCredentialAction = credentialsStatePart.createAction<{
|
||||
accessKeyId: string;
|
||||
secretAccessKey: string;
|
||||
}>(async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_AddCredential
|
||||
>('/typedrequest', 'addCredential');
|
||||
await typedRequest.fire({
|
||||
identity: context.identity!,
|
||||
accessKeyId: dataArg.accessKeyId,
|
||||
secretAccessKey: dataArg.secretAccessKey,
|
||||
});
|
||||
// Re-fetch credentials
|
||||
const listReq = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetCredentials
|
||||
>('/typedrequest', 'getCredentials');
|
||||
const listResp = await listReq.fire({ identity: context.identity! });
|
||||
return { credentials: listResp.credentials };
|
||||
} catch (err) {
|
||||
console.error('Failed to add credential:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
});
|
||||
|
||||
export const removeCredentialAction = credentialsStatePart.createAction<{
|
||||
accessKeyId: string;
|
||||
}>(async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_RemoveCredential
|
||||
>('/typedrequest', 'removeCredential');
|
||||
await typedRequest.fire({
|
||||
identity: context.identity!,
|
||||
accessKeyId: dataArg.accessKeyId,
|
||||
});
|
||||
const state = statePartArg.getState();
|
||||
return {
|
||||
credentials: state.credentials.filter((c) => c.accessKeyId !== dataArg.accessKeyId),
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Failed to remove credential:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Config Actions
|
||||
// ============================================================================
|
||||
|
||||
export const fetchConfigAction = configStatePart.createAction(async (statePartArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetServerConfig
|
||||
>('/typedrequest', 'getServerConfig');
|
||||
const response = await typedRequest.fire({ identity: context.identity! });
|
||||
return { config: response.config };
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch config:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// UI Actions
|
||||
// ============================================================================
|
||||
|
||||
export const setActiveViewAction = uiStatePart.createAction<{ view: string }>(
|
||||
async (statePartArg, dataArg) => {
|
||||
return { ...statePartArg.getState(), activeView: dataArg.view };
|
||||
},
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// Auto-refresh system
|
||||
// ============================================================================
|
||||
|
||||
let refreshIntervalHandle: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
const dispatchCombinedRefreshAction = async () => {
|
||||
const loginState = loginStatePart.getState();
|
||||
if (!loginState.isLoggedIn) return;
|
||||
|
||||
try {
|
||||
await serverStatePart.dispatchAction(fetchServerStatusAction, null);
|
||||
} catch (_err) {
|
||||
// Silently fail on auto-refresh
|
||||
}
|
||||
};
|
||||
|
||||
const startAutoRefresh = () => {
|
||||
const uiState = uiStatePart.getState();
|
||||
const loginState = loginStatePart.getState();
|
||||
|
||||
if (uiState.autoRefresh && loginState.isLoggedIn) {
|
||||
if (refreshIntervalHandle) {
|
||||
clearInterval(refreshIntervalHandle);
|
||||
}
|
||||
refreshIntervalHandle = setInterval(() => {
|
||||
dispatchCombinedRefreshAction();
|
||||
}, uiState.refreshInterval);
|
||||
} else {
|
||||
if (refreshIntervalHandle) {
|
||||
clearInterval(refreshIntervalHandle);
|
||||
refreshIntervalHandle = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
uiStatePart.select((s) => s).subscribe(() => startAutoRefresh());
|
||||
loginStatePart.select((s) => s).subscribe(() => startAutoRefresh());
|
||||
startAutoRefresh();
|
||||
115
ts_web/dataprovider.ts
Normal file
115
ts_web/dataprovider.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as appstate from './appstate.js';
|
||||
import * as interfaces from '../ts_interfaces/index.js';
|
||||
import type { IS3DataProvider } from '@design.estate/dees-catalog';
|
||||
|
||||
const getIdentity = (): interfaces.data.IIdentity => {
|
||||
return appstate.loginStatePart.getState().identity!;
|
||||
};
|
||||
|
||||
export const createDataProvider = (): IS3DataProvider => ({
|
||||
async listObjects(bucket: string, prefix?: string, delimiter?: string) {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_ListObjects
|
||||
>('/typedrequest', 'listObjects');
|
||||
const response = await typedRequest.fire({
|
||||
identity: getIdentity(),
|
||||
bucketName: bucket,
|
||||
prefix: prefix || '',
|
||||
delimiter: delimiter || '/',
|
||||
});
|
||||
return {
|
||||
objects: response.result.objects.map((obj) => ({
|
||||
key: obj.key,
|
||||
size: obj.size,
|
||||
lastModified: new Date(obj.lastModified).toISOString(),
|
||||
})),
|
||||
prefixes: response.result.commonPrefixes,
|
||||
};
|
||||
},
|
||||
|
||||
async getObject(bucket: string, key: string) {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetObject
|
||||
>('/typedrequest', 'getObject');
|
||||
return await typedRequest.fire({
|
||||
identity: getIdentity(),
|
||||
bucketName: bucket,
|
||||
key,
|
||||
});
|
||||
},
|
||||
|
||||
async putObject(bucket: string, key: string, base64Content: string, contentType: string) {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_PutObject
|
||||
>('/typedrequest', 'putObject');
|
||||
const response = await typedRequest.fire({
|
||||
identity: getIdentity(),
|
||||
bucketName: bucket,
|
||||
key,
|
||||
base64Content,
|
||||
contentType,
|
||||
});
|
||||
return response.ok;
|
||||
},
|
||||
|
||||
async deleteObject(bucket: string, key: string) {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_DeleteObject
|
||||
>('/typedrequest', 'deleteObject');
|
||||
const response = await typedRequest.fire({
|
||||
identity: getIdentity(),
|
||||
bucketName: bucket,
|
||||
key,
|
||||
});
|
||||
return response.ok;
|
||||
},
|
||||
|
||||
async deletePrefix(bucket: string, prefix: string) {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_DeletePrefix
|
||||
>('/typedrequest', 'deletePrefix');
|
||||
const response = await typedRequest.fire({
|
||||
identity: getIdentity(),
|
||||
bucketName: bucket,
|
||||
prefix,
|
||||
});
|
||||
return response.ok;
|
||||
},
|
||||
|
||||
async getObjectUrl(bucket: string, key: string) {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetObjectUrl
|
||||
>('/typedrequest', 'getObjectUrl');
|
||||
const response = await typedRequest.fire({
|
||||
identity: getIdentity(),
|
||||
bucketName: bucket,
|
||||
key,
|
||||
});
|
||||
return response.url;
|
||||
},
|
||||
|
||||
async moveObject(bucket: string, sourceKey: string, destKey: string) {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_MoveObject
|
||||
>('/typedrequest', 'moveObject');
|
||||
return await typedRequest.fire({
|
||||
identity: getIdentity(),
|
||||
bucketName: bucket,
|
||||
sourceKey,
|
||||
destKey,
|
||||
});
|
||||
},
|
||||
|
||||
async movePrefix(bucket: string, sourcePrefix: string, destPrefix: string) {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_MovePrefix
|
||||
>('/typedrequest', 'movePrefix');
|
||||
return await typedRequest.fire({
|
||||
identity: getIdentity(),
|
||||
bucketName: bucket,
|
||||
sourcePrefix,
|
||||
destPrefix,
|
||||
});
|
||||
},
|
||||
});
|
||||
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>`;
|
||||
}
|
||||
}
|
||||
7
ts_web/index.ts
Normal file
7
ts_web/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import { html } from '@design.estate/dees-element';
|
||||
import { appRouter } from './router.js';
|
||||
import './elements/index.js';
|
||||
|
||||
appRouter.init();
|
||||
plugins.deesElement.render(html`<objst-app-shell></objst-app-shell>`, document.body);
|
||||
4
ts_web/plugins.ts
Normal file
4
ts_web/plugins.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import * as deesElement from '@design.estate/dees-element';
|
||||
import * as deesCatalog from '@design.estate/dees-catalog';
|
||||
export { deesElement, deesCatalog };
|
||||
export const domtools = deesElement.domtools;
|
||||
109
ts_web/router.ts
Normal file
109
ts_web/router.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as appstate from './appstate.js';
|
||||
|
||||
const SmartRouter = plugins.domtools.plugins.smartrouter.SmartRouter;
|
||||
|
||||
export const validViews = ['overview', 'buckets', 'browser', 'policies', 'config', 'access keys'] as const;
|
||||
|
||||
export type TValidView = typeof validViews[number];
|
||||
|
||||
class AppRouter {
|
||||
private router: InstanceType<typeof SmartRouter>;
|
||||
private initialized = false;
|
||||
private suppressStateUpdate = false;
|
||||
|
||||
constructor() {
|
||||
this.router = new SmartRouter({ debug: false });
|
||||
}
|
||||
|
||||
public init(): void {
|
||||
if (this.initialized) return;
|
||||
this.setupRoutes();
|
||||
this.setupStateSync();
|
||||
this.handleInitialRoute();
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
private viewToPath(view: string): string {
|
||||
return `/${view.replace(/\s+/g, '-')}`;
|
||||
}
|
||||
|
||||
private pathToView(path: string): string {
|
||||
const segment = path.split('/').filter(Boolean)[0] || '';
|
||||
return segment.replace(/-/g, ' ');
|
||||
}
|
||||
|
||||
private setupRoutes(): void {
|
||||
for (const view of validViews) {
|
||||
this.router.on(this.viewToPath(view), async () => {
|
||||
this.updateViewState(view);
|
||||
});
|
||||
}
|
||||
|
||||
// Root redirect
|
||||
this.router.on('/', async () => {
|
||||
this.navigateTo(this.viewToPath('overview'));
|
||||
});
|
||||
}
|
||||
|
||||
private setupStateSync(): void {
|
||||
appstate.uiStatePart.state.subscribe((uiState) => {
|
||||
if (this.suppressStateUpdate) return;
|
||||
|
||||
const currentPath = window.location.pathname;
|
||||
const expectedPath = this.viewToPath(uiState.activeView);
|
||||
|
||||
if (currentPath !== expectedPath) {
|
||||
this.suppressStateUpdate = true;
|
||||
this.router.pushUrl(expectedPath);
|
||||
this.suppressStateUpdate = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private handleInitialRoute(): void {
|
||||
const path = window.location.pathname;
|
||||
|
||||
if (!path || path === '/') {
|
||||
this.router.pushUrl(this.viewToPath('overview'));
|
||||
} else {
|
||||
const view = this.pathToView(path);
|
||||
if (validViews.includes(view as TValidView)) {
|
||||
this.updateViewState(view as TValidView);
|
||||
} else {
|
||||
this.router.pushUrl(this.viewToPath('overview'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateViewState(view: string): void {
|
||||
this.suppressStateUpdate = true;
|
||||
const currentState = appstate.uiStatePart.getState();
|
||||
if (currentState.activeView !== view) {
|
||||
appstate.uiStatePart.setState({
|
||||
...currentState,
|
||||
activeView: view,
|
||||
});
|
||||
}
|
||||
this.suppressStateUpdate = false;
|
||||
}
|
||||
|
||||
public navigateTo(path: string): void {
|
||||
this.router.pushUrl(path);
|
||||
}
|
||||
|
||||
public navigateToView(view: string): void {
|
||||
if (validViews.includes(view as TValidView)) {
|
||||
this.navigateTo(this.viewToPath(view));
|
||||
} else {
|
||||
this.navigateTo(this.viewToPath('overview'));
|
||||
}
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
this.router.destroy();
|
||||
this.initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
export const appRouter = new AppRouter();
|
||||
Reference in New Issue
Block a user