diff --git a/package-lock.json b/package-lock.json index 51cf5208..1d8b59de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,8 +8,7 @@ "name": "@layer5/sistent", "version": "0.14.11", "dependencies": { - "lodash": "^4.17.21", - "@types/lodash": "^4.17.7" + "lodash": "^4.17.21" }, "devDependencies": { "@commitlint/cli": "^17.7.2", @@ -3161,7 +3160,8 @@ "node_modules/@types/lodash": { "version": "4.17.7", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", - "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==" + "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==", + "dev": true }, "node_modules/@types/mdast": { "version": "3.0.15", @@ -9299,8 +9299,7 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "devOptional": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash.assignwith": { "version": "4.2.0", @@ -16410,7 +16409,8 @@ "@types/lodash": { "version": "4.17.7", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", - "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==" + "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==", + "dev": true }, "@types/mdast": { "version": "3.0.15", @@ -20639,8 +20639,7 @@ "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "devOptional": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "lodash.assignwith": { "version": "4.2.0", diff --git a/src/actors/eventBus.ts b/src/actors/eventBus.ts new file mode 100644 index 00000000..ecc6f494 --- /dev/null +++ b/src/actors/eventBus.ts @@ -0,0 +1,42 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Observable, Subject, filter } from 'rxjs'; + +// Generic Event interface +export interface EventBusEvent { + type: Type; + data: Data; +} + +// Helper type to extract event types from a union +export type EventType = T extends EventBusEvent ? Type : never; + +// Helper type to extract payload type for a given event type +export type DataType = T extends EventBusEvent + ? Data + : never; + +// Generic EventBus class +export class EventBus> { + private eventSubject: Subject; + private eventObservable: Observable; + + constructor() { + this.eventSubject = new Subject(); + this.eventObservable = this.eventSubject.asObservable(); + } + + // Method to publish an event + publish(event: E): void { + this.eventSubject.next(event); + } + + // Method to subscribe to a specific event type + on(eventType: EventType): Observable { + return this.eventObservable.pipe(filter((event): event is T => event.type === eventType)); + } + + // Method to subscribe to all events + onAny(): Observable { + return this.eventObservable; + } +} diff --git a/src/actors/index.ts b/src/actors/index.ts index 2923105d..a366767d 100644 --- a/src/actors/index.ts +++ b/src/actors/index.ts @@ -37,3 +37,5 @@ export { sendToActor, sendToActors } from './utils'; + +export * from './eventBus'; diff --git a/src/custom/index.tsx b/src/custom/index.tsx index 65885946..b7734382 100644 --- a/src/custom/index.tsx +++ b/src/custom/index.tsx @@ -122,3 +122,4 @@ export type { }; export * from './Dialog'; +export * from './permissions'; diff --git a/src/custom/permissions.tsx b/src/custom/permissions.tsx new file mode 100644 index 00000000..0951708d --- /dev/null +++ b/src/custom/permissions.tsx @@ -0,0 +1,110 @@ +// import { CAN, getCapabilitiesRegistry, getMesheryEventBus } from '@/globals/mesherySdk'; +import React from 'react'; +import { EventBus } from '../actors/eventBus'; + +export interface Key { + subject: string; + action: string; +} + +export type InvertAction = 'disable' | 'hide'; + +export type MissingPermissionReason = { + type: 'MISSING_PERMISSION'; + data: { + keyId: string; + }; +}; + +export type MissingCapabilityReason = { + type: 'MISSING_CAPABILITY'; + data: { + capabilityId: string; + }; +}; + +export type ReasonEvent = MissingPermissionReason | MissingCapabilityReason; + +export interface HasKeyProps { + Key?: Key; + predicate?: (capabilitiesRegistry: unknown) => [boolean, ReasonEvent]; // returns a boolean and an event if the user does not have the permission + children: React.ReactNode; + notifyOnclick?: boolean; + invert_action?: InvertAction[]; +} + +// returns the children if the user has the permission to view the component or if a key is not provided +// if the user does not have the permission to view the component, it will return null or a disabled version of the component specified by the invert_action prop +export const createCanShow = ( + getCapabilitiesRegistry = () => {}, + CAN: (action: string, subject: string) => boolean, + eventBus: () => EventBus +) => { + return ({ + Key, + children, + notifyOnclick = true, + predicate, + invert_action = ['disable'] + }: HasKeyProps) => { + if (!children) { + return null; + } + + const hasKey = Key?.subject ? CAN(Key?.action, Key?.subject) : true; + const predicateRes = predicate && predicate(getCapabilitiesRegistry()); + + const can = predicateRes ? predicateRes[0] && hasKey : hasKey; + + const reason = predicateRes?.[1] || { + type: 'MISSING_PERMISSION', + data: { + keyId: Key?.action as string + } + }; + + if (can) { + return children; + } + + if (invert_action.includes('hide')) { + return null; + } + + const pointerEvents = notifyOnclick ? 'auto' : 'none'; + + const onClick = notifyOnclick + ? (e: React.MouseEvent) => { + e.stopPropagation(); + console.log('cant perform action : reason', reason, eventBus); + const mesheryEventBus = eventBus(); + mesheryEventBus.publish(reason); + } + : () => {}; + + const opacity = invert_action.includes('disable') ? 0.5 : 1; + + return ( +
+ {React.cloneElement(children as React.ReactElement, { + style: { + ...((children as React.ReactElement).props.style as React.CSSProperties), + cursor: 'pointer', + pointerEvents, + opacity: opacity + }, + onClick: onClick + })} +
+ ); + + return null; + }; +};