diff --git a/apps/build.gradle b/apps/build.gradle index 11fd5ceeee..a8e5319909 100644 --- a/apps/build.gradle +++ b/apps/build.gradle @@ -70,10 +70,11 @@ subprojects { subproject -> dependsOn ":apps:${match[1]}:buildJavascript"; } //run the build - args = ['run', 'build'] + def command = packageJson.contains("\"name\": \"ui\"") ? "generate" : "build" + args = ['run', command] doLast { copy { - from "dist" + from packageJson.contains("\"name\": \"ui\"") ? ".output/public" : "dist" into "${parent.buildDir}/generated/main/resources/public_html/apps/" + project.name } } diff --git a/apps/catalogue.code-workspace b/apps/catalogue.code-workspace index a0703163a8..e5ab0ae728 100644 --- a/apps/catalogue.code-workspace +++ b/apps/catalogue.code-workspace @@ -5,6 +5,12 @@ }, { "path": "tailwind-components" + }, + { + "path": "ui" + }, + { + "path": "metadata-utils" } ], "settings": {} diff --git a/apps/dev-proxy.config.js b/apps/dev-proxy.config.js index a13c15b4f1..94e36f0c26 100644 --- a/apps/dev-proxy.config.js +++ b/apps/dev-proxy.config.js @@ -20,6 +20,10 @@ module.exports = { target: HOST, ...opts, }, + "^/[a-zA-Z0-9_.%-]+/api/trigger": { + target: HOST, + ...opts, + }, "/api": { target: `${HOST}/api`, ...opts, diff --git a/apps/emx2-analytics/.gitignore b/apps/emx2-analytics/.gitignore new file mode 100644 index 0000000000..a547bf36d8 --- /dev/null +++ b/apps/emx2-analytics/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/apps/emx2-analytics/README.md b/apps/emx2-analytics/README.md new file mode 100644 index 0000000000..29d1c45bb5 --- /dev/null +++ b/apps/emx2-analytics/README.md @@ -0,0 +1,49 @@ +# Analytics module + +Goal of the module is to allow service maintainers to track certain user actions. +For example maintainer of a Data Catalogue application might what to know when and by who a 'Contact' button is clicked. + +This module when installed sets up 'Triggers' attched to page elements ( for example a Button in a given location). +Besides the need to install and configure this module in a app ( or compoment ), +the triggers ( what events to run when ) should be created using the analytics api or web ui. + +### Install the module + +use package manager to install the module '@molgenis/emx2-analytics' + +```yarn add @molgenis/emx2-analytics``` (or use the * option for yarn workspaces) + +### Setup the triggers in the app +```import { setupAnalytics } from "@molgenis/emx2-analytics"; ``` + +... + +``` setupAnalytics(schema, providers);``` + +schema: The name of the emx schema/database + +providers: A list of analytics profiders and there config options, for example; ```providers = [{ id: "site-improve", options: { analyticsKey } }];``` + +This setup should be run before the user interacts with the page + +During the setupAnalytics call 3 steps are taken + - 1. For each provider the nessasary code is (fetched and) loaded + - 2. The triggers configured for this schema are fetched from the backend + - 3. For each triggers the DOM elements are located in the page and a eventhandler gets attached for the configured provider + + When the end user visits the page and triggers the event the attached eventhandler uses the provider script to send the event. + + The whole analyics module works an a fire-and-forget basis, if something goes wrong the end user is not notified ( except for the browser console error log) + + +## Development + +includes playground 'app', run via `yarn dev` + +## Build + +`yarn build` + +## Release + +todo \ No newline at end of file diff --git a/apps/emx2-analytics/index.html b/apps/emx2-analytics/index.html new file mode 100644 index 0000000000..143557b528 --- /dev/null +++ b/apps/emx2-analytics/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + Vue + TS + + +
+ + + diff --git a/apps/emx2-analytics/package.json b/apps/emx2-analytics/package.json new file mode 100644 index 0000000000..395949ed2a --- /dev/null +++ b/apps/emx2-analytics/package.json @@ -0,0 +1,48 @@ +{ + "name": "@molgenis/emx2-analytics", + "description": "Molgenis EMX2 Analytics, client module", + "license": "LGPL-3.0-or-later", + "private": false, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc -b && vite build", + "preview": "vite preview", + "format": "prettier src --write --config ../.prettierrc.js", + "checkFormat": "prettier src --check --config ../.prettierrc.js" + }, + "dependencies": { + "vue": "^3.4.29" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.5", + "prettier": "2.8.8", + "typescript": "^5.2.2", + "vite": "^5.3.1", + "vite-plugin-dts": "^3.9.1", + "vue-tsc": "^2.0.21" + }, + "publishConfig": { + "access": "public" + }, + "files": [ + "dist", + "src", + "package.json", + "README.md" + ], + "main": "./dist/analytics.umd.js", + "module": "./dist/analytics.es.js", + "typings": "./dist/lib/**/*.d.ts", + "exports": { + ".": { + "import": "./dist/analytics.es.js", + "require": "./dist/analytics.umd.js" + }, + "./dist/style.css": { + "import": "./dist/style.css", + "require": "./dist/style.css" + } + } +} diff --git a/apps/emx2-analytics/src/App.vue b/apps/emx2-analytics/src/App.vue new file mode 100644 index 0000000000..e9221f9995 --- /dev/null +++ b/apps/emx2-analytics/src/App.vue @@ -0,0 +1,176 @@ + + + + + diff --git a/apps/emx2-analytics/src/lib/analytics.ts b/apps/emx2-analytics/src/lib/analytics.ts new file mode 100644 index 0000000000..9b0a036a33 --- /dev/null +++ b/apps/emx2-analytics/src/lib/analytics.ts @@ -0,0 +1,64 @@ +import { Provider, siteImproveOptions } from "../types/Provider"; +import { Trigger } from "../types/Trigger"; +import { + handleEvent as siteImprove, + initialize as siteImproveInit, +} from "./providers/siteImprove"; + +function setupAnalytics(schemaName: string, providers: Provider[]) { + for (let provider of providers) { + switch ( + provider.id // todo explore fancy dynamic import instead of switch + ) { + case "site-improve": + siteImproveInit(provider.options as siteImproveOptions); + break; + default: + console.error(`Provider ${provider} not supported`); + } + } + + fetch(`/${schemaName}/api/trigger`) + .then((response) => { + response.json().then((data) => { + data.forEach((trigger: Trigger) => { + try { + const elements = document.querySelectorAll(trigger.cssSelector); + elements.forEach((element) => { + console.log( + `add trigger for ${trigger.name} to ${element.nodeName}` + ); + element.addEventListener("click", (e) => { + for (let provider of providers) { + handleEvent(e, trigger, element, provider); + } + }); + }); + } catch (e) { + console.error("Failed to select elements for trigger", trigger); + console.error(`Error: ${e} for ${trigger.name}`); + } + }); + }); + }) + .catch((error) => { + console.error("Error:", error); + }); +} + +function handleEvent( + event: Event, + trigger: Trigger, + element: Element, + provider: Provider +) { + switch (provider.id) { + case "site-improve": + siteImprove(event, trigger, element); + break; + default: + console.error(`Provider ${provider.id} not supported`); + } +} + +export { setupAnalytics }; diff --git a/apps/emx2-analytics/src/lib/providers/siteImprove.ts b/apps/emx2-analytics/src/lib/providers/siteImprove.ts new file mode 100644 index 0000000000..3667ef106c --- /dev/null +++ b/apps/emx2-analytics/src/lib/providers/siteImprove.ts @@ -0,0 +1,38 @@ +import { siteImproveOptions } from "../../types/Provider"; +import { Trigger } from "../../types/Trigger"; + +declare const _sz: any; + +function initialize(options: siteImproveOptions) { + const scriptSrc = `https://siteimproveanalytics.com/js/siteanalyze_${options.analyticsKey}.js`; + if (!scriptLoaded(scriptSrc)) { + // Load the script in scoped script context ( due to cors restrictions) + const script = document.createElement("script"); + script.async = true; + script.src = scriptSrc; + document.body.appendChild(script); + console.log("site improve script loaded"); + } +} + +function handleEvent(event: Event, trigger: Trigger, element: Element) { + console.log("handel site improve event", event, trigger, element.tagName); + + if (_sz) { + _sz.push(["event", "demo cat", "demo action", trigger.name]); + } else { + console.error("site improve not loaded"); + } +} + +function scriptLoaded(scriptSrc: string) { + const scripts = document.querySelectorAll("script[src]"); + const regex = new RegExp(`^${scriptSrc}`); + return Boolean( + Object.values(scripts).filter((value) => + regex.test((value as HTMLScriptElement).src) + ).length + ); +} + +export { initialize, handleEvent }; diff --git a/apps/emx2-analytics/src/main.ts b/apps/emx2-analytics/src/main.ts new file mode 100644 index 0000000000..3c9bfeb788 --- /dev/null +++ b/apps/emx2-analytics/src/main.ts @@ -0,0 +1,5 @@ +import { createApp } from "vue"; +import "./style.css"; +import App from "./App.vue"; + +createApp(App).mount("#app"); diff --git a/apps/emx2-analytics/src/style.css b/apps/emx2-analytics/src/style.css new file mode 100644 index 0000000000..bb131d6b8f --- /dev/null +++ b/apps/emx2-analytics/src/style.css @@ -0,0 +1,79 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +.card { + padding: 2em; +} + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/apps/emx2-analytics/src/types/Provider.ts b/apps/emx2-analytics/src/types/Provider.ts new file mode 100644 index 0000000000..2463255787 --- /dev/null +++ b/apps/emx2-analytics/src/types/Provider.ts @@ -0,0 +1,12 @@ +export type providerId = "site-improve"; + +export interface Provider { + id: providerId; + options: ProviderOptions; +} + +export interface ProviderOptions {} + +export interface siteImproveOptions extends ProviderOptions { + analyticsKey: string; +} diff --git a/apps/emx2-analytics/src/types/Trigger.ts b/apps/emx2-analytics/src/types/Trigger.ts new file mode 100644 index 0000000000..016184e65b --- /dev/null +++ b/apps/emx2-analytics/src/types/Trigger.ts @@ -0,0 +1,6 @@ +export interface Trigger { + name: string; + cssSelector: string; + schemaName: string; + appName?: string; +} diff --git a/apps/emx2-analytics/src/vite-env.d.ts b/apps/emx2-analytics/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/apps/emx2-analytics/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/apps/emx2-analytics/tsconfig.app.json b/apps/emx2-analytics/tsconfig.app.json new file mode 100644 index 0000000000..b8e9723f5f --- /dev/null +++ b/apps/emx2-analytics/tsconfig.app.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "preserve", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] +} diff --git a/apps/emx2-analytics/tsconfig.json b/apps/emx2-analytics/tsconfig.json new file mode 100644 index 0000000000..fcd80ad4d5 --- /dev/null +++ b/apps/emx2-analytics/tsconfig.json @@ -0,0 +1,35 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "types": ["vite/client", "node"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "baseUrl": "./", + "paths": { + "~/*": ["src/*"] + } + }, + + "exclude": ["node_modules"], + "files": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.node.json" + } + ], + +} diff --git a/apps/emx2-analytics/tsconfig.node.json b/apps/emx2-analytics/tsconfig.node.json new file mode 100644 index 0000000000..3afdd6e384 --- /dev/null +++ b/apps/emx2-analytics/tsconfig.node.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true, + "noEmit": true + }, + "include": ["vite.config.ts"] +} diff --git a/apps/emx2-analytics/vite.config.ts b/apps/emx2-analytics/vite.config.ts new file mode 100644 index 0000000000..6de34891fb --- /dev/null +++ b/apps/emx2-analytics/vite.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import path from 'path'; // Import the 'path' module +import dts from 'vite-plugin-dts' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [vue(), dts()], + server: { + proxy: require("../dev-proxy.config"), + }, + build: { + lib: { + entry: path.resolve(__dirname, "src/lib/analytics.ts"), + name: "analytics", + fileName: (format) => `analytics.${format}.js`, + }, + sourcemap: true, + emptyOutDir: true, + }, +}); diff --git a/apps/nuxt3-ssr/components/CollectionEventDisplay.vue b/apps/nuxt3-ssr/components/CollectionEventDisplay.vue index 0dc1568909..714435c231 100644 --- a/apps/nuxt3-ssr/components/CollectionEventDisplay.vue +++ b/apps/nuxt3-ssr/components/CollectionEventDisplay.vue @@ -1,6 +1,5 @@ + + + \ No newline at end of file diff --git a/apps/nuxt3-ssr/components/BreadCrumbs.vue b/apps/tailwind-components/components/BreadCrumbs.vue similarity index 95% rename from apps/nuxt3-ssr/components/BreadCrumbs.vue rename to apps/tailwind-components/components/BreadCrumbs.vue index 0d5a218942..f10d32a751 100644 --- a/apps/nuxt3-ssr/components/BreadCrumbs.vue +++ b/apps/tailwind-components/components/BreadCrumbs.vue @@ -1,4 +1,3 @@ - + + + + diff --git a/apps/nuxt3-ssr/components/PageHeader.vue b/apps/tailwind-components/components/PageHeader.vue similarity index 100% rename from apps/nuxt3-ssr/components/PageHeader.vue rename to apps/tailwind-components/components/PageHeader.vue diff --git a/apps/nuxt3-ssr/components/SideModal.vue b/apps/tailwind-components/components/SideModal.vue similarity index 95% rename from apps/nuxt3-ssr/components/SideModal.vue rename to apps/tailwind-components/components/SideModal.vue index 0c89c22aca..dfc994f6f1 100644 --- a/apps/nuxt3-ssr/components/SideModal.vue +++ b/apps/tailwind-components/components/SideModal.vue @@ -1,5 +1,5 @@