diff --git a/package-lock.json b/package-lock.json
index 5fa99c1e..2f2cf6c9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,7 +16,7 @@
"@gliff-ai/curate": "^7.6.2",
"@gliff-ai/etebase": "^0.44.0",
"@gliff-ai/manage": "^8.0.0",
- "@gliff-ai/style": "^16.2.1",
+ "@gliff-ai/style": "^16.3.1",
"@gliff-ai/upload": "^1.3.0",
"@material-ui/types": "^5.1.0",
"@mui/icons-material": "^5.8.4",
@@ -2155,9 +2155,9 @@
"integrity": "sha512-3l1S4fzDBR+uXkSl8PBKxj3zG4Rjgqolu/fRGoZyAqcgkDZ0AO66fi+gLDBherKuIKzJ+X7e4s1/oYzh7B4keg=="
},
"node_modules/@gliff-ai/style": {
- "version": "16.2.1",
- "resolved": "https://registry.npmjs.org/@gliff-ai/style/-/style-16.2.1.tgz",
- "integrity": "sha512-oCfi7M6JQ7qnKhI6PMtJfNtaZtQJEu/HD4TPt/q77s3HjMulBcLpFnaz02hPTpqwwtS1/aYv1LdMbKL5B6NaxQ==",
+ "version": "16.3.1",
+ "resolved": "https://registry.npmjs.org/@gliff-ai/style/-/style-16.3.1.tgz",
+ "integrity": "sha512-1gN18JpUs1d50EKK2XHj/rlg/U8Vs0BG2VHZkW57U/ZLQ6I5JPdp84ipORyeXM5ldOjrbqBvuqAyVSAes4wXGg==",
"dependencies": {
"react-inlinesvg": "^2.3.0"
},
@@ -15520,9 +15520,9 @@
"integrity": "sha512-3l1S4fzDBR+uXkSl8PBKxj3zG4Rjgqolu/fRGoZyAqcgkDZ0AO66fi+gLDBherKuIKzJ+X7e4s1/oYzh7B4keg=="
},
"@gliff-ai/style": {
- "version": "16.2.1",
- "resolved": "https://registry.npmjs.org/@gliff-ai/style/-/style-16.2.1.tgz",
- "integrity": "sha512-oCfi7M6JQ7qnKhI6PMtJfNtaZtQJEu/HD4TPt/q77s3HjMulBcLpFnaz02hPTpqwwtS1/aYv1LdMbKL5B6NaxQ==",
+ "version": "16.3.1",
+ "resolved": "https://registry.npmjs.org/@gliff-ai/style/-/style-16.3.1.tgz",
+ "integrity": "sha512-1gN18JpUs1d50EKK2XHj/rlg/U8Vs0BG2VHZkW57U/ZLQ6I5JPdp84ipORyeXM5ldOjrbqBvuqAyVSAes4wXGg==",
"requires": {
"react-inlinesvg": "^2.3.0"
}
diff --git a/package.json b/package.json
index 3905f841..0e8f77de 100644
--- a/package.json
+++ b/package.json
@@ -48,7 +48,7 @@
"@gliff-ai/curate": "^7.6.2",
"@gliff-ai/etebase": "^0.44.0",
"@gliff-ai/manage": "^8.0.0",
- "@gliff-ai/style": "^16.2.1",
+ "@gliff-ai/style": "^16.3.1",
"@gliff-ai/upload": "^1.3.0",
"@material-ui/types": "^5.1.0",
"@mui/icons-material": "^5.8.4",
diff --git a/src/components/NavBar.tsx b/src/components/NavBar.tsx
index 3dadeabb..86022202 100644
--- a/src/components/NavBar.tsx
+++ b/src/components/NavBar.tsx
@@ -15,8 +15,8 @@ import {
Typography,
} from "@gliff-ai/style";
import { imgSrc } from "@/imgSrc";
-
import { useAuth } from "@/hooks/use-auth";
+import { getInitialsFromFullname } from "@/helpers";
import { ProductsNavbar, ProductNavbarData } from "@/components";
const documentButton = {
@@ -147,10 +147,8 @@ export const NavBar = (props: Props): ReactElement | null => {
useEffect(() => {
if (!auth?.userProfile?.name) return;
- const initials = auth?.userProfile?.name
- .split(" ")
- .map((l) => l[0].toUpperCase())
- .join("");
+
+ const initials = getInitialsFromFullname(auth?.userProfile?.name);
setUserInitials(initials);
}, [auth]);
diff --git a/src/components/index.ts b/src/components/index.ts
index 3fef5d07..60b9bad2 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -10,5 +10,6 @@ export {
export { GliffCard } from "./GliffCard";
export { CookieConsent } from "./CookieConsent";
+export { ZooDialog } from "./zoo/ZooDialog";
export { ProductsNavbar } from "./ProductsNavbar";
export type { ProductNavbarData } from "./ProductsNavbar";
diff --git a/src/components/zoo/PluginsZooCard.tsx b/src/components/zoo/PluginsZooCard.tsx
new file mode 100644
index 00000000..4584ea52
--- /dev/null
+++ b/src/components/zoo/PluginsZooCard.tsx
@@ -0,0 +1,147 @@
+import { ReactElement, useMemo } from "react";
+import { Plugin } from "@gliff-ai/manage";
+import { Divider, icons, MuiCard, Box, theme, Button } from "@gliff-ai/style";
+import { DialogActions } from "@mui/material";
+import SVG from "react-inlinesvg";
+
+const InconText = ({
+ icon,
+ text,
+ marginLeft = null,
+}: {
+ icon: string;
+ text: string;
+ marginLeft?: string | null;
+}) => (
+
+
+ {text}
+
+);
+
+InconText.defaultProps = { marginLeft: null };
+
+interface Props {
+ data: Plugin;
+ isOpen: boolean;
+ openCard: () => void;
+ closeCard: () => void;
+ activatePlugin: (plugin: Plugin) => Promise;
+}
+
+export const PluginsZooCard = ({
+ data,
+ isOpen,
+ openCard,
+ closeCard,
+ activatePlugin,
+}: Props): ReactElement => {
+ const scale = useMemo((): number => Number(isOpen) + 1, [isOpen]);
+
+ return (
+
+
+
+
+
Plugin: {data.name}
+
+ {data?.author}
+
+
+
+ {!isOpen && (
+
+ {data.description}
+
+ )}
+
+
+
+
+
+ {isOpen && (
+ {data.description}
+ )}
+
+
+
+
+
+ );
+};
diff --git a/src/components/zoo/ZooDialog.tsx b/src/components/zoo/ZooDialog.tsx
new file mode 100644
index 00000000..d5c8d235
--- /dev/null
+++ b/src/components/zoo/ZooDialog.tsx
@@ -0,0 +1,184 @@
+import { ReactElement, useMemo, useState } from "react";
+import {
+ Dialogue,
+ Grid,
+ IconButton,
+ icons,
+ MuiCard,
+ theme,
+} from "@gliff-ai/style";
+import { Plugin } from "@gliff-ai/manage";
+import {
+ SortPopover,
+ Filters,
+ getLabelsFromKeys,
+ SearchBar,
+ SearchFilterCard,
+} from "@gliff-ai/curate";
+import { PluginsZooCard } from "./PluginsZooCard";
+import { useZooData, ExtendedPlugin } from "@/hooks";
+
+const PLUGINS_KEYLABELS_MAP = {
+ name: "Name",
+ type: "Type",
+ description: "Description",
+ author: "Author",
+ security: "Security",
+ architecture: "Architecture",
+ url: "URL",
+ enabled: "Enabled",
+ is_public: "Public",
+ collection_uids: "Project UIDs",
+ products: "Products",
+};
+
+const EXCLUDED_KEYS = ["public_key", "encrypted_access_key", "username"];
+
+export enum ActiveSection {
+ plugins,
+ datasets,
+}
+
+interface Props {
+ rerender?: number;
+ activatePlugin: (plugin: Plugin) => Promise;
+}
+
+export function ZooDialog({
+ rerender,
+ activatePlugin,
+}: Props): ReactElement | null {
+ const [openCard, setOpenCard] = useState(null);
+ const [activeSection, setActiveSection] = useState(
+ ActiveSection.plugins
+ );
+ const zoo = useZooData({ activeSection, rerender });
+
+ const filters = useMemo(() => new Filters(), [activeSection]);
+
+ if (!zoo) return null;
+
+ return (
+
+ }
+ backgroundColor={theme.palette.background.default}
+ >
+
+
+
+
+ setActiveSection(ActiveSection.plugins)}
+ />
+ setActiveSection(ActiveSection.datasets)}
+ />
+
+
+
+
+
+
+
+
+
+
+
+ {(
+ zoo.data.filter(
+ ({ filterShow }) => filterShow
+ ) as ExtendedPlugin[]
+ ).map(
+ (item) =>
+ (!openCard || openCard === item.name) && (
+
+ {activeSection === ActiveSection.plugins ? (
+ setOpenCard(item.name)}
+ closeCard={() => setOpenCard(null)}
+ activatePlugin={activatePlugin}
+ />
+ ) : null}
+
+ )
+ )}
+
+
+
+
+ );
+}
+
+ZooDialog.defaultProps = {
+ rerender: null,
+};
diff --git a/src/crypto/SealedCryptoBox.ts b/src/crypto/SealedCryptoBox.ts
new file mode 100644
index 00000000..d390dc53
--- /dev/null
+++ b/src/crypto/SealedCryptoBox.ts
@@ -0,0 +1,69 @@
+// source: https://libsodium.gitbook.io/doc/public-key_cryptography/sealed_boxes
+import * as sodium from "libsodium-wrappers";
+
+export class SealedCryptoBox {
+ private keypair: sodium.KeyPair;
+
+ constructor(keypair: sodium.KeyPair) {
+ this.keypair = keypair;
+ }
+
+ public static keygen(seed?: Uint8Array) {
+ if (seed) {
+ return new this(sodium.crypto_box_seed_keypair(seed));
+ }
+ return new this(sodium.crypto_box_keypair());
+ }
+
+ public static fromPrivkey(privkey: Uint8Array) {
+ return new this({
+ keyType: "x25519",
+ privateKey: privkey,
+ publicKey: sodium.crypto_scalarmult_base(privkey),
+ });
+ }
+
+ public static to_uint8array(value: string | Uint8Array): Uint8Array {
+ return typeof value === "string"
+ ? sodium.from_base64(value, sodium.base64_variants.URLSAFE)
+ : value;
+ }
+
+ public static encrypt(
+ message: Uint8Array | string,
+ publicKey: Uint8Array | string
+ ): string {
+ const encodedMessage = sodium.crypto_box_seal(
+ message,
+ typeof publicKey === "string" ? this.to_uint8array(publicKey) : publicKey
+ );
+ return sodium.to_base64(encodedMessage, sodium.base64_variants.URLSAFE);
+ }
+
+ public static decrypt(
+ cipher: Uint8Array | string,
+ publicKey: Uint8Array | string,
+ privateKey: Uint8Array | string
+ ): string {
+ const decodedMessage = sodium.crypto_box_seal_open(
+ cipher,
+ this.to_uint8array(publicKey),
+ this.to_uint8array(privateKey)
+ );
+ return sodium.to_base64(decodedMessage, sodium.base64_variants.URLSAFE);
+ }
+
+ public get publicKey() {
+ return sodium.to_base64(
+ this.keypair.publicKey,
+ sodium.base64_variants.URLSAFE
+ );
+ }
+
+ public get privateKey() {
+ return sodium.to_base64(
+ this.keypair.privateKey,
+ sodium.base64_variants.URLSAFE
+ );
+ }
+}
diff --git a/src/helpers.ts b/src/helpers.ts
index b34b6fbc..120c5ac4 100644
--- a/src/helpers.ts
+++ b/src/helpers.ts
@@ -150,3 +150,10 @@ export function convertMetadataToGalleryTiles(
});
return newTiles;
}
+
+export const getInitialsFromFullname = (fullname: string): string =>
+ fullname
+ .trim()
+ .split(" ")
+ .map((l) => l[0].toUpperCase())
+ .join("");
diff --git a/src/hooks/index.ts b/src/hooks/index.ts
index 96c26146..2a241439 100644
--- a/src/hooks/index.ts
+++ b/src/hooks/index.ts
@@ -3,3 +3,5 @@ export { useAuth } from "@/hooks/use-auth";
export { useStore } from "@/hooks/use-store";
export { usePlugins } from "@/hooks/use-plugins";
export { useMountEffect } from "@/hooks/use-mountEffect";
+export { useZooData } from "@/hooks/use-zoo-data";
+export type { ExtendedPlugin, ExtendedDataset } from "@/hooks/use-zoo-data";
diff --git a/src/hooks/use-plugins.tsx b/src/hooks/use-plugins.tsx
index 726098e6..45a3dc4a 100644
--- a/src/hooks/use-plugins.tsx
+++ b/src/hooks/use-plugins.tsx
@@ -1,12 +1,14 @@
import { useEffect, useState } from "react";
-import { initPluginObjects, PluginObject, Product } from "@/plugins";
+import { Product } from "@gliff-ai/manage";
+import { initPluginObjects, PluginObject } from "@/plugins";
import { AuthContext } from "./use-auth";
export function usePlugins(
collectionUid: string,
auth: AuthContext | null,
- product: Product
-) {
+ product: Product,
+ rerender?: number
+): PluginObject | null {
const [plugins, setPlugins] = useState(null);
useEffect(() => {
@@ -15,7 +17,7 @@ export function usePlugins(
void initPluginObjects(product, collectionUid, auth.user.username).then(
setPlugins
);
- }, [auth?.user?.username, collectionUid]);
+ }, [auth?.user?.username, collectionUid, product, rerender]);
return plugins;
}
diff --git a/src/hooks/use-zoo-data.tsx b/src/hooks/use-zoo-data.tsx
new file mode 100644
index 00000000..3251f9c6
--- /dev/null
+++ b/src/hooks/use-zoo-data.tsx
@@ -0,0 +1,67 @@
+import { useEffect, useState } from "react";
+import type { Plugin } from "@gliff-ai/manage";
+import { FilterData, FilterDataItem } from "@gliff-ai/curate";
+import { getZooPlugins } from "@/services/plugins";
+import { ActiveSection } from "../components/zoo/ZooDialog";
+
+interface Output {
+ data: FilterData;
+ updateData: (func: (data: FilterData) => FilterData) => void;
+}
+
+// TODO: fix this interface when working on datasets
+interface Dataset {
+ name?: string;
+ url?: string;
+}
+
+interface Props {
+ activeSection: number;
+ rerender?: number;
+}
+
+export interface ExtendedPlugin extends Plugin, FilterDataItem {}
+export interface ExtendedDataset extends Dataset, FilterDataItem {}
+
+export function useZooData({ activeSection, rerender }: Props): Output | null {
+ const [data, setData] =
+ useState(null);
+
+ const addFilterDataKeys = (
+ _data: Plugin[] | Dataset[]
+ ): ExtendedPlugin[] | ExtendedDataset[] =>
+ _data.map((d: Plugin | Dataset) => ({
+ ...d,
+ filterShow: true,
+ newGroup: false,
+ }));
+
+ useEffect(() => {
+ // work with plugins
+ if (activeSection === ActiveSection.plugins) {
+ void getZooPlugins().then((newPlugins) => {
+ const plugins = newPlugins.filter((p) => p.is_public);
+ setData(addFilterDataKeys(plugins));
+ });
+ return;
+ }
+ // work with datasets
+ const dataset: Dataset[] = []; // TODO: fetch list of datasets from STORE
+ setData(addFilterDataKeys(dataset));
+ }, [rerender, activeSection]);
+
+ const updateData = (func: (data: FilterData) => FilterData): void => {
+ setData((prevData) => func(prevData as FilterData));
+ };
+
+ return data
+ ? {
+ data,
+ updateData,
+ }
+ : null;
+}
+
+useZooData.defaultProps = {
+ rerender: 0,
+};
diff --git a/src/plugins/index.ts b/src/plugins/index.ts
index 10e6b51e..755c94ce 100644
--- a/src/plugins/index.ts
+++ b/src/plugins/index.ts
@@ -1,29 +1,30 @@
-import { jsPluginsAPI } from "@/services/plugins";
-import { trustedServicesAPI } from "@/services/trustedServices";
-import { Plugin, Product, PluginType, PluginObject } from "./interfaces";
-
+import { Product, PluginWithExtra, Plugin, PluginType } from "@gliff-ai/manage";
+import { pluginsAPI } from "@/services/plugins";
+import { PluginObject } from "./interfaces";
import { initJsPluginObjects } from "./jsPlugin";
-
import { initTrustedServiceObjects } from "./trustedService";
-async function getPlugins(
+async function getActivePlugins(
currentProduct: Product,
collectionUid: string
): Promise {
// Get plugins data from STORE
try {
- const newPlugins = (
- (await trustedServicesAPI.getTrustedService()).map((p) => ({
+ const newPlugins = await pluginsAPI.getPlugins();
+
+ return (
+ newPlugins.map((p: PluginWithExtra) => ({
...p,
- collection_uids: p.collection_uids.map(({ uid }) => uid),
+ collection_uids:
+ p.type !== PluginType.Javascript
+ ? p.collection_uids.map(({ uid }) => uid)
+ : p.collection_uids,
})) as Plugin[]
- ).concat((await jsPluginsAPI.getPlugins()) as Plugin[]);
-
- return newPlugins.filter(
- ({ collection_uids, products, enabled }) =>
- collection_uids.includes(collectionUid) &&
- (products === currentProduct || products === Product.ALL) &&
- enabled
+ ).filter(
+ (p) =>
+ p.collection_uids.includes(collectionUid) &&
+ (p.products === currentProduct || p.products === Product.ALL) &&
+ p.enabled
);
} catch (e) {
console.error(e);
@@ -37,10 +38,8 @@ async function initPluginObjects(
user_username: string
): Promise {
// Initialise plugin objects
-
try {
- const plugins = await getPlugins(currentProduct, collectionUid);
-
+ const plugins = await getActivePlugins(currentProduct, collectionUid);
if (plugins) {
const jsPlugins = await initJsPluginObjects(plugins);
const trustedServices = await initTrustedServiceObjects(
@@ -56,5 +55,5 @@ async function initPluginObjects(
return null;
}
-export { getPlugins, initPluginObjects, Product, PluginType };
-export type { Plugin, PluginObject };
+export { initPluginObjects };
+export type { PluginObject };
diff --git a/src/plugins/interfaces.ts b/src/plugins/interfaces.ts
index 199d3db5..1e6496b3 100644
--- a/src/plugins/interfaces.ts
+++ b/src/plugins/interfaces.ts
@@ -1,33 +1,4 @@
import { MetaItem } from "@/interfaces";
-import { CollectionUidsWithExtra } from "@/services/trustedServices/interfaces";
-
-// NOTE: Product, PluginType and Plugin are also defined in MANAGE
-
-enum Product {
- "CURATE" = "CURATE",
- "ANNOTATE" = "ANNOTATE",
- "ALL" = "ALL",
-}
-
-enum PluginType {
- "Javascript" = "Javascript",
- "Python" = "Python",
- "AI" = "AI",
-}
-
-interface Plugin {
- username?: string; // trusted-service username (i.e., email address)
- name: string; // plugin name
- type: PluginType;
- url: string; // base_url for trusted-services and url for plugins
- products: Product;
- enabled: boolean;
- collection_uids: string[];
-}
-
-interface PluginWithExtra extends Omit {
- collection_uids: CollectionUidsWithExtra[];
-}
interface PluginElement {
type?: string; // added by DOMINATE, not by the plugin's creator
@@ -41,7 +12,7 @@ type PluginObject = { [name: string]: PluginElement[] };
interface PluginDataIn {
usernames?: { plugin: string; user: string };
collectionUid?: string;
- imageUid?: string;
+ imageUids?: string[];
metadata?: MetaItem[] | null;
}
@@ -51,12 +22,4 @@ interface PluginDataOut {
domElement?: JSX.Element | null;
}
-export { Product, PluginType };
-export type {
- Plugin,
- PluginWithExtra,
- PluginObject,
- PluginDataIn,
- PluginDataOut,
- PluginElement,
-};
+export type { PluginObject, PluginDataIn, PluginDataOut, PluginElement };
diff --git a/src/plugins/jsPlugin/index.ts b/src/plugins/jsPlugin/index.ts
index 9b453dbd..7568cf9f 100644
--- a/src/plugins/jsPlugin/index.ts
+++ b/src/plugins/jsPlugin/index.ts
@@ -1,6 +1,7 @@
+import { Plugin, PluginType } from "@gliff-ai/manage";
import { IPluginConstructor, IObjectKeys } from "./interfaces";
import { SamplePlugin } from "./example/SamplePlugin";
-import { Plugin, PluginType, PluginElement } from "@/plugins/interfaces";
+import { PluginElement } from "@/plugins/interfaces";
const builtinPlugins: IObjectKeys = { SamplePlugin };
diff --git a/src/plugins/trustedService/TrustedServiceClass.tsx b/src/plugins/trustedService/TrustedServiceClass.tsx
index bfc3e6f0..8a1c6637 100644
--- a/src/plugins/trustedService/TrustedServiceClass.tsx
+++ b/src/plugins/trustedService/TrustedServiceClass.tsx
@@ -8,33 +8,38 @@ class TrustedServiceClass implements PluginElement {
private baseUrl: string;
- private apiEndpoint: string;
-
private username: { plugin: string; user: string };
+ private encryptedAccessKey: string;
+
tooltip: string;
constructor(
type: string,
name: string,
baseUrl: string,
- apiEndpoint: string,
tooltip: string,
- username: { plugin: string; user: string }
+ username: { plugin: string; user: string },
+ encryptedAccessKey: string
) {
this.type = type;
this.name = name;
this.baseUrl = baseUrl;
- this.apiEndpoint = apiEndpoint;
this.tooltip = tooltip;
this.username = username;
+ this.encryptedAccessKey = encryptedAccessKey;
}
onClick = async (data: PluginDataIn): Promise => {
+ const requestBody = {
+ ...data,
+ username: this.username,
+ encrypted_access_key: this.encryptedAccessKey,
+ };
const response = await apiRequest(
- this.apiEndpoint,
+ "/run/", // trusted-service API endpoint
"POST",
- { ...data, username: this.username },
+ requestBody,
this.baseUrl
);
return response;
diff --git a/src/plugins/trustedService/index.ts b/src/plugins/trustedService/index.ts
index a9011d0a..1d0c9586 100644
--- a/src/plugins/trustedService/index.ts
+++ b/src/plugins/trustedService/index.ts
@@ -1,34 +1,48 @@
import Ajv from "ajv";
-import { Plugin, PluginType, PluginElement } from "@/plugins/interfaces";
+import { Plugin, PluginType } from "@gliff-ai/manage";
+import { apiRequest } from "@/api";
+import { PluginElement } from "@/plugins/interfaces";
import { TrustedServiceClass } from "./TrustedServiceClass";
import { UiTemplateSchema } from "./schemas";
-import { trustedServicesAPI } from "@/services/trustedServices";
-import { UiTemplate } from "@/services/trustedServices/interfaces";
+import { UiTemplate } from "./interfaces";
+import { SealedCryptoBox } from "@/crypto/SealedCryptoBox";
+
+const getUiTemplate = (apiUrl: string): Promise =>
+ apiRequest("/ui-template/", "POST", {}, apiUrl);
function unpackUiElements(
- { type, username, name, url: baseUrl }: Plugin,
+ plugin: Plugin,
template: UiTemplate,
- user_username: string
+ username: string
): PluginElement[] {
- return template.uiElements.map(
- ({ apiEndpoint, uiParams }) =>
- new TrustedServiceClass(
- type,
- name,
- baseUrl,
- apiEndpoint,
- uiParams.tooltip,
- {
- plugin: username as string,
- user: user_username,
- }
- )
- );
+ // NOTE: having an array will make sense again once we introduce the toolbar.
+
+ const usernames = {
+ plugin: SealedCryptoBox.encrypt(
+ plugin.username as string,
+ plugin.public_key as string
+ ),
+ user: SealedCryptoBox.encrypt(
+ username as string,
+ plugin.public_key as string
+ ),
+ };
+
+ return [
+ new TrustedServiceClass(
+ plugin.type,
+ plugin.name,
+ plugin.url,
+ template.ui.button.tooltip,
+ usernames,
+ plugin.encrypted_access_key as string
+ ),
+ ];
}
async function initTrustedServiceObjects(
plugins: Plugin[],
- user_username: string
+ username: string
): Promise<{ [name: string]: PluginElement[] }> {
// prepare for validating JSON file
const ajv = new Ajv();
@@ -43,7 +57,7 @@ async function initTrustedServiceObjects(
// get UI template store as JSON file
return {
plugin,
- template: await trustedServicesAPI.getUiTemplate(plugin.url),
+ template: await getUiTemplate(plugin.url),
};
})
);
@@ -58,7 +72,7 @@ async function initTrustedServiceObjects(
trustedServices[plugin.name] = unpackUiElements(
plugin,
template,
- user_username
+ username
);
} else {
console.error(
diff --git a/src/plugins/trustedService/interfaces.ts b/src/plugins/trustedService/interfaces.ts
new file mode 100644
index 00000000..f92844da
--- /dev/null
+++ b/src/plugins/trustedService/interfaces.ts
@@ -0,0 +1,7 @@
+interface UiTemplate {
+ ui: {
+ button: { name?: string; tooltip: string };
+ };
+}
+
+export { UiTemplate };
diff --git a/src/plugins/trustedService/schemas.ts b/src/plugins/trustedService/schemas.ts
index 7aab3d03..2edd0dc8 100644
--- a/src/plugins/trustedService/schemas.ts
+++ b/src/plugins/trustedService/schemas.ts
@@ -1,38 +1,23 @@
import { JSONSchemaType } from "ajv";
-import { UiTemplate } from "../../services/trustedServices/interfaces";
+import { UiTemplate } from "./interfaces";
const UiTemplateSchema: JSONSchemaType = {
type: "object",
- required: ["trustedService", "uiElements"],
+ required: ["ui"],
additionalProperties: false,
properties: {
- trustedService: { type: "string" },
- uiElements: {
- type: "array",
- minItems: 1,
- items: {
- type: "object",
- required: ["apiEndpoint", "uiParams", "placement"],
- additionalProperties: false,
- properties: {
- apiEndpoint: { type: "string" },
- uiParams: {
- type: "object",
- required: ["icon", "tooltip"],
- additionalProperties: false,
- properties: {
- tag: { type: "string", nullable: true },
- value: { type: "string", nullable: true },
- icon: { type: "string" },
- tooltip: { type: "string" },
- },
- },
- placement: {
- type: "array",
- items: {
- type: "string",
- enum: ["curate", "annotate"],
- },
+ ui: {
+ type: "object",
+ required: ["button"],
+ additionalProperties: false,
+ properties: {
+ button: {
+ type: "object",
+ required: ["tooltip"],
+ additionalProperties: true,
+ properties: {
+ name: { type: "string", nullable: true },
+ tooltip: { type: "string" },
},
},
},
diff --git a/src/services/plugins/api/index.ts b/src/services/plugins/api/index.ts
new file mode 100644
index 00000000..02e57791
--- /dev/null
+++ b/src/services/plugins/api/index.ts
@@ -0,0 +1,25 @@
+import type { Plugin, PluginWithExtra } from "@gliff-ai/manage";
+import { apiRequest } from "@/api";
+
+const getPlugins = (): Promise =>
+ apiRequest(`/plugin/`, "GET");
+
+const getZooPlugins = (): Promise =>
+ apiRequest(`/plugin/zoo/`, "GET");
+
+const createPlugin = (plugin: Omit): Promise =>
+ apiRequest("/plugin/", "POST", { ...plugin });
+
+const updatePlugin = (plugin: Omit): Promise =>
+ apiRequest("/plugin/", "PUT", { ...plugin });
+
+const deletePlugin = (url: string): Promise =>
+ apiRequest("/plugin/", "DELETE", { url });
+
+export const pluginsAPI = {
+ getPlugins,
+ createPlugin,
+ updatePlugin,
+ deletePlugin,
+ getZooPlugins,
+};
diff --git a/src/services/plugins/index.ts b/src/services/plugins/index.ts
index fc1449f4..4c561618 100644
--- a/src/services/plugins/index.ts
+++ b/src/services/plugins/index.ts
@@ -1,19 +1,63 @@
-import { apiRequest } from "@/api";
-import type { JsPlugin } from "./interfaces";
+import { Plugin, PluginType, PluginWithExtra } from "@gliff-ai/manage";
+import { pluginsAPI } from "./api";
+import { DominateStore } from "@/store";
-const getPlugins = (): Promise =>
- apiRequest(`/plugin/`, "GET");
+const getPlugins = async (): Promise => {
+ let plugins: PluginWithExtra[] = [];
-const createPlugin = (plugin: JsPlugin): Promise =>
- apiRequest("/plugin/", "POST", { ...plugin });
+ try {
+ plugins = await pluginsAPI.getPlugins();
+ } catch (e) {
+ console.error(e);
+ }
+ return plugins;
+};
-const updatePlugin = (plugin: JsPlugin): Promise =>
- apiRequest("/plugin/", "PUT", { ...plugin });
+const getZooPlugins = async (): Promise => {
+ let plugins: Plugin[] = [];
-const deletePlugin = (plugin: JsPlugin): Promise =>
- apiRequest("/plugin/", "DELETE", { ...plugin });
+ try {
+ plugins = await pluginsAPI.getZooPlugins();
+ } catch (e) {
+ console.error(e);
+ }
+ return plugins;
+};
-const jsPluginsAPI = { getPlugins, createPlugin, updatePlugin, deletePlugin };
+const createPlugin =
+ (storeInstance: DominateStore) =>
+ async (plugin: Plugin): Promise<{ key?: string; email: string } | null> => {
+ if (plugin.type === PluginType.Javascript) {
+ await pluginsAPI.createPlugin(plugin);
+ return null;
+ }
-export { jsPluginsAPI };
-export type { JsPlugin };
+ // if the plugin has an origin and is therefore copied over from another team, use the origin's public-key
+ const originPublicKey =
+ plugin.origin_id !== null ? plugin.public_key : undefined;
+
+ const { publicKey, encryptedAccessKey, privateKey, email } =
+ await storeInstance.createTrustedServiceUser(originPublicKey);
+
+ const res = await pluginsAPI.createPlugin({
+ ...plugin,
+ username: email,
+ public_key: publicKey,
+ encrypted_access_key: encryptedAccessKey,
+ });
+
+ return { key: privateKey, email };
+ };
+
+const updatePlugin = async (plugin: Plugin): Promise => {
+ const pluginId = await pluginsAPI.updatePlugin(plugin);
+ return pluginId;
+};
+
+const deletePlugin = async ({ url }: Plugin): Promise => {
+ const pluginId = await pluginsAPI.deletePlugin(url);
+ return pluginId;
+};
+
+export { getPlugins, getZooPlugins, createPlugin, updatePlugin, deletePlugin };
+export { pluginsAPI };
diff --git a/src/services/plugins/interfaces.ts b/src/services/plugins/interfaces.ts
deleted file mode 100644
index 2c9d1e45..00000000
--- a/src/services/plugins/interfaces.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-interface JsPlugin {
- type: "Javascript" | "Python" | "AI";
- name: string;
- url: string;
- enabled: boolean;
- products: "CURATE" | "ANNOTATE" | "ALL";
- collection_uids: string[];
-}
-
-export type { JsPlugin };
diff --git a/src/services/trustedServices/index.ts b/src/services/trustedServices/index.ts
deleted file mode 100644
index c1e8ab80..00000000
--- a/src/services/trustedServices/index.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import { apiRequest } from "@/api";
-import type {
- TrustedServiceIn,
- TrustedServiceOut,
- UiTemplate,
-} from "./interfaces";
-
-const getTrustedService = (): Promise =>
- apiRequest(`/trusted_service/`, "GET");
-
-const createTrustedService = (
- trustedService: TrustedServiceOut
-): Promise =>
- apiRequest("/trusted_service/", "POST", { ...trustedService });
-
-const updateTrustedService = (
- trustedService: TrustedServiceOut
-): Promise =>
- apiRequest("/trusted_service/", "PUT", { ...trustedService });
-
-const deleteTrustedService = (
- trustedService: TrustedServiceOut
-): Promise =>
- apiRequest("/trusted_service/", "DELETE", { ...trustedService });
-
-const getUiTemplate = (apiUrl: string): Promise =>
- apiRequest("/ui-template/", "POST", {}, apiUrl);
-
-const trustedServicesAPI = {
- createTrustedService,
- getTrustedService,
- updateTrustedService,
- deleteTrustedService,
- getUiTemplate,
-};
-
-export { trustedServicesAPI };
-export type { TrustedServiceIn, TrustedServiceOut };
diff --git a/src/services/trustedServices/interfaces.ts b/src/services/trustedServices/interfaces.ts
deleted file mode 100644
index 2975d65c..00000000
--- a/src/services/trustedServices/interfaces.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { JsPlugin } from "../plugins/interfaces";
-
-export type CollectionUidsWithExtra = {
- uid: string;
- is_invite_pending: boolean;
-};
-
-export interface TrustedServiceOut extends JsPlugin {
- username: string;
-}
-export interface TrustedServiceIn
- extends Omit {
- collection_uids: CollectionUidsWithExtra[];
-}
-
-export interface UiTemplate {
- trustedService: string;
- uiElements: UiElement[];
-}
-
-export interface UiElement {
- placement: string[]; // TODO: delete
- apiEndpoint: string;
- uiParams: {
- tag?: string; // NOTE: unused
- value?: string; // NOTE: unused
- icon: string; // TODO: delete
- tooltip: string;
- };
-}
diff --git a/src/store/index.ts b/src/store/index.ts
index bd97bd4b..aace8304 100644
--- a/src/store/index.ts
+++ b/src/store/index.ts
@@ -31,6 +31,7 @@ import {
AuditMeta,
migrations,
} from "@/interfaces";
+import { SealedCryptoBox } from "@/crypto/SealedCryptoBox";
const logger = console;
@@ -41,6 +42,13 @@ type ProjectMember = {
isPending: boolean;
};
+type TSUserCreated = {
+ publicKey: string;
+ encryptedAccessKey: string;
+ privateKey?: string;
+ email: string;
+};
+
const getRandomValueFromArrayOrString = (
dictionary: string | string[],
count: number
@@ -135,18 +143,38 @@ export class DominateStore {
};
};
- createTrustedServiceUser = async (): Promise<{
- key: string;
- email: string;
- }> => {
- function base64AddPadding(str: string) {
- return `${str}${Array(((4 - (str.length % 4)) % 4) + 1).join("=")}`;
- }
-
+ createTrustedServiceUser = async (
+ publicKey?: string
+ ): Promise => {
const email = `${sodium.randombytes_random()}@trustedservice.gliff.app`;
const password = sodium.randombytes_buf(64, "base64");
- const key = base64AddPadding(toBase64(`${email}:${password}`));
+ const accessKey = sodium.to_base64(
+ `${email}:${password}`,
+ sodium.base64_variants.URLSAFE // url-safe base64 encoding with padding
+ );
+
+ let result: TSUserCreated;
+ if (publicKey) {
+ console.log("activating plugin");
+ const ecryptedKey = SealedCryptoBox.encrypt(accessKey, publicKey);
+ result = {
+ publicKey,
+ encryptedAccessKey: ecryptedKey,
+ email,
+ };
+ } else {
+ console.log("creating plugin");
+ const crypto = SealedCryptoBox.keygen();
+ const ecryptedKey = SealedCryptoBox.encrypt(accessKey, crypto.publicKey);
+
+ result = {
+ publicKey: crypto.publicKey,
+ encryptedAccessKey: ecryptedKey,
+ privateKey: crypto.privateKey,
+ email,
+ };
+ }
await Account.signup(
{
username: toBase64(email),
@@ -156,7 +184,7 @@ export class DominateStore {
SERVER_URL
);
- return { key, email };
+ return result;
};
signup = async (email: string, password: string): Promise => {
@@ -612,7 +640,7 @@ export class DominateStore {
return resolved;
};
- createCollection = async ({
+ createGallery = async ({
name,
description,
}: {
diff --git a/src/ui.tsx b/src/ui.tsx
index 7f634d64..dc678d68 100644
--- a/src/ui.tsx
+++ b/src/ui.tsx
@@ -63,6 +63,7 @@ const UserInterface = ({ storeInstance }: Props): ReactElement | null => {
const [productSection, setProductSection] =
useState(null);
const [tooSmall, setTooSmall] = useState(false);
+ const [pluginsRerender, setPluginsRerender] = useState(0);
const [productNavbarData, setProductNavbarData] = useState(
{
teamName: "",
@@ -176,6 +177,8 @@ const UserInterface = ({ storeInstance }: Props): ReactElement | null => {
setIsLoading={setIsLoading}
task={task}
setTask={setTask}
+ pluginsRerender={pluginsRerender}
+ setPluginsRerender={setPluginsRerender}
setProductNavbarData={setProductNavbarData}
/>
}
@@ -243,6 +246,8 @@ const UserInterface = ({ storeInstance }: Props): ReactElement | null => {
storeInstance={storeInstance}
setProductNavbarData={setProductNavbarData}
setTask={setTask}
+ pluginsRerender={pluginsRerender}
+ setPluginsRerender={setPluginsRerender}
/>
}
/>
diff --git a/src/views/Account.tsx b/src/views/Account.tsx
index eef7ec88..19fed849 100644
--- a/src/views/Account.tsx
+++ b/src/views/Account.tsx
@@ -14,6 +14,7 @@ import { theme, HtmlTooltip } from "@gliff-ai/style";
import { Link } from "react-router-dom";
import { useAuth } from "@/hooks/use-auth";
import { imgSrc } from "@/imgSrc";
+import { getInitialsFromFullname } from "@/helpers";
const useStyles = makeStyles({
avatar: {
@@ -74,12 +75,6 @@ export function Account(): ReactElement | null {
const auth = useAuth();
const classes = useStyles();
- const getInitials = (name: string): string =>
- name
- .split(" ")
- .map((l) => l[0].toUpperCase())
- .join("");
-
return auth?.user && auth?.userProfile ? (
@@ -92,7 +87,7 @@ export function Account(): ReactElement | null {
- {getInitials(auth?.userProfile?.name)}
+ {getInitialsFromFullname(auth?.userProfile?.name)}
diff --git a/src/wrappers/AnnotateWrapper.tsx b/src/wrappers/AnnotateWrapper.tsx
index 77f563f7..5057d70a 100644
--- a/src/wrappers/AnnotateWrapper.tsx
+++ b/src/wrappers/AnnotateWrapper.tsx
@@ -12,6 +12,7 @@ import makeStyles from "@mui/styles/makeStyles";
import { UserInterface, Annotations } from "@gliff-ai/annotate"; // note: Annotations is the annotation data / audit handling class, usually assigned to annotationsObject
import { ImageFileInfo } from "@gliff-ai/upload";
+import { Product } from "@gliff-ai/manage";
import { icons, IconButton, Task } from "@gliff-ai/style";
import { OutputFormat } from "@gliff-ai/etebase";
import { ProductNavbarData } from "@/components";
@@ -30,7 +31,6 @@ import {
setStateIfMounted,
MetaItemWithId,
} from "@/helpers";
-import { Product } from "@/plugins";
interface Props {
storeInstance: DominateStore;
diff --git a/src/wrappers/CurateWrapper.tsx b/src/wrappers/CurateWrapper.tsx
index efc9e12b..de3b12de 100644
--- a/src/wrappers/CurateWrapper.tsx
+++ b/src/wrappers/CurateWrapper.tsx
@@ -5,6 +5,8 @@ import {
useRef,
useCallback,
useMemo,
+ SetStateAction,
+ Dispatch,
} from "react";
import { useParams, useNavigate } from "react-router-dom";
@@ -15,6 +17,7 @@ import { ImageFileInfo } from "@gliff-ai/upload";
import { saveAs } from "file-saver";
import JSZip from "jszip";
import { Annotations, getTiffData } from "@gliff-ai/annotate";
+import { Product, Plugin } from "@gliff-ai/manage";
import { DominateStore } from "@/store";
import { ProductNavbarData } from "@/components";
import { GalleryTile, Slices, MetaItem } from "@/interfaces";
@@ -34,9 +37,10 @@ import {
MetaItemWithId,
setStateIfMounted,
} from "@/helpers";
-import { Product } from "@/plugins";
import { UserAccess } from "@/services/user";
+import { createPlugin } from "@/services/plugins";
import { getTeam, Profile } from "@/services/team";
+import { ZooDialog } from "@/components";
const logger = console;
interface Props {
@@ -44,6 +48,8 @@ interface Props {
setIsLoading: (isLoading: boolean) => void;
task: Task;
setTask: (task: Task) => void;
+ pluginsRerender: number;
+ setPluginsRerender: Dispatch>;
setProductNavbarData: (data: ProductNavbarData) => void;
}
@@ -52,6 +58,8 @@ export const CurateWrapper = ({
setIsLoading,
task,
setTask,
+ pluginsRerender,
+ setPluginsRerender,
setProductNavbarData,
}: Props): ReactElement | null => {
const navigate = useNavigate();
@@ -69,7 +77,6 @@ export const CurateWrapper = ({
const [showMultilabelConfirm, setShowMultilabelConfirm] =
useState(false);
const [multi, setMulti] = useState(false);
- const plugins = usePlugins(collectionUid, auth, Product.CURATE);
// image deletion dialog state:
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
@@ -78,6 +85,12 @@ export const CurateWrapper = ({
// no images to download message state:
const [showNoImageMessage, setShowNoImageMessage] = useState(false);
const [profiles, setProfiles] = useState(null);
+ const plugins = usePlugins(
+ collectionUid,
+ auth,
+ Product.CURATE,
+ pluginsRerender
+ );
const isMounted = useRef(false);
@@ -117,6 +130,7 @@ export const CurateWrapper = ({
isOwnerOrMember ||
(assignees as string[]).includes(auth?.user?.username as string)
);
+
setMetadata(newMetadata);
setDefaultLabels(galleryMeta.defaultLabels);
@@ -571,6 +585,18 @@ export const CurateWrapper = ({
: null
}
saveMetadataCallback={saveMetadataCallback}
+ ZooDialog={
+ isOwnerOrMember && (
+ => {
+ const result = await createPlugin(storeInstance)(plugin);
+ setPluginsRerender((count) => count + 1); // trigger a re-render so that plunginsView updates
+ return result;
+ }}
+ />
+ )
+ }
/>
void;
setTask: Dispatch>;
+ pluginsRerender: number;
+ setPluginsRerender: Dispatch>;
}
export const ManageWrapper = ({
storeInstance,
setTask,
+ pluginsRerender,
+ setPluginsRerender,
setProductNavbarData,
}: Props): ReactElement | null => {
const auth = useAuth();
+
const navigate = useNavigate();
const getProjects = useCallback(async (): Promise => {
@@ -76,7 +79,7 @@ export const ManageWrapper = ({
const createProject = useCallback(
async (projectDetails: { name: string; description?: string }) => {
- const uid = await storeInstance.createCollection(projectDetails);
+ const uid = await storeInstance.createGallery(projectDetails);
return uid;
},
@@ -112,67 +115,13 @@ export const ManageWrapper = ({
[storeInstance]
);
- const getPlugins = useCallback(async (): Promise => {
- let allPlugins: PluginWithExtra[] = [];
-
- try {
- const trustedServices =
- (await trustedServicesAPI.getTrustedService()) as PluginWithExtra[];
- allPlugins = allPlugins.concat(trustedServices);
- } catch (e) {
- console.error(e);
- }
-
- try {
- const jsplugins = (await jsPluginsAPI.getPlugins()).map((p) => ({
- ...p,
- collection_uids: p.collection_uids.map((uid) => ({
- uid,
- is_invite_pending: false,
- })),
- })) as PluginWithExtra[];
- allPlugins = allPlugins.concat(jsplugins);
- } catch (e) {
- console.error(e);
- }
-
- return allPlugins;
- }, []);
-
- const createPlugin = useCallback(
- async (plugin: Plugin): Promise<{ key: string; email: string } | null> => {
- if (plugin.type === PluginType.Javascript) {
- await jsPluginsAPI.createPlugin(plugin as JsPlugin);
- return null;
- }
- // First create a trusted service base user
- const { key, email } = await storeInstance.createTrustedServiceUser();
-
- // Set the user profile
- const res = await trustedServicesAPI.createTrustedService({
- username: email,
- ...plugin,
- } as TrustedServiceOut);
-
- return { key, email };
- },
- [storeInstance]
+ const isOwnerOrMember = useMemo(
+ (): boolean =>
+ auth?.userAccess === UserAccess.Owner ||
+ auth?.userAccess === UserAccess.Member,
+ [auth?.userAccess]
);
- const updatePlugin = useCallback(async (plugin: Plugin): Promise => {
- if (plugin.type === PluginType.Javascript) {
- return jsPluginsAPI.updatePlugin(plugin as JsPlugin);
- }
- return trustedServicesAPI.updateTrustedService(plugin as TrustedServiceOut);
- }, []);
-
- const deletePlugin = useCallback(async (plugin: Plugin): Promise => {
- if (plugin.type === PluginType.Javascript) {
- return jsPluginsAPI.deletePlugin(plugin as JsPlugin);
- }
- return trustedServicesAPI.deleteTrustedService(plugin as TrustedServiceOut);
- }, []);
-
const getAnnotationProgress = useCallback(
async ({
username,
@@ -181,10 +130,6 @@ export const ManageWrapper = ({
username: string;
projectUid?: string;
}): Promise