├── .gitignore ├── Readme.md ├── images ├── add-query.png ├── department-panel.png ├── double-mustache.png ├── dynamic-fields.png └── file-label.png ├── package-lock.json ├── package.json └── src ├── custom-query-panel ├── index.js └── panel.vue └── custom-query ├── index.js └── utils └── config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Directus Custom Query Panel 2 | 3 | Behold the magic 🎩✨ of our simple panel! View your data without the hassle of writing custom endpoints or preparing views - it's like having your cake 🍰 and eating it too, but for data! 💾🎉 4 | 5 | 6 | ## Details 7 | 8 | - Execute custom SQL queries directly from the panel. 9 | - Use dynamic parameters to reuse queries with different values. 10 | - Display query results in a simple tabular structure for easy readability. 11 | - Enhance data visibility and understanding by enabling users to extract meaningful insights from their data which is crucial for CRM and data-driven applications. 12 | 13 | 14 | ## 👀Set Up Instructions 15 | 16 | - Either install through NPM [npm install @7span/directus-extension-custom-query-panel](https://www.npmjs.com/) or [Installing Through the Extensions Folder](https://docs.directus.io/extensions/installing-extensions.html#installing-through-the-extensions-folder). 17 | - Once Installed/configured extension you need hit the below curl request to create a default table(`cqp_queries`) to store the queries. 18 | 19 | **NOTE**: Replace `localhost:8055` with your domain 20 | 21 | ```bash 22 | curl --location --request POST 'http://localhost:8055/custom-query-panel/create-table' 23 | ``` 24 | 25 | - Table named `cqp_queries` will be available in your project. 26 | 27 | ## How To use This Extension 28 | 29 | 1. Create the queries in the table (`cqp_queries`) as explained in below example. 30 | 31 | ```bash 32 | select first_name, last_name from employees where department = ${department} 33 | ``` 34 | 35 | 36 | 2. This extension provides support of *`global variables`* added in insights like department or week. 37 | 38 | 39 | 3. Use `variables` field give in panel settings below `fields`. 40 | 42 | 43 | 4. Use `double mustache` syntax for entering `value` field to get value from variables. 44 | 45 | 46 | ## 👀 Environment Variables 47 | - You can provide collection name in CUSTOM_QUERY_COLLECTION as per your needs. 48 | - It Also provide support for custom query length using CUSTOM_QUERY_FIELD_LENGTH 49 | ```bash 50 | # default value CUSTOM_QUERY_COLLECTION = "cqp_queries" 51 | CUSTOM_QUERY_COLLECTION="custom_query" 52 | # default value CUSTOM_QUERY_FIELD_LENGTH = 5000 53 | CUSTOM_QUERY_FIELD_LENGTH=10000 54 | ``` 55 | 56 | 57 | ## Problem 58 | 59 | - Getting data from database query and show it in a insights panel was missing. 60 | 61 | - The ability to extract and display data from a database query within an insights panel is crucial for CRM and data-driven applications. 62 | - Insights panels serve as a hub for users to access valuable information, gain actionable insights, and make informed decisions. 63 | - This feature bridges the gap between raw data stored in the database and its meaningful interpretation, making Directus an even more versatile platform for managing customer relationships and data-driven business operations. 64 | 65 | ## Extension Type 66 | 67 | - 📦 Bundle ( Panel + Custom Endpoint ) 68 | 69 | ## Screenshots 70 | 71 | ![Add Query](/images/add-query.png) 72 | ![Department Example](/images/department-panel.png) 73 | ![dynamic-fields](/images/dynamic-fields.png) 74 | ![Adding File Label](/images/file-label.png) 75 | ![Alt text](/images/double-mustache.png) 76 | ## Collaborators 77 | 78 | - [Harsh Kansagara](https://github.com/theharshin) 79 | - [Jay Bharadia](https://github.com/jay-p-b-7span) 80 | - [Bhagyesh Radiya](https://github.com/bhagyesh-7span) 81 | 82 | ## Contact Details 83 | 84 | - [Linkedin](https://www.linkedin.com/company/7span) 85 | - [Gmail](mailto:yo@7span.com) 86 | 87 | ## 🚧 Please note 88 | 89 | - this extension uses raw query. Use with caution. It might do uninteded actions. 90 | - Roles and permission check for query 91 | 92 | ### Table Fields 93 | 94 | - We have repeater interface with multiple columns support 95 | 96 | -------------------------------------------------------------------------------- /images/add-query.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7span/directus-extension-custom-query-panel/fdce7837ef2d8d0be4ce4e66fd6f1eefaa56e471/images/add-query.png -------------------------------------------------------------------------------- /images/department-panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7span/directus-extension-custom-query-panel/fdce7837ef2d8d0be4ce4e66fd6f1eefaa56e471/images/department-panel.png -------------------------------------------------------------------------------- /images/double-mustache.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7span/directus-extension-custom-query-panel/fdce7837ef2d8d0be4ce4e66fd6f1eefaa56e471/images/double-mustache.png -------------------------------------------------------------------------------- /images/dynamic-fields.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7span/directus-extension-custom-query-panel/fdce7837ef2d8d0be4ce4e66fd6f1eefaa56e471/images/dynamic-fields.png -------------------------------------------------------------------------------- /images/file-label.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7span/directus-extension-custom-query-panel/fdce7837ef2d8d0be4ce4e66fd6f1eefaa56e471/images/file-label.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@7span/directus-extension-custom-query-panel", 3 | "version": "1.1.7", 4 | "scripts": { 5 | "build": "directus-extension build", 6 | "dev": "directus-extension build -w --no-minify", 7 | "link": "directus-extension link", 8 | "add": "directus-extension add", 9 | "prepublishOnly": "npm run build" 10 | }, 11 | "directus:extension": { 12 | "host": "^9.22.4", 13 | "type": "bundle", 14 | "path": { 15 | "app": "dist/app.js", 16 | "api": "dist/api.js" 17 | }, 18 | "entries": [ 19 | { 20 | "type": "panel", 21 | "name": "custom-query-panel", 22 | "source": "src/custom-query-panel/index.js" 23 | }, 24 | { 25 | "type": "endpoint", 26 | "name": "custom-query", 27 | "source": "src/custom-query/index.js" 28 | } 29 | ] 30 | }, 31 | "devDependencies": { 32 | "@directus/extensions-sdk": "9.22.4", 33 | "vue": "^3.2.47" 34 | }, 35 | "author": { 36 | "name": "7span", 37 | "email": "yo@7span.com" 38 | }, 39 | "repository": { 40 | "type": "git", 41 | "url": "https://github.com/7span/directus-extension-custom-query-panel.git" 42 | }, 43 | "homepage": "https://github.com/7span/directus-extension-custom-query-panel#Readme", 44 | "publishConfig": { 45 | "access": "public" 46 | }, 47 | "dependencies": { 48 | "dotenv": "^16.3.1", 49 | "paraphrase": "^3.1.1" 50 | }, 51 | "files": [ 52 | "dist" 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /src/custom-query-panel/index.js: -------------------------------------------------------------------------------- 1 | import PanelComponent from "./panel.vue"; 2 | 3 | export default { 4 | id: "custom-query", 5 | name: "Custom Query Panel", 6 | icon: "dynamic_form", 7 | description: "This is my custom Query panel!", 8 | component: PanelComponent, 9 | options: [ 10 | { 11 | field: "query_id", 12 | name: "Query ID", 13 | type: "string", 14 | meta: { 15 | note: "Enter your Query ID here to retrieve data", 16 | interface: "input", 17 | width: "full", 18 | }, 19 | }, 20 | { 21 | field: "fields", 22 | name: "Fields", 23 | type: "standard", 24 | meta: { 25 | note: "Enter Table Column(s) for displaying data", 26 | interface: "list", 27 | width: "full", 28 | options: { 29 | fields: [ 30 | { 31 | field: "key", 32 | name: "Key", 33 | type: "string", 34 | meta: { 35 | field: "key", 36 | width: "half", 37 | type: "string", 38 | interface: "input", 39 | options: { 40 | placeholder: "first_name", 41 | }, 42 | }, 43 | }, 44 | 45 | { 46 | field: "label", 47 | name: "Label", 48 | type: "string", 49 | meta: { 50 | field: "label", 51 | width: "half", 52 | options: { 53 | placeholder: "First Name", 54 | }, 55 | type: "string", 56 | interface: "input", 57 | }, 58 | }, 59 | { 60 | field: "width", 61 | name: "Width", 62 | type: "string", 63 | schema: { 64 | default_value: "300", 65 | }, 66 | meta: { 67 | field: "width", 68 | width: "half", 69 | type: "string", 70 | interface: "input", 71 | }, 72 | }, 73 | ], 74 | }, 75 | }, 76 | }, 77 | 78 | // Variables 79 | { 80 | field: "variables", 81 | name: "Variables", 82 | type: "standard", 83 | meta: { 84 | interface: "list", 85 | width: "full", 86 | options: { 87 | fields: [ 88 | { 89 | field: "key", 90 | name: "Key", 91 | type: "string", 92 | meta: { 93 | field: "key", 94 | width: "half", 95 | options: { 96 | placeholder: 97 | "Enter Dynamic Variable key here", 98 | }, 99 | type: "string", 100 | interface: "input", 101 | }, 102 | }, 103 | { 104 | field: "value", 105 | name: "Value", 106 | type: "string", 107 | meta: { 108 | note: "Enter {{ variable_name }} to work properly", 109 | field: "value", 110 | width: "half", 111 | options: { 112 | placeholder: "{{department}}", 113 | }, 114 | type: "string", 115 | interface: "input", 116 | }, 117 | }, 118 | ], 119 | }, 120 | }, 121 | }, 122 | ], 123 | minWidth: 12, 124 | minHeight: 8, 125 | }; 126 | -------------------------------------------------------------------------------- /src/custom-query-panel/panel.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 95 | 96 | 110 | -------------------------------------------------------------------------------- /src/custom-query/index.js: -------------------------------------------------------------------------------- 1 | import { dollar as phrase } from "paraphrase"; 2 | import { CUSTOM_QUERY_COLLECTION, CUSTOM_QUERY_FIELD_LENGTH, DIRECTUS_FIELDS_OBJ } from "./utils/config"; 3 | 4 | export default { 5 | id: "custom-query-panel", 6 | handler: (router, { services, logger, database }) => { 7 | const { ItemsService } = services; 8 | 9 | // Endpoint to execute a custom query 10 | router.get("/execute", async (req, res) => { 11 | try { 12 | // Check if query_id is provided 13 | if (!req.query.query_id) { 14 | return res.status(400).json({ error: "Query ID is required" }); 15 | } 16 | 17 | // Initialize the custom query service 18 | const customQueryService = new ItemsService(CUSTOM_QUERY_COLLECTION, { schema: req.schema }); 19 | 20 | // Retrieve the custom query based on the provided ID 21 | const customQueryData = await customQueryService.readOne(req.query.query_id); 22 | 23 | // Check if custom query data is retrieved 24 | if (!customQueryData) { 25 | return res.status(404).json({ error: "Custom query not found" }); 26 | } 27 | 28 | // Prepare the variables for the custom query 29 | const queryVariables = req.query.variables || []; 30 | const preparedVariables = prepareVariables(queryVariables); 31 | 32 | // Execute the custom query 33 | const RAW_QUERY = phrase(`${customQueryData.query}`, preparedVariables); 34 | logger.debug(`Raw query: ${RAW_QUERY}`); 35 | const executedQueryData = await database.raw(RAW_QUERY); 36 | const fetchedQueryData = executedQueryData[0]; 37 | 38 | logger.debug("Custom Query Executed"); 39 | return res.status(200).json({ data: fetchedQueryData }); 40 | } catch (error) { 41 | logger.error(`error: ${error}`); 42 | return res.status(500).json({ error: `${error.message}` }); 43 | } 44 | }); 45 | 46 | // Endpoint to create a table for storing custom queries 47 | router.post("/create-table", async (req, res) => { 48 | try { 49 | const customQueryService = new ItemsService('directus_fields', { schema: req.schema }); 50 | 51 | // Create a table for custom queries 52 | await database.schema.createTable(CUSTOM_QUERY_COLLECTION, (table) => { 53 | table.increments(), table.string("name"), table.varchar("query", CUSTOM_QUERY_FIELD_LENGTH); 54 | }); 55 | 56 | // Create a collection field of type code editor for MYSQL query 57 | await customQueryService.createOne(DIRECTUS_FIELDS_OBJ); 58 | 59 | return res 60 | .status(200) 61 | .json({ 62 | message: `Table Created for Custom Query Extension ${CUSTOM_QUERY_COLLECTION}`, 63 | }); 64 | } catch (error) { 65 | logger.error(`error: ${error}`); 66 | return res.status(500).json({ error: `${error.message}` }); 67 | } 68 | }); 69 | }, 70 | }; 71 | 72 | // Function to prepare the variables for the custom query 73 | function prepareVariables(variables) { 74 | const preparedVariables = {}; 75 | variables.forEach((item) => { 76 | const { key, value } = item; 77 | preparedVariables[key] = value 78 | // if (Number.isInteger(value)) { 79 | // preparedVariables[key] = `${parseInt(value)}`; 80 | // } else if (!isNaN(parseFloat(value))) { 81 | // preparedVariables[key] = `${parseFloat(value)}`; 82 | // } else if (typeof value === "string") { 83 | // preparedVariables[key] = `'${value}'`; 84 | // } else { 85 | // console.log(`Unsupported data type for key ${key}`) 86 | // } 87 | }); 88 | return preparedVariables; 89 | } -------------------------------------------------------------------------------- /src/custom-query/utils/config.js: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | dotenv.config(); 3 | 4 | const CUSTOM_QUERY_COLLECTION = 5 | process.env.CUSTOM_QUERY_COLLECTION || "cqp_queries"; 6 | 7 | const DIRECTUS_FIELDS_OBJ = { 8 | collection: CUSTOM_QUERY_COLLECTION, 9 | field: "query", 10 | interface: "input-code", 11 | options: { language: "sql" }, 12 | readonly: 0, 13 | hidden: 0, 14 | sort: 2, 15 | width: "full", 16 | required: 0, 17 | }; 18 | 19 | // Setting the length of the custom query field, default is 5000 20 | const CUSTOM_QUERY_FIELD_LENGTH = parseInt(process.env.CUSTOM_QUERY_FIELD_LENGTH) || 5000; 21 | 22 | export { CUSTOM_QUERY_COLLECTION, CUSTOM_QUERY_FIELD_LENGTH, DIRECTUS_FIELDS_OBJ }; 23 | --------------------------------------------------------------------------------