From 319732dde379ac27225d04e2c641e496ac2c9840 Mon Sep 17 00:00:00 2001 From: Ivan Borshchov Date: Wed, 3 Jul 2024 12:31:53 +0300 Subject: [PATCH] Deploy website - based on f39402ba4f81e937686c5ab9320ad8c1c44fe209 --- 404.html | 2 +- assets/js/0058754d.0dc70dcf.js | 1 + assets/js/0058754d.5a65823b.js | 1 - assets/js/322eff50.eac3ea2e.js | 1 - assets/js/322eff50.fe38aa95.js | 1 + assets/js/813b1aa0.a50518e8.js | 1 + assets/js/813b1aa0.e330a24c.js | 1 - ...~main.f9ad0425.js => runtime~main.6a4e2ffc.js} | 2 +- blog/archive/index.html | 2 +- blog/first-blog-post/index.html | 2 +- blog/index.html | 2 +- blog/long-blog-post/index.html | 2 +- blog/mdx-blog-post/index.html | 2 +- blog/tags/docusaurus/index.html | 2 +- blog/tags/facebook/index.html | 2 +- blog/tags/hello/index.html | 2 +- blog/tags/hola/index.html | 2 +- blog/tags/index.html | 2 +- blog/welcome/index.html | 2 +- docs/api/index.html | 2 +- docs/api/plugins/AuditLogPlugin/types/index.html | 2 +- .../types/type-aliases/PluginOptions/index.html | 2 +- .../ForeignInlineListPlugin/types/index.html | 2 +- .../types/type-aliases/PluginOptions/index.html | 2 +- .../enumerations/ActionCheckSource/index.html | 2 +- .../enumerations/AdminForthDataTypes/index.html | 2 +- .../AdminForthFilterOperators/index.html | 2 +- .../enumerations/AdminForthMenuTypes/index.html | 2 +- .../AdminForthResourcePages/index.html | 2 +- .../AdminForthSortDirections/index.html | 2 +- .../enumerations/AllowedActionsEnum/index.html | 2 +- docs/api/types/AdminForthConfig/index.html | 2 +- .../interfaces/AdminForthClass/index.html | 2 +- .../AdminForthDataSourceConnector/index.html | 2 +- .../index.html | 2 +- .../interfaces/AdminForthPluginType/index.html | 2 +- .../interfaces/CodeInjectorType/index.html | 2 +- .../interfaces/ExpressHttpServer/index.html | 2 +- .../interfaces/GenericHttpServer/index.html | 2 +- .../AdminForthColumnEnumItem/index.html | 2 +- .../AdminForthComponentDeclaration/index.html | 2 +- .../AdminForthComponentDeclarationFull/index.html | 2 +- .../type-aliases/AdminForthConfig/index.html | 2 +- .../AdminForthConfigMenuItem/index.html | 2 +- .../type-aliases/AdminForthDataSource/index.html | 2 +- .../AdminForthFieldComponents/index.html | 2 +- .../AdminForthForeignResource/index.html | 2 +- .../type-aliases/AdminForthResource/index.html | 2 +- .../AdminForthResourceColumn/index.html | 2 +- .../type-aliases/AdminUser/index.html | 2 +- .../AfterDataSourceResponseFunction/index.html | 2 +- .../type-aliases/AfterSaveFunction/index.html | 2 +- .../type-aliases/AllowedActionValue/index.html | 2 +- .../type-aliases/AllowedActions/index.html | 2 +- .../BeforeDataSourceRequestFunction/index.html | 2 +- .../type-aliases/BeforeSaveFunction/index.html | 2 +- .../type-aliases/ValidationObject/index.html | 2 +- .../enumerations/AlertVariant/index.html | 2 +- docs/api/types/FrontendAPI/index.html | 2 +- .../interfaces/FrontendAPIInterface/index.html | 2 +- .../type-aliases/AlertParams/index.html | 2 +- .../type-aliases/ConfirmParams/index.html | 2 +- docs/tutorial/Customization/alert/index.html | 2 +- docs/tutorial/Customization/branding/index.html | 2 +- .../tutorial/Customization/bulkActions/index.html | 2 +- .../Customization/customFieldRendering/index.html | 2 +- .../tutorial/Customization/customPages/index.html | 2 +- docs/tutorial/Customization/hooks/index.html | 2 +- .../Customization/limitingAccess/index.html | 2 +- .../Customization/pageInjections/index.html | 2 +- .../Customization/virtualColumns/index.html | 2 +- docs/tutorial/Plugins/AuditLog/index.html | 15 +++++++++------ .../tutorial/Plugins/ForeignInlineList/index.html | 4 ++-- docs/tutorial/deploy/index.html | 2 +- docs/tutorial/gettingStarted/index.html | 6 +++--- docs/tutorial/glossary/index.html | 2 +- index.html | 2 +- markdown-page/index.html | 2 +- search/index.html | 2 +- 79 files changed, 87 insertions(+), 84 deletions(-) create mode 100644 assets/js/0058754d.0dc70dcf.js delete mode 100644 assets/js/0058754d.5a65823b.js delete mode 100644 assets/js/322eff50.eac3ea2e.js create mode 100644 assets/js/322eff50.fe38aa95.js create mode 100644 assets/js/813b1aa0.a50518e8.js delete mode 100644 assets/js/813b1aa0.e330a24c.js rename assets/js/{runtime~main.f9ad0425.js => runtime~main.6a4e2ffc.js} (97%) diff --git a/404.html b/404.html index 010a77a12..1f11f8eaa 100644 --- a/404.html +++ b/404.html @@ -13,7 +13,7 @@ - + diff --git a/assets/js/0058754d.0dc70dcf.js b/assets/js/0058754d.0dc70dcf.js new file mode 100644 index 000000000..b91162caa --- /dev/null +++ b/assets/js/0058754d.0dc70dcf.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkadminforth=self.webpackChunkadminforth||[]).push([[3800],{2681:(e,n,i)=>{i.r(n),i.d(n,{assets:()=>d,contentTitle:()=>r,default:()=>u,frontMatter:()=>o,metadata:()=>l,toc:()=>a});var s=i(4848),t=i(8453);const o={},r=void 0,l={id:"tutorial/Plugins/ForeignInlineList",title:"ForeignInlineList",description:"Foreign inline list plugin allows to display a list (table) of items from a foreign table in the show view.",source:"@site/docs/tutorial/Plugins/ForeignInlineList.md",sourceDirName:"tutorial/Plugins",slug:"/tutorial/Plugins/ForeignInlineList",permalink:"/docs/tutorial/Plugins/ForeignInlineList",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"AuditLog",permalink:"/docs/tutorial/Plugins/AuditLog"}},d={},a=[{value:"Usage",id:"usage",level:2}];function c(e){const n={a:"a",code:"code",h2:"h2",img:"img",p:"p",pre:"pre",...(0,t.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.p,{children:"Foreign inline list plugin allows to display a list (table) of items from a foreign table in the show view."}),"\n",(0,s.jsx)(n.h2,{id:"usage",children:"Usage"}),"\n",(0,s.jsx)(n.p,{children:"Import plugin:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-ts",children:"import ForeignInlineListPlugin from 'adminforth/plugins/ForeignInlineListPlugin';\n"})}),"\n",(0,s.jsx)(n.p,{children:"If you are using pure Node without TypeScript, you can use the following code:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:"import ForeignInlineListPlugin from 'adminforth/dist/plugins/ForeignInlineListPlugin/index.js';\n"})}),"\n",(0,s.jsxs)(n.p,{children:["In ",(0,s.jsx)(n.a,{href:"/docs/tutorial/gettingStarted",children:"Getting Started"})," we created a ",(0,s.jsx)(n.code,{children:"'aparts'"})," resource which has a field ",(0,s.jsx)(n.code,{children:"'realtor_id'"}),".\nThis field refers to record from ",(0,s.jsx)(n.code,{children:"'users'"})," resource. This means that we can display a list of appartments in the user's show view."]}),"\n",(0,s.jsxs)(n.p,{children:["Add to your ",(0,s.jsx)(n.code,{children:"'users'"})," resource configuration (which we created in ), plugin instance:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-ts",metastring:'title="./index.ts"',children:"{ \n ...\n resourceId: 'users',\n ...\n//diff-add\n plugins: [\n//diff-add\n new ForeignInlineListPlugin({\n//diff-add\n foreignResourceId: 'aparts',\n//diff-add\n modifyTableResourceConfig: (resourceConfig: AdminForthResource) => {\n//diff-add\n // hide column 'square_meter' from both 'list' and 'filter'\n//diff-add\n const column = resourceConfig.columns.find((c: AdminForthResourceColumn) => c.name === 'square_meter')!.showIn = [];\n//diff-add\n resourceConfig.options!.listPageSize = 1;\n//diff-add\n // feel free to console.log and edit resourceConfig as you need\n//diff-add\n },\n//diff-add\n }),\n//diff-add\n ],\n}\n"})}),"\n",(0,s.jsxs)(n.p,{children:["You can use ",(0,s.jsx)(n.code,{children:"modifyTableResourceConfig"})," callback to modify what columns to show in the list and filter of the foreign table."]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"alt text",src:i(3986).A+"",width:"3670",height:"2044"})}),"\n",(0,s.jsxs)(n.p,{children:["See ",(0,s.jsx)(n.a,{href:"/docs/api/plugins/ForeignInlineListPlugin/types/type-aliases/PluginOptions",children:"API Reference"})," for more all options."]})]})}function u(e={}){const{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(c,{...e})}):c(e)}},3986:(e,n,i)=>{i.d(n,{A:()=>s});const s=i.p+"assets/images/localhost_3500_resource_users_show_08dpfh-a8f5bd63bb09e071c67a13d7121e95fd.png"},8453:(e,n,i)=>{i.d(n,{R:()=>r,x:()=>l});var s=i(6540);const t={},o=s.createContext(t);function r(e){const n=s.useContext(o);return s.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:r(e.components),s.createElement(o.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/0058754d.5a65823b.js b/assets/js/0058754d.5a65823b.js deleted file mode 100644 index 965a34e83..000000000 --- a/assets/js/0058754d.5a65823b.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkadminforth=self.webpackChunkadminforth||[]).push([[3800],{2681:(e,n,i)=>{i.r(n),i.d(n,{assets:()=>a,contentTitle:()=>r,default:()=>d,frontMatter:()=>o,metadata:()=>l,toc:()=>c});var s=i(4848),t=i(8453);const o={},r=void 0,l={id:"tutorial/Plugins/ForeignInlineList",title:"ForeignInlineList",description:"Foreign inline list plugin allows to display a list (table) of items from a foreign table in the show view.",source:"@site/docs/tutorial/Plugins/ForeignInlineList.md",sourceDirName:"tutorial/Plugins",slug:"/tutorial/Plugins/ForeignInlineList",permalink:"/docs/tutorial/Plugins/ForeignInlineList",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"AuditLog",permalink:"/docs/tutorial/Plugins/AuditLog"}},a={},c=[{value:"Usage",id:"usage",level:2}];function u(e){const n={a:"a",code:"code",h2:"h2",img:"img",p:"p",pre:"pre",...(0,t.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.p,{children:"Foreign inline list plugin allows to display a list (table) of items from a foreign table in the show view."}),"\n",(0,s.jsx)(n.h2,{id:"usage",children:"Usage"}),"\n",(0,s.jsx)(n.p,{children:"Import plugin:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-ts",children:"import ForeignInlineListPlugin from 'adminforth/plugins/ForeignInlineListPlugin';\n"})}),"\n",(0,s.jsx)(n.p,{children:"If you are using pure Node without TypeScript, you can use the following code:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:"import ForeignInlineListPlugin from 'adminforth/dist/plugins/ForeignInlineListPlugin/index.js';\n"})}),"\n",(0,s.jsxs)(n.p,{children:["In ",(0,s.jsx)(n.a,{href:"/docs/tutorial/gettingStarted",children:"Getting Started"})," we created a ",(0,s.jsx)(n.code,{children:"'aparts'"})," resource which has a field ",(0,s.jsx)(n.code,{children:"'realtor_id'"}),".\nThis field refers to record from ",(0,s.jsx)(n.code,{children:"'users'"})," resource. This means that we can display a list of appartments in the user's show view."]}),"\n",(0,s.jsxs)(n.p,{children:["Add to your ",(0,s.jsx)(n.code,{children:"'users'"})," resource configuration (which we created in ), plugin instance:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-ts",children:"{ \n ...\n resourceId: 'users',\n ...\n plugins: [\n new ForeignInlineListPlugin({\n foreignResourceId: 'aparts',\n modifyTableResourceConfig: (resourceConfig: AdminForthResource) => {\n // hide column 'square_meter' from both 'list' and 'filter'\n const column = resourceConfig.columns.find((c: AdminForthResourceColumn) => c.name === 'square_meter')!.showIn = [];\n resourceConfig.options!.listPageSize = 1;\n\n // feel free to console.log and edit resourceConfig as you need\n },\n }),\n ],\n}\n"})}),"\n",(0,s.jsxs)(n.p,{children:["You can use ",(0,s.jsx)(n.code,{children:"modifyTableResourceConfig"})," callback to modify what columns to show in the list and filter of the foreign table."]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"alt text",src:i(3986).A+"",width:"3670",height:"2044"})}),"\n",(0,s.jsxs)(n.p,{children:["See ",(0,s.jsx)(n.a,{href:"/docs/api/plugins/ForeignInlineListPlugin/types/type-aliases/PluginOptions",children:"API Reference"})," for more all options."]})]})}function d(e={}){const{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(u,{...e})}):u(e)}},3986:(e,n,i)=>{i.d(n,{A:()=>s});const s=i.p+"assets/images/localhost_3500_resource_users_show_08dpfh-a8f5bd63bb09e071c67a13d7121e95fd.png"},8453:(e,n,i)=>{i.d(n,{R:()=>r,x:()=>l});var s=i(6540);const t={},o=s.createContext(t);function r(e){const n=s.useContext(o);return s.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:r(e.components),s.createElement(o.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/322eff50.eac3ea2e.js b/assets/js/322eff50.eac3ea2e.js deleted file mode 100644 index b1d9fb561..000000000 --- a/assets/js/322eff50.eac3ea2e.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkadminforth=self.webpackChunkadminforth||[]).push([[6956],{2143:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>l,contentTitle:()=>i,default:()=>u,frontMatter:()=>s,metadata:()=>o,toc:()=>d});var a=t(4848),r=t(8453);const s={},i="Getting Started",o={id:"tutorial/gettingStarted",title:"Getting Started",description:"Prerequisites",source:"@site/docs/tutorial/01-gettingStarted.md",sourceDirName:"tutorial",slug:"/tutorial/gettingStarted",permalink:"/docs/tutorial/gettingStarted",draft:!1,unlisted:!1,tags:[],version:"current",sidebarPosition:1,frontMatter:{},sidebar:"tutorialSidebar",next:{title:"Glossary",permalink:"/docs/tutorial/glossary"}},l={},d=[{value:"Prerequisites",id:"prerequisites",level:2},{value:"Installation",id:"installation",level:2},{value:"Basic Philosophy",id:"basic-philosophy",level:2},{value:"Setting up a first demo",id:"setting-up-a-first-demo",level:2},{value:"Possible configuration options",id:"possible-configuration-options",level:2}];function c(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",img:"img",p:"p",pre:"pre",...(0,r.R)(),...e.components};return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(n.h1,{id:"getting-started",children:"Getting Started"}),"\n",(0,a.jsx)(n.h2,{id:"prerequisites",children:"Prerequisites"}),"\n",(0,a.jsx)(n.p,{children:"We recommend using Node v18 and higher:"}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-bash",children:"nvm install 18\nnvm alias default 18\nnvm use 18\n"})}),"\n",(0,a.jsx)(n.h2,{id:"installation",children:"Installation"}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-bash",children:"mkdir myadmin\ncd myadmin\nnpm install adminforth\n"})}),"\n",(0,a.jsxs)(n.p,{children:["AdminForth does not provide own HTTP server, but can add own listeners over exisitng ",(0,a.jsx)(n.a,{href:"https://expressjs.com/",children:"Express"})," server (Fastify support is planned in future). This allows to create custom APIs for backoffice in a way you know."]}),"\n",(0,a.jsx)(n.p,{children:"Let's install express:"}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-bash",children:"npm install express@4.19.2\n"})}),"\n",(0,a.jsx)(n.p,{children:"For demo purposes we will use SQLite data source. You can use postgress, Mongo or Clickhouse as well."}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-bash",children:"npm install better-sqlite3@10.0.0\n"})}),"\n",(0,a.jsx)(n.p,{children:"You can use adminforth in pure Node, but we recommend using TypeScript for better development experience:"}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-bash",children:"npm install typescript@5.4.5 tsx@4.11.2 --save-dev\n"})}),"\n",(0,a.jsx)(n.h2,{id:"basic-philosophy",children:"Basic Philosophy"}),"\n",(0,a.jsx)(n.p,{children:"AdminForth connects to existing databases and provides a backoffice for managing data including CRUD operations, filtering, sorting, and more."}),"\n",(0,a.jsx)(n.p,{children:"Database should be already created by using any database management tool, ORM or migrator. AdminForth does not provide a way to create tables or columns in the database."}),"\n",(0,a.jsx)(n.p,{children:'Once you have a database, you pass a connection string to AdminForth and define resources(tables) and columns you would like to see in backoffice. For most DB AdminForth can "discover" column types and constraints (e.g. max-length) by connecting to DB. However you can redefine them in AdminForth configuration. Type and constraints definition are take precedence over DB schema.'}),"\n",(0,a.jsx)(n.p,{children:'Also in AdminForth you can define in "Vue" way how each field will be rendered, and create own pages e.g. Dashboards.'}),"\n",(0,a.jsxs)(n.p,{children:["In the demo we will create a simple database with 2 tables: ",(0,a.jsx)(n.code,{children:"apartments"})," and ",(0,a.jsx)(n.code,{children:"users"}),". We will just use plain SQL to create tables and insert some fake data."]}),"\n",(0,a.jsx)(n.p,{children:"Users table will be used to store a credentials for login into backoffice itself."}),"\n",(0,a.jsx)(n.h2,{id:"setting-up-a-first-demo",children:"Setting up a first demo"}),"\n",(0,a.jsxs)(n.p,{children:["Open ",(0,a.jsx)(n.code,{children:"package.json"}),", set ",(0,a.jsx)(n.code,{children:"type"})," to ",(0,a.jsx)(n.code,{children:"module"})," and add ",(0,a.jsx)(n.code,{children:"start"})," script:"]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-json",children:'{\n ...\n "type": "module",\n "scripts": {\n ...\n "start": "ADMINFORTH_SECRET=CHANGE_ME_IN_PRODUCTION NODE_ENV=development tsx watch index.ts"\n },\n}\n'})}),"\n",(0,a.jsxs)(n.p,{children:["Create ",(0,a.jsx)(n.code,{children:"index.ts"})," file in root directory with following content:"]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-ts",children:"import betterSqlite3 from 'better-sqlite3';\nimport express from 'express';\nimport AdminForth from 'adminforth';\n\nconst DB_FILE = 'test.sqlite';\nlet db;\n\nconst ADMIN_BASE_URL = '';\n\nexport const admin = new AdminForth({\n baseUrl : ADMIN_BASE_URL,\n rootUser: {\n username: 'adminforth', // use these as credentials to login\n password: 'adminforth',\n },\n auth: {\n resourceId: 'users', // resource for getting user\n usernameField: 'email',\n passwordHashField: 'password_hash',\n },\n customization: {\n brandName: 'My Admin',\n datesFormat: 'D MMM YY HH:mm:ss',\n emptyFieldPlaceholder: '-',\n },\n\n dataSources: [\n {\n id: 'maindb',\n url: `sqlite://${DB_FILE}`\n },\n ],\n resources: [\n {\n dataSource: 'maindb', \n table: 'apartments',\n resourceId: 'aparts', // resourceId is defaulted to table name but you can change it e.g. \n // in case of same table names from different data sources\n label: 'Apartments', // label is defaulted to table name but you can change it\n recordLabel: (r) => `\ud83c\udfe1 ${r.title}`,\n columns: [\n { \n name: 'id', \n label: 'Identifier', // if you wish you can redefine label\n showIn: ['filter', 'show'], // show in filter and in show page\n primaryKey: true,\n fillOnCreate: ({initialRecord, adminUser}) => Math.random().toString(36).substring(7), // initialRecord is values user entered, adminUser object of user who creates record\n },\n { \n name: 'title',\n required: true,\n showIn: ['list', 'create', 'edit', 'filter', 'show'], // the default is full set\n maxLength: 255, // you can set max length for string fields\n minLength: 3, // you can set min length for string fields\n }, \n {\n name: 'created_at',\n type: AdminForth.Types.DATETIME ,\n allowMinMaxQuery: true,\n showIn: ['list', 'filter', 'show', 'edit'],\n fillOnCreate: ({initialRecord, adminUser}) => (new Date()).toISOString(),\n },\n { \n name: 'price',\n min: 10,\n max: 10000.12,\n allowMinMaxQuery: true, // use better experience for filtering e.g. date range, set it only if you have index on this column or if there will be low number of rows\n editingNote: 'Price is in USD', // you can appear note on editing or creating page\n },\n { \n name: 'square_meter', \n label: 'Square', \n allowMinMaxQuery: true,\n minValue: 1, // you can set min /max value for number fields\n maxValue: 1000,\n },\n { \n name: 'number_of_rooms',\n allowMinMaxQuery: true,\n enum: [\n { value: 1, label: '1 room' },\n { value: 2, label: '2 rooms' },\n { value: 3, label: '3 rooms' },\n { value: 4, label: '4 rooms' },\n { value: 5, label: '5 rooms' },\n ],\n },\n { \n name: 'description',\n sortable: false,\n showIn: ['show', 'edit', 'create', 'filter'],\n },\n {\n name: 'country',\n enum: [{\n value: 'US',\n label: 'United States'\n }, {\n value: 'DE',\n label: 'Germany'\n }, {\n value: 'FR',\n label: 'France'\n }, {\n value: 'UK',\n label: 'United Kingdom'\n }, {\n value:'NL',\n label: 'Netherlands'\n }, {\n value: 'IT',\n label: 'Italy'\n }, {\n value: 'ES',\n label: 'Spain'\n }, {\n value: 'DK',\n label: 'Denmark'\n }, {\n value: 'PL',\n label: 'Poland'\n }, {\n value: 'UA',\n label: 'Ukraine'\n }, {\n value: null,\n label: 'Not defined'\n }],\n },\n {\n name: 'listed',\n required: true, // will be required on create/edit\n },\n {\n name: 'realtor_id',\n foreignResource: {\n resourceId: 'users',\n }\n }\n ],\n options: {\n listPageSize: 12,\n allowedActions:{\n edit: true,\n delete: true,\n show: true,\n filter: true,\n },\n },\n },\n { \n dataSource: 'maindb', \n table: 'users',\n resourceId: 'users',\n label: 'Users', \n recordLabel: (r) => `\ud83d\udc64 ${r.email}`,\n columns: [\n { \n name: 'id', \n primaryKey: true,\n fillOnCreate: ({initialRecord, adminUser}) => Math.random().toString(36).substring(7),\n showIn: ['list', 'filter', 'show'],\n },\n { \n name: 'email', \n required: true,\n isUnique: true,\n validation: [\n {\n regExp: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,}$',\n message: 'Email is not valid, must be in format example@test.com'\n },\n ]\n },\n { \n name: 'created_at', \n type: AdminForth.Types.DATETIME,\n showIn: ['list', 'filter', 'show'],\n fillOnCreate: ({initialRecord, adminUser}) => (new Date()).toISOString(),\n },\n {\n name: 'role',\n enum: [\n { value: 'superadmin', label: 'Super Admin' },\n { value: 'user', label: 'User' },\n ]\n },\n {\n name: 'password',\n virtual: true, // field will not be persisted into db\n required: { create: true }, // make required only on create page\n editingNote: { edit: 'Leave empty to keep password unchanged' },\n minLength: 8,\n type: AdminForth.Types.STRING,\n showIn: ['create', 'edit'], // to show field only on create and edit pages\n masked: true, // to show stars in input field\n }\n ],\n hooks: {\n create: {\n beforeSave: async ({ record, adminUser, resource }) => {\n record.password_hash = await AdminForth.Utils.generatePasswordHash(record.password);\n return { ok: true };\n }\n },\n edit: {\n beforeSave: async ({ record, adminUser, resource}) => {\n if (record.password) {\n record.password_hash = await AdminForth.Utils.generatePasswordHash(record.password);\n }\n return { ok: true }\n },\n },\n }\n },\n ],\n menu: [\n {\n label: 'Core',\n icon: 'flowbite:brain-solid', // any icon from iconify supported in format :, e.g. from here https://icon-sets.iconify.design/flowbite/\n open: true,\n children: [\n {\n homepage: true,\n label: 'Appartments',\n icon: 'flowbite:home-solid',\n resourceId: 'aparts',\n },\n ]\n },\n {\n type: 'gap'\n },\n {\n type: 'divider'\n },\n {\n type: 'heading',\n label: 'SYSTEM',\n },\n {\n label: 'Users',\n icon: 'flowbite:user-solid',\n resourceId: 'users',\n }\n ],\n});\n\n\nasync function initDataBase() {\n db = betterSqlite3(DB_FILE);\n\n const tableExists = db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='apartments';`).get();\n if (!tableExists) {\n // if no table - create couple of tables and fill them with some mock data\n await db.prepare(`\n CREATE TABLE apartments (\n id VARCHAR(20) PRIMARY KEY NOT NULL,\n title VARCHAR(255) NOT NULL,\n square_meter REAL,\n price DECIMAL(10, 2) NOT NULL,\n number_of_rooms INT,\n description TEXT,\n country VARCHAR(2),\n listed BOOLEAN DEFAULT FALSE,\n created_at TIMESTAMP,\n realtor_id VARCHAR(255)\n );`).run();\n\n await db.prepare(`\n CREATE TABLE users (\n id VARCHAR(255) PRIMARY KEY NOT NULL,\n email VARCHAR(255) NOT NULL,\n password_hash VARCHAR(255) NOT NULL,\n created_at VARCHAR(255) NOT NULL,\n role VARCHAR(255) NOT NULL\n );`).run();\n\n for (let i = 0; i < 100; i++) {\n await db.prepare(`\n INSERT INTO apartments (\n id, title, square_meter, price, \n number_of_rooms, description, \n created_at, listed, \n country\n ) VALUES (\n '${i}', 'Apartment ${i}', ${(Math.random() * 100).toFixed(1)}, ${(Math.random() * 10000).toFixed(2)}, \n ${ Math.floor(Math.random() * 5) }, 'Next gen apartments', \n ${ Date.now() / 1000 - Math.random() * 14 * 60 * 60 * 24 }, ${i % 2 == 0}, \n '${['US', 'DE', 'FR', 'UK', 'NL', 'IT', 'ES', 'DK', 'PL', 'UA'][Math.floor(Math.random() * 10)]}'\n )`).run();\n }\n }\n}\n\n\nif (import.meta.url === `file://${process.argv[1]}`) {\n // if script is executed directly e.g. node index.ts or npm start\n\n await initDataBase();\n\n const app = express()\n app.use(express.json());\n const port = 3500;\n\n (async () => {\n // needed to compile SPA. Call it here or from a build script e.g. in Docker build time to reduce downtime\n await admin.bundleNow({ hotReload: process.env.NODE_ENV === 'development'});\n console.log('Bundling AdminForth done. For faster serving consider calling bundleNow() from a build script.');\n })();\n\n\n // serve after you added all api\n admin.express.serve(app)\n admin.discoverDatabases();\n\n\n app.listen(port, () => {\n console.log(`Example app listening at http://localhost:${port}`)\n console.log(`\\n\u26a1 AdminForth is available at http://localhost:${port}${ADMIN_BASE_URL}\\n`)\n });\n}\n"})}),"\n",(0,a.jsx)(n.p,{children:"Now you can run your app:"}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-bash",children:"npm start\n"})}),"\n",(0,a.jsxs)(n.p,{children:["Open ",(0,a.jsx)(n.a,{href:"http://localhost:3500",children:"http://localhost:3500"})," in your browser and login with credentials ",(0,a.jsx)(n.code,{children:"adminforth"})," / ",(0,a.jsx)(n.code,{children:"adminforth"}),"."]}),"\n",(0,a.jsx)(n.p,{children:(0,a.jsx)(n.img,{alt:"alt text",src:t(8332).A+"",width:"2428",height:"1932"})}),"\n",(0,a.jsxs)(n.p,{children:["After Login you should see:\n",(0,a.jsx)(n.img,{alt:"alt text",src:t(4973).A+"",width:"3700",height:"1932"})]}),"\n",(0,a.jsx)(n.h2,{id:"possible-configuration-options",children:"Possible configuration options"}),"\n",(0,a.jsx)(n.p,{children:"We will use schema with different column types for apartments to show many of AdminForth features."}),"\n",(0,a.jsxs)(n.p,{children:["Check ",(0,a.jsx)(n.a,{href:"/docs/api/types/AdminForthConfig/type-aliases/AdminForthConfig",children:"AdminForthConfig"})," for all possible options."]})]})}function u(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,a.jsx)(n,{...e,children:(0,a.jsx)(c,{...e})}):c(e)}},8332:(e,n,t)=>{t.d(n,{A:()=>a});const a=t.p+"assets/images/localhost_3500_login-22b59511349c51948267c9a4080e4d87.png"},4973:(e,n,t)=>{t.d(n,{A:()=>a});const a=t.p+"assets/images/localhost_3500_resource_aparts-dddac951816a2a7b58c84b6348828ecb.png"},8453:(e,n,t)=>{t.d(n,{R:()=>i,x:()=>o});var a=t(6540);const r={},s=a.createContext(r);function i(e){const n=a.useContext(s);return a.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function o(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:i(e.components),a.createElement(s.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/322eff50.fe38aa95.js b/assets/js/322eff50.fe38aa95.js new file mode 100644 index 000000000..ec357d48e --- /dev/null +++ b/assets/js/322eff50.fe38aa95.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkadminforth=self.webpackChunkadminforth||[]).push([[6956],{2143:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>l,contentTitle:()=>i,default:()=>u,frontMatter:()=>s,metadata:()=>o,toc:()=>d});var a=t(4848),r=t(8453);const s={},i="Getting Started",o={id:"tutorial/gettingStarted",title:"Getting Started",description:"Prerequisites",source:"@site/docs/tutorial/01-gettingStarted.md",sourceDirName:"tutorial",slug:"/tutorial/gettingStarted",permalink:"/docs/tutorial/gettingStarted",draft:!1,unlisted:!1,tags:[],version:"current",sidebarPosition:1,frontMatter:{},sidebar:"tutorialSidebar",next:{title:"Glossary",permalink:"/docs/tutorial/glossary"}},l={},d=[{value:"Prerequisites",id:"prerequisites",level:2},{value:"Installation",id:"installation",level:2},{value:"Basic Philosophy",id:"basic-philosophy",level:2},{value:"Setting up a first demo",id:"setting-up-a-first-demo",level:2},{value:"Possible configuration options",id:"possible-configuration-options",level:2}];function c(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",img:"img",p:"p",pre:"pre",...(0,r.R)(),...e.components};return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(n.h1,{id:"getting-started",children:"Getting Started"}),"\n",(0,a.jsx)(n.h2,{id:"prerequisites",children:"Prerequisites"}),"\n",(0,a.jsx)(n.p,{children:"We recommend using Node v18 and higher:"}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-bash",children:"nvm install 18\nnvm alias default 18\nnvm use 18\n"})}),"\n",(0,a.jsx)(n.h2,{id:"installation",children:"Installation"}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-bash",children:"mkdir myadmin\ncd myadmin\nnpm install adminforth\n"})}),"\n",(0,a.jsxs)(n.p,{children:["AdminForth does not provide own HTTP server, but can add own listeners over exisitng ",(0,a.jsx)(n.a,{href:"https://expressjs.com/",children:"Express"})," server (Fastify support is planned in future). This allows to create custom APIs for backoffice in a way you know."]}),"\n",(0,a.jsx)(n.p,{children:"Let's install express:"}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-bash",children:"npm install express@4.19.2\n"})}),"\n",(0,a.jsx)(n.p,{children:"For demo purposes we will use SQLite data source. You can use postgress, Mongo or Clickhouse as well."}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-bash",children:"npm install better-sqlite3@10.0.0\n"})}),"\n",(0,a.jsx)(n.p,{children:"You can use adminforth in pure Node, but we recommend using TypeScript for better development experience:"}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-bash",children:"npm install typescript@5.4.5 tsx@4.11.2 --save-dev\n"})}),"\n",(0,a.jsx)(n.h2,{id:"basic-philosophy",children:"Basic Philosophy"}),"\n",(0,a.jsx)(n.p,{children:"AdminForth connects to existing databases and provides a backoffice for managing data including CRUD operations, filtering, sorting, and more."}),"\n",(0,a.jsx)(n.p,{children:"Database should be already created by using any database management tool, ORM or migrator. AdminForth does not provide a way to create tables or columns in the database."}),"\n",(0,a.jsx)(n.p,{children:'Once you have a database, you pass a connection string to AdminForth and define resources(tables) and columns you would like to see in backoffice. For most DB AdminForth can "discover" column types and constraints (e.g. max-length) by connecting to DB. However you can redefine them in AdminForth configuration. Type and constraints definition are take precedence over DB schema.'}),"\n",(0,a.jsx)(n.p,{children:'Also in AdminForth you can define in "Vue" way how each field will be rendered, and create own pages e.g. Dashboards.'}),"\n",(0,a.jsxs)(n.p,{children:["In the demo we will create a simple database with 2 tables: ",(0,a.jsx)(n.code,{children:"apartments"})," and ",(0,a.jsx)(n.code,{children:"users"}),". We will just use plain SQL to create tables and insert some fake data."]}),"\n",(0,a.jsx)(n.p,{children:"Users table will be used to store a credentials for login into backoffice itself."}),"\n",(0,a.jsx)(n.h2,{id:"setting-up-a-first-demo",children:"Setting up a first demo"}),"\n",(0,a.jsxs)(n.p,{children:["Open ",(0,a.jsx)(n.code,{children:"package.json"}),", set ",(0,a.jsx)(n.code,{children:"type"})," to ",(0,a.jsx)(n.code,{children:"module"})," and add ",(0,a.jsx)(n.code,{children:"start"})," script:"]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-json",metastring:'title="./package.json"',children:'{\n ...\n//diff-add\n "type": "module",\n "scripts": {\n ...\n//diff-add\n "start": "ADMINFORTH_SECRET=CHANGE_ME_IN_PRODUCTION NODE_ENV=development tsx watch index.ts"\n },\n}\n'})}),"\n",(0,a.jsxs)(n.p,{children:["Create ",(0,a.jsx)(n.code,{children:"index.ts"})," file in root directory with following content:"]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-ts",metastring:'title="./index.ts"',children:"import betterSqlite3 from 'better-sqlite3';\nimport express from 'express';\nimport AdminForth from 'adminforth';\n\nconst DB_FILE = 'test.sqlite';\nlet db;\n\nconst ADMIN_BASE_URL = '';\n\nexport const admin = new AdminForth({\n baseUrl : ADMIN_BASE_URL,\n rootUser: {\n username: 'adminforth', // use these as credentials to login\n password: 'adminforth',\n },\n auth: {\n resourceId: 'users', // resource for getting user\n usernameField: 'email',\n passwordHashField: 'password_hash',\n },\n customization: {\n brandName: 'My Admin',\n datesFormat: 'D MMM YY HH:mm:ss',\n emptyFieldPlaceholder: '-',\n },\n\n dataSources: [\n {\n id: 'maindb',\n url: `sqlite://${DB_FILE}`\n },\n ],\n resources: [\n {\n dataSource: 'maindb', \n table: 'apartments',\n resourceId: 'aparts', // resourceId is defaulted to table name but you can change it e.g. \n // in case of same table names from different data sources\n label: 'Apartments', // label is defaulted to table name but you can change it\n recordLabel: (r) => `\ud83c\udfe1 ${r.title}`,\n columns: [\n { \n name: 'id', \n label: 'Identifier', // if you wish you can redefine label\n showIn: ['filter', 'show'], // show in filter and in show page\n primaryKey: true,\n fillOnCreate: ({initialRecord, adminUser}) => Math.random().toString(36).substring(7), // initialRecord is values user entered, adminUser object of user who creates record\n },\n { \n name: 'title',\n required: true,\n showIn: ['list', 'create', 'edit', 'filter', 'show'], // the default is full set\n maxLength: 255, // you can set max length for string fields\n minLength: 3, // you can set min length for string fields\n }, \n {\n name: 'created_at',\n type: AdminForth.Types.DATETIME ,\n allowMinMaxQuery: true,\n showIn: ['list', 'filter', 'show', 'edit'],\n fillOnCreate: ({initialRecord, adminUser}) => (new Date()).toISOString(),\n },\n { \n name: 'price',\n min: 10,\n max: 10000.12,\n allowMinMaxQuery: true, // use better experience for filtering e.g. date range, set it only if you have index on this column or if there will be low number of rows\n editingNote: 'Price is in USD', // you can appear note on editing or creating page\n },\n { \n name: 'square_meter', \n label: 'Square', \n allowMinMaxQuery: true,\n minValue: 1, // you can set min /max value for number fields\n maxValue: 1000,\n },\n { \n name: 'number_of_rooms',\n allowMinMaxQuery: true,\n enum: [\n { value: 1, label: '1 room' },\n { value: 2, label: '2 rooms' },\n { value: 3, label: '3 rooms' },\n { value: 4, label: '4 rooms' },\n { value: 5, label: '5 rooms' },\n ],\n },\n { \n name: 'description',\n sortable: false,\n showIn: ['show', 'edit', 'create', 'filter'],\n },\n {\n name: 'country',\n enum: [{\n value: 'US',\n label: 'United States'\n }, {\n value: 'DE',\n label: 'Germany'\n }, {\n value: 'FR',\n label: 'France'\n }, {\n value: 'UK',\n label: 'United Kingdom'\n }, {\n value:'NL',\n label: 'Netherlands'\n }, {\n value: 'IT',\n label: 'Italy'\n }, {\n value: 'ES',\n label: 'Spain'\n }, {\n value: 'DK',\n label: 'Denmark'\n }, {\n value: 'PL',\n label: 'Poland'\n }, {\n value: 'UA',\n label: 'Ukraine'\n }, {\n value: null,\n label: 'Not defined'\n }],\n },\n {\n name: 'listed',\n required: true, // will be required on create/edit\n },\n {\n name: 'realtor_id',\n foreignResource: {\n resourceId: 'users',\n }\n }\n ],\n options: {\n listPageSize: 12,\n allowedActions:{\n edit: true,\n delete: true,\n show: true,\n filter: true,\n },\n },\n },\n { \n dataSource: 'maindb', \n table: 'users',\n resourceId: 'users',\n label: 'Users', \n recordLabel: (r) => `\ud83d\udc64 ${r.email}`,\n columns: [\n { \n name: 'id', \n primaryKey: true,\n fillOnCreate: ({initialRecord, adminUser}) => Math.random().toString(36).substring(7),\n showIn: ['list', 'filter', 'show'],\n },\n { \n name: 'email', \n required: true,\n isUnique: true,\n validation: [\n {\n regExp: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,}$',\n message: 'Email is not valid, must be in format example@test.com'\n },\n ]\n },\n { \n name: 'created_at', \n type: AdminForth.Types.DATETIME,\n showIn: ['list', 'filter', 'show'],\n fillOnCreate: ({initialRecord, adminUser}) => (new Date()).toISOString(),\n },\n {\n name: 'role',\n enum: [\n { value: 'superadmin', label: 'Super Admin' },\n { value: 'user', label: 'User' },\n ]\n },\n {\n name: 'password',\n virtual: true, // field will not be persisted into db\n required: { create: true }, // make required only on create page\n editingNote: { edit: 'Leave empty to keep password unchanged' },\n minLength: 8,\n type: AdminForth.Types.STRING,\n showIn: ['create', 'edit'], // to show field only on create and edit pages\n masked: true, // to show stars in input field\n }\n ],\n hooks: {\n create: {\n beforeSave: async ({ record, adminUser, resource }) => {\n record.password_hash = await AdminForth.Utils.generatePasswordHash(record.password);\n return { ok: true };\n }\n },\n edit: {\n beforeSave: async ({ record, adminUser, resource}) => {\n if (record.password) {\n record.password_hash = await AdminForth.Utils.generatePasswordHash(record.password);\n }\n return { ok: true }\n },\n },\n }\n },\n ],\n menu: [\n {\n label: 'Core',\n icon: 'flowbite:brain-solid', // any icon from iconify supported in format :, e.g. from here https://icon-sets.iconify.design/flowbite/\n open: true,\n children: [\n {\n homepage: true,\n label: 'Appartments',\n icon: 'flowbite:home-solid',\n resourceId: 'aparts',\n },\n ]\n },\n {\n type: 'gap'\n },\n {\n type: 'divider'\n },\n {\n type: 'heading',\n label: 'SYSTEM',\n },\n {\n label: 'Users',\n icon: 'flowbite:user-solid',\n resourceId: 'users',\n }\n ],\n});\n\n\nasync function initDataBase() {\n db = betterSqlite3(DB_FILE);\n\n const tableExists = db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='apartments';`).get();\n if (!tableExists) {\n // if no table - create couple of tables and fill them with some mock data\n await db.prepare(`\n CREATE TABLE apartments (\n id VARCHAR(20) PRIMARY KEY NOT NULL,\n title VARCHAR(255) NOT NULL,\n square_meter REAL,\n price DECIMAL(10, 2) NOT NULL,\n number_of_rooms INT,\n description TEXT,\n country VARCHAR(2),\n listed BOOLEAN DEFAULT FALSE,\n created_at TIMESTAMP,\n realtor_id VARCHAR(255)\n );`).run();\n\n await db.prepare(`\n CREATE TABLE users (\n id VARCHAR(255) PRIMARY KEY NOT NULL,\n email VARCHAR(255) NOT NULL,\n password_hash VARCHAR(255) NOT NULL,\n created_at VARCHAR(255) NOT NULL,\n role VARCHAR(255) NOT NULL\n );`).run();\n\n for (let i = 0; i < 100; i++) {\n await db.prepare(`\n INSERT INTO apartments (\n id, title, square_meter, price, \n number_of_rooms, description, \n created_at, listed, \n country\n ) VALUES (\n '${i}', 'Apartment ${i}', ${(Math.random() * 100).toFixed(1)}, ${(Math.random() * 10000).toFixed(2)}, \n ${ Math.floor(Math.random() * 5) }, 'Next gen apartments', \n ${ Date.now() / 1000 - Math.random() * 14 * 60 * 60 * 24 }, ${i % 2 == 0}, \n '${['US', 'DE', 'FR', 'UK', 'NL', 'IT', 'ES', 'DK', 'PL', 'UA'][Math.floor(Math.random() * 10)]}'\n )`).run();\n }\n }\n}\n\n\nif (import.meta.url === `file://${process.argv[1]}`) {\n // if script is executed directly e.g. node index.ts or npm start\n\n await initDataBase();\n\n const app = express()\n app.use(express.json());\n const port = 3500;\n\n (async () => {\n // needed to compile SPA. Call it here or from a build script e.g. in Docker build time to reduce downtime\n await admin.bundleNow({ hotReload: process.env.NODE_ENV === 'development'});\n console.log('Bundling AdminForth done. For faster serving consider calling bundleNow() from a build script.');\n })();\n\n\n // serve after you added all api\n admin.express.serve(app)\n admin.discoverDatabases();\n\n\n app.listen(port, () => {\n console.log(`Example app listening at http://localhost:${port}`)\n console.log(`\\n\u26a1 AdminForth is available at http://localhost:${port}${ADMIN_BASE_URL}\\n`)\n });\n}\n"})}),"\n",(0,a.jsx)(n.p,{children:"Now you can run your app:"}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-bash",children:"npm start\n"})}),"\n",(0,a.jsxs)(n.p,{children:["Open ",(0,a.jsx)(n.a,{href:"http://localhost:3500",children:"http://localhost:3500"})," in your browser and login with credentials ",(0,a.jsx)(n.code,{children:"adminforth"})," / ",(0,a.jsx)(n.code,{children:"adminforth"}),"."]}),"\n",(0,a.jsx)(n.p,{children:(0,a.jsx)(n.img,{alt:"alt text",src:t(8332).A+"",width:"2428",height:"1932"})}),"\n",(0,a.jsxs)(n.p,{children:["After Login you should see:\n",(0,a.jsx)(n.img,{alt:"alt text",src:t(4973).A+"",width:"3700",height:"1932"})]}),"\n",(0,a.jsx)(n.h2,{id:"possible-configuration-options",children:"Possible configuration options"}),"\n",(0,a.jsx)(n.p,{children:"We will use schema with different column types for apartments to show many of AdminForth features."}),"\n",(0,a.jsxs)(n.p,{children:["Check ",(0,a.jsx)(n.a,{href:"/docs/api/types/AdminForthConfig/type-aliases/AdminForthConfig",children:"AdminForthConfig"})," for all possible options."]})]})}function u(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,a.jsx)(n,{...e,children:(0,a.jsx)(c,{...e})}):c(e)}},8332:(e,n,t)=>{t.d(n,{A:()=>a});const a=t.p+"assets/images/localhost_3500_login-22b59511349c51948267c9a4080e4d87.png"},4973:(e,n,t)=>{t.d(n,{A:()=>a});const a=t.p+"assets/images/localhost_3500_resource_aparts-dddac951816a2a7b58c84b6348828ecb.png"},8453:(e,n,t)=>{t.d(n,{R:()=>i,x:()=>o});var a=t(6540);const r={},s=a.createContext(r);function i(e){const n=a.useContext(s);return a.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function o(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:i(e.components),a.createElement(s.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/813b1aa0.a50518e8.js b/assets/js/813b1aa0.a50518e8.js new file mode 100644 index 000000000..5920236aa --- /dev/null +++ b/assets/js/813b1aa0.a50518e8.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkadminforth=self.webpackChunkadminforth||[]).push([[9996],{7276:(e,n,d)=>{d.r(n),d.d(n,{assets:()=>o,contentTitle:()=>r,default:()=>u,frontMatter:()=>a,metadata:()=>s,toc:()=>l});var i=d(4848),t=d(8453);const a={},r=void 0,s={id:"tutorial/Plugins/AuditLog",title:"AuditLog",description:"AuditLog plugin allows to limit access to the resource actions (list, show, create, update, delete) based on custom callback.",source:"@site/docs/tutorial/Plugins/AuditLog.md",sourceDirName:"tutorial/Plugins",slug:"/tutorial/Plugins/AuditLog",permalink:"/docs/tutorial/Plugins/AuditLog",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Deploy in Docker",permalink:"/docs/tutorial/deploy"},next:{title:"ForeignInlineList",permalink:"/docs/tutorial/Plugins/ForeignInlineList"}},o={},l=[{value:"Installation",id:"installation",level:2},{value:"Creating table for storing activity data",id:"creating-table-for-storing-activity-data",level:2},{value:"Setting up the resource and dataSource for plugin",id:"setting-up-the-resource-and-datasource-for-plugin",level:2}];function c(e){const n={a:"a",code:"code",h2:"h2",img:"img",p:"p",pre:"pre",...(0,t.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsxs)(n.p,{children:["AuditLog plugin allows to limit access to the resource actions (list, show, create, update, delete) based on custom callback.\nCallback accepts ",(0,i.jsx)(n.a,{href:"/docs/api/types/AdminForthConfig/type-aliases/AdminUser/",children:"AdminUser"})," which you can use to define access rules."]}),"\n",(0,i.jsx)(n.h2,{id:"installation",children:"Installation"}),"\n",(0,i.jsx)(n.p,{children:"Plugin is already installed into adminforth, to import:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"import AuditLogPlugin from 'adminforth/plugins/AuditLogPlugin';\n"})}),"\n",(0,i.jsx)(n.p,{children:"If yu are using pure Node without TypeScript, you can use the following code:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-js",children:"import AuditLogPlugin from 'adminforth/dist/plugins/AuditLogPlugin/index.ts';\n"})}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.a,{href:"/docs/tutorial/gettingStarted",children:"Getting Started"})," will be used as base for this example."]}),"\n",(0,i.jsx)(n.h2,{id:"creating-table-for-storing-activity-data",children:"Creating table for storing activity data"}),"\n",(0,i.jsx)(n.p,{children:"For the first, to track records changes, we need to set up the database and table with certain fields inside where tracked data will be stored."}),"\n",(0,i.jsxs)(n.p,{children:["First of all you should create this table in your own database. In our example we will continue using SQLite database we created\nin ",(0,i.jsx)(n.a,{href:"/docs/tutorial/gettingStarted",children:"Getting Started"}),". Just add the following code to the end of ",(0,i.jsx)(n.code,{children:"initDataBase"})," function in ",(0,i.jsx)(n.code,{children:"index.ts"})," file:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",metastring:"title='./index.ts'",children:"async function initDataBase() {\n ...\n//diff-add\n const auditTableExists = db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='audit_logs';`).get();\n//diff-add\n if (!auditTableExists) {\n//diff-add\n await db.prepare(`\n//diff-add\n CREATE TABLE audit_logs (\n//diff-add\n id uuid NOT NULL, -- identifier of applied change record \n//diff-add\n created_at timestamp without time zone, -- timestamp of applied change\n//diff-add\n resource_id varchar(255), -- identifier of resource where change were applied\n//diff-add\n user_id uuid, -- identifier of user who made the changes\n//diff-add\n \"action\" varchar(255), -- type of change (create, edit, delete)\n//diff-add\n diff text, -- delta betwen before/after versions\n//diff-add\n record_id varchar, -- identifier of record that been changed\n//diff-add\n PRIMARY KEY(id)\n//diff-add\n );`).run();\n//diff-add\n }\n}\n"})}),"\n",(0,i.jsx)(n.p,{children:"Also to make this code start"}),"\n",(0,i.jsx)(n.h2,{id:"setting-up-the-resource-and-datasource-for-plugin",children:"Setting up the resource and dataSource for plugin"}),"\n",(0,i.jsx)(n.p,{children:'Logger sets up for all the resources by default. But you can exclude unwanted resources with option "excludeResourceIds". In this example, we\'ll exclude resource "users" from logging.'}),"\n",(0,i.jsx)(n.p,{children:"Also, it excludes itself to avoid infinte logging loop."}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",metastring:"title='./index.ts'",children:" resources: [\n ...\n//diff-add\n {\n//diff-add\n dataSource: 'maindb', \n//diff-add\n table: 'audit_logs',\n//diff-add\n columns: [\n//diff-add\n { name: 'id', primaryKey: true, required: false, fillOnCreate: ({initialRecord}: any) => uuid() },\n//diff-add\n { name: 'created_at', required: false },\n//diff-add\n { name: 'resource_id', required: false },\n//diff-add\n { name: 'user_id', required: false },\n//diff-add\n { name: 'action', required: false },\n//diff-add\n { name: 'diff', required: false },\n//diff-add\n { name: 'record_id', required: false },\n//diff-add\n ],\n//diff-add\n options: {\n//diff-add\n allowedActions: {\n//diff-add\n edit: false,\n//diff-add\n delete: false,\n//diff-add\n }\n//diff-add\n },\n//diff-add\n plugins: [\n//diff-add\n new AuditLogPlugin({\n//diff-add\n // if you want to exclude some resources from logging\n//diff-add\n //excludeResourceIds: ['users'],\n//diff-add\n resourceColumns: {\n//diff-add\n resourceIdColumnName: 'resource_id',\n//diff-add\n resourceActionColumnName: 'action',\n//diff-add\n resourceDataColumnName: 'diff',\n//diff-add\n resourceUserIdColumnName: 'user_id',\n//diff-add\n resourceRecordIdColumnName: 'record_id',\n//diff-add\n resourceCreatedColumnName: 'created_at'\n//diff-add\n }\n//diff-add\n }),\n//diff-add\n ],\n//diff-add\n }\n ]\n"})}),"\n",(0,i.jsx)(n.p,{children:"Also, we need to add it to menu:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"menu: [\n ...\n//diff-add\n {\n//diff-add\n label: 'Audit Logs',\n//diff-add\n icon: 'flowbite:search-outline',\n//diff-add\n resourceId: 'audit_logs',\n//diff-add\n }\n]\n"})}),"\n",(0,i.jsx)(n.p,{children:"That's it! Now you can see the logs in the table"}),"\n",(0,i.jsxs)(n.p,{children:['< replace this screenshot, make same but "Audit Logs" should be in menu >\n',(0,i.jsx)(n.img,{alt:"alt text",src:d(6746).A+"",width:"3811",height:"1251"})]}),"\n",(0,i.jsxs)(n.p,{children:["See ",(0,i.jsx)(n.a,{href:"/docs/api/plugins/AuditLogPlugin/types/type-aliases/PluginOptions",children:"API Reference"})," for more all options."]})]})}function u(e={}){const{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(c,{...e})}):c(e)}},6746:(e,n,d)=>{d.d(n,{A:()=>i});const i=d.p+"assets/images/localhost_3500_resource_audit_logs-8996765a6d534b48c3e52b9949e4cc89.png"},8453:(e,n,d)=>{d.d(n,{R:()=>r,x:()=>s});var i=d(6540);const t={},a=i.createContext(t);function r(e){const n=i.useContext(a);return i.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function s(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:r(e.components),i.createElement(a.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/813b1aa0.e330a24c.js b/assets/js/813b1aa0.e330a24c.js deleted file mode 100644 index ce6401c30..000000000 --- a/assets/js/813b1aa0.e330a24c.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkadminforth=self.webpackChunkadminforth||[]).push([[9996],{7276:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>l,contentTitle:()=>s,default:()=>u,frontMatter:()=>a,metadata:()=>o,toc:()=>d});var i=t(4848),r=t(8453);const a={},s=void 0,o={id:"tutorial/Plugins/AuditLog",title:"AuditLog",description:"AuditLog plugin allows to limit access to the resource actions (list, show, create, update, delete) based on custom callback.",source:"@site/docs/tutorial/Plugins/AuditLog.md",sourceDirName:"tutorial/Plugins",slug:"/tutorial/Plugins/AuditLog",permalink:"/docs/tutorial/Plugins/AuditLog",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Deploy in Docker",permalink:"/docs/tutorial/deploy"},next:{title:"ForeignInlineList",permalink:"/docs/tutorial/Plugins/ForeignInlineList"}},l={},d=[{value:"Installation",id:"installation",level:2},{value:"Creating table for storing activity data",id:"creating-table-for-storing-activity-data",level:2},{value:"Setting up the resource and dataSource for plugin",id:"setting-up-the-resource-and-datasource-for-plugin",level:2}];function c(e){const n={a:"a",code:"code",h2:"h2",img:"img",p:"p",pre:"pre",...(0,r.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsxs)(n.p,{children:["AuditLog plugin allows to limit access to the resource actions (list, show, create, update, delete) based on custom callback.\nCallback accepts ",(0,i.jsx)(n.a,{href:"/docs/api/types/AdminForthConfig/type-aliases/AdminUser/",children:"AdminUser"})," which you can use to define access rules."]}),"\n",(0,i.jsx)(n.h2,{id:"installation",children:"Installation"}),"\n",(0,i.jsx)(n.p,{children:"Plugin is already installed into adminforth, to import:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"import AuditLogPlugin from 'adminforth/plugins/AuditLogPlugin';\n"})}),"\n",(0,i.jsx)(n.p,{children:"If yu are using pure Node without TypeScript, you can use the following code:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-js",children:"import AuditLogPlugin from 'adminforth/dist/plugins/AuditLogPlugin/index.ts';\n"})}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.a,{href:"/docs/tutorial/gettingStarted",children:"Getting Started"})," will be used as base for this example."]}),"\n",(0,i.jsx)(n.h2,{id:"creating-table-for-storing-activity-data",children:"Creating table for storing activity data"}),"\n",(0,i.jsx)(n.p,{children:"For the first, to track records changes, we need to set up the database and table with certain fields inside where tracked data will be stored."}),"\n",(0,i.jsx)(n.p,{children:"In this example, you should create this table in your own database:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-sql",children:'CREATE TABLE audit_logs(\n id uuid NOT NULL, -- identifier of applied change record \n created_at timestamp without time zone, -- timestamp of applied change\n resource_id varchar(255), -- identifier of resource where change were applied\n user_id uuid, -- identifier of user who made the changes\n "action" varchar(255), -- type of change (create, edit, delete)\n diff text, -- delta betwen before/after versions\n record_id varchar, -- identifier of record that been changed\n PRIMARY KEY(id)\n);\n'})}),"\n",(0,i.jsx)(n.h2,{id:"setting-up-the-resource-and-datasource-for-plugin",children:"Setting up the resource and dataSource for plugin"}),"\n",(0,i.jsx)(n.p,{children:'Logger sets up for all the resources by default. But you can exclude unwanted resources with option "excludeResourceIds". In this example, we\'ll exclude resource "users" from logging.'}),"\n",(0,i.jsx)(n.p,{children:"Also, it excludes itself to avoid infinte logging loop."}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:" dataSources: [\n ...\n {\n id: 'db2',\n url: '',\n },\n ],\n resources: [\n ...\n {\n dataSource: 'db2', table: 'audit_logs',\n columns: [\n { name: 'id', primaryKey: true, required: false, fillOnCreate: ({initialRecord}: any) => uuid() },\n { name: 'created_at', required: false },\n { name: 'resource_id', required: false },\n { name: 'user_id', required: false },\n { name: 'action', required: false },\n { name: 'diff', required: false },\n { name: 'record_id', required: false },\n ],\n options: {\n allowedActions: {\n edit: false,\n delete: false,\n }\n },\n plugins: [\n new AuditLogPlugin({\n excludeResourceIds: ['users'],\n resourceColumns: {\n resourceIdColumnName: 'resource_id',\n resourceActionColumnName: 'action',\n resourceDataColumnName: 'diff',\n resourceUserIdColumnName: 'user_id',\n resourceRecordIdColumnName: 'record_id',\n resourceCreatedColumnName: 'created_at'\n }\n }),\n ],\n }\n ]\n"})}),"\n",(0,i.jsx)(n.p,{children:"Also, we need to add it to menu:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"{\n label: 'Logs',\n icon: 'flowbite:search-outline',\n resourceId: 'audit_logs',\n}\n"})}),"\n",(0,i.jsx)(n.p,{children:"That's it! Now you can see the logs in the table"}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.img,{alt:"alt text",src:t(6746).A+"",width:"3811",height:"1251"})}),"\n",(0,i.jsxs)(n.p,{children:["See ",(0,i.jsx)(n.a,{href:"/docs/api/plugins/AuditLogPlugin/types/type-aliases/PluginOptions",children:"API Reference"})," for more all options."]})]})}function u(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(c,{...e})}):c(e)}},6746:(e,n,t)=>{t.d(n,{A:()=>i});const i=t.p+"assets/images/localhost_3500_resource_audit_logs-8996765a6d534b48c3e52b9949e4cc89.png"},8453:(e,n,t)=>{t.d(n,{R:()=>s,x:()=>o});var i=t(6540);const r={},a=i.createContext(r);function s(e){const n=i.useContext(a);return i.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function o(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:s(e.components),i.createElement(a.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/runtime~main.f9ad0425.js b/assets/js/runtime~main.6a4e2ffc.js similarity index 97% rename from assets/js/runtime~main.f9ad0425.js rename to assets/js/runtime~main.6a4e2ffc.js index 9ecdd09f5..857a121be 100644 --- a/assets/js/runtime~main.f9ad0425.js +++ b/assets/js/runtime~main.6a4e2ffc.js @@ -1 +1 @@ -(()=>{"use strict";var e,a,f,c,d,b={},t={};function r(e){var a=t[e];if(void 0!==a)return a.exports;var f=t[e]={exports:{}};return b[e].call(f.exports,f,f.exports,r),f.exports}r.m=b,e=[],r.O=(a,f,c,d)=>{if(!f){var b=1/0;for(i=0;i=d)&&Object.keys(r.O).every((e=>r.O[e](f[o])))?f.splice(o--,1):(t=!1,d0&&e[i-1][2]>d;i--)e[i]=e[i-1];e[i]=[f,c,d]},r.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return r.d(a,{a:a}),a},f=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,r.t=function(e,c){if(1&c&&(e=this(e)),8&c)return e;if("object"==typeof e&&e){if(4&c&&e.__esModule)return e;if(16&c&&"function"==typeof e.then)return e}var d=Object.create(null);r.r(d);var b={};a=a||[null,f({}),f([]),f(f)];for(var t=2&c&&e;"object"==typeof t&&!~a.indexOf(t);t=f(t))Object.getOwnPropertyNames(t).forEach((a=>b[a]=()=>e[a]));return b.default=()=>e,r.d(d,b),d},r.d=(e,a)=>{for(var f in a)r.o(a,f)&&!r.o(e,f)&&Object.defineProperty(e,f,{enumerable:!0,get:a[f]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce(((a,f)=>(r.f[f](e,a),a)),[])),r.u=e=>"assets/js/"+({72:"325586e3",191:"096dedec",381:"485ec31e",406:"9c41ee96",594:"5e8c322a",845:"e2509f05",849:"0058b4c6",957:"c141421f",1101:"a2d8571d",1122:"ed5bf3ae",1235:"a7456010",1259:"ce9294c7",1339:"e401bd4d",1343:"695ba452",1485:"3ccf3569",1768:"0f6f7c87",1903:"acecf23e",1972:"73664a40",2138:"1a4e3797",2262:"e6fecdbd",2409:"02ba3073",2452:"d63a007d",2467:"0963d772",2560:"3b483d89",2583:"c89cbdb5",2711:"9e4087bc",2925:"857d4e3f",3212:"d4284aa7",3249:"ccc49370",3276:"e5aefb32",3403:"a74188c8",3520:"322a4173",3531:"f5a78775",3637:"f4f34a3a",3694:"8717b14a",3738:"c40bfeb3",3755:"8b12d01d",3800:"0058754d",3896:"7b6accd5",3919:"bbbca958",3925:"a4048931",3976:"bb00feff",4134:"393be207",4196:"af5821cc",4346:"f995d61e",4583:"1df93b7f",4584:"f82cd581",4813:"6875c492",5064:"fbaff884",5557:"d9f32620",5742:"aba21aa0",6061:"1f391b9e",6190:"6480425b",6352:"7bbf514f",6354:"3b4d2dbf",6507:"1bb47565",6594:"35105669",6708:"afb10fe8",6956:"322eff50",6991:"5e825ac7",7098:"a7bd4aaa",7184:"1e563bf9",7472:"814f3328",7556:"c401fedd",7643:"a6aa9e1f",7758:"bb551ae2",7865:"024de627",7930:"50bcd713",8025:"5e90a9b3",8121:"3a2db09e",8130:"f81c1134",8146:"c15d9823",8209:"01a85c17",8246:"9faee089",8266:"953f1d2e",8275:"694c912d",8401:"17896441",8462:"3217192f",8609:"925b3f96",8737:"7661071f",8830:"45d7547e",8910:"8a5469a7",9048:"a94703ab",9088:"6edf06a3",9325:"59362658",9328:"e273c56f",9647:"5e95c892",9729:"f87cbaa6",9773:"347ca734",9858:"36994c47",9873:"ca1b7bc1",9996:"813b1aa0"}[e]||e)+"."+{72:"2140e734",191:"30379ecf",381:"3b8764e1",406:"0bc9789f",416:"8fe0370d",594:"27078a25",845:"4ca4b571",849:"fa2156de",957:"534e51bb",1101:"569688b7",1122:"ce9fb296",1235:"01c51c11",1259:"f9970bc7",1339:"21be5ca2",1343:"49e6d7bb",1485:"7e6f0b85",1768:"c6cf02a5",1903:"408603ab",1972:"1dbb218a",2138:"3b9f1811",2237:"964ba571",2262:"73bb9dd1",2409:"5186eff1",2452:"705f2514",2467:"593cf592",2560:"528eda14",2583:"5aa3eaf2",2711:"594ee1b1",2925:"3bbe26c4",3212:"81d46ac2",3242:"eff6ba00",3249:"e48ced1d",3276:"074405ba",3403:"f942db12",3520:"52711a4b",3531:"dd9cc360",3637:"f878fecd",3694:"defcb0a0",3738:"d1baf258",3755:"3541dc88",3800:"5a65823b",3896:"6478b48e",3919:"13ea875d",3925:"3c625ea2",3976:"65d19b50",4134:"eb56c4bb",4196:"e4f3bb8b",4346:"79f390d6",4583:"b24c0f41",4584:"f58dc1df",4813:"6d49a07f",5064:"b7f7a2be",5557:"64356def",5742:"5b35ab57",6061:"d8686820",6190:"eed7dcce",6352:"73b26f58",6354:"f165ea6c",6507:"e54fee76",6594:"573430e0",6708:"7b643256",6956:"eac3ea2e",6991:"9f8d5c5e",7098:"1ccb7c00",7184:"d24d8c92",7472:"7b603e5e",7556:"83af0b6b",7643:"848cc9ea",7758:"1ad75dcd",7865:"8360fc62",7930:"46c0faad",8025:"eeeb6ef1",8121:"70c9a4ae",8130:"6e2c5413",8146:"3e8fd083",8209:"e63d16a5",8246:"9857c5d5",8266:"94c28e4e",8275:"f68d2bc2",8401:"50ee32b7",8462:"9a878f15",8498:"28fb9d4d",8609:"7d402955",8737:"bdcf50cf",8830:"36b76f50",8910:"8d0addb0",8913:"a57d5824",9048:"672a973b",9088:"8fb9bf65",9325:"8c339f08",9328:"2490c944",9462:"12b53926",9647:"757d1c74",9729:"e10aa3f9",9773:"9270cf4a",9858:"e2f41c0d",9873:"c5ad8a65",9996:"e330a24c"}[e]+".js",r.miniCssF=e=>{},r.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),r.o=(e,a)=>Object.prototype.hasOwnProperty.call(e,a),c={},d="adminforth:",r.l=(e,a,f,b)=>{if(c[e])c[e].push(a);else{var t,o;if(void 0!==f)for(var n=document.getElementsByTagName("script"),i=0;i{t.onerror=t.onload=null,clearTimeout(s);var d=c[e];if(delete c[e],t.parentNode&&t.parentNode.removeChild(t),d&&d.forEach((e=>e(f))),a)return a(f)},s=setTimeout(l.bind(null,void 0,{type:"timeout",target:t}),12e4);t.onerror=l.bind(null,t.onerror),t.onload=l.bind(null,t.onload),o&&document.head.appendChild(t)}},r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.p="/",r.gca=function(e){return e={17896441:"8401",35105669:"6594",59362658:"9325","325586e3":"72","096dedec":"191","485ec31e":"381","9c41ee96":"406","5e8c322a":"594",e2509f05:"845","0058b4c6":"849",c141421f:"957",a2d8571d:"1101",ed5bf3ae:"1122",a7456010:"1235",ce9294c7:"1259",e401bd4d:"1339","695ba452":"1343","3ccf3569":"1485","0f6f7c87":"1768",acecf23e:"1903","73664a40":"1972","1a4e3797":"2138",e6fecdbd:"2262","02ba3073":"2409",d63a007d:"2452","0963d772":"2467","3b483d89":"2560",c89cbdb5:"2583","9e4087bc":"2711","857d4e3f":"2925",d4284aa7:"3212",ccc49370:"3249",e5aefb32:"3276",a74188c8:"3403","322a4173":"3520",f5a78775:"3531",f4f34a3a:"3637","8717b14a":"3694",c40bfeb3:"3738","8b12d01d":"3755","0058754d":"3800","7b6accd5":"3896",bbbca958:"3919",a4048931:"3925",bb00feff:"3976","393be207":"4134",af5821cc:"4196",f995d61e:"4346","1df93b7f":"4583",f82cd581:"4584","6875c492":"4813",fbaff884:"5064",d9f32620:"5557",aba21aa0:"5742","1f391b9e":"6061","6480425b":"6190","7bbf514f":"6352","3b4d2dbf":"6354","1bb47565":"6507",afb10fe8:"6708","322eff50":"6956","5e825ac7":"6991",a7bd4aaa:"7098","1e563bf9":"7184","814f3328":"7472",c401fedd:"7556",a6aa9e1f:"7643",bb551ae2:"7758","024de627":"7865","50bcd713":"7930","5e90a9b3":"8025","3a2db09e":"8121",f81c1134:"8130",c15d9823:"8146","01a85c17":"8209","9faee089":"8246","953f1d2e":"8266","694c912d":"8275","3217192f":"8462","925b3f96":"8609","7661071f":"8737","45d7547e":"8830","8a5469a7":"8910",a94703ab:"9048","6edf06a3":"9088",e273c56f:"9328","5e95c892":"9647",f87cbaa6:"9729","347ca734":"9773","36994c47":"9858",ca1b7bc1:"9873","813b1aa0":"9996"}[e]||e,r.p+r.u(e)},(()=>{var e={5354:0,1869:0};r.f.j=(a,f)=>{var c=r.o(e,a)?e[a]:void 0;if(0!==c)if(c)f.push(c[2]);else if(/^(1869|5354)$/.test(a))e[a]=0;else{var d=new Promise(((f,d)=>c=e[a]=[f,d]));f.push(c[2]=d);var b=r.p+r.u(a),t=new Error;r.l(b,(f=>{if(r.o(e,a)&&(0!==(c=e[a])&&(e[a]=void 0),c)){var d=f&&("load"===f.type?"missing":f.type),b=f&&f.target&&f.target.src;t.message="Loading chunk "+a+" failed.\n("+d+": "+b+")",t.name="ChunkLoadError",t.type=d,t.request=b,c[1](t)}}),"chunk-"+a,a)}},r.O.j=a=>0===e[a];var a=(a,f)=>{var c,d,b=f[0],t=f[1],o=f[2],n=0;if(b.some((a=>0!==e[a]))){for(c in t)r.o(t,c)&&(r.m[c]=t[c]);if(o)var i=o(r)}for(a&&a(f);n{"use strict";var e,a,f,c,d,b={},t={};function r(e){var a=t[e];if(void 0!==a)return a.exports;var f=t[e]={exports:{}};return b[e].call(f.exports,f,f.exports,r),f.exports}r.m=b,e=[],r.O=(a,f,c,d)=>{if(!f){var b=1/0;for(i=0;i=d)&&Object.keys(r.O).every((e=>r.O[e](f[o])))?f.splice(o--,1):(t=!1,d0&&e[i-1][2]>d;i--)e[i]=e[i-1];e[i]=[f,c,d]},r.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return r.d(a,{a:a}),a},f=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,r.t=function(e,c){if(1&c&&(e=this(e)),8&c)return e;if("object"==typeof e&&e){if(4&c&&e.__esModule)return e;if(16&c&&"function"==typeof e.then)return e}var d=Object.create(null);r.r(d);var b={};a=a||[null,f({}),f([]),f(f)];for(var t=2&c&&e;"object"==typeof t&&!~a.indexOf(t);t=f(t))Object.getOwnPropertyNames(t).forEach((a=>b[a]=()=>e[a]));return b.default=()=>e,r.d(d,b),d},r.d=(e,a)=>{for(var f in a)r.o(a,f)&&!r.o(e,f)&&Object.defineProperty(e,f,{enumerable:!0,get:a[f]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce(((a,f)=>(r.f[f](e,a),a)),[])),r.u=e=>"assets/js/"+({72:"325586e3",191:"096dedec",381:"485ec31e",406:"9c41ee96",594:"5e8c322a",845:"e2509f05",849:"0058b4c6",957:"c141421f",1101:"a2d8571d",1122:"ed5bf3ae",1235:"a7456010",1259:"ce9294c7",1339:"e401bd4d",1343:"695ba452",1485:"3ccf3569",1768:"0f6f7c87",1903:"acecf23e",1972:"73664a40",2138:"1a4e3797",2262:"e6fecdbd",2409:"02ba3073",2452:"d63a007d",2467:"0963d772",2560:"3b483d89",2583:"c89cbdb5",2711:"9e4087bc",2925:"857d4e3f",3212:"d4284aa7",3249:"ccc49370",3276:"e5aefb32",3403:"a74188c8",3520:"322a4173",3531:"f5a78775",3637:"f4f34a3a",3694:"8717b14a",3738:"c40bfeb3",3755:"8b12d01d",3800:"0058754d",3896:"7b6accd5",3919:"bbbca958",3925:"a4048931",3976:"bb00feff",4134:"393be207",4196:"af5821cc",4346:"f995d61e",4583:"1df93b7f",4584:"f82cd581",4813:"6875c492",5064:"fbaff884",5557:"d9f32620",5742:"aba21aa0",6061:"1f391b9e",6190:"6480425b",6352:"7bbf514f",6354:"3b4d2dbf",6507:"1bb47565",6594:"35105669",6708:"afb10fe8",6956:"322eff50",6991:"5e825ac7",7098:"a7bd4aaa",7184:"1e563bf9",7472:"814f3328",7556:"c401fedd",7643:"a6aa9e1f",7758:"bb551ae2",7865:"024de627",7930:"50bcd713",8025:"5e90a9b3",8121:"3a2db09e",8130:"f81c1134",8146:"c15d9823",8209:"01a85c17",8246:"9faee089",8266:"953f1d2e",8275:"694c912d",8401:"17896441",8462:"3217192f",8609:"925b3f96",8737:"7661071f",8830:"45d7547e",8910:"8a5469a7",9048:"a94703ab",9088:"6edf06a3",9325:"59362658",9328:"e273c56f",9647:"5e95c892",9729:"f87cbaa6",9773:"347ca734",9858:"36994c47",9873:"ca1b7bc1",9996:"813b1aa0"}[e]||e)+"."+{72:"2140e734",191:"30379ecf",381:"3b8764e1",406:"0bc9789f",416:"8fe0370d",594:"27078a25",845:"4ca4b571",849:"fa2156de",957:"534e51bb",1101:"569688b7",1122:"ce9fb296",1235:"01c51c11",1259:"f9970bc7",1339:"21be5ca2",1343:"49e6d7bb",1485:"7e6f0b85",1768:"c6cf02a5",1903:"408603ab",1972:"1dbb218a",2138:"3b9f1811",2237:"964ba571",2262:"73bb9dd1",2409:"5186eff1",2452:"705f2514",2467:"593cf592",2560:"528eda14",2583:"5aa3eaf2",2711:"594ee1b1",2925:"3bbe26c4",3212:"81d46ac2",3242:"eff6ba00",3249:"e48ced1d",3276:"074405ba",3403:"f942db12",3520:"52711a4b",3531:"dd9cc360",3637:"f878fecd",3694:"defcb0a0",3738:"d1baf258",3755:"3541dc88",3800:"0dc70dcf",3896:"6478b48e",3919:"13ea875d",3925:"3c625ea2",3976:"65d19b50",4134:"eb56c4bb",4196:"e4f3bb8b",4346:"79f390d6",4583:"b24c0f41",4584:"f58dc1df",4813:"6d49a07f",5064:"b7f7a2be",5557:"64356def",5742:"5b35ab57",6061:"d8686820",6190:"eed7dcce",6352:"73b26f58",6354:"f165ea6c",6507:"e54fee76",6594:"573430e0",6708:"7b643256",6956:"fe38aa95",6991:"9f8d5c5e",7098:"1ccb7c00",7184:"d24d8c92",7472:"7b603e5e",7556:"83af0b6b",7643:"848cc9ea",7758:"1ad75dcd",7865:"8360fc62",7930:"46c0faad",8025:"eeeb6ef1",8121:"70c9a4ae",8130:"6e2c5413",8146:"3e8fd083",8209:"e63d16a5",8246:"9857c5d5",8266:"94c28e4e",8275:"f68d2bc2",8401:"50ee32b7",8462:"9a878f15",8498:"28fb9d4d",8609:"7d402955",8737:"bdcf50cf",8830:"36b76f50",8910:"8d0addb0",8913:"a57d5824",9048:"672a973b",9088:"8fb9bf65",9325:"8c339f08",9328:"2490c944",9462:"12b53926",9647:"757d1c74",9729:"e10aa3f9",9773:"9270cf4a",9858:"e2f41c0d",9873:"c5ad8a65",9996:"a50518e8"}[e]+".js",r.miniCssF=e=>{},r.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),r.o=(e,a)=>Object.prototype.hasOwnProperty.call(e,a),c={},d="adminforth:",r.l=(e,a,f,b)=>{if(c[e])c[e].push(a);else{var t,o;if(void 0!==f)for(var n=document.getElementsByTagName("script"),i=0;i{t.onerror=t.onload=null,clearTimeout(s);var d=c[e];if(delete c[e],t.parentNode&&t.parentNode.removeChild(t),d&&d.forEach((e=>e(f))),a)return a(f)},s=setTimeout(l.bind(null,void 0,{type:"timeout",target:t}),12e4);t.onerror=l.bind(null,t.onerror),t.onload=l.bind(null,t.onload),o&&document.head.appendChild(t)}},r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.p="/",r.gca=function(e){return e={17896441:"8401",35105669:"6594",59362658:"9325","325586e3":"72","096dedec":"191","485ec31e":"381","9c41ee96":"406","5e8c322a":"594",e2509f05:"845","0058b4c6":"849",c141421f:"957",a2d8571d:"1101",ed5bf3ae:"1122",a7456010:"1235",ce9294c7:"1259",e401bd4d:"1339","695ba452":"1343","3ccf3569":"1485","0f6f7c87":"1768",acecf23e:"1903","73664a40":"1972","1a4e3797":"2138",e6fecdbd:"2262","02ba3073":"2409",d63a007d:"2452","0963d772":"2467","3b483d89":"2560",c89cbdb5:"2583","9e4087bc":"2711","857d4e3f":"2925",d4284aa7:"3212",ccc49370:"3249",e5aefb32:"3276",a74188c8:"3403","322a4173":"3520",f5a78775:"3531",f4f34a3a:"3637","8717b14a":"3694",c40bfeb3:"3738","8b12d01d":"3755","0058754d":"3800","7b6accd5":"3896",bbbca958:"3919",a4048931:"3925",bb00feff:"3976","393be207":"4134",af5821cc:"4196",f995d61e:"4346","1df93b7f":"4583",f82cd581:"4584","6875c492":"4813",fbaff884:"5064",d9f32620:"5557",aba21aa0:"5742","1f391b9e":"6061","6480425b":"6190","7bbf514f":"6352","3b4d2dbf":"6354","1bb47565":"6507",afb10fe8:"6708","322eff50":"6956","5e825ac7":"6991",a7bd4aaa:"7098","1e563bf9":"7184","814f3328":"7472",c401fedd:"7556",a6aa9e1f:"7643",bb551ae2:"7758","024de627":"7865","50bcd713":"7930","5e90a9b3":"8025","3a2db09e":"8121",f81c1134:"8130",c15d9823:"8146","01a85c17":"8209","9faee089":"8246","953f1d2e":"8266","694c912d":"8275","3217192f":"8462","925b3f96":"8609","7661071f":"8737","45d7547e":"8830","8a5469a7":"8910",a94703ab:"9048","6edf06a3":"9088",e273c56f:"9328","5e95c892":"9647",f87cbaa6:"9729","347ca734":"9773","36994c47":"9858",ca1b7bc1:"9873","813b1aa0":"9996"}[e]||e,r.p+r.u(e)},(()=>{var e={5354:0,1869:0};r.f.j=(a,f)=>{var c=r.o(e,a)?e[a]:void 0;if(0!==c)if(c)f.push(c[2]);else if(/^(1869|5354)$/.test(a))e[a]=0;else{var d=new Promise(((f,d)=>c=e[a]=[f,d]));f.push(c[2]=d);var b=r.p+r.u(a),t=new Error;r.l(b,(f=>{if(r.o(e,a)&&(0!==(c=e[a])&&(e[a]=void 0),c)){var d=f&&("load"===f.type?"missing":f.type),b=f&&f.target&&f.target.src;t.message="Loading chunk "+a+" failed.\n("+d+": "+b+")",t.name="ChunkLoadError",t.type=d,t.request=b,c[1](t)}}),"chunk-"+a,a)}},r.O.j=a=>0===e[a];var a=(a,f)=>{var c,d,b=f[0],t=f[1],o=f[2],n=0;if(b.some((a=>0!==e[a]))){for(c in t)r.o(t,c)&&(r.m[c]=t[c]);if(o)var i=o(r)}for(a&&a(f);n