first pass at access control setup
This commit is contained in:
@@ -47,6 +47,15 @@ const TriggerCreatePage = lazy(
|
||||
);
|
||||
const TriggerEditPage = lazy(() => import("@/pages/triggers/TriggerEditPage"));
|
||||
const SensorsPage = lazy(() => import("@/pages/sensors/SensorsPage"));
|
||||
const AccessControlPage = lazy(
|
||||
() => import("@/pages/access-control/AccessControlPage"),
|
||||
);
|
||||
const IdentityDetailPage = lazy(
|
||||
() => import("@/pages/access-control/IdentityDetailPage"),
|
||||
);
|
||||
const PermissionSetDetailPage = lazy(
|
||||
() => import("@/pages/access-control/PermissionSetDetailPage"),
|
||||
);
|
||||
|
||||
function PageLoader() {
|
||||
return (
|
||||
@@ -134,6 +143,18 @@ function App() {
|
||||
/>
|
||||
<Route path="sensors" element={<SensorsPage />} />
|
||||
<Route path="sensors/:ref" element={<SensorsPage />} />
|
||||
<Route
|
||||
path="access-control"
|
||||
element={<AccessControlPage />}
|
||||
/>
|
||||
<Route
|
||||
path="access-control/identities/:id"
|
||||
element={<IdentityDetailPage />}
|
||||
/>
|
||||
<Route
|
||||
path="access-control/permission-sets/:ref"
|
||||
element={<PermissionSetDetailPage />}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
{/* Catch all - redirect to dashboard */}
|
||||
|
||||
@@ -9,12 +9,15 @@ export type { OpenAPIConfig } from './core/OpenAPI';
|
||||
|
||||
export type { ActionResponse } from './models/ActionResponse';
|
||||
export type { ActionSummary } from './models/ActionSummary';
|
||||
export type { AgentArchInfo } from './models/AgentArchInfo';
|
||||
export type { AgentBinaryInfo } from './models/AgentBinaryInfo';
|
||||
export type { ApiResponse_ActionResponse } from './models/ApiResponse_ActionResponse';
|
||||
export type { ApiResponse_AuthSettingsResponse } from './models/ApiResponse_AuthSettingsResponse';
|
||||
export type { ApiResponse_CurrentUserResponse } from './models/ApiResponse_CurrentUserResponse';
|
||||
export type { ApiResponse_EnforcementResponse } from './models/ApiResponse_EnforcementResponse';
|
||||
export type { ApiResponse_EventResponse } from './models/ApiResponse_EventResponse';
|
||||
export type { ApiResponse_ExecutionResponse } from './models/ApiResponse_ExecutionResponse';
|
||||
export type { ApiResponse_IdentitySummary } from './models/ApiResponse_IdentitySummary';
|
||||
export type { ApiResponse_IdentityResponse } from './models/ApiResponse_IdentityResponse';
|
||||
export type { ApiResponse_InquiryResponse } from './models/ApiResponse_InquiryResponse';
|
||||
export type { ApiResponse_KeyResponse } from './models/ApiResponse_KeyResponse';
|
||||
export type { ApiResponse_PackInstallResponse } from './models/ApiResponse_PackInstallResponse';
|
||||
@@ -32,10 +35,12 @@ export type { ApiResponse_WorkflowResponse } from './models/ApiResponse_Workflow
|
||||
export type { ChangePasswordRequest } from './models/ChangePasswordRequest';
|
||||
export type { CreateActionRequest } from './models/CreateActionRequest';
|
||||
export type { CreateIdentityRequest } from './models/CreateIdentityRequest';
|
||||
export type { CreateIdentityRoleAssignmentRequest } from './models/CreateIdentityRoleAssignmentRequest';
|
||||
export type { CreateInquiryRequest } from './models/CreateInquiryRequest';
|
||||
export type { CreateKeyRequest } from './models/CreateKeyRequest';
|
||||
export type { CreatePackRequest } from './models/CreatePackRequest';
|
||||
export type { CreatePermissionAssignmentRequest } from './models/CreatePermissionAssignmentRequest';
|
||||
export type { CreatePermissionSetRoleAssignmentRequest } from './models/CreatePermissionSetRoleAssignmentRequest';
|
||||
export type { CreateRuleRequest } from './models/CreateRuleRequest';
|
||||
export type { CreateRuntimeRequest } from './models/CreateRuntimeRequest';
|
||||
export type { CreateSensorRequest } from './models/CreateSensorRequest';
|
||||
@@ -53,6 +58,8 @@ export { ExecutionStatus } from './models/ExecutionStatus';
|
||||
export type { ExecutionSummary } from './models/ExecutionSummary';
|
||||
export type { HealthResponse } from './models/HealthResponse';
|
||||
export type { i64 } from './models/i64';
|
||||
export type { IdentityResponse } from './models/IdentityResponse';
|
||||
export type { IdentityRoleAssignmentResponse } from './models/IdentityRoleAssignmentResponse';
|
||||
export type { IdentitySummary } from './models/IdentitySummary';
|
||||
export type { InquiryRespondRequest } from './models/InquiryRespondRequest';
|
||||
export type { InquiryResponse } from './models/InquiryResponse';
|
||||
@@ -61,10 +68,12 @@ export type { InquirySummary } from './models/InquirySummary';
|
||||
export type { InstallPackRequest } from './models/InstallPackRequest';
|
||||
export type { KeyResponse } from './models/KeyResponse';
|
||||
export type { KeySummary } from './models/KeySummary';
|
||||
export type { LdapLoginRequest } from './models/LdapLoginRequest';
|
||||
export type { LoginRequest } from './models/LoginRequest';
|
||||
export { NullableJsonPatch } from './models/NullableJsonPatch';
|
||||
export { NullableStringPatch } from './models/NullableStringPatch';
|
||||
export { OwnerType } from './models/OwnerType';
|
||||
export { PackDescriptionPatch } from './models/PackDescriptionPatch';
|
||||
export type { PackInstallResponse } from './models/PackInstallResponse';
|
||||
export type { PackResponse } from './models/PackResponse';
|
||||
export type { PackSummary } from './models/PackSummary';
|
||||
@@ -89,6 +98,7 @@ export type { PaginatedResponse_TriggerSummary } from './models/PaginatedRespons
|
||||
export type { PaginatedResponse_WorkflowSummary } from './models/PaginatedResponse_WorkflowSummary';
|
||||
export type { PaginationMeta } from './models/PaginationMeta';
|
||||
export type { PermissionAssignmentResponse } from './models/PermissionAssignmentResponse';
|
||||
export type { PermissionSetRoleAssignmentResponse } from './models/PermissionSetRoleAssignmentResponse';
|
||||
export type { PermissionSetSummary } from './models/PermissionSetSummary';
|
||||
export type { QueueStatsResponse } from './models/QueueStatsResponse';
|
||||
export type { RefreshTokenRequest } from './models/RefreshTokenRequest';
|
||||
@@ -98,6 +108,7 @@ export type { RuleResponse } from './models/RuleResponse';
|
||||
export type { RuleSummary } from './models/RuleSummary';
|
||||
export type { RuntimeResponse } from './models/RuntimeResponse';
|
||||
export type { RuntimeSummary } from './models/RuntimeSummary';
|
||||
export { RuntimeVersionConstraintPatch } from './models/RuntimeVersionConstraintPatch';
|
||||
export type { SensorResponse } from './models/SensorResponse';
|
||||
export type { SensorSummary } from './models/SensorSummary';
|
||||
export type { SuccessResponse } from './models/SuccessResponse';
|
||||
@@ -106,6 +117,7 @@ export { TestStatus } from './models/TestStatus';
|
||||
export type { TestSuiteResult } from './models/TestSuiteResult';
|
||||
export type { TokenResponse } from './models/TokenResponse';
|
||||
export type { TriggerResponse } from './models/TriggerResponse';
|
||||
export { TriggerStringPatch } from './models/TriggerStringPatch';
|
||||
export type { TriggerSummary } from './models/TriggerSummary';
|
||||
export type { UpdateActionRequest } from './models/UpdateActionRequest';
|
||||
export type { UpdateIdentityRequest } from './models/UpdateIdentityRequest';
|
||||
@@ -126,6 +138,7 @@ export type { WorkflowSummary } from './models/WorkflowSummary';
|
||||
export type { WorkflowSyncResult } from './models/WorkflowSyncResult';
|
||||
|
||||
export { ActionsService } from './services/ActionsService';
|
||||
export { AgentService } from './services/AgentService';
|
||||
export { AuthService } from './services/AuthService';
|
||||
export { EnforcementsService } from './services/EnforcementsService';
|
||||
export { EventsService } from './services/EventsService';
|
||||
|
||||
@@ -6,65 +6,64 @@
|
||||
* Response DTO for action information
|
||||
*/
|
||||
export type ActionResponse = {
|
||||
/**
|
||||
* Creation timestamp
|
||||
*/
|
||||
created: string;
|
||||
/**
|
||||
* Action description
|
||||
*/
|
||||
description: string;
|
||||
/**
|
||||
* Entry point
|
||||
*/
|
||||
entrypoint: string;
|
||||
/**
|
||||
* Action ID
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Whether this is an ad-hoc action (not from pack installation)
|
||||
*/
|
||||
is_adhoc: boolean;
|
||||
/**
|
||||
* Human-readable label
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* Output schema
|
||||
*/
|
||||
out_schema: any | null;
|
||||
/**
|
||||
* Pack ID
|
||||
*/
|
||||
pack: number;
|
||||
/**
|
||||
* Pack reference
|
||||
*/
|
||||
pack_ref: string;
|
||||
/**
|
||||
* Parameter schema (StackStorm-style with inline required/secret)
|
||||
*/
|
||||
param_schema: any | null;
|
||||
/**
|
||||
* Unique reference identifier
|
||||
*/
|
||||
ref: string;
|
||||
/**
|
||||
* Runtime ID
|
||||
*/
|
||||
runtime?: number | null;
|
||||
/**
|
||||
* Semver version constraint for the runtime (e.g., ">=3.12", ">=3.12,<4.0", "~18.0")
|
||||
*/
|
||||
runtime_version_constraint?: string | null;
|
||||
/**
|
||||
* Last update timestamp
|
||||
*/
|
||||
updated: string;
|
||||
/**
|
||||
* Workflow definition ID (non-null if this action is a workflow)
|
||||
*/
|
||||
workflow_def?: number | null;
|
||||
/**
|
||||
* Creation timestamp
|
||||
*/
|
||||
created: string;
|
||||
/**
|
||||
* Action description
|
||||
*/
|
||||
description: string | null;
|
||||
/**
|
||||
* Entry point
|
||||
*/
|
||||
entrypoint: string;
|
||||
/**
|
||||
* Action ID
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Whether this is an ad-hoc action (not from pack installation)
|
||||
*/
|
||||
is_adhoc: boolean;
|
||||
/**
|
||||
* Human-readable label
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* Output schema
|
||||
*/
|
||||
out_schema: any | null;
|
||||
/**
|
||||
* Pack ID
|
||||
*/
|
||||
pack: number;
|
||||
/**
|
||||
* Pack reference
|
||||
*/
|
||||
pack_ref: string;
|
||||
/**
|
||||
* Parameter schema (StackStorm-style with inline required/secret)
|
||||
*/
|
||||
param_schema: any | null;
|
||||
/**
|
||||
* Unique reference identifier
|
||||
*/
|
||||
ref: string;
|
||||
/**
|
||||
* Runtime ID
|
||||
*/
|
||||
runtime?: number | null;
|
||||
/**
|
||||
* Semver version constraint for the runtime (e.g., ">=3.12", ">=3.12,<4.0", "~18.0")
|
||||
*/
|
||||
runtime_version_constraint?: string | null;
|
||||
/**
|
||||
* Last update timestamp
|
||||
*/
|
||||
updated: string;
|
||||
/**
|
||||
* Workflow definition ID (non-null if this action is a workflow)
|
||||
*/
|
||||
workflow_def?: number | null;
|
||||
};
|
||||
|
||||
|
||||
@@ -6,49 +6,48 @@
|
||||
* Simplified action response (for list endpoints)
|
||||
*/
|
||||
export type ActionSummary = {
|
||||
/**
|
||||
* Creation timestamp
|
||||
*/
|
||||
created: string;
|
||||
/**
|
||||
* Action description
|
||||
*/
|
||||
description: string;
|
||||
/**
|
||||
* Entry point
|
||||
*/
|
||||
entrypoint: string;
|
||||
/**
|
||||
* Action ID
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Human-readable label
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* Pack reference
|
||||
*/
|
||||
pack_ref: string;
|
||||
/**
|
||||
* Unique reference identifier
|
||||
*/
|
||||
ref: string;
|
||||
/**
|
||||
* Runtime ID
|
||||
*/
|
||||
runtime?: number | null;
|
||||
/**
|
||||
* Semver version constraint for the runtime
|
||||
*/
|
||||
runtime_version_constraint?: string | null;
|
||||
/**
|
||||
* Last update timestamp
|
||||
*/
|
||||
updated: string;
|
||||
/**
|
||||
* Workflow definition ID (non-null if this action is a workflow)
|
||||
*/
|
||||
workflow_def?: number | null;
|
||||
/**
|
||||
* Creation timestamp
|
||||
*/
|
||||
created: string;
|
||||
/**
|
||||
* Action description
|
||||
*/
|
||||
description: string | null;
|
||||
/**
|
||||
* Entry point
|
||||
*/
|
||||
entrypoint: string;
|
||||
/**
|
||||
* Action ID
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Human-readable label
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* Pack reference
|
||||
*/
|
||||
pack_ref: string;
|
||||
/**
|
||||
* Unique reference identifier
|
||||
*/
|
||||
ref: string;
|
||||
/**
|
||||
* Runtime ID
|
||||
*/
|
||||
runtime?: number | null;
|
||||
/**
|
||||
* Semver version constraint for the runtime
|
||||
*/
|
||||
runtime_version_constraint?: string | null;
|
||||
/**
|
||||
* Last update timestamp
|
||||
*/
|
||||
updated: string;
|
||||
/**
|
||||
* Workflow definition ID (non-null if this action is a workflow)
|
||||
*/
|
||||
workflow_def?: number | null;
|
||||
};
|
||||
|
||||
|
||||
22
web/src/api/models/AgentArchInfo.ts
Normal file
22
web/src/api/models/AgentArchInfo.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Per-architecture binary info
|
||||
*/
|
||||
export type AgentArchInfo = {
|
||||
/**
|
||||
* Architecture name
|
||||
*/
|
||||
arch: string;
|
||||
/**
|
||||
* Whether this binary is available
|
||||
*/
|
||||
available: boolean;
|
||||
/**
|
||||
* Binary size in bytes
|
||||
*/
|
||||
size_bytes: number;
|
||||
};
|
||||
|
||||
19
web/src/api/models/AgentBinaryInfo.ts
Normal file
19
web/src/api/models/AgentBinaryInfo.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { AgentArchInfo } from './AgentArchInfo';
|
||||
/**
|
||||
* Agent binary metadata
|
||||
*/
|
||||
export type AgentBinaryInfo = {
|
||||
/**
|
||||
* Available architectures
|
||||
*/
|
||||
architectures: Array<AgentArchInfo>;
|
||||
/**
|
||||
* Agent version (from build)
|
||||
*/
|
||||
version: string;
|
||||
};
|
||||
|
||||
@@ -6,74 +6,73 @@
|
||||
* Standard API response wrapper
|
||||
*/
|
||||
export type ApiResponse_ActionResponse = {
|
||||
/**
|
||||
* Response DTO for action information
|
||||
*/
|
||||
data: {
|
||||
/**
|
||||
* Response DTO for action information
|
||||
* Creation timestamp
|
||||
*/
|
||||
data: {
|
||||
/**
|
||||
* Creation timestamp
|
||||
*/
|
||||
created: string;
|
||||
/**
|
||||
* Action description
|
||||
*/
|
||||
description: string;
|
||||
/**
|
||||
* Entry point
|
||||
*/
|
||||
entrypoint: string;
|
||||
/**
|
||||
* Action ID
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Whether this is an ad-hoc action (not from pack installation)
|
||||
*/
|
||||
is_adhoc: boolean;
|
||||
/**
|
||||
* Human-readable label
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* Output schema
|
||||
*/
|
||||
out_schema: any | null;
|
||||
/**
|
||||
* Pack ID
|
||||
*/
|
||||
pack: number;
|
||||
/**
|
||||
* Pack reference
|
||||
*/
|
||||
pack_ref: string;
|
||||
/**
|
||||
* Parameter schema (StackStorm-style with inline required/secret)
|
||||
*/
|
||||
param_schema: any | null;
|
||||
/**
|
||||
* Unique reference identifier
|
||||
*/
|
||||
ref: string;
|
||||
/**
|
||||
* Runtime ID
|
||||
*/
|
||||
runtime?: number | null;
|
||||
/**
|
||||
* Semver version constraint for the runtime (e.g., ">=3.12", ">=3.12,<4.0", "~18.0")
|
||||
*/
|
||||
runtime_version_constraint?: string | null;
|
||||
/**
|
||||
* Last update timestamp
|
||||
*/
|
||||
updated: string;
|
||||
/**
|
||||
* Workflow definition ID (non-null if this action is a workflow)
|
||||
*/
|
||||
workflow_def?: number | null;
|
||||
};
|
||||
created: string;
|
||||
/**
|
||||
* Optional message
|
||||
* Action description
|
||||
*/
|
||||
message?: string | null;
|
||||
description: string | null;
|
||||
/**
|
||||
* Entry point
|
||||
*/
|
||||
entrypoint: string;
|
||||
/**
|
||||
* Action ID
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Whether this is an ad-hoc action (not from pack installation)
|
||||
*/
|
||||
is_adhoc: boolean;
|
||||
/**
|
||||
* Human-readable label
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* Output schema
|
||||
*/
|
||||
out_schema: any | null;
|
||||
/**
|
||||
* Pack ID
|
||||
*/
|
||||
pack: number;
|
||||
/**
|
||||
* Pack reference
|
||||
*/
|
||||
pack_ref: string;
|
||||
/**
|
||||
* Parameter schema (StackStorm-style with inline required/secret)
|
||||
*/
|
||||
param_schema: any | null;
|
||||
/**
|
||||
* Unique reference identifier
|
||||
*/
|
||||
ref: string;
|
||||
/**
|
||||
* Runtime ID
|
||||
*/
|
||||
runtime?: number | null;
|
||||
/**
|
||||
* Semver version constraint for the runtime (e.g., ">=3.12", ">=3.12,<4.0", "~18.0")
|
||||
*/
|
||||
runtime_version_constraint?: string | null;
|
||||
/**
|
||||
* Last update timestamp
|
||||
*/
|
||||
updated: string;
|
||||
/**
|
||||
* Workflow definition ID (non-null if this action is a workflow)
|
||||
*/
|
||||
workflow_def?: number | null;
|
||||
};
|
||||
/**
|
||||
* Optional message
|
||||
*/
|
||||
message?: string | null;
|
||||
};
|
||||
|
||||
|
||||
75
web/src/api/models/ApiResponse_AuthSettingsResponse.ts
Normal file
75
web/src/api/models/ApiResponse_AuthSettingsResponse.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Standard API response wrapper
|
||||
*/
|
||||
export type ApiResponse_AuthSettingsResponse = {
|
||||
/**
|
||||
* Public authentication settings for the login page.
|
||||
*/
|
||||
data: {
|
||||
/**
|
||||
* Whether authentication is enabled for the server.
|
||||
*/
|
||||
authentication_enabled: boolean;
|
||||
/**
|
||||
* Whether LDAP login is configured and enabled.
|
||||
*/
|
||||
ldap_enabled: boolean;
|
||||
/**
|
||||
* Optional icon URL shown beside the provider label.
|
||||
*/
|
||||
ldap_provider_icon_url?: string | null;
|
||||
/**
|
||||
* User-facing provider label for the login button.
|
||||
*/
|
||||
ldap_provider_label?: string | null;
|
||||
/**
|
||||
* Provider name for `?auth=<provider>`.
|
||||
*/
|
||||
ldap_provider_name?: string | null;
|
||||
/**
|
||||
* Whether LDAP login should be shown by default.
|
||||
*/
|
||||
ldap_visible_by_default: boolean;
|
||||
/**
|
||||
* Whether local username/password login is configured.
|
||||
*/
|
||||
local_password_enabled: boolean;
|
||||
/**
|
||||
* Whether local username/password login should be shown by default.
|
||||
*/
|
||||
local_password_visible_by_default: boolean;
|
||||
/**
|
||||
* Whether OIDC login is configured and enabled.
|
||||
*/
|
||||
oidc_enabled: boolean;
|
||||
/**
|
||||
* Optional icon URL shown beside the provider label.
|
||||
*/
|
||||
oidc_provider_icon_url?: string | null;
|
||||
/**
|
||||
* User-facing provider label for the login button.
|
||||
*/
|
||||
oidc_provider_label?: string | null;
|
||||
/**
|
||||
* Provider name for `?auth=<provider>`.
|
||||
*/
|
||||
oidc_provider_name?: string | null;
|
||||
/**
|
||||
* Whether OIDC login should be shown by default.
|
||||
*/
|
||||
oidc_visible_by_default: boolean;
|
||||
/**
|
||||
* Whether unauthenticated self-service registration is allowed.
|
||||
*/
|
||||
self_registration_enabled: boolean;
|
||||
};
|
||||
/**
|
||||
* Optional message
|
||||
*/
|
||||
message?: string | null;
|
||||
};
|
||||
|
||||
@@ -2,16 +2,21 @@
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { IdentityRoleAssignmentResponse } from './IdentityRoleAssignmentResponse';
|
||||
import type { PermissionAssignmentResponse } from './PermissionAssignmentResponse';
|
||||
import type { Value } from './Value';
|
||||
/**
|
||||
* Standard API response wrapper
|
||||
*/
|
||||
export type ApiResponse_IdentitySummary = {
|
||||
export type ApiResponse_IdentityResponse = {
|
||||
data: {
|
||||
attributes: Value;
|
||||
direct_permissions: Array<PermissionAssignmentResponse>;
|
||||
display_name?: string | null;
|
||||
frozen: boolean;
|
||||
id: number;
|
||||
login: string;
|
||||
roles: Array<IdentityRoleAssignmentResponse>;
|
||||
};
|
||||
/**
|
||||
* Optional message
|
||||
@@ -6,82 +6,81 @@
|
||||
* Standard API response wrapper
|
||||
*/
|
||||
export type ApiResponse_RuleResponse = {
|
||||
/**
|
||||
* Response DTO for rule information
|
||||
*/
|
||||
data: {
|
||||
/**
|
||||
* Response DTO for rule information
|
||||
* Action ID (null if the referenced action has been deleted)
|
||||
*/
|
||||
data: {
|
||||
/**
|
||||
* Action ID (null if the referenced action has been deleted)
|
||||
*/
|
||||
action?: number | null;
|
||||
/**
|
||||
* Parameters to pass to the action when rule is triggered
|
||||
*/
|
||||
action_params: Record<string, any>;
|
||||
/**
|
||||
* Action reference
|
||||
*/
|
||||
action_ref: string;
|
||||
/**
|
||||
* Conditions for rule evaluation
|
||||
*/
|
||||
conditions: Record<string, any>;
|
||||
/**
|
||||
* Creation timestamp
|
||||
*/
|
||||
created: string;
|
||||
/**
|
||||
* Rule description
|
||||
*/
|
||||
description: string;
|
||||
/**
|
||||
* Whether the rule is enabled
|
||||
*/
|
||||
enabled: boolean;
|
||||
/**
|
||||
* Rule ID
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Whether this is an ad-hoc rule (not from pack installation)
|
||||
*/
|
||||
is_adhoc: boolean;
|
||||
/**
|
||||
* Human-readable label
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* Pack ID
|
||||
*/
|
||||
pack: number;
|
||||
/**
|
||||
* Pack reference
|
||||
*/
|
||||
pack_ref: string;
|
||||
/**
|
||||
* Unique reference identifier
|
||||
*/
|
||||
ref: string;
|
||||
/**
|
||||
* Trigger ID (null if the referenced trigger has been deleted)
|
||||
*/
|
||||
trigger?: number | null;
|
||||
/**
|
||||
* Parameters for trigger configuration and event filtering
|
||||
*/
|
||||
trigger_params: Record<string, any>;
|
||||
/**
|
||||
* Trigger reference
|
||||
*/
|
||||
trigger_ref: string;
|
||||
/**
|
||||
* Last update timestamp
|
||||
*/
|
||||
updated: string;
|
||||
};
|
||||
action?: number | null;
|
||||
/**
|
||||
* Optional message
|
||||
* Parameters to pass to the action when rule is triggered
|
||||
*/
|
||||
message?: string | null;
|
||||
action_params: Record<string, any>;
|
||||
/**
|
||||
* Action reference
|
||||
*/
|
||||
action_ref: string;
|
||||
/**
|
||||
* Conditions for rule evaluation
|
||||
*/
|
||||
conditions: Record<string, any>;
|
||||
/**
|
||||
* Creation timestamp
|
||||
*/
|
||||
created: string;
|
||||
/**
|
||||
* Rule description
|
||||
*/
|
||||
description: string | null;
|
||||
/**
|
||||
* Whether the rule is enabled
|
||||
*/
|
||||
enabled: boolean;
|
||||
/**
|
||||
* Rule ID
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Whether this is an ad-hoc rule (not from pack installation)
|
||||
*/
|
||||
is_adhoc: boolean;
|
||||
/**
|
||||
* Human-readable label
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* Pack ID
|
||||
*/
|
||||
pack: number;
|
||||
/**
|
||||
* Pack reference
|
||||
*/
|
||||
pack_ref: string;
|
||||
/**
|
||||
* Unique reference identifier
|
||||
*/
|
||||
ref: string;
|
||||
/**
|
||||
* Trigger ID (null if the referenced trigger has been deleted)
|
||||
*/
|
||||
trigger?: number | null;
|
||||
/**
|
||||
* Parameters for trigger configuration and event filtering
|
||||
*/
|
||||
trigger_params: Record<string, any>;
|
||||
/**
|
||||
* Trigger reference
|
||||
*/
|
||||
trigger_ref: string;
|
||||
/**
|
||||
* Last update timestamp
|
||||
*/
|
||||
updated: string;
|
||||
};
|
||||
/**
|
||||
* Optional message
|
||||
*/
|
||||
message?: string | null;
|
||||
};
|
||||
|
||||
|
||||
@@ -6,74 +6,73 @@
|
||||
* Standard API response wrapper
|
||||
*/
|
||||
export type ApiResponse_SensorResponse = {
|
||||
/**
|
||||
* Response DTO for sensor information
|
||||
*/
|
||||
data: {
|
||||
/**
|
||||
* Response DTO for sensor information
|
||||
* Creation timestamp
|
||||
*/
|
||||
data: {
|
||||
/**
|
||||
* Creation timestamp
|
||||
*/
|
||||
created: string;
|
||||
/**
|
||||
* Sensor description
|
||||
*/
|
||||
description: string;
|
||||
/**
|
||||
* Whether the sensor is enabled
|
||||
*/
|
||||
enabled: boolean;
|
||||
/**
|
||||
* Entry point
|
||||
*/
|
||||
entrypoint: string;
|
||||
/**
|
||||
* Sensor ID
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Human-readable label
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* Pack ID (optional)
|
||||
*/
|
||||
pack?: number | null;
|
||||
/**
|
||||
* Pack reference (optional)
|
||||
*/
|
||||
pack_ref?: string | null;
|
||||
/**
|
||||
* Parameter schema (StackStorm-style with inline required/secret)
|
||||
*/
|
||||
param_schema: any | null;
|
||||
/**
|
||||
* Unique reference identifier
|
||||
*/
|
||||
ref: string;
|
||||
/**
|
||||
* Runtime ID
|
||||
*/
|
||||
runtime: number;
|
||||
/**
|
||||
* Runtime reference
|
||||
*/
|
||||
runtime_ref: string;
|
||||
/**
|
||||
* Trigger ID
|
||||
*/
|
||||
trigger: number;
|
||||
/**
|
||||
* Trigger reference
|
||||
*/
|
||||
trigger_ref: string;
|
||||
/**
|
||||
* Last update timestamp
|
||||
*/
|
||||
updated: string;
|
||||
};
|
||||
created: string;
|
||||
/**
|
||||
* Optional message
|
||||
* Sensor description
|
||||
*/
|
||||
message?: string | null;
|
||||
description: string | null;
|
||||
/**
|
||||
* Whether the sensor is enabled
|
||||
*/
|
||||
enabled: boolean;
|
||||
/**
|
||||
* Entry point
|
||||
*/
|
||||
entrypoint: string;
|
||||
/**
|
||||
* Sensor ID
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Human-readable label
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* Pack ID (optional)
|
||||
*/
|
||||
pack?: number | null;
|
||||
/**
|
||||
* Pack reference (optional)
|
||||
*/
|
||||
pack_ref?: string | null;
|
||||
/**
|
||||
* Parameter schema (StackStorm-style with inline required/secret)
|
||||
*/
|
||||
param_schema: any | null;
|
||||
/**
|
||||
* Unique reference identifier
|
||||
*/
|
||||
ref: string;
|
||||
/**
|
||||
* Runtime ID
|
||||
*/
|
||||
runtime: number;
|
||||
/**
|
||||
* Runtime reference
|
||||
*/
|
||||
runtime_ref: string;
|
||||
/**
|
||||
* Trigger ID
|
||||
*/
|
||||
trigger: number;
|
||||
/**
|
||||
* Trigger reference
|
||||
*/
|
||||
trigger_ref: string;
|
||||
/**
|
||||
* Last update timestamp
|
||||
*/
|
||||
updated: string;
|
||||
};
|
||||
/**
|
||||
* Optional message
|
||||
*/
|
||||
message?: string | null;
|
||||
};
|
||||
|
||||
|
||||
@@ -6,41 +6,40 @@
|
||||
* Request DTO for creating a new action
|
||||
*/
|
||||
export type CreateActionRequest = {
|
||||
/**
|
||||
* Action description
|
||||
*/
|
||||
description: string;
|
||||
/**
|
||||
* Entry point for action execution (e.g., path to script, function name)
|
||||
*/
|
||||
entrypoint: string;
|
||||
/**
|
||||
* Human-readable label
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* Output schema (flat format) defining expected outputs with inline required/secret
|
||||
*/
|
||||
out_schema?: any | null;
|
||||
/**
|
||||
* Pack reference this action belongs to
|
||||
*/
|
||||
pack_ref: string;
|
||||
/**
|
||||
* Parameter schema (StackStorm-style) defining expected inputs with inline required/secret
|
||||
*/
|
||||
param_schema?: any | null;
|
||||
/**
|
||||
* Unique reference identifier (e.g., "core.http", "aws.ec2.start_instance")
|
||||
*/
|
||||
ref: string;
|
||||
/**
|
||||
* Optional runtime ID for this action
|
||||
*/
|
||||
runtime?: number | null;
|
||||
/**
|
||||
* Optional semver version constraint for the runtime (e.g., ">=3.12", ">=3.12,<4.0", "~18.0")
|
||||
*/
|
||||
runtime_version_constraint?: string | null;
|
||||
/**
|
||||
* Action description
|
||||
*/
|
||||
description?: string | null;
|
||||
/**
|
||||
* Entry point for action execution (e.g., path to script, function name)
|
||||
*/
|
||||
entrypoint: string;
|
||||
/**
|
||||
* Human-readable label
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* Output schema (flat format) defining expected outputs with inline required/secret
|
||||
*/
|
||||
out_schema?: any | null;
|
||||
/**
|
||||
* Pack reference this action belongs to
|
||||
*/
|
||||
pack_ref: string;
|
||||
/**
|
||||
* Parameter schema (StackStorm-style) defining expected inputs with inline required/secret
|
||||
*/
|
||||
param_schema?: any | null;
|
||||
/**
|
||||
* Unique reference identifier (e.g., "core.http", "aws.ec2.start_instance")
|
||||
*/
|
||||
ref: string;
|
||||
/**
|
||||
* Optional runtime ID for this action
|
||||
*/
|
||||
runtime?: number | null;
|
||||
/**
|
||||
* Optional semver version constraint for the runtime (e.g., ">=3.12", ">=3.12,<4.0", "~18.0")
|
||||
*/
|
||||
runtime_version_constraint?: string | null;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type CreateIdentityRoleAssignmentRequest = {
|
||||
role: string;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type CreatePermissionSetRoleAssignmentRequest = {
|
||||
role: string;
|
||||
};
|
||||
|
||||
@@ -6,45 +6,44 @@
|
||||
* Request DTO for creating a new rule
|
||||
*/
|
||||
export type CreateRuleRequest = {
|
||||
/**
|
||||
* Parameters to pass to the action when rule is triggered
|
||||
*/
|
||||
action_params?: Record<string, any>;
|
||||
/**
|
||||
* Action reference to execute when rule matches
|
||||
*/
|
||||
action_ref: string;
|
||||
/**
|
||||
* Conditions for rule evaluation (JSON Logic or custom format)
|
||||
*/
|
||||
conditions?: Record<string, any>;
|
||||
/**
|
||||
* Rule description
|
||||
*/
|
||||
description: string;
|
||||
/**
|
||||
* Whether the rule is enabled
|
||||
*/
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* Human-readable label
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* Pack reference this rule belongs to
|
||||
*/
|
||||
pack_ref: string;
|
||||
/**
|
||||
* Unique reference identifier (e.g., "mypack.notify_on_error")
|
||||
*/
|
||||
ref: string;
|
||||
/**
|
||||
* Parameters for trigger configuration and event filtering
|
||||
*/
|
||||
trigger_params?: Record<string, any>;
|
||||
/**
|
||||
* Trigger reference that activates this rule
|
||||
*/
|
||||
trigger_ref: string;
|
||||
/**
|
||||
* Parameters to pass to the action when rule is triggered
|
||||
*/
|
||||
action_params?: Record<string, any>;
|
||||
/**
|
||||
* Action reference to execute when rule matches
|
||||
*/
|
||||
action_ref: string;
|
||||
/**
|
||||
* Conditions for rule evaluation (JSON Logic or custom format)
|
||||
*/
|
||||
conditions?: Record<string, any>;
|
||||
/**
|
||||
* Rule description
|
||||
*/
|
||||
description?: string | null;
|
||||
/**
|
||||
* Whether the rule is enabled
|
||||
*/
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* Human-readable label
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* Pack reference this rule belongs to
|
||||
*/
|
||||
pack_ref: string;
|
||||
/**
|
||||
* Unique reference identifier (e.g., "mypack.notify_on_error")
|
||||
*/
|
||||
ref: string;
|
||||
/**
|
||||
* Parameters for trigger configuration and event filtering
|
||||
*/
|
||||
trigger_params?: Record<string, any>;
|
||||
/**
|
||||
* Trigger reference that activates this rule
|
||||
*/
|
||||
trigger_ref: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -6,45 +6,44 @@
|
||||
* Request DTO for creating a new sensor
|
||||
*/
|
||||
export type CreateSensorRequest = {
|
||||
/**
|
||||
* Configuration values for this sensor instance (conforms to param_schema)
|
||||
*/
|
||||
config?: any | null;
|
||||
/**
|
||||
* Sensor description
|
||||
*/
|
||||
description: string;
|
||||
/**
|
||||
* Whether the sensor is enabled
|
||||
*/
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* Entry point for sensor execution (e.g., path to script, function name)
|
||||
*/
|
||||
entrypoint: string;
|
||||
/**
|
||||
* Human-readable label
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* Pack reference this sensor belongs to
|
||||
*/
|
||||
pack_ref: string;
|
||||
/**
|
||||
* Parameter schema (flat format) for sensor configuration
|
||||
*/
|
||||
param_schema?: any | null;
|
||||
/**
|
||||
* Unique reference identifier (e.g., "mypack.cpu_monitor")
|
||||
*/
|
||||
ref: string;
|
||||
/**
|
||||
* Runtime reference for this sensor
|
||||
*/
|
||||
runtime_ref: string;
|
||||
/**
|
||||
* Trigger reference this sensor monitors for
|
||||
*/
|
||||
trigger_ref: string;
|
||||
/**
|
||||
* Configuration values for this sensor instance (conforms to param_schema)
|
||||
*/
|
||||
config?: any | null;
|
||||
/**
|
||||
* Sensor description
|
||||
*/
|
||||
description?: string | null;
|
||||
/**
|
||||
* Whether the sensor is enabled
|
||||
*/
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* Entry point for sensor execution (e.g., path to script, function name)
|
||||
*/
|
||||
entrypoint: string;
|
||||
/**
|
||||
* Human-readable label
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* Pack reference this sensor belongs to
|
||||
*/
|
||||
pack_ref: string;
|
||||
/**
|
||||
* Parameter schema (flat format) for sensor configuration
|
||||
*/
|
||||
param_schema?: any | null;
|
||||
/**
|
||||
* Unique reference identifier (e.g., "mypack.cpu_monitor")
|
||||
*/
|
||||
ref: string;
|
||||
/**
|
||||
* Runtime reference for this sensor
|
||||
*/
|
||||
runtime_ref: string;
|
||||
/**
|
||||
* Trigger reference this sensor monitors for
|
||||
*/
|
||||
trigger_ref: string;
|
||||
};
|
||||
|
||||
|
||||
17
web/src/api/models/IdentityResponse.ts
Normal file
17
web/src/api/models/IdentityResponse.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { IdentityRoleAssignmentResponse } from './IdentityRoleAssignmentResponse';
|
||||
import type { PermissionAssignmentResponse } from './PermissionAssignmentResponse';
|
||||
import type { Value } from './Value';
|
||||
export type IdentityResponse = {
|
||||
attributes: Value;
|
||||
direct_permissions: Array<PermissionAssignmentResponse>;
|
||||
display_name?: string | null;
|
||||
frozen: boolean;
|
||||
id: number;
|
||||
login: string;
|
||||
roles: Array<IdentityRoleAssignmentResponse>;
|
||||
};
|
||||
|
||||
14
web/src/api/models/IdentityRoleAssignmentResponse.ts
Normal file
14
web/src/api/models/IdentityRoleAssignmentResponse.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type IdentityRoleAssignmentResponse = {
|
||||
created: string;
|
||||
id: number;
|
||||
identity_id: number;
|
||||
managed: boolean;
|
||||
role: string;
|
||||
source: string;
|
||||
updated: string;
|
||||
};
|
||||
|
||||
@@ -6,7 +6,9 @@ import type { Value } from './Value';
|
||||
export type IdentitySummary = {
|
||||
attributes: Value;
|
||||
display_name?: string | null;
|
||||
frozen: boolean;
|
||||
id: number;
|
||||
login: string;
|
||||
roles: Array<string>;
|
||||
};
|
||||
|
||||
|
||||
18
web/src/api/models/LdapLoginRequest.ts
Normal file
18
web/src/api/models/LdapLoginRequest.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Request body for LDAP login.
|
||||
*/
|
||||
export type LdapLoginRequest = {
|
||||
/**
|
||||
* User login name (uid, sAMAccountName, etc.)
|
||||
*/
|
||||
login: string;
|
||||
/**
|
||||
* User password
|
||||
*/
|
||||
password: string;
|
||||
};
|
||||
|
||||
18
web/src/api/models/PackDescriptionPatch.ts
Normal file
18
web/src/api/models/PackDescriptionPatch.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type PackDescriptionPatch =
|
||||
| {
|
||||
op: PackDescriptionPatch.op;
|
||||
value: string;
|
||||
}
|
||||
| {
|
||||
op: PackDescriptionPatch.op;
|
||||
};
|
||||
export namespace PackDescriptionPatch {
|
||||
export enum op {
|
||||
SET = "set",
|
||||
CLEAR = "clear",
|
||||
}
|
||||
}
|
||||
@@ -2,63 +2,62 @@
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { PaginationMeta } from './PaginationMeta';
|
||||
import type { PaginationMeta } from "./PaginationMeta";
|
||||
/**
|
||||
* Paginated response wrapper
|
||||
*/
|
||||
export type PaginatedResponse_ActionSummary = {
|
||||
/**
|
||||
* The data items
|
||||
*/
|
||||
data: Array<{
|
||||
/**
|
||||
* The data items
|
||||
* Creation timestamp
|
||||
*/
|
||||
data: Array<{
|
||||
/**
|
||||
* Creation timestamp
|
||||
*/
|
||||
created: string;
|
||||
/**
|
||||
* Action description
|
||||
*/
|
||||
description: string;
|
||||
/**
|
||||
* Entry point
|
||||
*/
|
||||
entrypoint: string;
|
||||
/**
|
||||
* Action ID
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Human-readable label
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* Pack reference
|
||||
*/
|
||||
pack_ref: string;
|
||||
/**
|
||||
* Unique reference identifier
|
||||
*/
|
||||
ref: string;
|
||||
/**
|
||||
* Runtime ID
|
||||
*/
|
||||
runtime?: number | null;
|
||||
/**
|
||||
* Semver version constraint for the runtime
|
||||
*/
|
||||
runtime_version_constraint?: string | null;
|
||||
/**
|
||||
* Last update timestamp
|
||||
*/
|
||||
updated: string;
|
||||
/**
|
||||
* Workflow definition ID (non-null if this action is a workflow)
|
||||
*/
|
||||
workflow_def?: number | null;
|
||||
}>;
|
||||
created: string;
|
||||
/**
|
||||
* Pagination metadata
|
||||
* Action description
|
||||
*/
|
||||
pagination: PaginationMeta;
|
||||
description: string | null;
|
||||
/**
|
||||
* Entry point
|
||||
*/
|
||||
entrypoint: string;
|
||||
/**
|
||||
* Action ID
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Human-readable label
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* Pack reference
|
||||
*/
|
||||
pack_ref: string;
|
||||
/**
|
||||
* Unique reference identifier
|
||||
*/
|
||||
ref: string;
|
||||
/**
|
||||
* Runtime ID
|
||||
*/
|
||||
runtime?: number | null;
|
||||
/**
|
||||
* Semver version constraint for the runtime
|
||||
*/
|
||||
runtime_version_constraint?: string | null;
|
||||
/**
|
||||
* Last update timestamp
|
||||
*/
|
||||
updated: string;
|
||||
/**
|
||||
* Workflow definition ID (non-null if this action is a workflow)
|
||||
*/
|
||||
workflow_def?: number | null;
|
||||
}>;
|
||||
/**
|
||||
* Pagination metadata
|
||||
*/
|
||||
pagination: PaginationMeta;
|
||||
};
|
||||
|
||||
|
||||
@@ -14,8 +14,10 @@ export type PaginatedResponse_IdentitySummary = {
|
||||
data: Array<{
|
||||
attributes: Value;
|
||||
display_name?: string | null;
|
||||
frozen: boolean;
|
||||
id: number;
|
||||
login: string;
|
||||
roles: Array<string>;
|
||||
}>;
|
||||
/**
|
||||
* Pagination metadata
|
||||
|
||||
@@ -2,67 +2,66 @@
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { PaginationMeta } from './PaginationMeta';
|
||||
import type { PaginationMeta } from "./PaginationMeta";
|
||||
/**
|
||||
* Paginated response wrapper
|
||||
*/
|
||||
export type PaginatedResponse_RuleSummary = {
|
||||
/**
|
||||
* The data items
|
||||
*/
|
||||
data: Array<{
|
||||
/**
|
||||
* The data items
|
||||
* Parameters to pass to the action when rule is triggered
|
||||
*/
|
||||
data: Array<{
|
||||
/**
|
||||
* Parameters to pass to the action when rule is triggered
|
||||
*/
|
||||
action_params: Record<string, any>;
|
||||
/**
|
||||
* Action reference
|
||||
*/
|
||||
action_ref: string;
|
||||
/**
|
||||
* Creation timestamp
|
||||
*/
|
||||
created: string;
|
||||
/**
|
||||
* Rule description
|
||||
*/
|
||||
description: string;
|
||||
/**
|
||||
* Whether the rule is enabled
|
||||
*/
|
||||
enabled: boolean;
|
||||
/**
|
||||
* Rule ID
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Human-readable label
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* Pack reference
|
||||
*/
|
||||
pack_ref: string;
|
||||
/**
|
||||
* Unique reference identifier
|
||||
*/
|
||||
ref: string;
|
||||
/**
|
||||
* Parameters for trigger configuration and event filtering
|
||||
*/
|
||||
trigger_params: Record<string, any>;
|
||||
/**
|
||||
* Trigger reference
|
||||
*/
|
||||
trigger_ref: string;
|
||||
/**
|
||||
* Last update timestamp
|
||||
*/
|
||||
updated: string;
|
||||
}>;
|
||||
action_params: Record<string, any>;
|
||||
/**
|
||||
* Pagination metadata
|
||||
* Action reference
|
||||
*/
|
||||
pagination: PaginationMeta;
|
||||
action_ref: string;
|
||||
/**
|
||||
* Creation timestamp
|
||||
*/
|
||||
created: string;
|
||||
/**
|
||||
* Rule description
|
||||
*/
|
||||
description: string | null;
|
||||
/**
|
||||
* Whether the rule is enabled
|
||||
*/
|
||||
enabled: boolean;
|
||||
/**
|
||||
* Rule ID
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Human-readable label
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* Pack reference
|
||||
*/
|
||||
pack_ref: string;
|
||||
/**
|
||||
* Unique reference identifier
|
||||
*/
|
||||
ref: string;
|
||||
/**
|
||||
* Parameters for trigger configuration and event filtering
|
||||
*/
|
||||
trigger_params: Record<string, any>;
|
||||
/**
|
||||
* Trigger reference
|
||||
*/
|
||||
trigger_ref: string;
|
||||
/**
|
||||
* Last update timestamp
|
||||
*/
|
||||
updated: string;
|
||||
}>;
|
||||
/**
|
||||
* Pagination metadata
|
||||
*/
|
||||
pagination: PaginationMeta;
|
||||
};
|
||||
|
||||
|
||||
@@ -2,55 +2,54 @@
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { PaginationMeta } from './PaginationMeta';
|
||||
import type { PaginationMeta } from "./PaginationMeta";
|
||||
/**
|
||||
* Paginated response wrapper
|
||||
*/
|
||||
export type PaginatedResponse_SensorSummary = {
|
||||
/**
|
||||
* The data items
|
||||
*/
|
||||
data: Array<{
|
||||
/**
|
||||
* The data items
|
||||
* Creation timestamp
|
||||
*/
|
||||
data: Array<{
|
||||
/**
|
||||
* Creation timestamp
|
||||
*/
|
||||
created: string;
|
||||
/**
|
||||
* Sensor description
|
||||
*/
|
||||
description: string;
|
||||
/**
|
||||
* Whether the sensor is enabled
|
||||
*/
|
||||
enabled: boolean;
|
||||
/**
|
||||
* Sensor ID
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Human-readable label
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* Pack reference (optional)
|
||||
*/
|
||||
pack_ref?: string | null;
|
||||
/**
|
||||
* Unique reference identifier
|
||||
*/
|
||||
ref: string;
|
||||
/**
|
||||
* Trigger reference
|
||||
*/
|
||||
trigger_ref: string;
|
||||
/**
|
||||
* Last update timestamp
|
||||
*/
|
||||
updated: string;
|
||||
}>;
|
||||
created: string;
|
||||
/**
|
||||
* Pagination metadata
|
||||
* Sensor description
|
||||
*/
|
||||
pagination: PaginationMeta;
|
||||
description: string | null;
|
||||
/**
|
||||
* Whether the sensor is enabled
|
||||
*/
|
||||
enabled: boolean;
|
||||
/**
|
||||
* Sensor ID
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Human-readable label
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* Pack reference (optional)
|
||||
*/
|
||||
pack_ref?: string | null;
|
||||
/**
|
||||
* Unique reference identifier
|
||||
*/
|
||||
ref: string;
|
||||
/**
|
||||
* Trigger reference
|
||||
*/
|
||||
trigger_ref: string;
|
||||
/**
|
||||
* Last update timestamp
|
||||
*/
|
||||
updated: string;
|
||||
}>;
|
||||
/**
|
||||
* Pagination metadata
|
||||
*/
|
||||
pagination: PaginationMeta;
|
||||
};
|
||||
|
||||
|
||||
12
web/src/api/models/PermissionSetRoleAssignmentResponse.ts
Normal file
12
web/src/api/models/PermissionSetRoleAssignmentResponse.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type PermissionSetRoleAssignmentResponse = {
|
||||
created: string;
|
||||
id: number;
|
||||
permission_set_id: number;
|
||||
permission_set_ref?: string | null;
|
||||
role: string;
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { PermissionSetRoleAssignmentResponse } from './PermissionSetRoleAssignmentResponse';
|
||||
import type { Value } from './Value';
|
||||
export type PermissionSetSummary = {
|
||||
description?: string | null;
|
||||
@@ -10,5 +11,6 @@ export type PermissionSetSummary = {
|
||||
label?: string | null;
|
||||
pack_ref?: string | null;
|
||||
ref: string;
|
||||
roles: Array<PermissionSetRoleAssignmentResponse>;
|
||||
};
|
||||
|
||||
|
||||
@@ -6,73 +6,72 @@
|
||||
* Response DTO for rule information
|
||||
*/
|
||||
export type RuleResponse = {
|
||||
/**
|
||||
* Action ID (null if the referenced action has been deleted)
|
||||
*/
|
||||
action?: number | null;
|
||||
/**
|
||||
* Parameters to pass to the action when rule is triggered
|
||||
*/
|
||||
action_params: Record<string, any>;
|
||||
/**
|
||||
* Action reference
|
||||
*/
|
||||
action_ref: string;
|
||||
/**
|
||||
* Conditions for rule evaluation
|
||||
*/
|
||||
conditions: Record<string, any>;
|
||||
/**
|
||||
* Creation timestamp
|
||||
*/
|
||||
created: string;
|
||||
/**
|
||||
* Rule description
|
||||
*/
|
||||
description: string;
|
||||
/**
|
||||
* Whether the rule is enabled
|
||||
*/
|
||||
enabled: boolean;
|
||||
/**
|
||||
* Rule ID
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Whether this is an ad-hoc rule (not from pack installation)
|
||||
*/
|
||||
is_adhoc: boolean;
|
||||
/**
|
||||
* Human-readable label
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* Pack ID
|
||||
*/
|
||||
pack: number;
|
||||
/**
|
||||
* Pack reference
|
||||
*/
|
||||
pack_ref: string;
|
||||
/**
|
||||
* Unique reference identifier
|
||||
*/
|
||||
ref: string;
|
||||
/**
|
||||
* Trigger ID (null if the referenced trigger has been deleted)
|
||||
*/
|
||||
trigger?: number | null;
|
||||
/**
|
||||
* Parameters for trigger configuration and event filtering
|
||||
*/
|
||||
trigger_params: Record<string, any>;
|
||||
/**
|
||||
* Trigger reference
|
||||
*/
|
||||
trigger_ref: string;
|
||||
/**
|
||||
* Last update timestamp
|
||||
*/
|
||||
updated: string;
|
||||
/**
|
||||
* Action ID (null if the referenced action has been deleted)
|
||||
*/
|
||||
action?: number | null;
|
||||
/**
|
||||
* Parameters to pass to the action when rule is triggered
|
||||
*/
|
||||
action_params: Record<string, any>;
|
||||
/**
|
||||
* Action reference
|
||||
*/
|
||||
action_ref: string;
|
||||
/**
|
||||
* Conditions for rule evaluation
|
||||
*/
|
||||
conditions: Record<string, any>;
|
||||
/**
|
||||
* Creation timestamp
|
||||
*/
|
||||
created: string;
|
||||
/**
|
||||
* Rule description
|
||||
*/
|
||||
description: string | null;
|
||||
/**
|
||||
* Whether the rule is enabled
|
||||
*/
|
||||
enabled: boolean;
|
||||
/**
|
||||
* Rule ID
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Whether this is an ad-hoc rule (not from pack installation)
|
||||
*/
|
||||
is_adhoc: boolean;
|
||||
/**
|
||||
* Human-readable label
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* Pack ID
|
||||
*/
|
||||
pack: number;
|
||||
/**
|
||||
* Pack reference
|
||||
*/
|
||||
pack_ref: string;
|
||||
/**
|
||||
* Unique reference identifier
|
||||
*/
|
||||
ref: string;
|
||||
/**
|
||||
* Trigger ID (null if the referenced trigger has been deleted)
|
||||
*/
|
||||
trigger?: number | null;
|
||||
/**
|
||||
* Parameters for trigger configuration and event filtering
|
||||
*/
|
||||
trigger_params: Record<string, any>;
|
||||
/**
|
||||
* Trigger reference
|
||||
*/
|
||||
trigger_ref: string;
|
||||
/**
|
||||
* Last update timestamp
|
||||
*/
|
||||
updated: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -6,53 +6,52 @@
|
||||
* Simplified rule response (for list endpoints)
|
||||
*/
|
||||
export type RuleSummary = {
|
||||
/**
|
||||
* Parameters to pass to the action when rule is triggered
|
||||
*/
|
||||
action_params: Record<string, any>;
|
||||
/**
|
||||
* Action reference
|
||||
*/
|
||||
action_ref: string;
|
||||
/**
|
||||
* Creation timestamp
|
||||
*/
|
||||
created: string;
|
||||
/**
|
||||
* Rule description
|
||||
*/
|
||||
description: string;
|
||||
/**
|
||||
* Whether the rule is enabled
|
||||
*/
|
||||
enabled: boolean;
|
||||
/**
|
||||
* Rule ID
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Human-readable label
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* Pack reference
|
||||
*/
|
||||
pack_ref: string;
|
||||
/**
|
||||
* Unique reference identifier
|
||||
*/
|
||||
ref: string;
|
||||
/**
|
||||
* Parameters for trigger configuration and event filtering
|
||||
*/
|
||||
trigger_params: Record<string, any>;
|
||||
/**
|
||||
* Trigger reference
|
||||
*/
|
||||
trigger_ref: string;
|
||||
/**
|
||||
* Last update timestamp
|
||||
*/
|
||||
updated: string;
|
||||
/**
|
||||
* Parameters to pass to the action when rule is triggered
|
||||
*/
|
||||
action_params: Record<string, any>;
|
||||
/**
|
||||
* Action reference
|
||||
*/
|
||||
action_ref: string;
|
||||
/**
|
||||
* Creation timestamp
|
||||
*/
|
||||
created: string;
|
||||
/**
|
||||
* Rule description
|
||||
*/
|
||||
description: string | null;
|
||||
/**
|
||||
* Whether the rule is enabled
|
||||
*/
|
||||
enabled: boolean;
|
||||
/**
|
||||
* Rule ID
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Human-readable label
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* Pack reference
|
||||
*/
|
||||
pack_ref: string;
|
||||
/**
|
||||
* Unique reference identifier
|
||||
*/
|
||||
ref: string;
|
||||
/**
|
||||
* Parameters for trigger configuration and event filtering
|
||||
*/
|
||||
trigger_params: Record<string, any>;
|
||||
/**
|
||||
* Trigger reference
|
||||
*/
|
||||
trigger_ref: string;
|
||||
/**
|
||||
* Last update timestamp
|
||||
*/
|
||||
updated: string;
|
||||
};
|
||||
|
||||
|
||||
19
web/src/api/models/RuntimeVersionConstraintPatch.ts
Normal file
19
web/src/api/models/RuntimeVersionConstraintPatch.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Explicit patch operation for a nullable runtime version constraint.
|
||||
*/
|
||||
export type RuntimeVersionConstraintPatch = ({
|
||||
op: RuntimeVersionConstraintPatch.op;
|
||||
value: string;
|
||||
} | {
|
||||
op: RuntimeVersionConstraintPatch.op;
|
||||
});
|
||||
export namespace RuntimeVersionConstraintPatch {
|
||||
export enum op {
|
||||
SET = 'set',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,65 +6,64 @@
|
||||
* Response DTO for sensor information
|
||||
*/
|
||||
export type SensorResponse = {
|
||||
/**
|
||||
* Creation timestamp
|
||||
*/
|
||||
created: string;
|
||||
/**
|
||||
* Sensor description
|
||||
*/
|
||||
description: string;
|
||||
/**
|
||||
* Whether the sensor is enabled
|
||||
*/
|
||||
enabled: boolean;
|
||||
/**
|
||||
* Entry point
|
||||
*/
|
||||
entrypoint: string;
|
||||
/**
|
||||
* Sensor ID
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Human-readable label
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* Pack ID (optional)
|
||||
*/
|
||||
pack?: number | null;
|
||||
/**
|
||||
* Pack reference (optional)
|
||||
*/
|
||||
pack_ref?: string | null;
|
||||
/**
|
||||
* Parameter schema (StackStorm-style with inline required/secret)
|
||||
*/
|
||||
param_schema: any | null;
|
||||
/**
|
||||
* Unique reference identifier
|
||||
*/
|
||||
ref: string;
|
||||
/**
|
||||
* Runtime ID
|
||||
*/
|
||||
runtime: number;
|
||||
/**
|
||||
* Runtime reference
|
||||
*/
|
||||
runtime_ref: string;
|
||||
/**
|
||||
* Trigger ID
|
||||
*/
|
||||
trigger: number;
|
||||
/**
|
||||
* Trigger reference
|
||||
*/
|
||||
trigger_ref: string;
|
||||
/**
|
||||
* Last update timestamp
|
||||
*/
|
||||
updated: string;
|
||||
/**
|
||||
* Creation timestamp
|
||||
*/
|
||||
created: string;
|
||||
/**
|
||||
* Sensor description
|
||||
*/
|
||||
description: string | null;
|
||||
/**
|
||||
* Whether the sensor is enabled
|
||||
*/
|
||||
enabled: boolean;
|
||||
/**
|
||||
* Entry point
|
||||
*/
|
||||
entrypoint: string;
|
||||
/**
|
||||
* Sensor ID
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Human-readable label
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* Pack ID (optional)
|
||||
*/
|
||||
pack?: number | null;
|
||||
/**
|
||||
* Pack reference (optional)
|
||||
*/
|
||||
pack_ref?: string | null;
|
||||
/**
|
||||
* Parameter schema (StackStorm-style with inline required/secret)
|
||||
*/
|
||||
param_schema: any | null;
|
||||
/**
|
||||
* Unique reference identifier
|
||||
*/
|
||||
ref: string;
|
||||
/**
|
||||
* Runtime ID
|
||||
*/
|
||||
runtime: number;
|
||||
/**
|
||||
* Runtime reference
|
||||
*/
|
||||
runtime_ref: string;
|
||||
/**
|
||||
* Trigger ID
|
||||
*/
|
||||
trigger: number;
|
||||
/**
|
||||
* Trigger reference
|
||||
*/
|
||||
trigger_ref: string;
|
||||
/**
|
||||
* Last update timestamp
|
||||
*/
|
||||
updated: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -6,41 +6,40 @@
|
||||
* Simplified sensor response (for list endpoints)
|
||||
*/
|
||||
export type SensorSummary = {
|
||||
/**
|
||||
* Creation timestamp
|
||||
*/
|
||||
created: string;
|
||||
/**
|
||||
* Sensor description
|
||||
*/
|
||||
description: string;
|
||||
/**
|
||||
* Whether the sensor is enabled
|
||||
*/
|
||||
enabled: boolean;
|
||||
/**
|
||||
* Sensor ID
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Human-readable label
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* Pack reference (optional)
|
||||
*/
|
||||
pack_ref?: string | null;
|
||||
/**
|
||||
* Unique reference identifier
|
||||
*/
|
||||
ref: string;
|
||||
/**
|
||||
* Trigger reference
|
||||
*/
|
||||
trigger_ref: string;
|
||||
/**
|
||||
* Last update timestamp
|
||||
*/
|
||||
updated: string;
|
||||
/**
|
||||
* Creation timestamp
|
||||
*/
|
||||
created: string;
|
||||
/**
|
||||
* Sensor description
|
||||
*/
|
||||
description: string | null;
|
||||
/**
|
||||
* Whether the sensor is enabled
|
||||
*/
|
||||
enabled: boolean;
|
||||
/**
|
||||
* Sensor ID
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Human-readable label
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* Pack reference (optional)
|
||||
*/
|
||||
pack_ref?: string | null;
|
||||
/**
|
||||
* Unique reference identifier
|
||||
*/
|
||||
ref: string;
|
||||
/**
|
||||
* Trigger reference
|
||||
*/
|
||||
trigger_ref: string;
|
||||
/**
|
||||
* Last update timestamp
|
||||
*/
|
||||
updated: string;
|
||||
};
|
||||
|
||||
|
||||
18
web/src/api/models/TriggerStringPatch.ts
Normal file
18
web/src/api/models/TriggerStringPatch.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type TriggerStringPatch =
|
||||
| {
|
||||
op: TriggerStringPatch.op;
|
||||
value: string;
|
||||
}
|
||||
| {
|
||||
op: TriggerStringPatch.op;
|
||||
};
|
||||
export namespace TriggerStringPatch {
|
||||
export enum op {
|
||||
SET = "set",
|
||||
CLEAR = "clear",
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { RuntimeVersionConstraintPatch } from './RuntimeVersionConstraintPatch';
|
||||
/**
|
||||
* Request DTO for updating an action
|
||||
*/
|
||||
@@ -30,9 +31,6 @@ export type UpdateActionRequest = {
|
||||
* Runtime ID
|
||||
*/
|
||||
runtime?: number | null;
|
||||
/**
|
||||
* Optional semver version constraint for the runtime (e.g., ">=3.12", ">=3.12,<4.0", "~18.0")
|
||||
*/
|
||||
runtime_version_constraint?: string | null;
|
||||
runtime_version_constraint?: (null | RuntimeVersionConstraintPatch);
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { Value } from './Value';
|
||||
export type UpdateIdentityRequest = {
|
||||
attributes?: (null | Value);
|
||||
display_name?: string | null;
|
||||
frozen?: boolean | null;
|
||||
password?: string | null;
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { PackDescriptionPatch } from './PackDescriptionPatch';
|
||||
/**
|
||||
* Request DTO for updating a pack
|
||||
*/
|
||||
@@ -18,10 +19,7 @@ export type UpdatePackRequest = {
|
||||
* Pack dependencies (refs of required packs)
|
||||
*/
|
||||
dependencies?: any[] | null;
|
||||
/**
|
||||
* Pack description
|
||||
*/
|
||||
description?: string | null;
|
||||
description?: (null | PackDescriptionPatch);
|
||||
/**
|
||||
* Whether this is a standard pack
|
||||
*/
|
||||
|
||||
@@ -2,14 +2,12 @@
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { TriggerStringPatch } from './TriggerStringPatch';
|
||||
/**
|
||||
* Request DTO for updating a trigger
|
||||
*/
|
||||
export type UpdateTriggerRequest = {
|
||||
/**
|
||||
* Trigger description
|
||||
*/
|
||||
description?: string | null;
|
||||
description?: (null | TriggerStringPatch);
|
||||
/**
|
||||
* Whether the trigger is enabled
|
||||
*/
|
||||
|
||||
61
web/src/api/services/AgentService.ts
Normal file
61
web/src/api/services/AgentService.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { AgentBinaryInfo } from '../models/AgentBinaryInfo';
|
||||
import type { CancelablePromise } from '../core/CancelablePromise';
|
||||
import { OpenAPI } from '../core/OpenAPI';
|
||||
import { request as __request } from '../core/request';
|
||||
export class AgentService {
|
||||
/**
|
||||
* Download the agent binary
|
||||
* Returns the statically-linked attune-agent binary for the requested architecture.
|
||||
* The binary can be injected into any container to turn it into an Attune worker.
|
||||
* @returns any Agent binary
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static downloadAgentBinary({
|
||||
arch,
|
||||
token,
|
||||
}: {
|
||||
/**
|
||||
* Target architecture (x86_64, aarch64). Defaults to x86_64.
|
||||
*/
|
||||
arch?: string | null,
|
||||
/**
|
||||
* Optional bootstrap token for authentication
|
||||
*/
|
||||
token?: string | null,
|
||||
}): CancelablePromise<any> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/api/v1/agent/binary',
|
||||
query: {
|
||||
'arch': arch,
|
||||
'token': token,
|
||||
},
|
||||
errors: {
|
||||
400: `Invalid architecture`,
|
||||
401: `Invalid or missing bootstrap token`,
|
||||
404: `Agent binary not found`,
|
||||
503: `Agent binary distribution not configured`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Get agent binary metadata
|
||||
* Returns information about available agent binaries, including
|
||||
* supported architectures and binary sizes.
|
||||
* @returns AgentBinaryInfo Agent binary info
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static agentInfo(): CancelablePromise<AgentBinaryInfo> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/api/v1/agent/info',
|
||||
errors: {
|
||||
503: `Agent binary distribution not configured`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { ChangePasswordRequest } from '../models/ChangePasswordRequest';
|
||||
import type { LdapLoginRequest } from '../models/LdapLoginRequest';
|
||||
import type { LoginRequest } from '../models/LoginRequest';
|
||||
import type { RefreshTokenRequest } from '../models/RefreshTokenRequest';
|
||||
import type { RegisterRequest } from '../models/RegisterRequest';
|
||||
@@ -52,6 +53,55 @@ export class AuthService {
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Authenticate via LDAP directory.
|
||||
* POST /auth/ldap/login
|
||||
* @returns any Successfully authenticated via LDAP
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static ldapLogin({
|
||||
requestBody,
|
||||
}: {
|
||||
requestBody: LdapLoginRequest,
|
||||
}): CancelablePromise<{
|
||||
/**
|
||||
* Token response
|
||||
*/
|
||||
data: {
|
||||
/**
|
||||
* Access token (JWT)
|
||||
*/
|
||||
access_token: string;
|
||||
/**
|
||||
* Access token expiration in seconds
|
||||
*/
|
||||
expires_in: number;
|
||||
/**
|
||||
* Refresh token
|
||||
*/
|
||||
refresh_token: string;
|
||||
/**
|
||||
* Token type (always "Bearer")
|
||||
*/
|
||||
token_type: string;
|
||||
user?: (null | UserInfo);
|
||||
};
|
||||
/**
|
||||
* Optional message
|
||||
*/
|
||||
message?: string | null;
|
||||
}> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/auth/ldap/login',
|
||||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
401: `Invalid LDAP credentials`,
|
||||
501: `LDAP not configured`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Login endpoint
|
||||
* POST /auth/login
|
||||
@@ -237,4 +287,82 @@ export class AuthService {
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Authentication settings endpoint
|
||||
* GET /auth/settings
|
||||
* @returns any Authentication settings
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static authSettings(): CancelablePromise<{
|
||||
/**
|
||||
* Public authentication settings for the login page.
|
||||
*/
|
||||
data: {
|
||||
/**
|
||||
* Whether authentication is enabled for the server.
|
||||
*/
|
||||
authentication_enabled: boolean;
|
||||
/**
|
||||
* Whether LDAP login is configured and enabled.
|
||||
*/
|
||||
ldap_enabled: boolean;
|
||||
/**
|
||||
* Optional icon URL shown beside the provider label.
|
||||
*/
|
||||
ldap_provider_icon_url?: string | null;
|
||||
/**
|
||||
* User-facing provider label for the login button.
|
||||
*/
|
||||
ldap_provider_label?: string | null;
|
||||
/**
|
||||
* Provider name for `?auth=<provider>`.
|
||||
*/
|
||||
ldap_provider_name?: string | null;
|
||||
/**
|
||||
* Whether LDAP login should be shown by default.
|
||||
*/
|
||||
ldap_visible_by_default: boolean;
|
||||
/**
|
||||
* Whether local username/password login is configured.
|
||||
*/
|
||||
local_password_enabled: boolean;
|
||||
/**
|
||||
* Whether local username/password login should be shown by default.
|
||||
*/
|
||||
local_password_visible_by_default: boolean;
|
||||
/**
|
||||
* Whether OIDC login is configured and enabled.
|
||||
*/
|
||||
oidc_enabled: boolean;
|
||||
/**
|
||||
* Optional icon URL shown beside the provider label.
|
||||
*/
|
||||
oidc_provider_icon_url?: string | null;
|
||||
/**
|
||||
* User-facing provider label for the login button.
|
||||
*/
|
||||
oidc_provider_label?: string | null;
|
||||
/**
|
||||
* Provider name for `?auth=<provider>`.
|
||||
*/
|
||||
oidc_provider_name?: string | null;
|
||||
/**
|
||||
* Whether OIDC login should be shown by default.
|
||||
*/
|
||||
oidc_visible_by_default: boolean;
|
||||
/**
|
||||
* Whether unauthenticated self-service registration is allowed.
|
||||
*/
|
||||
self_registration_enabled: boolean;
|
||||
};
|
||||
/**
|
||||
* Optional message
|
||||
*/
|
||||
message?: string | null;
|
||||
}> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/auth/settings',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { CreateIdentityRequest } from '../models/CreateIdentityRequest';
|
||||
import type { CreateIdentityRoleAssignmentRequest } from '../models/CreateIdentityRoleAssignmentRequest';
|
||||
import type { CreatePermissionAssignmentRequest } from '../models/CreatePermissionAssignmentRequest';
|
||||
import type { CreatePermissionSetRoleAssignmentRequest } from '../models/CreatePermissionSetRoleAssignmentRequest';
|
||||
import type { IdentityRoleAssignmentResponse } from '../models/IdentityRoleAssignmentResponse';
|
||||
import type { PaginatedResponse_IdentitySummary } from '../models/PaginatedResponse_IdentitySummary';
|
||||
import type { PermissionAssignmentResponse } from '../models/PermissionAssignmentResponse';
|
||||
import type { PermissionSetSummary } from '../models/PermissionSetSummary';
|
||||
@@ -50,9 +53,12 @@ export class PermissionsService {
|
||||
}): CancelablePromise<{
|
||||
data: {
|
||||
attributes: Value;
|
||||
direct_permissions: Array<PermissionAssignmentResponse>;
|
||||
display_name?: string | null;
|
||||
frozen: boolean;
|
||||
id: number;
|
||||
login: string;
|
||||
roles: Array<IdentityRoleAssignmentResponse>;
|
||||
};
|
||||
/**
|
||||
* Optional message
|
||||
@@ -69,6 +75,47 @@ export class PermissionsService {
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @returns any Identity role assignment deleted
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static deleteIdentityRoleAssignment({
|
||||
id,
|
||||
}: {
|
||||
/**
|
||||
* Identity role assignment ID
|
||||
*/
|
||||
id: number,
|
||||
}): CancelablePromise<{
|
||||
/**
|
||||
* Success message response (for operations that don't return data)
|
||||
*/
|
||||
data: {
|
||||
/**
|
||||
* Message describing the operation
|
||||
*/
|
||||
message: string;
|
||||
/**
|
||||
* Success indicator
|
||||
*/
|
||||
success: boolean;
|
||||
};
|
||||
/**
|
||||
* Optional message
|
||||
*/
|
||||
message?: string | null;
|
||||
}> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'DELETE',
|
||||
url: '/api/v1/identities/roles/{id}',
|
||||
path: {
|
||||
'id': id,
|
||||
},
|
||||
errors: {
|
||||
404: `Identity role assignment not found`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @returns any Identity details
|
||||
* @throws ApiError
|
||||
@@ -83,9 +130,12 @@ export class PermissionsService {
|
||||
}): CancelablePromise<{
|
||||
data: {
|
||||
attributes: Value;
|
||||
direct_permissions: Array<PermissionAssignmentResponse>;
|
||||
display_name?: string | null;
|
||||
frozen: boolean;
|
||||
id: number;
|
||||
login: string;
|
||||
roles: Array<IdentityRoleAssignmentResponse>;
|
||||
};
|
||||
/**
|
||||
* Optional message
|
||||
@@ -119,9 +169,12 @@ export class PermissionsService {
|
||||
}): CancelablePromise<{
|
||||
data: {
|
||||
attributes: Value;
|
||||
direct_permissions: Array<PermissionAssignmentResponse>;
|
||||
display_name?: string | null;
|
||||
frozen: boolean;
|
||||
id: number;
|
||||
login: string;
|
||||
roles: Array<IdentityRoleAssignmentResponse>;
|
||||
};
|
||||
/**
|
||||
* Optional message
|
||||
@@ -182,6 +235,47 @@ export class PermissionsService {
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @returns any Identity frozen
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static freezeIdentity({
|
||||
id,
|
||||
}: {
|
||||
/**
|
||||
* Identity ID
|
||||
*/
|
||||
id: number,
|
||||
}): CancelablePromise<{
|
||||
/**
|
||||
* Success message response (for operations that don't return data)
|
||||
*/
|
||||
data: {
|
||||
/**
|
||||
* Message describing the operation
|
||||
*/
|
||||
message: string;
|
||||
/**
|
||||
* Success indicator
|
||||
*/
|
||||
success: boolean;
|
||||
};
|
||||
/**
|
||||
* Optional message
|
||||
*/
|
||||
message?: string | null;
|
||||
}> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/api/v1/identities/{id}/freeze',
|
||||
path: {
|
||||
'id': id,
|
||||
},
|
||||
errors: {
|
||||
404: `Identity not found`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @returns PermissionAssignmentResponse List permission assignments for an identity
|
||||
* @throws ApiError
|
||||
@@ -205,6 +299,88 @@ export class PermissionsService {
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @returns any Identity role assignment created
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static createIdentityRoleAssignment({
|
||||
id,
|
||||
requestBody,
|
||||
}: {
|
||||
/**
|
||||
* Identity ID
|
||||
*/
|
||||
id: number,
|
||||
requestBody: CreateIdentityRoleAssignmentRequest,
|
||||
}): CancelablePromise<{
|
||||
data: {
|
||||
created: string;
|
||||
id: number;
|
||||
identity_id: number;
|
||||
managed: boolean;
|
||||
role: string;
|
||||
source: string;
|
||||
updated: string;
|
||||
};
|
||||
/**
|
||||
* Optional message
|
||||
*/
|
||||
message?: string | null;
|
||||
}> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/api/v1/identities/{id}/roles',
|
||||
path: {
|
||||
'id': id,
|
||||
},
|
||||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
404: `Identity not found`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @returns any Identity unfrozen
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static unfreezeIdentity({
|
||||
id,
|
||||
}: {
|
||||
/**
|
||||
* Identity ID
|
||||
*/
|
||||
id: number,
|
||||
}): CancelablePromise<{
|
||||
/**
|
||||
* Success message response (for operations that don't return data)
|
||||
*/
|
||||
data: {
|
||||
/**
|
||||
* Message describing the operation
|
||||
*/
|
||||
message: string;
|
||||
/**
|
||||
* Success indicator
|
||||
*/
|
||||
success: boolean;
|
||||
};
|
||||
/**
|
||||
* Optional message
|
||||
*/
|
||||
message?: string | null;
|
||||
}> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/api/v1/identities/{id}/unfreeze',
|
||||
path: {
|
||||
'id': id,
|
||||
},
|
||||
errors: {
|
||||
404: `Identity not found`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @returns any Permission assignment created
|
||||
* @throws ApiError
|
||||
@@ -295,4 +471,84 @@ export class PermissionsService {
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @returns any Permission set role assignment deleted
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static deletePermissionSetRoleAssignment({
|
||||
id,
|
||||
}: {
|
||||
/**
|
||||
* Permission set role assignment ID
|
||||
*/
|
||||
id: number,
|
||||
}): CancelablePromise<{
|
||||
/**
|
||||
* Success message response (for operations that don't return data)
|
||||
*/
|
||||
data: {
|
||||
/**
|
||||
* Message describing the operation
|
||||
*/
|
||||
message: string;
|
||||
/**
|
||||
* Success indicator
|
||||
*/
|
||||
success: boolean;
|
||||
};
|
||||
/**
|
||||
* Optional message
|
||||
*/
|
||||
message?: string | null;
|
||||
}> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'DELETE',
|
||||
url: '/api/v1/permissions/sets/roles/{id}',
|
||||
path: {
|
||||
'id': id,
|
||||
},
|
||||
errors: {
|
||||
404: `Permission set role assignment not found`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @returns any Permission set role assignment created
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static createPermissionSetRoleAssignment({
|
||||
id,
|
||||
requestBody,
|
||||
}: {
|
||||
/**
|
||||
* Permission set ID
|
||||
*/
|
||||
id: number,
|
||||
requestBody: CreatePermissionSetRoleAssignmentRequest,
|
||||
}): CancelablePromise<{
|
||||
data: {
|
||||
created: string;
|
||||
id: number;
|
||||
permission_set_id: number;
|
||||
permission_set_ref?: string | null;
|
||||
role: string;
|
||||
};
|
||||
/**
|
||||
* Optional message
|
||||
*/
|
||||
message?: string | null;
|
||||
}> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/api/v1/permissions/sets/{id}/roles',
|
||||
path: {
|
||||
'id': id,
|
||||
},
|
||||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
404: `Permission set not found`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useCreatePack, useUpdatePack } from "@/hooks/usePacks";
|
||||
import type { PackResponse } from "@/api";
|
||||
import { PackDescriptionPatch, type PackResponse } from "@/api";
|
||||
import { labelToRef } from "@/lib/format-utils";
|
||||
import SchemaBuilder from "@/components/common/SchemaBuilder";
|
||||
import ParamSchemaForm, {
|
||||
@@ -173,7 +173,9 @@ export default function PackForm({ pack, onSuccess, onCancel }: PackFormProps) {
|
||||
if (isEditing) {
|
||||
const updateData = {
|
||||
label: label.trim(),
|
||||
description: description.trim() || undefined,
|
||||
description: description.trim()
|
||||
? { op: PackDescriptionPatch.op.SET, value: description.trim() }
|
||||
: { op: PackDescriptionPatch.op.CLEAR },
|
||||
version: version.trim(),
|
||||
conf_schema: parsedConfSchema,
|
||||
config: configValues,
|
||||
|
||||
@@ -9,6 +9,7 @@ import ParamSchemaForm, {
|
||||
type ParamSchema,
|
||||
} from "@/components/common/ParamSchemaForm";
|
||||
import SearchableSelect from "@/components/common/SearchableSelect";
|
||||
import RuleMatchConditionsEditor from "@/components/forms/RuleMatchConditionsEditor";
|
||||
import type {
|
||||
RuleResponse,
|
||||
ActionSummary,
|
||||
@@ -40,9 +41,21 @@ export default function RuleForm({ rule, onSuccess, onCancel }: RuleFormProps) {
|
||||
const [description, setDescription] = useState(rule?.description || "");
|
||||
const [triggerId, setTriggerId] = useState<number>(rule?.trigger || 0);
|
||||
const [actionId, setActionId] = useState<number>(rule?.action || 0);
|
||||
const [conditions, setConditions] = useState(
|
||||
rule?.conditions ? JSON.stringify(rule.conditions, null, 2) : "",
|
||||
);
|
||||
const [conditions, setConditions] = useState<JsonValue | undefined>(() => {
|
||||
if (!rule?.conditions) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof rule.conditions === "object" &&
|
||||
!Array.isArray(rule.conditions) &&
|
||||
Object.keys(rule.conditions).length === 0
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return rule.conditions;
|
||||
});
|
||||
const [triggerParameters, setTriggerParameters] = useState<
|
||||
Record<string, JsonValue>
|
||||
>(rule?.trigger_params || {});
|
||||
@@ -57,6 +70,7 @@ export default function RuleForm({ rule, onSuccess, onCancel }: RuleFormProps) {
|
||||
const [actionParamErrors, setActionParamErrors] = useState<
|
||||
Record<string, string>
|
||||
>({});
|
||||
const [conditionsError, setConditionsError] = useState<string | undefined>();
|
||||
|
||||
// Data fetching
|
||||
const { data: packsData } = usePacks({ pageSize: 1000 });
|
||||
@@ -143,10 +157,6 @@ export default function RuleForm({ rule, onSuccess, onCancel }: RuleFormProps) {
|
||||
newErrors.label = "Label is required";
|
||||
}
|
||||
|
||||
if (!description.trim()) {
|
||||
newErrors.description = "Description is required";
|
||||
}
|
||||
|
||||
if (!packId) {
|
||||
newErrors.pack = "Pack is required";
|
||||
}
|
||||
@@ -159,13 +169,8 @@ export default function RuleForm({ rule, onSuccess, onCancel }: RuleFormProps) {
|
||||
newErrors.action = "Action is required";
|
||||
}
|
||||
|
||||
// Validate conditions JSON if provided
|
||||
if (conditions.trim()) {
|
||||
try {
|
||||
JSON.parse(conditions);
|
||||
} catch {
|
||||
newErrors.conditions = "Invalid JSON format";
|
||||
}
|
||||
if (conditionsError) {
|
||||
newErrors.conditions = conditionsError;
|
||||
}
|
||||
|
||||
// Validate trigger parameters (allow templates in rule context)
|
||||
@@ -210,15 +215,18 @@ export default function RuleForm({ rule, onSuccess, onCancel }: RuleFormProps) {
|
||||
pack_ref: selectedPackData?.ref || "",
|
||||
ref: fullRef,
|
||||
label: label.trim(),
|
||||
description: description.trim(),
|
||||
trigger_ref: selectedTrigger?.ref || "",
|
||||
action_ref: selectedAction?.ref || "",
|
||||
enabled,
|
||||
};
|
||||
|
||||
if (description.trim()) {
|
||||
formData.description = description.trim();
|
||||
}
|
||||
|
||||
// Only add optional fields if they have values
|
||||
if (conditions.trim()) {
|
||||
formData.conditions = JSON.parse(conditions);
|
||||
if (conditions !== undefined) {
|
||||
formData.conditions = conditions;
|
||||
}
|
||||
|
||||
// Add trigger parameters if any
|
||||
@@ -274,280 +282,252 @@ export default function RuleForm({ rule, onSuccess, onCancel }: RuleFormProps) {
|
||||
)}
|
||||
|
||||
{/* Basic Information */}
|
||||
<div className="bg-white rounded-lg shadow p-6 space-y-4">
|
||||
<div className="bg-white rounded-lg shadow p-5 lg:p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
Basic Information
|
||||
</h3>
|
||||
|
||||
{/* Pack Selection */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="pack"
|
||||
className="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
Pack <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<SearchableSelect
|
||||
id="pack"
|
||||
value={packId}
|
||||
onChange={(v) => setPackId(Number(v))}
|
||||
options={packs.map((pack) => ({
|
||||
value: pack.id,
|
||||
label: `${pack.label} (${pack.version})`,
|
||||
}))}
|
||||
placeholder="Select a pack..."
|
||||
disabled={isEditing}
|
||||
error={!!errors.pack}
|
||||
/>
|
||||
{errors.pack && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.pack}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-4 grid grid-cols-1 gap-4 lg:grid-cols-12">
|
||||
{/* Pack Selection */}
|
||||
<div className="lg:col-span-4">
|
||||
<label
|
||||
htmlFor="pack"
|
||||
className="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
Pack <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<SearchableSelect
|
||||
id="pack"
|
||||
value={packId}
|
||||
onChange={(v) => setPackId(Number(v))}
|
||||
options={packs.map((pack) => ({
|
||||
value: pack.id,
|
||||
label: `${pack.label} (${pack.version})`,
|
||||
}))}
|
||||
placeholder="Select a pack..."
|
||||
disabled={isEditing}
|
||||
error={!!errors.pack}
|
||||
/>
|
||||
{errors.pack && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.pack}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Label - MOVED FIRST */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="label"
|
||||
className="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
Label <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="label"
|
||||
value={label}
|
||||
onChange={(e) => setLabel(e.target.value)}
|
||||
onBlur={() => {
|
||||
// Auto-populate localRef from label if localRef is empty and not editing
|
||||
if (!isEditing && !localRef.trim() && label.trim()) {
|
||||
setLocalRef(labelToRef(label));
|
||||
}
|
||||
}}
|
||||
placeholder="e.g., Notify on Error"
|
||||
className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
||||
errors.label ? "border-red-500" : "border-gray-300"
|
||||
}`}
|
||||
/>
|
||||
{errors.label && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.label}</p>
|
||||
)}
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
Human-readable name for display
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Reference - MOVED AFTER LABEL with Pack Prefix */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="ref"
|
||||
className="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
Reference <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="input-with-prefix">
|
||||
<span className={`prefix ${errors.ref ? "error" : ""}`}>
|
||||
{selectedPack?.ref || "pack"}.
|
||||
</span>
|
||||
{/* Label */}
|
||||
<div className="lg:col-span-8">
|
||||
<label
|
||||
htmlFor="label"
|
||||
className="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
Label <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="ref"
|
||||
value={localRef}
|
||||
onChange={(e) => setLocalRef(e.target.value)}
|
||||
placeholder="e.g., notify_on_error"
|
||||
disabled={isEditing}
|
||||
className={errors.ref ? "error" : ""}
|
||||
id="label"
|
||||
value={label}
|
||||
onChange={(e) => setLabel(e.target.value)}
|
||||
onBlur={() => {
|
||||
// Auto-populate localRef from label if localRef is empty and not editing
|
||||
if (!isEditing && !localRef.trim() && label.trim()) {
|
||||
setLocalRef(labelToRef(label));
|
||||
}
|
||||
}}
|
||||
placeholder="e.g., Notify on Error"
|
||||
className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
||||
errors.label ? "border-red-500" : "border-gray-300"
|
||||
}`}
|
||||
/>
|
||||
{errors.label && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.label}</p>
|
||||
)}
|
||||
</div>
|
||||
{errors.ref && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.ref}</p>
|
||||
)}
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
Local identifier within the pack. Auto-populated from label.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="description"
|
||||
className="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
Description <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="description"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="Describe what this rule does..."
|
||||
rows={3}
|
||||
className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
||||
errors.description ? "border-red-500" : "border-gray-300"
|
||||
}`}
|
||||
/>
|
||||
{errors.description && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.description}</p>
|
||||
)}
|
||||
</div>
|
||||
{/* Reference */}
|
||||
<div className="lg:col-span-7">
|
||||
<label
|
||||
htmlFor="ref"
|
||||
className="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
Reference <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="flex flex-col gap-3 xl:flex-row xl:items-center">
|
||||
<div className="input-with-prefix flex-1">
|
||||
<span className={`prefix ${errors.ref ? "error" : ""}`}>
|
||||
{selectedPack?.ref || "pack"}.
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
id="ref"
|
||||
value={localRef}
|
||||
onChange={(e) => setLocalRef(e.target.value)}
|
||||
placeholder="e.g., notify_on_error"
|
||||
disabled={isEditing}
|
||||
className={errors.ref ? "error" : ""}
|
||||
/>
|
||||
</div>
|
||||
<label
|
||||
htmlFor="enabled"
|
||||
className="flex items-center gap-2 whitespace-nowrap rounded-lg border border-gray-200 px-3 py-2.5 text-sm text-gray-700"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="enabled"
|
||||
checked={enabled}
|
||||
onChange={(e) => setEnabled(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
Enable immediately
|
||||
</label>
|
||||
</div>
|
||||
{errors.ref && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.ref}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Enabled Toggle */}
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="enabled"
|
||||
checked={enabled}
|
||||
onChange={(e) => setEnabled(e.target.checked)}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label htmlFor="enabled" className="ml-2 text-sm text-gray-700">
|
||||
Enable rule immediately
|
||||
</label>
|
||||
{/* Description */}
|
||||
<div className="lg:col-span-12">
|
||||
<label
|
||||
htmlFor="description"
|
||||
className="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
id="description"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="Describe what this rule does..."
|
||||
rows={2}
|
||||
className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
||||
errors.description ? "border-red-500" : "border-gray-300"
|
||||
}`}
|
||||
/>
|
||||
{errors.description && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.description}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Trigger Configuration */}
|
||||
<div className="bg-white rounded-lg shadow p-6 space-y-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
Trigger Configuration
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 gap-6 xl:grid-cols-2">
|
||||
{/* Trigger Configuration */}
|
||||
<div className="bg-white rounded-lg shadow p-5 lg:p-6 space-y-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
Trigger Configuration
|
||||
</h3>
|
||||
|
||||
{!packId ? (
|
||||
<p className="text-sm text-gray-500">
|
||||
Select a pack first to choose a trigger
|
||||
</p>
|
||||
) : !triggers || triggers.length === 0 ? (
|
||||
<p className="text-sm text-gray-500">
|
||||
No triggers available in the system
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
{/* Trigger Selection */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="trigger"
|
||||
className="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
Trigger <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<SearchableSelect
|
||||
id="trigger"
|
||||
value={triggerId}
|
||||
onChange={(v) => setTriggerId(Number(v))}
|
||||
options={triggers.map((trigger) => ({
|
||||
value: trigger.id,
|
||||
label: `${trigger.ref} - ${trigger.label}`,
|
||||
}))}
|
||||
placeholder="Select a trigger..."
|
||||
disabled={isEditing}
|
||||
error={!!errors.trigger}
|
||||
/>
|
||||
{errors.trigger && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.trigger}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Trigger Parameters - Dynamic Form */}
|
||||
{selectedTrigger && (
|
||||
{!triggers || triggers.length === 0 ? (
|
||||
<p className="text-sm text-gray-500">
|
||||
No triggers available in the system
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
{/* Trigger Selection */}
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-3">
|
||||
Trigger Parameters
|
||||
</h4>
|
||||
<ParamSchemaForm
|
||||
schema={triggerParamSchema}
|
||||
values={triggerParameters}
|
||||
onChange={setTriggerParameters}
|
||||
errors={triggerParamErrors}
|
||||
allowTemplates
|
||||
<label
|
||||
htmlFor="trigger"
|
||||
className="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
Trigger <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<SearchableSelect
|
||||
id="trigger"
|
||||
value={triggerId}
|
||||
onChange={(v) => setTriggerId(Number(v))}
|
||||
options={triggers.map((trigger) => ({
|
||||
value: trigger.id,
|
||||
label: `${trigger.ref} - ${trigger.label}`,
|
||||
}))}
|
||||
placeholder="Select a trigger..."
|
||||
disabled={isEditing}
|
||||
error={!!errors.trigger}
|
||||
/>
|
||||
{errors.trigger && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.trigger}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Conditions (JSON) */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="conditions"
|
||||
className="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
Match Conditions (JSON)
|
||||
</label>
|
||||
<textarea
|
||||
id="conditions"
|
||||
{/* Trigger Parameters - Dynamic Form */}
|
||||
{selectedTrigger && (
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-3">
|
||||
Trigger Parameters
|
||||
</h4>
|
||||
<ParamSchemaForm
|
||||
schema={triggerParamSchema}
|
||||
values={triggerParameters}
|
||||
onChange={setTriggerParameters}
|
||||
errors={triggerParamErrors}
|
||||
allowTemplates
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<RuleMatchConditionsEditor
|
||||
value={conditions}
|
||||
onChange={(e) => setConditions(e.target.value)}
|
||||
placeholder={`{\n "and": [\n {"var": "payload.severity", ">=": 3},\n {"var": "payload.status", "==": "error"}\n ]\n}`}
|
||||
rows={8}
|
||||
className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono text-sm ${
|
||||
errors.conditions ? "border-red-500" : "border-gray-300"
|
||||
}`}
|
||||
onChange={setConditions}
|
||||
error={errors.conditions}
|
||||
onErrorChange={setConditionsError}
|
||||
/>
|
||||
{errors.conditions && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.conditions}</p>
|
||||
)}
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
Optional. Leave empty to match all events from this trigger.
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Action Configuration */}
|
||||
<div className="bg-white rounded-lg shadow p-6 space-y-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
Action Configuration
|
||||
</h3>
|
||||
{/* Action Configuration */}
|
||||
<div className="bg-white rounded-lg shadow p-5 lg:p-6 space-y-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
Action Configuration
|
||||
</h3>
|
||||
|
||||
{!packId ? (
|
||||
<p className="text-sm text-gray-500">
|
||||
Select a pack first to choose an action
|
||||
</p>
|
||||
) : !actions || actions.length === 0 ? (
|
||||
<p className="text-sm text-gray-500">
|
||||
No actions available in the system
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
{/* Action Selection */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="action"
|
||||
className="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
Action <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<SearchableSelect
|
||||
id="action"
|
||||
value={actionId}
|
||||
onChange={(v) => setActionId(Number(v))}
|
||||
options={actions.map((action) => ({
|
||||
value: action.id,
|
||||
label: `${action.ref} - ${action.label}`,
|
||||
}))}
|
||||
placeholder="Select an action..."
|
||||
disabled={isEditing}
|
||||
error={!!errors.action}
|
||||
/>
|
||||
{errors.action && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.action}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Action Parameters - Dynamic Form */}
|
||||
{selectedAction && (
|
||||
{!actions || actions.length === 0 ? (
|
||||
<p className="text-sm text-gray-500">
|
||||
No actions available in the system
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
{/* Action Selection */}
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-3">
|
||||
Action Parameters
|
||||
</h4>
|
||||
<ParamSchemaForm
|
||||
schema={actionParamSchema}
|
||||
values={actionParameters}
|
||||
onChange={setActionParameters}
|
||||
errors={actionParamErrors}
|
||||
allowTemplates
|
||||
<label
|
||||
htmlFor="action"
|
||||
className="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
Action <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<SearchableSelect
|
||||
id="action"
|
||||
value={actionId}
|
||||
onChange={(v) => setActionId(Number(v))}
|
||||
options={actions.map((action) => ({
|
||||
value: action.id,
|
||||
label: `${action.ref} - ${action.label}`,
|
||||
}))}
|
||||
placeholder="Select an action..."
|
||||
disabled={isEditing}
|
||||
error={!!errors.action}
|
||||
/>
|
||||
{errors.action && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.action}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Action Parameters - Dynamic Form */}
|
||||
{selectedAction && (
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-3">
|
||||
Action Parameters
|
||||
</h4>
|
||||
<ParamSchemaForm
|
||||
schema={actionParamSchema}
|
||||
values={actionParameters}
|
||||
onChange={setActionParameters}
|
||||
errors={actionParamErrors}
|
||||
allowTemplates
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Form Actions */}
|
||||
|
||||
507
web/src/components/forms/RuleMatchConditionsEditor.tsx
Normal file
507
web/src/components/forms/RuleMatchConditionsEditor.tsx
Normal file
@@ -0,0 +1,507 @@
|
||||
import { useEffect, useId, useState } from "react";
|
||||
import { Braces, ListFilter, Plus, Trash2 } from "lucide-react";
|
||||
import SearchableSelect from "@/components/common/SearchableSelect";
|
||||
|
||||
type JsonValue =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| JsonValue[]
|
||||
| { [key: string]: JsonValue };
|
||||
|
||||
type ConditionOperator = "equals" | "not_equals" | "contains";
|
||||
type ConditionValueType = "string" | "number" | "boolean" | "null" | "json";
|
||||
type EditorMode = "guided" | "raw";
|
||||
|
||||
interface ConditionRow {
|
||||
id: string;
|
||||
field: string;
|
||||
operator: ConditionOperator;
|
||||
valueType: ConditionValueType;
|
||||
valueInput: string;
|
||||
}
|
||||
|
||||
interface RuleMatchConditionsEditorProps {
|
||||
value: unknown;
|
||||
onChange: (value: JsonValue[] | JsonValue | undefined) => void;
|
||||
error?: string;
|
||||
onErrorChange?: (message?: string) => void;
|
||||
}
|
||||
|
||||
const OPERATOR_OPTIONS = [
|
||||
{
|
||||
value: "equals",
|
||||
label: "Equals",
|
||||
},
|
||||
{
|
||||
value: "not_equals",
|
||||
label: "Does not equal",
|
||||
},
|
||||
{
|
||||
value: "contains",
|
||||
label: "Contains",
|
||||
},
|
||||
] satisfies Array<{ value: ConditionOperator; label: string }>;
|
||||
|
||||
const VALUE_TYPE_OPTIONS = [
|
||||
{
|
||||
value: "string",
|
||||
label: "Text",
|
||||
},
|
||||
{
|
||||
value: "number",
|
||||
label: "Number",
|
||||
},
|
||||
{
|
||||
value: "boolean",
|
||||
label: "True/False",
|
||||
},
|
||||
{
|
||||
value: "null",
|
||||
label: "Empty",
|
||||
},
|
||||
{
|
||||
value: "json",
|
||||
label: "JSON",
|
||||
},
|
||||
] satisfies Array<{ value: ConditionValueType; label: string }>;
|
||||
|
||||
const DEFAULT_OPERATOR: ConditionOperator = "equals";
|
||||
const DEFAULT_VALUE_TYPE: ConditionValueType = "string";
|
||||
|
||||
function createRow(partial?: Partial<ConditionRow>): ConditionRow {
|
||||
return {
|
||||
id: Math.random().toString(36).slice(2, 10),
|
||||
field: partial?.field || "",
|
||||
operator: partial?.operator || DEFAULT_OPERATOR,
|
||||
valueType: partial?.valueType || DEFAULT_VALUE_TYPE,
|
||||
valueInput: partial?.valueInput || "",
|
||||
};
|
||||
}
|
||||
|
||||
function inferValueType(value: unknown): ConditionValueType {
|
||||
if (value === null) {
|
||||
return "null";
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
return "string";
|
||||
}
|
||||
if (typeof value === "number") {
|
||||
return "number";
|
||||
}
|
||||
if (typeof value === "boolean") {
|
||||
return "boolean";
|
||||
}
|
||||
return "json";
|
||||
}
|
||||
|
||||
function formatValueInput(
|
||||
value: unknown,
|
||||
valueType: ConditionValueType,
|
||||
): string {
|
||||
if (valueType === "null") {
|
||||
return "";
|
||||
}
|
||||
if (valueType === "json") {
|
||||
return JSON.stringify(value, null, 2);
|
||||
}
|
||||
return String(value ?? "");
|
||||
}
|
||||
|
||||
function isGuidedCondition(value: unknown): value is {
|
||||
field: string;
|
||||
operator: ConditionOperator;
|
||||
value: unknown;
|
||||
} {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const condition = value as Record<string, unknown>;
|
||||
return (
|
||||
typeof condition.field === "string" &&
|
||||
typeof condition.operator === "string" &&
|
||||
Object.prototype.hasOwnProperty.call(condition, "value") &&
|
||||
OPERATOR_OPTIONS.some((option) => option.value === condition.operator)
|
||||
);
|
||||
}
|
||||
|
||||
function parseInitialState(value: unknown): {
|
||||
mode: EditorMode;
|
||||
rows: ConditionRow[];
|
||||
rawText: string;
|
||||
unsupportedMessage?: string;
|
||||
} {
|
||||
if (
|
||||
value == null ||
|
||||
(Array.isArray(value) && value.length === 0) ||
|
||||
(typeof value === "object" &&
|
||||
!Array.isArray(value) &&
|
||||
Object.keys(value as Record<string, unknown>).length === 0)
|
||||
) {
|
||||
return {
|
||||
mode: "guided",
|
||||
rows: [],
|
||||
rawText: "",
|
||||
};
|
||||
}
|
||||
|
||||
if (Array.isArray(value) && value.every(isGuidedCondition)) {
|
||||
return {
|
||||
mode: "guided",
|
||||
rows: value.map((condition) => {
|
||||
const valueType = inferValueType(condition.value);
|
||||
return createRow({
|
||||
field: condition.field,
|
||||
operator: condition.operator,
|
||||
valueType,
|
||||
valueInput: formatValueInput(condition.value, valueType),
|
||||
});
|
||||
}),
|
||||
rawText: JSON.stringify(value, null, 2),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
mode: "raw",
|
||||
rows: [],
|
||||
rawText: JSON.stringify(value, null, 2),
|
||||
unsupportedMessage:
|
||||
"This rule uses a condition shape outside the guided builder. Edit it in raw JSON to preserve it.",
|
||||
};
|
||||
}
|
||||
|
||||
function parseConditionValue(row: ConditionRow): {
|
||||
value?: JsonValue;
|
||||
error?: string;
|
||||
} {
|
||||
switch (row.valueType) {
|
||||
case "string":
|
||||
return { value: row.valueInput };
|
||||
case "number": {
|
||||
const trimmed = row.valueInput.trim();
|
||||
if (!trimmed) {
|
||||
return { error: "Number value is required." };
|
||||
}
|
||||
const parsed = Number(trimmed);
|
||||
if (Number.isNaN(parsed)) {
|
||||
return { error: "Enter a valid number." };
|
||||
}
|
||||
return { value: parsed };
|
||||
}
|
||||
case "boolean":
|
||||
return { value: row.valueInput === "true" };
|
||||
case "null":
|
||||
return { value: null };
|
||||
case "json":
|
||||
if (!row.valueInput.trim()) {
|
||||
return { error: "JSON value is required." };
|
||||
}
|
||||
try {
|
||||
return { value: JSON.parse(row.valueInput) as JsonValue };
|
||||
} catch {
|
||||
return { error: "Enter valid JSON." };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default function RuleMatchConditionsEditor({
|
||||
value,
|
||||
onChange,
|
||||
error,
|
||||
onErrorChange,
|
||||
}: RuleMatchConditionsEditorProps) {
|
||||
const fieldId = useId();
|
||||
const [mode, setMode] = useState<EditorMode>(
|
||||
() => parseInitialState(value).mode,
|
||||
);
|
||||
const [rows, setRows] = useState<ConditionRow[]>(
|
||||
() => parseInitialState(value).rows,
|
||||
);
|
||||
const [rawText, setRawText] = useState(
|
||||
() => parseInitialState(value).rawText,
|
||||
);
|
||||
const [unsupportedMessage] = useState<string | undefined>(
|
||||
() => parseInitialState(value).unsupportedMessage,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (mode === "raw") {
|
||||
if (!rawText.trim()) {
|
||||
onErrorChange?.(undefined);
|
||||
onChange(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
onErrorChange?.(undefined);
|
||||
onChange(JSON.parse(rawText) as JsonValue);
|
||||
} catch {
|
||||
onErrorChange?.("Invalid JSON format");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const nextConditions: JsonValue[] = [];
|
||||
|
||||
for (let index = 0; index < rows.length; index += 1) {
|
||||
const row = rows[index];
|
||||
if (!row.field.trim()) {
|
||||
onErrorChange?.(`Condition ${index + 1}: field is required.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedValue = parseConditionValue(row);
|
||||
if (parsedValue.error) {
|
||||
onErrorChange?.(`Condition ${index + 1}: ${parsedValue.error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
nextConditions.push({
|
||||
field: row.field.trim(),
|
||||
operator: row.operator,
|
||||
value: parsedValue.value ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
onErrorChange?.(undefined);
|
||||
onChange(nextConditions.length > 0 ? nextConditions : undefined);
|
||||
}, [mode, onChange, onErrorChange, rawText, rows]);
|
||||
|
||||
const addCondition = () => {
|
||||
setRows((current) => [...current, createRow()]);
|
||||
};
|
||||
|
||||
const updateRow = (
|
||||
id: string,
|
||||
updater: (row: ConditionRow) => ConditionRow,
|
||||
) => {
|
||||
setRows((current) =>
|
||||
current.map((row) => (row.id === id ? updater(row) : row)),
|
||||
);
|
||||
};
|
||||
|
||||
const removeCondition = (id: string) => {
|
||||
setRows((current) => current.filter((row) => row.id !== id));
|
||||
};
|
||||
|
||||
const currentError = error;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-700">
|
||||
Match Conditions
|
||||
</h4>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
All conditions must match. Leave this empty to match every event
|
||||
from the selected trigger.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="inline-flex rounded-lg border border-gray-200 bg-gray-50 p-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMode("guided")}
|
||||
className={`inline-flex items-center gap-2 rounded-md px-3 py-1.5 text-sm transition-colors ${
|
||||
mode === "guided"
|
||||
? "bg-white text-gray-900 shadow-sm"
|
||||
: "text-gray-600 hover:text-gray-900"
|
||||
}`}
|
||||
>
|
||||
<ListFilter className="h-4 w-4" />
|
||||
Guided
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMode("raw")}
|
||||
className={`inline-flex items-center gap-2 rounded-md px-3 py-1.5 text-sm transition-colors ${
|
||||
mode === "raw"
|
||||
? "bg-white text-gray-900 shadow-sm"
|
||||
: "text-gray-600 hover:text-gray-900"
|
||||
}`}
|
||||
>
|
||||
<Braces className="h-4 w-4" />
|
||||
Raw JSON
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{unsupportedMessage && (
|
||||
<div className="rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-800">
|
||||
{unsupportedMessage}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mode === "guided" ? (
|
||||
<div className="space-y-3">
|
||||
{rows.length === 0 ? (
|
||||
<div className="rounded-xl border border-dashed border-gray-300 bg-gray-50 px-4 py-5 text-sm text-gray-500">
|
||||
No conditions configured.
|
||||
</div>
|
||||
) : (
|
||||
rows.map((row, index) => (
|
||||
<div
|
||||
key={row.id}
|
||||
className="rounded-xl border border-gray-200 bg-gray-50/70 p-4"
|
||||
>
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
Condition {index + 1}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeCondition(row.id)}
|
||||
className="inline-flex items-center gap-1 rounded-md px-2 py-1 text-sm text-gray-500 hover:bg-white hover:text-red-600"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-3 xl:grid-cols-12">
|
||||
<div className="xl:col-span-5">
|
||||
<label
|
||||
htmlFor={`${fieldId}-${row.id}-field`}
|
||||
className="mb-1 block text-xs font-medium uppercase tracking-wide text-gray-500"
|
||||
>
|
||||
Event field
|
||||
</label>
|
||||
<input
|
||||
id={`${fieldId}-${row.id}-field`}
|
||||
type="text"
|
||||
value={row.field}
|
||||
onChange={(e) =>
|
||||
updateRow(row.id, (current) => ({
|
||||
...current,
|
||||
field: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="status or nested.path"
|
||||
className="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="xl:col-span-3">
|
||||
<label className="mb-1 block text-xs font-medium uppercase tracking-wide text-gray-500">
|
||||
Operator
|
||||
</label>
|
||||
<SearchableSelect
|
||||
value={row.operator}
|
||||
onChange={(nextValue) =>
|
||||
updateRow(row.id, (current) => ({
|
||||
...current,
|
||||
operator: nextValue as ConditionOperator,
|
||||
}))
|
||||
}
|
||||
options={OPERATOR_OPTIONS}
|
||||
placeholder="Choose operator"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="xl:col-span-4">
|
||||
<label className="mb-1 block text-xs font-medium uppercase tracking-wide text-gray-500">
|
||||
Value type
|
||||
</label>
|
||||
<SearchableSelect
|
||||
value={row.valueType}
|
||||
onChange={(nextValue) =>
|
||||
updateRow(row.id, (current) => ({
|
||||
...current,
|
||||
valueType: nextValue as ConditionValueType,
|
||||
valueInput:
|
||||
nextValue === "boolean"
|
||||
? "true"
|
||||
: nextValue === "null"
|
||||
? ""
|
||||
: current.valueInput,
|
||||
}))
|
||||
}
|
||||
options={VALUE_TYPE_OPTIONS}
|
||||
placeholder="Choose type"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="xl:col-span-12">
|
||||
<label className="mb-1 block text-xs font-medium uppercase tracking-wide text-gray-500">
|
||||
Expected value
|
||||
</label>
|
||||
|
||||
{row.valueType === "boolean" ? (
|
||||
<SearchableSelect
|
||||
value={row.valueInput || "true"}
|
||||
onChange={(nextValue) =>
|
||||
updateRow(row.id, (current) => ({
|
||||
...current,
|
||||
valueInput: String(nextValue),
|
||||
}))
|
||||
}
|
||||
options={[
|
||||
{ value: "true", label: "True" },
|
||||
{ value: "false", label: "False" },
|
||||
]}
|
||||
/>
|
||||
) : row.valueType === "null" ? (
|
||||
<div className="rounded-lg border border-dashed border-gray-300 bg-white px-3 py-2 text-sm text-gray-500">
|
||||
This condition matches a null value.
|
||||
</div>
|
||||
) : row.valueType === "json" ? (
|
||||
<textarea
|
||||
value={row.valueInput}
|
||||
onChange={(e) =>
|
||||
updateRow(row.id, (current) => ({
|
||||
...current,
|
||||
valueInput: e.target.value,
|
||||
}))
|
||||
}
|
||||
rows={4}
|
||||
placeholder='{"expected": "value"}'
|
||||
className="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 font-mono text-sm text-gray-900 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
type={row.valueType === "number" ? "number" : "text"}
|
||||
value={row.valueInput}
|
||||
onChange={(e) =>
|
||||
updateRow(row.id, (current) => ({
|
||||
...current,
|
||||
valueInput: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder={
|
||||
row.valueType === "number" ? "42" : "expected value"
|
||||
}
|
||||
className="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={addCondition}
|
||||
className="inline-flex items-center gap-2 rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-700 hover:bg-gray-50"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Add condition
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<textarea
|
||||
value={rawText}
|
||||
onChange={(e) => setRawText(e.target.value)}
|
||||
rows={10}
|
||||
placeholder={`[\n {\n "field": "status",\n "operator": "equals",\n "value": "error"\n }\n]`}
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 font-mono text-sm text-gray-900 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentError && <p className="text-sm text-red-600">{currentError}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from "@/lib/format-utils";
|
||||
import SchemaBuilder from "@/components/common/SchemaBuilder";
|
||||
import SearchableSelect from "@/components/common/SearchableSelect";
|
||||
import { WebhooksService } from "@/api";
|
||||
import { TriggerStringPatch, WebhooksService } from "@/api";
|
||||
import type { TriggerResponse, PackSummary } from "@/api";
|
||||
|
||||
/** Flat schema format: each key is a parameter name mapped to its definition */
|
||||
@@ -116,7 +116,6 @@ export default function TriggerForm({
|
||||
pack_ref: selectedPackData.ref,
|
||||
ref: fullRef,
|
||||
label: label.trim(),
|
||||
description: description.trim() || undefined,
|
||||
enabled,
|
||||
param_schema:
|
||||
Object.keys(paramSchema).length > 0 ? paramSchema : undefined,
|
||||
@@ -124,9 +123,16 @@ export default function TriggerForm({
|
||||
};
|
||||
|
||||
if (isEditing && initialData?.ref) {
|
||||
const updateData = {
|
||||
...formData,
|
||||
description: description.trim()
|
||||
? { op: TriggerStringPatch.op.SET, value: description.trim() }
|
||||
: { op: TriggerStringPatch.op.CLEAR },
|
||||
};
|
||||
|
||||
await updateTrigger.mutateAsync({
|
||||
ref: initialData.ref,
|
||||
data: formData,
|
||||
data: updateData,
|
||||
});
|
||||
|
||||
// Handle webhook enable/disable separately for updates
|
||||
@@ -152,7 +158,12 @@ export default function TriggerForm({
|
||||
navigate(`/triggers/${encodeURIComponent(initialData.ref)}`);
|
||||
return;
|
||||
} else {
|
||||
const response = await createTrigger.mutateAsync(formData);
|
||||
const createData = {
|
||||
...formData,
|
||||
description: description.trim() || undefined,
|
||||
};
|
||||
|
||||
const response = await createTrigger.mutateAsync(createData);
|
||||
const newTrigger = response?.data;
|
||||
if (newTrigger?.ref) {
|
||||
// If webhook is enabled, enable it after trigger creation
|
||||
|
||||
@@ -1,24 +1,8 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Link, Outlet, useLocation } from "react-router-dom";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
import {
|
||||
Package,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
User,
|
||||
LogOut,
|
||||
CirclePlay,
|
||||
CircleArrowRight,
|
||||
SquareArrowRight,
|
||||
SquarePlay,
|
||||
SquareDot,
|
||||
CircleDot,
|
||||
SquareAsterisk,
|
||||
KeyRound,
|
||||
Home,
|
||||
FolderArchive,
|
||||
TerminalSquare,
|
||||
} from "lucide-react";
|
||||
import { ChevronLeft, ChevronRight, User, LogOut } from "lucide-react";
|
||||
import { navIcons } from "./navIcons";
|
||||
|
||||
// Color mappings for navigation items — defined outside component for stable reference
|
||||
const colorClasses = {
|
||||
@@ -68,29 +52,36 @@ const colorClasses = {
|
||||
// Navigation sections with dividers and colors
|
||||
const navSections = [
|
||||
{
|
||||
items: [{ to: "/", label: "Dashboard", icon: Home, color: "gray" }],
|
||||
items: [
|
||||
{ to: "/", label: "Dashboard", icon: navIcons.dashboard, color: "gray" },
|
||||
],
|
||||
},
|
||||
{
|
||||
// Component Management - Cool colors (cyan -> blue -> violet)
|
||||
items: [
|
||||
{ to: "/actions", label: "Actions", icon: SquarePlay, color: "cyan" },
|
||||
{
|
||||
to: "/actions",
|
||||
label: "Actions",
|
||||
icon: navIcons.actions,
|
||||
color: "cyan",
|
||||
},
|
||||
{
|
||||
to: "/runtimes",
|
||||
label: "Runtimes",
|
||||
icon: TerminalSquare,
|
||||
icon: navIcons.runtimes,
|
||||
color: "blue",
|
||||
},
|
||||
{ to: "/rules", label: "Rules", icon: SquareArrowRight, color: "blue" },
|
||||
{ to: "/rules", label: "Rules", icon: navIcons.rules, color: "blue" },
|
||||
{
|
||||
to: "/triggers",
|
||||
label: "Triggers",
|
||||
icon: SquareDot,
|
||||
icon: navIcons.triggers,
|
||||
color: "violet",
|
||||
},
|
||||
{
|
||||
to: "/sensors",
|
||||
label: "Sensors",
|
||||
icon: SquareAsterisk,
|
||||
icon: navIcons.sensors,
|
||||
color: "purple",
|
||||
},
|
||||
],
|
||||
@@ -101,36 +92,47 @@ const navSections = [
|
||||
{
|
||||
to: "/executions",
|
||||
label: "Execution History",
|
||||
icon: CirclePlay,
|
||||
icon: navIcons.executions,
|
||||
color: "fuchsia",
|
||||
},
|
||||
{
|
||||
to: "/enforcements",
|
||||
label: "Enforcement History",
|
||||
icon: CircleArrowRight,
|
||||
icon: navIcons.enforcements,
|
||||
color: "rose",
|
||||
},
|
||||
{
|
||||
to: "/events",
|
||||
label: "Event History",
|
||||
icon: CircleDot,
|
||||
icon: navIcons.events,
|
||||
color: "orange",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
items: [
|
||||
{ to: "/keys", label: "Keys & Secrets", icon: KeyRound, color: "gray" },
|
||||
{
|
||||
to: "/keys",
|
||||
label: "Keys & Secrets",
|
||||
icon: navIcons.keys,
|
||||
color: "gray",
|
||||
},
|
||||
{
|
||||
to: "/artifacts",
|
||||
label: "Artifacts",
|
||||
icon: FolderArchive,
|
||||
icon: navIcons.artifacts,
|
||||
color: "gray",
|
||||
},
|
||||
{
|
||||
to: "/access-control",
|
||||
label: "Access Control",
|
||||
icon: navIcons.accessControl,
|
||||
color: "gray",
|
||||
},
|
||||
{
|
||||
to: "/packs",
|
||||
label: "Pack Management",
|
||||
icon: Package,
|
||||
icon: navIcons.packs,
|
||||
color: "gray",
|
||||
},
|
||||
],
|
||||
|
||||
31
web/src/components/layout/navIcons.tsx
Normal file
31
web/src/components/layout/navIcons.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import {
|
||||
CircleArrowRight,
|
||||
CircleDot,
|
||||
CirclePlay,
|
||||
FolderArchive,
|
||||
Home,
|
||||
KeyRound,
|
||||
Package,
|
||||
ShieldCheck,
|
||||
SquareArrowRight,
|
||||
SquareAsterisk,
|
||||
SquareDot,
|
||||
SquarePlay,
|
||||
TerminalSquare,
|
||||
} from "lucide-react";
|
||||
|
||||
export const navIcons = {
|
||||
dashboard: Home,
|
||||
actions: SquarePlay,
|
||||
runtimes: TerminalSquare,
|
||||
rules: SquareArrowRight,
|
||||
triggers: SquareDot,
|
||||
sensors: SquareAsterisk,
|
||||
executions: CirclePlay,
|
||||
enforcements: CircleArrowRight,
|
||||
events: CircleDot,
|
||||
keys: KeyRound,
|
||||
artifacts: FolderArchive,
|
||||
accessControl: ShieldCheck,
|
||||
packs: Package,
|
||||
} as const;
|
||||
243
web/src/hooks/usePermissions.ts
Normal file
243
web/src/hooks/usePermissions.ts
Normal file
@@ -0,0 +1,243 @@
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { PermissionsService } from "@/api";
|
||||
import type {
|
||||
CreateIdentityRequest,
|
||||
UpdateIdentityRequest,
|
||||
CreatePermissionAssignmentRequest,
|
||||
} from "@/api";
|
||||
|
||||
// Fetch all identities with pagination
|
||||
export function useIdentities(params?: { page?: number; pageSize?: number }) {
|
||||
return useQuery({
|
||||
queryKey: ["identities", params],
|
||||
queryFn: async () => {
|
||||
return await PermissionsService.listIdentities({
|
||||
page: params?.page || 1,
|
||||
pageSize: params?.pageSize || 50,
|
||||
});
|
||||
},
|
||||
staleTime: 30000,
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch single identity by ID
|
||||
export function useIdentity(id: number) {
|
||||
return useQuery({
|
||||
queryKey: ["identities", id],
|
||||
queryFn: async () => {
|
||||
return await PermissionsService.getIdentity({ id });
|
||||
},
|
||||
enabled: id > 0,
|
||||
staleTime: 30000,
|
||||
});
|
||||
}
|
||||
|
||||
// Create a new identity
|
||||
export function useCreateIdentity() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (data: CreateIdentityRequest) => {
|
||||
return await PermissionsService.createIdentity({ requestBody: data });
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["identities"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Update an existing identity
|
||||
export function useUpdateIdentity() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
id,
|
||||
data,
|
||||
}: {
|
||||
id: number;
|
||||
data: UpdateIdentityRequest;
|
||||
}) => {
|
||||
return await PermissionsService.updateIdentity({ id, requestBody: data });
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: ["identities"] });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["identities", variables.id],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Delete an identity
|
||||
export function useDeleteIdentity() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (id: number) => {
|
||||
return await PermissionsService.deleteIdentity({ id });
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["identities"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch permission sets
|
||||
export function usePermissionSets(packRef?: string | null) {
|
||||
return useQuery({
|
||||
queryKey: ["permission-sets", packRef],
|
||||
queryFn: async () => {
|
||||
return await PermissionsService.listPermissionSets({ packRef });
|
||||
},
|
||||
staleTime: 30000,
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch permission assignments for an identity
|
||||
export function useIdentityPermissions(id: number) {
|
||||
return useQuery({
|
||||
queryKey: ["identity-permissions", id],
|
||||
queryFn: async () => {
|
||||
return await PermissionsService.listIdentityPermissions({ id });
|
||||
},
|
||||
enabled: id > 0,
|
||||
staleTime: 30000,
|
||||
});
|
||||
}
|
||||
|
||||
// Create a permission assignment
|
||||
export function useCreatePermissionAssignment() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (data: CreatePermissionAssignmentRequest) => {
|
||||
return await PermissionsService.createPermissionAssignment({
|
||||
requestBody: data,
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["identities"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["identity-permissions"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["permission-sets"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Delete a permission assignment
|
||||
export function useDeletePermissionAssignment() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (id: number) => {
|
||||
return await PermissionsService.deletePermissionAssignment({ id });
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["identities"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["identity-permissions"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["permission-sets"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Create a role assignment for an identity
|
||||
export function useCreateIdentityRoleAssignment() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
identityId,
|
||||
role,
|
||||
}: {
|
||||
identityId: number;
|
||||
role: string;
|
||||
}) => {
|
||||
return await PermissionsService.createIdentityRoleAssignment({
|
||||
id: identityId,
|
||||
requestBody: { role },
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["identities"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Delete a role assignment from an identity
|
||||
export function useDeleteIdentityRoleAssignment() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (id: number) => {
|
||||
return await PermissionsService.deleteIdentityRoleAssignment({ id });
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["identities"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Create a role assignment for a permission set
|
||||
export function useCreatePermissionSetRoleAssignment() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
permissionSetId,
|
||||
role,
|
||||
}: {
|
||||
permissionSetId: number;
|
||||
role: string;
|
||||
}) => {
|
||||
return await PermissionsService.createPermissionSetRoleAssignment({
|
||||
id: permissionSetId,
|
||||
requestBody: { role },
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["permission-sets"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Delete a role assignment from a permission set
|
||||
export function useDeletePermissionSetRoleAssignment() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (id: number) => {
|
||||
return await PermissionsService.deletePermissionSetRoleAssignment({ id });
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["permission-sets"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Freeze an identity
|
||||
export function useFreezeIdentity() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (id: number) => {
|
||||
return await PermissionsService.freezeIdentity({ id });
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["identities"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Unfreeze an identity
|
||||
export function useUnfreezeIdentity() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (id: number) => {
|
||||
return await PermissionsService.unfreezeIdentity({ id });
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["identities"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
707
web/src/pages/access-control/AccessControlPage.tsx
Normal file
707
web/src/pages/access-control/AccessControlPage.tsx
Normal file
@@ -0,0 +1,707 @@
|
||||
import { useState } from "react";
|
||||
import { useSearchParams, Link } from "react-router-dom";
|
||||
import {
|
||||
Shield,
|
||||
User,
|
||||
Users,
|
||||
Plus,
|
||||
Search,
|
||||
ShieldCheck,
|
||||
X,
|
||||
Snowflake,
|
||||
Sun,
|
||||
Package,
|
||||
Tag,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
useIdentities,
|
||||
useCreateIdentity,
|
||||
usePermissionSets,
|
||||
useFreezeIdentity,
|
||||
useUnfreezeIdentity,
|
||||
} from "@/hooks/usePermissions";
|
||||
|
||||
// The backend IdentitySummary includes `frozen` and `roles` but the generated client type doesn't declare them
|
||||
interface IdentityRow {
|
||||
id: number;
|
||||
login: string;
|
||||
display_name?: string | null;
|
||||
frozen?: boolean;
|
||||
roles?: string[];
|
||||
attributes: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// The backend PermissionSetSummary includes `roles` but the generated client type doesn't declare it
|
||||
interface PermissionSetRow {
|
||||
id: number;
|
||||
ref: string;
|
||||
pack_ref?: string | null;
|
||||
label?: string | null;
|
||||
description?: string | null;
|
||||
grants: unknown;
|
||||
roles?: Array<{
|
||||
id: number;
|
||||
permission_set_id: number;
|
||||
permission_set_ref?: string | null;
|
||||
role: string;
|
||||
created: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
function CreateIdentityModal({ onClose }: { onClose: () => void }) {
|
||||
const [login, setLogin] = useState("");
|
||||
const [displayName, setDisplayName] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const createIdentity = useCreateIdentity();
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
try {
|
||||
await createIdentity.mutateAsync({
|
||||
login,
|
||||
display_name: displayName || undefined,
|
||||
password: password || undefined,
|
||||
});
|
||||
onClose();
|
||||
} catch (err) {
|
||||
setError(
|
||||
err instanceof Error ? err.message : "Failed to create identity",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-md mx-4">
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200">
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
Create Identity
|
||||
</h3>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit} className="p-6 space-y-4">
|
||||
{error && (
|
||||
<div className="p-3 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="login"
|
||||
className="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
Login <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="login"
|
||||
type="text"
|
||||
value={login}
|
||||
onChange={(e) => setLogin(e.target.value)}
|
||||
required
|
||||
minLength={3}
|
||||
maxLength={255}
|
||||
placeholder="e.g. jane.doe"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="display-name"
|
||||
className="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
Display Name
|
||||
</label>
|
||||
<input
|
||||
id="display-name"
|
||||
type="text"
|
||||
value={displayName}
|
||||
onChange={(e) => setDisplayName(e.target.value)}
|
||||
placeholder="e.g. Jane Doe"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
minLength={8}
|
||||
maxLength={128}
|
||||
placeholder="Min 8 characters (optional)"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end gap-3 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-sm text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={createIdentity.isPending || !login}
|
||||
className="px-4 py-2 text-sm text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{createIdentity.isPending ? "Creating\u2026" : "Create Identity"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RoleBadges({ roles }: { roles: string[] }) {
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
const visible = roles.slice(0, 3);
|
||||
const remaining = roles.length - visible.length;
|
||||
|
||||
if (roles.length === 0) {
|
||||
return <span className="text-xs text-gray-400">None</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1 flex-wrap">
|
||||
{visible.map((role) => (
|
||||
<span
|
||||
key={role}
|
||||
className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800"
|
||||
>
|
||||
{role}
|
||||
</span>
|
||||
))}
|
||||
{remaining > 0 && (
|
||||
<div
|
||||
className="relative"
|
||||
onMouseEnter={() => setShowTooltip(true)}
|
||||
onMouseLeave={() => setShowTooltip(false)}
|
||||
>
|
||||
<span className="text-xs text-gray-500 cursor-default">
|
||||
and {remaining} more {remaining === 1 ? "role" : "roles"}
|
||||
</span>
|
||||
{showTooltip && (
|
||||
<div className="absolute bottom-full left-0 mb-2 z-20 bg-gray-900 text-white text-xs rounded-lg shadow-lg p-3 whitespace-nowrap">
|
||||
<p className="font-medium mb-1">All roles:</p>
|
||||
<ul className="space-y-0.5">
|
||||
{roles.map((role) => (
|
||||
<li key={role}>{role}</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="absolute top-full left-4 w-2 h-2 bg-gray-900 rotate-45 -mt-1" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function IdentitiesTab() {
|
||||
const [page, setPage] = useState(1);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const pageSize = 20;
|
||||
|
||||
const { data, isLoading, error } = useIdentities({ page, pageSize });
|
||||
const freezeIdentity = useFreezeIdentity();
|
||||
const unfreezeIdentity = useUnfreezeIdentity();
|
||||
|
||||
const identities: IdentityRow[] = (data?.data as IdentityRow[]) || [];
|
||||
const total = data?.pagination?.total_items || 0;
|
||||
const totalPages = total ? Math.ceil(total / pageSize) : 0;
|
||||
|
||||
const filteredIdentities = searchTerm
|
||||
? identities.filter((i) => {
|
||||
const q = searchTerm.toLowerCase();
|
||||
return (
|
||||
i.login.toLowerCase().includes(q) ||
|
||||
(i.display_name || "").toLowerCase().includes(q) ||
|
||||
(i.roles || []).some((r) => r.toLowerCase().includes(q))
|
||||
);
|
||||
})
|
||||
: identities;
|
||||
|
||||
const handleToggleFreeze = async (identity: IdentityRow) => {
|
||||
const action = identity.frozen ? "unfreeze" : "freeze";
|
||||
if (
|
||||
!window.confirm(
|
||||
"Are you sure you want to " +
|
||||
action +
|
||||
' identity "' +
|
||||
identity.login +
|
||||
'"?',
|
||||
)
|
||||
)
|
||||
return;
|
||||
try {
|
||||
if (identity.frozen) {
|
||||
await unfreezeIdentity.mutateAsync(identity.id);
|
||||
} else {
|
||||
await freezeIdentity.mutateAsync(identity.id);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to " + action + " identity:", err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900 flex items-center gap-2">
|
||||
<Users className="w-5 h-5 text-blue-600" />
|
||||
Identities
|
||||
</h2>
|
||||
<p className="mt-1 text-sm text-gray-600">
|
||||
Manage user and service identities, their roles, and permission
|
||||
assignments
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Create Identity
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow mb-6 p-4">
|
||||
<div className="max-w-md">
|
||||
<label
|
||||
htmlFor="identity-search"
|
||||
className="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Search className="w-4 h-4" />
|
||||
Search Identities
|
||||
</div>
|
||||
</label>
|
||||
<input
|
||||
id="identity-search"
|
||||
type="text"
|
||||
value={searchTerm}
|
||||
onChange={(e) => {
|
||||
setSearchTerm(e.target.value);
|
||||
setPage(1);
|
||||
}}
|
||||
placeholder="Search by login, display name, or role..."
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
{filteredIdentities.length > 0 && (
|
||||
<div className="mt-3 text-sm text-gray-600">
|
||||
Showing {filteredIdentities.length} of {total} identities
|
||||
{searchTerm && " (filtered)"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow overflow-hidden">
|
||||
{isLoading ? (
|
||||
<div className="p-12 text-center">
|
||||
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||
<p className="mt-4 text-gray-600">Loading identities...</p>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="p-12 text-center">
|
||||
<p className="text-red-600">Failed to load identities</p>
|
||||
<p className="text-sm text-gray-600 mt-2">
|
||||
{error instanceof Error ? error.message : "Unknown error"}
|
||||
</p>
|
||||
</div>
|
||||
) : !filteredIdentities || filteredIdentities.length === 0 ? (
|
||||
<div className="p-12 text-center">
|
||||
<Users className="mx-auto h-12 w-12 text-gray-400" />
|
||||
<p className="mt-4 text-gray-600">No identities found</p>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
{searchTerm
|
||||
? "Try adjusting your search"
|
||||
: "Create your first identity to get started"}
|
||||
</p>
|
||||
{!searchTerm && (
|
||||
<button
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
Create Identity
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Login
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Display Name
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Roles
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Status
|
||||
</th>
|
||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{filteredIdentities.map((identity) => (
|
||||
<tr key={identity.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<Link
|
||||
to={"/access-control/identities/" + identity.id}
|
||||
className="flex items-center gap-2 group"
|
||||
>
|
||||
<User className="w-4 h-4 text-gray-400 group-hover:text-blue-500" />
|
||||
<span className="text-sm font-medium text-blue-600 group-hover:text-blue-800 group-hover:underline">
|
||||
{identity.login}
|
||||
</span>
|
||||
</Link>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className="text-sm text-gray-600">
|
||||
{identity.display_name || "—"}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<RoleBadges roles={identity.roles || []} />
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{identity.frozen ? (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold bg-blue-100 text-blue-800">
|
||||
<Snowflake className="w-3 h-3" />
|
||||
Frozen
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-semibold bg-green-100 text-green-800">
|
||||
Active
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-right">
|
||||
<button
|
||||
onClick={() => handleToggleFreeze(identity)}
|
||||
disabled={
|
||||
freezeIdentity.isPending ||
|
||||
unfreezeIdentity.isPending
|
||||
}
|
||||
className={
|
||||
identity.frozen
|
||||
? "inline-flex items-center gap-1 px-2.5 py-1 text-xs font-medium rounded-md text-green-700 bg-green-50 hover:bg-green-100 border border-green-200 disabled:opacity-50 transition-colors"
|
||||
: "inline-flex items-center gap-1 px-2.5 py-1 text-xs font-medium rounded-md text-blue-700 bg-blue-50 hover:bg-blue-100 border border-blue-200 disabled:opacity-50 transition-colors"
|
||||
}
|
||||
title={
|
||||
identity.frozen
|
||||
? "Unfreeze identity"
|
||||
: "Freeze identity"
|
||||
}
|
||||
>
|
||||
{identity.frozen ? (
|
||||
<>
|
||||
<Sun className="w-3 h-3" />
|
||||
Unfreeze
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Snowflake className="w-3 h-3" />
|
||||
Freeze
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{totalPages > 1 && (
|
||||
<div className="bg-gray-50 px-6 py-4 flex items-center justify-between border-t border-gray-200">
|
||||
<p className="text-sm text-gray-700">
|
||||
Page <span className="font-medium">{page}</span> of{" "}
|
||||
<span className="font-medium">{totalPages}</span>
|
||||
</p>
|
||||
<nav className="relative z-0 inline-flex rounded-md shadow-sm -space-x-px">
|
||||
<button
|
||||
onClick={() => setPage(page - 1)}
|
||||
disabled={page === 1}
|
||||
className="relative inline-flex items-center px-3 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setPage(page + 1)}
|
||||
disabled={page === totalPages}
|
||||
className="relative inline-flex items-center px-3 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showCreateModal && (
|
||||
<CreateIdentityModal onClose={() => setShowCreateModal(false)} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function PermissionSetsTab() {
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
|
||||
const { data: rawData, isLoading, error } = usePermissionSets();
|
||||
|
||||
const permissionSets: PermissionSetRow[] =
|
||||
(rawData as PermissionSetRow[]) || [];
|
||||
|
||||
const filteredSets = searchTerm
|
||||
? permissionSets.filter(
|
||||
(ps) =>
|
||||
ps.ref.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
(ps.label || "").toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
(ps.pack_ref || "").toLowerCase().includes(searchTerm.toLowerCase()),
|
||||
)
|
||||
: permissionSets;
|
||||
|
||||
const grantsCount = (grants: unknown): number => {
|
||||
if (Array.isArray(grants)) return grants.length;
|
||||
if (typeof grants === "object" && grants !== null)
|
||||
return Object.keys(grants).length;
|
||||
return 0;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-6">
|
||||
<h2 className="text-xl font-semibold text-gray-900 flex items-center gap-2">
|
||||
<ShieldCheck className="w-5 h-5 text-indigo-600" />
|
||||
Permission Sets
|
||||
</h2>
|
||||
<p className="mt-1 text-sm text-gray-600">
|
||||
Browse permission sets and manage their role assignments
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow mb-6 p-4">
|
||||
<div className="max-w-md">
|
||||
<label
|
||||
htmlFor="permset-search"
|
||||
className="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Search className="w-4 h-4" />
|
||||
Search Permission Sets
|
||||
</div>
|
||||
</label>
|
||||
<input
|
||||
id="permset-search"
|
||||
type="text"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
placeholder="Search by ref, label, or pack..."
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
{filteredSets.length > 0 && (
|
||||
<div className="mt-3 text-sm text-gray-600">
|
||||
Showing {filteredSets.length} of {permissionSets.length} permission
|
||||
sets{searchTerm && " (filtered)"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow overflow-hidden">
|
||||
{isLoading ? (
|
||||
<div className="p-12 text-center">
|
||||
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||
<p className="mt-4 text-gray-600">Loading permission sets...</p>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="p-12 text-center">
|
||||
<p className="text-red-600">Failed to load permission sets</p>
|
||||
<p className="text-sm text-gray-600 mt-2">
|
||||
{error instanceof Error ? error.message : "Unknown error"}
|
||||
</p>
|
||||
</div>
|
||||
) : !filteredSets || filteredSets.length === 0 ? (
|
||||
<div className="p-12 text-center">
|
||||
<ShieldCheck className="mx-auto h-12 w-12 text-gray-400" />
|
||||
<p className="mt-4 text-gray-600">No permission sets found</p>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
{searchTerm
|
||||
? "Try adjusting your search"
|
||||
: "Permission sets are defined in packs"}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Reference
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Label
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Pack
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Roles
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Grants
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{filteredSets.map((ps) => (
|
||||
<tr key={ps.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<Link
|
||||
to={"/access-control/permission-sets/" + ps.ref}
|
||||
className="flex items-center gap-2 group"
|
||||
>
|
||||
<Shield className="w-4 h-4 text-indigo-400 group-hover:text-indigo-600" />
|
||||
<span className="text-sm font-mono font-medium text-blue-600 group-hover:text-blue-800 group-hover:underline">
|
||||
{ps.ref}
|
||||
</span>
|
||||
</Link>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className="text-sm text-gray-600">
|
||||
{ps.label || "\u2014"}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{ps.pack_ref ? (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||
<Package className="w-3 h-3" />
|
||||
{ps.pack_ref}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-sm text-gray-400">
|
||||
{"\u2014"}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{ps.roles && ps.roles.length > 0 ? (
|
||||
ps.roles.map((ra) => (
|
||||
<span
|
||||
key={ra.id}
|
||||
className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800"
|
||||
>
|
||||
<Tag className="w-3 h-3" />
|
||||
{ra.role}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<span className="text-xs text-gray-400">None</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className="text-sm text-gray-600">
|
||||
{grantsCount(ps.grants)} grant
|
||||
{grantsCount(ps.grants) !== 1 ? "s" : ""}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AccessControlPage() {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const activeTab = searchParams.get("tab") || "identities";
|
||||
|
||||
const setTab = (tab: string) => {
|
||||
setSearchParams({ tab });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-3xl font-bold text-gray-900 flex items-center gap-3">
|
||||
<Shield className="w-8 h-8 text-indigo-600" />
|
||||
Access Control
|
||||
</h1>
|
||||
<p className="mt-2 text-gray-600">
|
||||
Manage identities, permission sets, and role-based access control
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="border-b border-gray-200 mb-6">
|
||||
<nav className="-mb-px flex space-x-8">
|
||||
<button
|
||||
onClick={() => setTab("identities")}
|
||||
className={`whitespace-nowrap py-3 px-1 border-b-2 font-medium text-sm transition-colors ${
|
||||
activeTab === "identities"
|
||||
? "border-blue-500 text-blue-600"
|
||||
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Users className="w-4 h-4" />
|
||||
Identities
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setTab("permission-sets")}
|
||||
className={`whitespace-nowrap py-3 px-1 border-b-2 font-medium text-sm transition-colors ${
|
||||
activeTab === "permission-sets"
|
||||
? "border-indigo-500 text-indigo-600"
|
||||
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<ShieldCheck className="w-4 h-4" />
|
||||
Permission Sets
|
||||
</div>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{activeTab === "identities" ? <IdentitiesTab /> : <PermissionSetsTab />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
368
web/src/pages/access-control/IdentityDetailPage.tsx
Normal file
368
web/src/pages/access-control/IdentityDetailPage.tsx
Normal file
@@ -0,0 +1,368 @@
|
||||
import { useState } from "react";
|
||||
import { useParams, Link } from "react-router-dom";
|
||||
import {
|
||||
Shield,
|
||||
ArrowLeft,
|
||||
Trash2,
|
||||
Plus,
|
||||
Tag,
|
||||
Snowflake,
|
||||
Sun,
|
||||
ShieldCheck,
|
||||
User,
|
||||
FileJson,
|
||||
Search,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
useIdentity,
|
||||
usePermissionSets,
|
||||
useCreateIdentityRoleAssignment,
|
||||
useDeleteIdentityRoleAssignment,
|
||||
useCreatePermissionAssignment,
|
||||
useDeletePermissionAssignment,
|
||||
useFreezeIdentity,
|
||||
useUnfreezeIdentity,
|
||||
} from "@/hooks/usePermissions";
|
||||
|
||||
interface RoleAssignment {
|
||||
id: number;
|
||||
identity_id: number;
|
||||
role: string;
|
||||
source: string;
|
||||
managed: boolean;
|
||||
created: string;
|
||||
updated: string;
|
||||
}
|
||||
|
||||
interface DirectPermission {
|
||||
id: number;
|
||||
identity_id: number;
|
||||
permission_set_id: number;
|
||||
permission_set_ref: string;
|
||||
created: string;
|
||||
}
|
||||
|
||||
interface IdentityDetail {
|
||||
id: number;
|
||||
login: string;
|
||||
display_name: string | null;
|
||||
frozen: boolean;
|
||||
attributes: Record<string, unknown>;
|
||||
roles: RoleAssignment[];
|
||||
direct_permissions: DirectPermission[];
|
||||
}
|
||||
|
||||
export default function IdentityDetailPage() {
|
||||
const { id: idParam } = useParams<{ id: string }>();
|
||||
const id = Number(idParam) || 0;
|
||||
|
||||
const { data: rawData, isLoading, error } = useIdentity(id);
|
||||
const { data: permissionSets } = usePermissionSets();
|
||||
|
||||
const createRoleMutation = useCreateIdentityRoleAssignment();
|
||||
const deleteRoleMutation = useDeleteIdentityRoleAssignment();
|
||||
const createPermMutation = useCreatePermissionAssignment();
|
||||
const deletePermMutation = useDeletePermissionAssignment();
|
||||
const freezeMutation = useFreezeIdentity();
|
||||
const unfreezeMutation = useUnfreezeIdentity();
|
||||
|
||||
const [showAddRole, setShowAddRole] = useState(false);
|
||||
const [newRole, setNewRole] = useState("");
|
||||
const [showAssignPerm, setShowAssignPerm] = useState(false);
|
||||
const [selectedPermSetRef, setSelectedPermSetRef] = useState("");
|
||||
const [permSetSearch, setPermSetSearch] = useState("");
|
||||
|
||||
const identity = (rawData as unknown as { data: IdentityDetail } | undefined)?.data;
|
||||
|
||||
const handleAddRole = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!newRole.trim()) return;
|
||||
try {
|
||||
await createRoleMutation.mutateAsync({ identityId: id, role: newRole.trim() });
|
||||
setNewRole("");
|
||||
setShowAddRole(false);
|
||||
} catch (err) {
|
||||
console.error("Failed to add role:", err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteRole = async (assignmentId: number, role: string) => {
|
||||
if (window.confirm("Remove role \"" + role + "\" from this identity?")) {
|
||||
try {
|
||||
await deleteRoleMutation.mutateAsync(assignmentId);
|
||||
} catch (err) {
|
||||
console.error("Failed to delete role assignment:", err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleAssignPermission = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!selectedPermSetRef) return;
|
||||
try {
|
||||
await createPermMutation.mutateAsync({ identity_id: id, permission_set_ref: selectedPermSetRef });
|
||||
setSelectedPermSetRef("");
|
||||
setPermSetSearch("");
|
||||
setShowAssignPerm(false);
|
||||
} catch (err) {
|
||||
console.error("Failed to assign permission set:", err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeletePermission = async (assignmentId: number, ref: string) => {
|
||||
if (window.confirm("Remove permission set \"" + ref + "\" from this identity?")) {
|
||||
try {
|
||||
await deletePermMutation.mutateAsync(assignmentId);
|
||||
} catch (err) {
|
||||
console.error("Failed to remove permission assignment:", err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleFreeze = async () => {
|
||||
if (!identity) return;
|
||||
const action = identity.frozen ? "unfreeze" : "freeze";
|
||||
if (!window.confirm("Are you sure you want to " + action + " identity \"" + identity.login + "\"?")) return;
|
||||
try {
|
||||
if (identity.frozen) {
|
||||
await unfreezeMutation.mutateAsync(id);
|
||||
} else {
|
||||
await freezeMutation.mutateAsync(id);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to " + action + " identity:", err);
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => new Date(dateString).toLocaleString();
|
||||
|
||||
const assignedPermSetRefs = new Set(identity?.direct_permissions?.map((p) => p.permission_set_ref) ?? []);
|
||||
const availablePermSets = (permissionSets ?? []).filter((ps) => !assignedPermSetRefs.has(ps.ref));
|
||||
const filteredAvailablePermSets = permSetSearch.trim()
|
||||
? availablePermSets.filter((ps) =>
|
||||
ps.ref.toLowerCase().includes(permSetSearch.toLowerCase()) ||
|
||||
(ps.label ?? "").toLowerCase().includes(permSetSearch.toLowerCase())
|
||||
)
|
||||
: availablePermSets;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
|
||||
<p className="mt-3 text-sm text-gray-500">Loading identity...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !identity) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<Link to="/access-control" className="inline-flex items-center gap-1 text-sm text-blue-600 hover:text-blue-800 mb-6">
|
||||
<ArrowLeft className="w-4 h-4" /> Back to Access Control
|
||||
</Link>
|
||||
<div className="bg-white rounded-lg shadow p-12 text-center">
|
||||
<Shield className="mx-auto h-12 w-12 text-gray-400" />
|
||||
<p className="mt-4 text-red-600">Failed to load identity</p>
|
||||
<p className="text-sm text-gray-500 mt-1">{error instanceof Error ? error.message : "Identity not found"}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-5xl">
|
||||
<Link to="/access-control" className="inline-flex items-center gap-1 text-sm text-blue-600 hover:text-blue-800 mb-6">
|
||||
<ArrowLeft className="w-4 h-4" /> Back to Access Control
|
||||
</Link>
|
||||
|
||||
{/* Header */}
|
||||
<div className="bg-white rounded-lg shadow p-6 mb-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="bg-blue-100 p-3 rounded-full">
|
||||
<User className="w-6 h-6 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-2xl font-bold text-gray-900">{identity.login}</h1>
|
||||
{identity.frozen && (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold bg-blue-100 text-blue-800">
|
||||
<Snowflake className="w-3 h-3" /> Frozen
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{identity.display_name && <p className="text-gray-600 mt-1">{identity.display_name}</p>}
|
||||
<p className="text-sm text-gray-400 mt-1">ID: {identity.id}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={handleToggleFreeze} disabled={freezeMutation.isPending || unfreezeMutation.isPending}
|
||||
className={"inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors disabled:opacity-50 " + (identity.frozen ? "bg-green-50 text-green-700 hover:bg-green-100 border border-green-200" : "bg-blue-50 text-blue-700 hover:bg-blue-100 border border-blue-200")}>
|
||||
{identity.frozen ? (<><Sun className="w-4 h-4" /> Unfreeze</>) : (<><Snowflake className="w-4 h-4" /> Freeze</>)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Roles Section */}
|
||||
<div className="bg-white rounded-lg shadow p-6 mb-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Tag className="w-5 h-5 text-violet-500" />
|
||||
<h2 className="text-lg font-semibold text-gray-900">Role Assignments</h2>
|
||||
<span className="text-sm text-gray-500">({identity.roles?.length || 0})</span>
|
||||
</div>
|
||||
<button onClick={() => setShowAddRole(!showAddRole)} className="inline-flex items-center gap-1 px-3 py-1.5 text-sm bg-violet-600 text-white rounded-lg hover:bg-violet-700 transition-colors">
|
||||
<Plus className="w-4 h-4" /> Add Role
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showAddRole && (
|
||||
<form onSubmit={handleAddRole} className="flex items-center gap-3 mb-4 p-3 bg-gray-50 rounded-lg">
|
||||
<input type="text" value={newRole} onChange={(e) => setNewRole(e.target.value)} placeholder="Role name (e.g. admin, operator, viewer)" className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-violet-500 text-sm" autoFocus />
|
||||
<button type="submit" disabled={!newRole.trim() || createRoleMutation.isPending} className="px-4 py-2 bg-violet-600 text-white rounded-lg hover:bg-violet-700 disabled:opacity-50 text-sm transition-colors">
|
||||
{createRoleMutation.isPending ? "Adding..." : "Add"}
|
||||
</button>
|
||||
<button type="button" onClick={() => { setShowAddRole(false); setNewRole(""); }} className="px-4 py-2 text-gray-600 hover:text-gray-900 text-sm">Cancel</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{identity.roles && identity.roles.length > 0 ? (
|
||||
<div className="divide-y divide-gray-100">
|
||||
{identity.roles.map((ra) => (
|
||||
<div key={ra.id} className="flex items-center justify-between py-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="inline-flex items-center px-2.5 py-1 rounded-full text-sm font-medium bg-purple-100 text-purple-800">{ra.role}</span>
|
||||
<span className="text-xs text-gray-500">Source: {ra.source}</span>
|
||||
{ra.managed && (
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-800">Managed</span>
|
||||
)}
|
||||
<span className="text-xs text-gray-400">{formatDate(ra.created)}</span>
|
||||
</div>
|
||||
{!ra.managed && (
|
||||
<button onClick={() => handleDeleteRole(ra.id, ra.role)} className="text-red-400 hover:text-red-600 p-1" title="Remove role">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-6">
|
||||
<Tag className="mx-auto h-8 w-8 text-gray-300" />
|
||||
<p className="mt-2 text-sm text-gray-500">No roles assigned</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Direct Permission Sets Section */}
|
||||
<div className="bg-white rounded-lg shadow p-6 mb-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<ShieldCheck className="w-5 h-5 text-indigo-500" />
|
||||
<h2 className="text-lg font-semibold text-gray-900">Direct Permission Sets</h2>
|
||||
<span className="text-sm text-gray-500">({identity.direct_permissions?.length || 0})</span>
|
||||
</div>
|
||||
<button onClick={() => setShowAssignPerm(!showAssignPerm)} className="inline-flex items-center gap-1 px-3 py-1.5 text-sm bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors">
|
||||
<Plus className="w-4 h-4" /> Assign Permission Set
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showAssignPerm && (
|
||||
<form onSubmit={handleAssignPermission} className="mb-4 p-3 bg-gray-50 rounded-lg space-y-2">
|
||||
{selectedPermSetRef ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 bg-indigo-100 text-indigo-800 rounded-md text-sm font-mono flex-1">
|
||||
<Shield className="w-3.5 h-3.5 flex-shrink-0" />
|
||||
<span className="truncate">{selectedPermSetRef}</span>
|
||||
<button type="button" onClick={() => { setSelectedPermSetRef(""); setPermSetSearch(""); }} className="ml-auto text-indigo-500 hover:text-indigo-700">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<button type="submit" disabled={createPermMutation.isPending} className="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50 text-sm transition-colors whitespace-nowrap">
|
||||
{createPermMutation.isPending ? "Assigning..." : "Assign"}
|
||||
</button>
|
||||
<button type="button" onClick={() => { setShowAssignPerm(false); setSelectedPermSetRef(""); setPermSetSearch(""); }} className="px-3 py-2 text-gray-600 hover:text-gray-900 text-sm">Cancel</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400 pointer-events-none" />
|
||||
<input
|
||||
type="text"
|
||||
value={permSetSearch}
|
||||
onChange={(e) => setPermSetSearch(e.target.value)}
|
||||
placeholder="Search permission sets by ref or label..."
|
||||
className="w-full pl-9 pr-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 text-sm"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<button type="button" onClick={() => { setShowAssignPerm(false); setPermSetSearch(""); }} className="px-3 py-2 text-gray-600 hover:text-gray-900 text-sm">Cancel</button>
|
||||
</div>
|
||||
{permSetSearch.trim() && (
|
||||
<div className="mt-1 bg-white border border-gray-200 rounded-lg shadow-lg max-h-52 overflow-y-auto">
|
||||
{filteredAvailablePermSets.length === 0 ? (
|
||||
<div className="px-4 py-3 text-sm text-gray-500">No matching permission sets</div>
|
||||
) : (
|
||||
filteredAvailablePermSets.map((ps) => (
|
||||
<button
|
||||
key={ps.ref}
|
||||
type="button"
|
||||
onClick={() => { setSelectedPermSetRef(ps.ref); setPermSetSearch(""); }}
|
||||
className="w-full flex items-start gap-3 px-4 py-2.5 text-left hover:bg-indigo-50 transition-colors"
|
||||
>
|
||||
<Shield className="w-4 h-4 text-indigo-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-mono font-medium text-gray-900 truncate">{ps.ref}</div>
|
||||
{ps.label && <div className="text-xs text-gray-500 truncate">{ps.label}</div>}
|
||||
</div>
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
)}
|
||||
|
||||
{identity.direct_permissions && identity.direct_permissions.length > 0 ? (
|
||||
<div className="divide-y divide-gray-100">
|
||||
{identity.direct_permissions.map((dp) => (
|
||||
<div key={dp.id} className="flex items-center justify-between py-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<Shield className="w-4 h-4 text-indigo-400" />
|
||||
<span className="text-sm font-mono font-medium text-gray-900">{dp.permission_set_ref}</span>
|
||||
<span className="text-xs text-gray-400">Assigned {formatDate(dp.created)}</span>
|
||||
</div>
|
||||
<button onClick={() => handleDeletePermission(dp.id, dp.permission_set_ref)} className="text-red-400 hover:text-red-600 p-1" title="Remove permission set">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-6">
|
||||
<ShieldCheck className="mx-auto h-8 w-8 text-gray-300" />
|
||||
<p className="mt-2 text-sm text-gray-500">No direct permission sets assigned</p>
|
||||
<p className="text-xs text-gray-400 mt-1">Permission sets can also be inherited through roles</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Attributes Section */}
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<FileJson className="w-5 h-5 text-gray-500" />
|
||||
<h2 className="text-lg font-semibold text-gray-900">Attributes</h2>
|
||||
</div>
|
||||
<pre className="bg-gray-50 border border-gray-200 rounded-lg p-4 text-sm text-gray-800 overflow-x-auto max-h-64 overflow-y-auto font-mono leading-relaxed">
|
||||
{JSON.stringify(identity.attributes, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
619
web/src/pages/access-control/PermissionSetDetailPage.tsx
Normal file
619
web/src/pages/access-control/PermissionSetDetailPage.tsx
Normal file
@@ -0,0 +1,619 @@
|
||||
import { useState } from "react";
|
||||
import { useParams, Link } from "react-router-dom";
|
||||
import {
|
||||
ArrowLeft,
|
||||
BarChart3,
|
||||
Globe,
|
||||
History,
|
||||
Key,
|
||||
MessageSquare,
|
||||
Package,
|
||||
Plus,
|
||||
Shield,
|
||||
Tag,
|
||||
Trash2,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
usePermissionSets,
|
||||
useCreatePermissionSetRoleAssignment,
|
||||
useDeletePermissionSetRoleAssignment,
|
||||
} from "@/hooks/usePermissions";
|
||||
import { navIcons } from "@/components/layout/navIcons";
|
||||
|
||||
// ── Domain interfaces ──────────────────────────────────────────────────────────
|
||||
|
||||
interface PermissionSetRoleAssignment {
|
||||
id: number;
|
||||
permission_set_id: number;
|
||||
permission_set_ref: string | null;
|
||||
role: string;
|
||||
created: string;
|
||||
}
|
||||
|
||||
interface PermissionSetWithRoles {
|
||||
id: number;
|
||||
ref: string;
|
||||
pack_ref?: string | null;
|
||||
label?: string | null;
|
||||
description?: string | null;
|
||||
grants: unknown;
|
||||
roles?: PermissionSetRoleAssignment[];
|
||||
}
|
||||
|
||||
// ── Grants model ───────────────────────────────────────────────────────────────
|
||||
|
||||
interface GrantConstraints {
|
||||
pack_refs?: string[];
|
||||
owner?: string; // "self" | "any" | "none"
|
||||
owner_types?: string[];
|
||||
owner_refs?: string[];
|
||||
visibility?: string[];
|
||||
execution_scope?: string; // "self" | "descendants" | "any"
|
||||
refs?: string[];
|
||||
ids?: number[];
|
||||
encrypted?: boolean;
|
||||
attributes?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface ParsedGrant {
|
||||
resource: string;
|
||||
actions: string[];
|
||||
constraints?: GrantConstraints;
|
||||
}
|
||||
|
||||
function parseGrants(raw: unknown): ParsedGrant[] {
|
||||
if (!Array.isArray(raw)) return [];
|
||||
return raw.filter(
|
||||
(g): g is ParsedGrant =>
|
||||
typeof g === "object" &&
|
||||
g !== null &&
|
||||
typeof (g as ParsedGrant).resource === "string" &&
|
||||
Array.isArray((g as ParsedGrant).actions),
|
||||
);
|
||||
}
|
||||
|
||||
// ── Display metadata ───────────────────────────────────────────────────────────
|
||||
|
||||
type ResourceMeta = {
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
color: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
const RESOURCE_META: Record<string, ResourceMeta> = {
|
||||
packs: { icon: navIcons.packs, color: "text-green-600", label: "Packs" },
|
||||
actions: {
|
||||
icon: navIcons.actions,
|
||||
color: "text-yellow-500",
|
||||
label: "Actions",
|
||||
},
|
||||
rules: { icon: navIcons.rules, color: "text-blue-600", label: "Rules" },
|
||||
triggers: {
|
||||
icon: navIcons.triggers,
|
||||
color: "text-orange-500",
|
||||
label: "Triggers",
|
||||
},
|
||||
executions: {
|
||||
icon: navIcons.executions,
|
||||
color: "text-purple-600",
|
||||
label: "Executions",
|
||||
},
|
||||
events: { icon: navIcons.events, color: "text-cyan-600", label: "Events" },
|
||||
enforcements: {
|
||||
icon: navIcons.enforcements,
|
||||
color: "text-red-500",
|
||||
label: "Enforcements",
|
||||
},
|
||||
inquiries: {
|
||||
icon: MessageSquare,
|
||||
color: "text-teal-600",
|
||||
label: "Inquiries",
|
||||
},
|
||||
keys: { icon: navIcons.keys, color: "text-amber-600", label: "Keys" },
|
||||
artifacts: {
|
||||
icon: navIcons.artifacts,
|
||||
color: "text-indigo-500",
|
||||
label: "Artifacts",
|
||||
},
|
||||
webhooks: { icon: Globe, color: "text-sky-600", label: "Webhooks" },
|
||||
analytics: { icon: BarChart3, color: "text-rose-500", label: "Analytics" },
|
||||
history: { icon: History, color: "text-gray-500", label: "History" },
|
||||
identities: { icon: Users, color: "text-blue-700", label: "Identities" },
|
||||
permissions: {
|
||||
icon: navIcons.accessControl,
|
||||
color: "text-indigo-600",
|
||||
label: "Permissions",
|
||||
},
|
||||
runtimes: {
|
||||
icon: navIcons.runtimes,
|
||||
color: "text-blue-600",
|
||||
label: "Runtimes",
|
||||
},
|
||||
sensors: {
|
||||
icon: navIcons.sensors,
|
||||
color: "text-purple-600",
|
||||
label: "Sensors",
|
||||
},
|
||||
};
|
||||
|
||||
const ACTION_STYLE: Record<string, string> = {
|
||||
read: "bg-slate-100 text-slate-700",
|
||||
create: "bg-emerald-100 text-emerald-800",
|
||||
update: "bg-amber-100 text-amber-800",
|
||||
delete: "bg-red-100 text-red-800",
|
||||
execute: "bg-violet-100 text-violet-800",
|
||||
cancel: "bg-orange-100 text-orange-800",
|
||||
respond: "bg-cyan-100 text-cyan-800",
|
||||
manage: "bg-indigo-100 text-indigo-800",
|
||||
};
|
||||
|
||||
// ── Constraint chips ───────────────────────────────────────────────────────────
|
||||
|
||||
function ConstraintChips({ c }: { c: GrantConstraints }) {
|
||||
const chips: React.ReactNode[] = [];
|
||||
|
||||
if (c.pack_refs?.length) {
|
||||
chips.push(
|
||||
<span
|
||||
key="pack_refs"
|
||||
className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs bg-green-50 text-green-700 border border-green-200"
|
||||
>
|
||||
<Package className="w-3 h-3 shrink-0" />
|
||||
{c.pack_refs.join(", ")}
|
||||
</span>,
|
||||
);
|
||||
}
|
||||
|
||||
if (c.owner) {
|
||||
const labels: Record<string, string> = {
|
||||
self: "Own resources",
|
||||
any: "Any owner",
|
||||
none: "No owner",
|
||||
};
|
||||
chips.push(
|
||||
<span
|
||||
key="owner"
|
||||
className="inline-flex items-center px-1.5 py-0.5 rounded text-xs bg-blue-50 text-blue-700 border border-blue-200"
|
||||
>
|
||||
Owner: {labels[c.owner] ?? c.owner}
|
||||
</span>,
|
||||
);
|
||||
}
|
||||
|
||||
if (c.owner_types?.length) {
|
||||
chips.push(
|
||||
<span
|
||||
key="owner_types"
|
||||
className="inline-flex items-center px-1.5 py-0.5 rounded text-xs bg-slate-100 text-slate-600 border border-slate-200"
|
||||
>
|
||||
Type: {c.owner_types.join(", ")}
|
||||
</span>,
|
||||
);
|
||||
}
|
||||
|
||||
if (c.owner_refs?.length) {
|
||||
chips.push(
|
||||
<span
|
||||
key="owner_refs"
|
||||
className="inline-flex items-center px-1.5 py-0.5 rounded text-xs bg-slate-100 text-slate-600 border border-slate-200 font-mono"
|
||||
>
|
||||
Owner: {c.owner_refs.join(", ")}
|
||||
</span>,
|
||||
);
|
||||
}
|
||||
|
||||
if (c.visibility?.length) {
|
||||
chips.push(
|
||||
<span
|
||||
key="visibility"
|
||||
className="inline-flex items-center px-1.5 py-0.5 rounded text-xs bg-sky-50 text-sky-700 border border-sky-200"
|
||||
>
|
||||
Visibility: {c.visibility.join(", ")}
|
||||
</span>,
|
||||
);
|
||||
}
|
||||
|
||||
if (c.execution_scope) {
|
||||
const labels: Record<string, string> = {
|
||||
self: "Own executions",
|
||||
descendants: "Own + children",
|
||||
any: "All executions",
|
||||
};
|
||||
chips.push(
|
||||
<span
|
||||
key="execution_scope"
|
||||
className="inline-flex items-center px-1.5 py-0.5 rounded text-xs bg-purple-50 text-purple-700 border border-purple-200"
|
||||
>
|
||||
Scope: {labels[c.execution_scope] ?? c.execution_scope}
|
||||
</span>,
|
||||
);
|
||||
}
|
||||
|
||||
if (c.refs?.length) {
|
||||
chips.push(
|
||||
<span
|
||||
key="refs"
|
||||
className="inline-flex items-center px-1.5 py-0.5 rounded text-xs bg-slate-100 text-slate-600 border border-slate-200 font-mono"
|
||||
>
|
||||
{c.refs.join(", ")}
|
||||
</span>,
|
||||
);
|
||||
}
|
||||
|
||||
if (c.ids?.length) {
|
||||
chips.push(
|
||||
<span
|
||||
key="ids"
|
||||
className="inline-flex items-center px-1.5 py-0.5 rounded text-xs bg-slate-100 text-slate-600 border border-slate-200"
|
||||
>
|
||||
IDs: {c.ids.join(", ")}
|
||||
</span>,
|
||||
);
|
||||
}
|
||||
|
||||
if (c.encrypted !== undefined) {
|
||||
chips.push(
|
||||
<span
|
||||
key="encrypted"
|
||||
className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs bg-amber-50 text-amber-700 border border-amber-200"
|
||||
>
|
||||
<Key className="w-3 h-3 shrink-0" />
|
||||
{c.encrypted ? "Encrypted only" : "Unencrypted only"}
|
||||
</span>,
|
||||
);
|
||||
}
|
||||
|
||||
if (c.attributes && Object.keys(c.attributes).length > 0) {
|
||||
const text = Object.entries(c.attributes)
|
||||
.map(([k, v]) => `${k} = ${JSON.stringify(v)}`)
|
||||
.join(", ");
|
||||
chips.push(
|
||||
<span
|
||||
key="attributes"
|
||||
className="inline-flex items-center px-1.5 py-0.5 rounded text-xs bg-rose-50 text-rose-700 border border-rose-200 font-mono"
|
||||
>
|
||||
{text}
|
||||
</span>,
|
||||
);
|
||||
}
|
||||
|
||||
if (chips.length === 0) {
|
||||
return <span className="text-xs text-gray-300">—</span>;
|
||||
}
|
||||
|
||||
return <div className="flex flex-col gap-1">{chips}</div>;
|
||||
}
|
||||
|
||||
// ── Grants table ───────────────────────────────────────────────────────────────
|
||||
|
||||
function GrantsView({ grants }: { grants: ParsedGrant[] }) {
|
||||
if (grants.length === 0) {
|
||||
return (
|
||||
<div className="p-8 text-center">
|
||||
<Shield className="mx-auto h-8 w-8 text-gray-300" />
|
||||
<p className="mt-2 text-sm text-gray-500">No grants defined</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const hasConstraints = grants.some(
|
||||
(g) => g.constraints && Object.keys(g.constraints).length > 0,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="overflow-y-auto max-h-[28rem]">
|
||||
<table className="min-w-full">
|
||||
<thead className="bg-gray-50 border-b border-gray-200 sticky top-0 z-10">
|
||||
<tr>
|
||||
<th className="px-4 py-2.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-36">
|
||||
Resource
|
||||
</th>
|
||||
<th className="px-4 py-2.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Permissions
|
||||
</th>
|
||||
{hasConstraints && (
|
||||
<th className="px-4 py-2.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Conditions
|
||||
</th>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100">
|
||||
{grants.map((grant, i) => {
|
||||
const meta = RESOURCE_META[grant.resource];
|
||||
const Icon = meta?.icon ?? Shield;
|
||||
const iconColor = meta?.color ?? "text-gray-400";
|
||||
const label =
|
||||
meta?.label ??
|
||||
grant.resource.charAt(0).toUpperCase() + grant.resource.slice(1);
|
||||
|
||||
return (
|
||||
<tr key={i} className="hover:bg-gray-50">
|
||||
<td className="px-4 py-2.5 whitespace-nowrap">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Icon className={`w-3.5 h-3.5 shrink-0 ${iconColor}`} />
|
||||
<span className="text-sm font-medium text-gray-800">
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td className="px-4 py-2.5">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{grant.actions.map((action) => (
|
||||
<span
|
||||
key={action}
|
||||
className={`inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium ${
|
||||
ACTION_STYLE[action] ?? "bg-gray-100 text-gray-700"
|
||||
}`}
|
||||
>
|
||||
{action}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{hasConstraints && (
|
||||
<td className="px-4 py-2.5">
|
||||
{grant.constraints &&
|
||||
Object.keys(grant.constraints).length > 0 ? (
|
||||
<ConstraintChips c={grant.constraints} />
|
||||
) : (
|
||||
<span className="text-xs text-gray-300">—</span>
|
||||
)}
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Page ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
export default function PermissionSetDetailPage() {
|
||||
const { ref } = useParams<{ ref: string }>();
|
||||
|
||||
const { data: permissionSetsRaw, isLoading, error } = usePermissionSets();
|
||||
const createRoleAssignment = useCreatePermissionSetRoleAssignment();
|
||||
const deleteRoleAssignment = useDeletePermissionSetRoleAssignment();
|
||||
|
||||
const [newRole, setNewRole] = useState("");
|
||||
const [showAddRole, setShowAddRole] = useState(false);
|
||||
|
||||
const permissionSets = permissionSetsRaw as
|
||||
| PermissionSetWithRoles[]
|
||||
| undefined;
|
||||
const permissionSet = permissionSets?.find((ps) => ps.ref === ref);
|
||||
|
||||
const handleAddRole = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!newRole.trim()) return;
|
||||
try {
|
||||
await createRoleAssignment.mutateAsync({
|
||||
permissionSetId: permissionSet!.id,
|
||||
role: newRole.trim(),
|
||||
});
|
||||
setNewRole("");
|
||||
setShowAddRole(false);
|
||||
} catch (err) {
|
||||
console.error("Failed to add role:", err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteRole = async (assignmentId: number, roleName: string) => {
|
||||
if (window.confirm(`Remove role "${roleName}" from this permission set?`)) {
|
||||
try {
|
||||
await deleteRoleAssignment.mutateAsync(assignmentId);
|
||||
} catch (err) {
|
||||
console.error("Failed to delete role assignment:", err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
|
||||
<p className="mt-3 text-sm text-gray-500">
|
||||
Loading permission set…
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !permissionSet) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<Link
|
||||
to="/access-control?tab=permission-sets"
|
||||
className="inline-flex items-center gap-1 text-sm text-blue-600 hover:text-blue-800 mb-6"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
Back to Access Control
|
||||
</Link>
|
||||
<div className="bg-white rounded-lg shadow p-12 text-center">
|
||||
<Shield className="mx-auto h-12 w-12 text-gray-400" />
|
||||
<p className="mt-4 text-red-600">
|
||||
{error
|
||||
? "Failed to load permission set"
|
||||
: "Permission set not found"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const roles = permissionSet.roles || [];
|
||||
const parsedGrants = parseGrants(permissionSet.grants);
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
{/* Back link */}
|
||||
<Link
|
||||
to="/access-control?tab=permission-sets"
|
||||
className="inline-flex items-center gap-1 text-sm text-blue-600 hover:text-blue-800 mb-6"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
Back to Access Control
|
||||
</Link>
|
||||
|
||||
{/* Header */}
|
||||
<div className="bg-white rounded-lg shadow p-6 mb-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex-shrink-0 w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center">
|
||||
<Shield className="w-6 h-6 text-indigo-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 font-mono">
|
||||
{permissionSet.ref}
|
||||
</h1>
|
||||
{permissionSet.label && (
|
||||
<p className="text-lg text-gray-700 mt-0.5">
|
||||
{permissionSet.label}
|
||||
</p>
|
||||
)}
|
||||
{permissionSet.description && (
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
{permissionSet.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{permissionSet.pack_ref && (
|
||||
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-sm font-medium bg-green-100 text-green-800">
|
||||
<Package className="w-3.5 h-3.5" />
|
||||
{permissionSet.pack_ref}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Roles Section */}
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Tag className="w-5 h-5 text-gray-500" />
|
||||
<h2 className="text-lg font-semibold text-gray-900">
|
||||
Role Assignments
|
||||
</h2>
|
||||
<span className="text-sm text-gray-500">({roles.length})</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowAddRole(!showAddRole)}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-blue-600 hover:text-blue-800 hover:bg-blue-50 rounded-md transition-colors"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Add Role
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showAddRole && (
|
||||
<form
|
||||
onSubmit={handleAddRole}
|
||||
className="px-6 py-3 bg-blue-50 border-b border-blue-100 flex items-center gap-3"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={newRole}
|
||||
onChange={(e) => setNewRole(e.target.value)}
|
||||
placeholder="Enter role name…"
|
||||
className="flex-1 px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
autoFocus
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!newRole.trim() || createRoleAssignment.isPending}
|
||||
className="px-3 py-1.5 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{createRoleAssignment.isPending ? "Adding…" : "Add"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShowAddRole(false);
|
||||
setNewRole("");
|
||||
}}
|
||||
className="px-3 py-1.5 text-sm text-gray-600 hover:text-gray-900"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{createRoleAssignment.isError && (
|
||||
<div className="px-6 py-2 bg-red-50 border-b border-red-100">
|
||||
<p className="text-sm text-red-600">
|
||||
Failed to add role.{" "}
|
||||
{createRoleAssignment.error instanceof Error
|
||||
? createRoleAssignment.error.message
|
||||
: "Please try again."}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="divide-y divide-gray-100">
|
||||
{roles.length === 0 ? (
|
||||
<div className="px-6 py-8 text-center">
|
||||
<Tag className="mx-auto h-8 w-8 text-gray-300" />
|
||||
<p className="mt-2 text-sm text-gray-500">
|
||||
No roles assigned to this permission set
|
||||
</p>
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
Identities with a matching role will inherit these grants
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
roles.map((assignment) => (
|
||||
<div
|
||||
key={assignment.id}
|
||||
className="px-6 py-3 flex items-center justify-between hover:bg-gray-50"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="inline-flex items-center px-2.5 py-1 rounded-full text-sm font-medium bg-purple-100 text-purple-800">
|
||||
{assignment.role}
|
||||
</span>
|
||||
<span className="text-xs text-gray-400">
|
||||
Added {new Date(assignment.created).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() =>
|
||||
handleDeleteRole(assignment.id, assignment.role)
|
||||
}
|
||||
className="text-red-400 hover:text-red-600 p-1 rounded transition-colors"
|
||||
title="Remove role assignment"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Grants Section */}
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="px-6 py-4 border-b border-gray-200 flex items-center gap-2">
|
||||
<Shield className="w-5 h-5 text-gray-500" />
|
||||
<h2 className="text-lg font-semibold text-gray-900">Grants</h2>
|
||||
<span className="text-sm text-gray-500">
|
||||
({parsedGrants.length})
|
||||
</span>
|
||||
</div>
|
||||
<GrantsView grants={parsedGrants} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user