├── .dockerignore
├── src
├── themes
│ ├── custom-theme
│ │ ├── style.css
│ │ ├── components
│ │ │ ├── LoggedIn.tsx
│ │ │ └── SidebarFooter.tsx
│ │ ├── overrides.ts
│ │ ├── index.ts
│ │ └── theme.bundle.js
│ └── index.ts
├── sources
│ ├── typeorm
│ │ ├── interfaces.ts
│ │ ├── enums
│ │ │ └── country.enum.ts
│ │ ├── models
│ │ │ ├── index.ts
│ │ │ ├── organization.entity.ts
│ │ │ └── person.entity.ts
│ │ ├── seeds
│ │ │ ├── data
│ │ │ │ ├── index.ts
│ │ │ │ ├── persons.ts
│ │ │ │ └── organizations.ts
│ │ │ └── run.ts
│ │ ├── resources
│ │ │ ├── index.ts
│ │ │ ├── organization.resource.ts
│ │ │ └── person.resource.ts
│ │ ├── handlers
│ │ │ └── validate-email.handler.ts
│ │ ├── config.ts
│ │ └── migrations
│ │ │ └── 1644569575919-init.ts
│ ├── mikroorm
│ │ ├── models
│ │ │ ├── index.ts
│ │ │ ├── seller.model.ts
│ │ │ ├── car.model.ts
│ │ │ └── owner.model.ts
│ │ ├── seeds
│ │ │ ├── data
│ │ │ │ ├── index.ts
│ │ │ │ ├── sellers.ts
│ │ │ │ ├── owners.ts
│ │ │ │ └── cars.ts
│ │ │ └── run.ts
│ │ ├── resources
│ │ │ ├── index.ts
│ │ │ ├── seller.resource.ts
│ │ │ ├── owner.resource.ts
│ │ │ └── car.resource.ts
│ │ ├── config.ts
│ │ └── migrations
│ │ │ └── Migration20220714094312.ts
│ ├── objectionjs
│ │ ├── resources
│ │ │ ├── index.ts
│ │ │ ├── office.resource.ts
│ │ │ └── manager.resource.ts
│ │ ├── models
│ │ │ ├── index.ts
│ │ │ ├── manager.entity.ts
│ │ │ └── office.entity.ts
│ │ ├── knexfile.cjs
│ │ ├── migrations
│ │ │ └── 20220826123456_initial_schema.cjs
│ │ └── utils
│ │ │ └── base-model.ts
│ ├── prisma
│ │ ├── migrations
│ │ │ ├── migration_lock.toml
│ │ │ └── 20220218111814_init
│ │ │ │ └── migration.sql
│ │ ├── seeds
│ │ │ ├── data
│ │ │ │ ├── index.ts
│ │ │ │ ├── profiles.ts
│ │ │ │ ├── publishers.ts
│ │ │ │ └── posts.ts
│ │ │ └── run.ts
│ │ ├── resources
│ │ │ ├── index.ts
│ │ │ ├── profile.resource.ts
│ │ │ ├── publisher.resource.ts
│ │ │ └── post.resource.ts
│ │ └── config.ts
│ ├── sequelize
│ │ ├── models
│ │ │ ├── index.ts
│ │ │ ├── category.model.ts
│ │ │ ├── order.model.ts
│ │ │ ├── product.model.ts
│ │ │ └── cart.model.ts
│ │ ├── seeds
│ │ │ ├── data
│ │ │ │ ├── index.ts
│ │ │ │ ├── categories.ts
│ │ │ │ ├── carts.ts
│ │ │ │ ├── products.ts
│ │ │ │ └── orders.ts
│ │ │ └── run.ts
│ │ ├── resources
│ │ │ ├── index.ts
│ │ │ ├── cart.resource.ts
│ │ │ ├── category.resource.ts
│ │ │ ├── product.resource.ts
│ │ │ └── order.resource.ts
│ │ ├── interfaces.ts
│ │ ├── config.js
│ │ ├── index.ts
│ │ ├── functions
│ │ │ └── get-sum.function.ts
│ │ ├── hooks
│ │ │ ├── get-sum.hook.ts
│ │ │ └── get-products.hook.ts
│ │ └── migrations
│ │ │ └── 20220214101918-init.cjs
│ ├── mongoose
│ │ ├── seeds
│ │ │ ├── data
│ │ │ │ ├── index.ts
│ │ │ │ ├── comments.ts
│ │ │ │ ├── users.ts
│ │ │ │ ├── categories.ts
│ │ │ │ └── articles.ts
│ │ │ └── run.ts
│ │ ├── models
│ │ │ ├── index.ts
│ │ │ ├── admin.model.ts
│ │ │ ├── comment.model.ts
│ │ │ ├── category.model.ts
│ │ │ ├── user.model.ts
│ │ │ ├── article.model.ts
│ │ │ └── complicated.model.ts
│ │ └── resources
│ │ │ ├── index.ts
│ │ │ ├── user.resource.ts
│ │ │ ├── complicated.resource.ts
│ │ │ ├── comment.resource.ts
│ │ │ ├── category.resource.ts
│ │ │ ├── article.resource.ts
│ │ │ └── admin.resource.ts
│ └── rest
│ │ └── crypto-database.ts
├── admin
│ ├── locale
│ │ ├── en
│ │ │ ├── pages.json
│ │ │ ├── complicated.json
│ │ │ ├── person.json
│ │ │ ├── components.json
│ │ │ ├── common.json
│ │ │ └── index.ts
│ │ ├── de
│ │ │ ├── pages.json
│ │ │ ├── common.json
│ │ │ ├── components.json
│ │ │ └── index.ts
│ │ └── index.ts
│ ├── components
│ │ ├── design-system-examples
│ │ │ ├── index.ts
│ │ │ └── modal-example.tsx
│ │ ├── debug.tsx
│ │ ├── thumb.tsx
│ │ ├── detailed-stats.tsx
│ │ ├── dont-touch-this-action.tsx
│ │ ├── sidebar-resource-section.tsx
│ │ ├── top-bar.tsx
│ │ ├── products-list.tsx
│ │ └── login.tsx
│ ├── types
│ │ └── index.ts
│ ├── admin.utils.ts
│ ├── pages
│ │ ├── design-system-examples
│ │ │ ├── modal-page.tsx
│ │ │ ├── tabs-page.tsx
│ │ │ ├── icons-page.tsx
│ │ │ ├── illustrations-page.tsx
│ │ │ ├── messages-page.tsx
│ │ │ ├── blog-page.tsx
│ │ │ ├── index.tsx
│ │ │ ├── form-page.tsx
│ │ │ ├── typography-page.tsx
│ │ │ └── buttons-page.tsx
│ │ ├── index.ts
│ │ └── custom-page.tsx
│ ├── features
│ │ └── useEnvironmentVariableToDisableActions.ts
│ ├── constants
│ │ └── authUsers.ts
│ ├── components.bundler.ts
│ ├── router.ts
│ └── index.ts
├── servers
│ ├── nestjs
│ │ ├── app.service.ts
│ │ ├── database.providers.ts
│ │ ├── prisma
│ │ │ └── prisma.service.ts
│ │ ├── admin
│ │ │ └── admin.setup.ts
│ │ ├── app.controller.ts
│ │ ├── index.ts
│ │ ├── app.module.ts
│ │ └── mongoose
│ │ │ └── mongoose.module.ts
│ ├── hapijs.ts
│ ├── fastify.ts
│ └── express
│ │ └── index.ts
├── index.ts
└── scripts
│ ├── truncate-postgres.ts
│ └── truncate-mongodb.ts
├── public
├── locales
│ ├── en
│ │ └── translation.json
│ └── mk
│ │ └── translation.json
├── favicon.ico
├── custom.css
└── gtm.js
├── Procfile
├── .prettierrc
├── prisma
├── migrations
│ ├── migration_lock.toml
│ └── 20220713101214_init
│ │ └── migration.sql
└── schema.prisma
├── nodemon.json
├── Dockerfile
├── .sequelizerc
├── .env
├── tsconfig.json
├── .github
└── workflows
│ └── push.yml
├── docker-compose.yaml
├── LICENSE
├── .eslintrc.json
├── .gitignore
├── README.md
└── package.json
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/src/themes/custom-theme/style.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/locales/en/translation.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | release: yarn migration:up:production
2 | web: yarn start
3 |
--------------------------------------------------------------------------------
/src/sources/typeorm/interfaces.ts:
--------------------------------------------------------------------------------
1 | export interface PostPayload {
2 | email: string;
3 | }
4 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoftwareBrothers/adminjs-example-app/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/admin/locale/en/pages.json:
--------------------------------------------------------------------------------
1 | {
2 | "pages": {
3 | "customPage": "Custom Page example"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/admin/components/design-system-examples/index.ts:
--------------------------------------------------------------------------------
1 | export { default as ModalExample } from './modal-example.js';
2 |
--------------------------------------------------------------------------------
/src/admin/locale/de/pages.json:
--------------------------------------------------------------------------------
1 | {
2 | "pages": {
3 | "customPage": "Benutzerdefinierte Seite"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": true,
3 | "singleQuote": true,
4 | "printWidth": 120,
5 | "trailingComma": "all"
6 | }
7 |
--------------------------------------------------------------------------------
/src/sources/typeorm/enums/country.enum.ts:
--------------------------------------------------------------------------------
1 | export enum CountryEnum {
2 | Poland = 'PL',
3 | GreatBritain = 'GB',
4 | Germany = 'DE',
5 | }
6 |
--------------------------------------------------------------------------------
/src/sources/typeorm/models/index.ts:
--------------------------------------------------------------------------------
1 | export { Organization } from './organization.entity.js';
2 | export { Person } from './person.entity.js';
3 |
--------------------------------------------------------------------------------
/src/sources/typeorm/seeds/data/index.ts:
--------------------------------------------------------------------------------
1 | export { default as organizations } from './organizations.js';
2 | export { default as persons } from './persons.js';
3 |
--------------------------------------------------------------------------------
/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "mysql"
--------------------------------------------------------------------------------
/src/admin/components/debug.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Debug = (props) =>
{JSON.stringify(props, null, 4)};
4 |
5 | export default Debug;
6 |
--------------------------------------------------------------------------------
/src/sources/mikroorm/models/index.ts:
--------------------------------------------------------------------------------
1 | export { Owner } from './owner.model.js';
2 | export { Car } from './car.model.js';
3 | export { Seller } from './seller.model.js';
4 |
--------------------------------------------------------------------------------
/src/sources/objectionjs/resources/index.ts:
--------------------------------------------------------------------------------
1 | export { CreateManagerResource } from './manager.resource.js';
2 | export { CreateOfficeResource } from './office.resource.js';
3 |
--------------------------------------------------------------------------------
/src/sources/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "mysql"
--------------------------------------------------------------------------------
/src/sources/typeorm/resources/index.ts:
--------------------------------------------------------------------------------
1 | export { CreateOrganizationResource } from './organization.resource.js';
2 | export { CreatePersonResource } from './person.resource.js';
3 |
--------------------------------------------------------------------------------
/src/sources/mikroorm/seeds/data/index.ts:
--------------------------------------------------------------------------------
1 | export { default as cars } from './cars.js';
2 | export { default as owners } from './owners.js';
3 | export { default as sellers } from './sellers.js';
4 |
--------------------------------------------------------------------------------
/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": [
3 | "dist"
4 | ],
5 | "ext": "js,json",
6 | "ignore": [
7 | "*.spec.*",
8 | "test"
9 | ],
10 | "exec": "node ./dist/index.js"
11 | }
12 |
--------------------------------------------------------------------------------
/public/custom.css:
--------------------------------------------------------------------------------
1 | html {
2 | scroll-behavior: smooth;
3 | }
4 |
5 | a {
6 | text-decoration: none;
7 | color: inherit;
8 | }
9 |
10 | a.adminjs_Header {
11 | display: block;
12 | }
13 |
--------------------------------------------------------------------------------
/src/sources/prisma/seeds/data/index.ts:
--------------------------------------------------------------------------------
1 | export { default as posts } from './posts.js';
2 | export { default as profiles } from './profiles.js';
3 | export { default as publishers } from './publishers.js';
4 |
--------------------------------------------------------------------------------
/src/themes/custom-theme/components/LoggedIn.tsx:
--------------------------------------------------------------------------------
1 | import { Box } from '@adminjs/design-system';
2 | import React from 'react';
3 |
4 | const LoggedIn = () => ;
5 |
6 | export default LoggedIn;
7 |
--------------------------------------------------------------------------------
/src/themes/custom-theme/overrides.ts:
--------------------------------------------------------------------------------
1 | import type { ThemeConfig } from 'adminjs';
2 |
3 | export const overrides: ThemeConfig['overrides'] = {
4 | colors: {
5 | primary100: '#E67E22',
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/src/servers/nestjs/app.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 |
3 | @Injectable()
4 | export class AppService {
5 | public getHello(): string {
6 | return 'Hello World!';
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/sources/mikroorm/resources/index.ts:
--------------------------------------------------------------------------------
1 | export { CreateOwnerResource } from './owner.resource.js';
2 | export { CreateCarResource } from './car.resource.js';
3 | export { CreateSellerResource } from './seller.resource.js';
4 |
--------------------------------------------------------------------------------
/src/sources/prisma/resources/index.ts:
--------------------------------------------------------------------------------
1 | export { CreatePostResource } from './post.resource.js';
2 | export { CreateProfileResource } from './profile.resource.js';
3 | export { CreatePublisherResource } from './publisher.resource.js';
4 |
--------------------------------------------------------------------------------
/src/admin/locale/en/complicated.json:
--------------------------------------------------------------------------------
1 | {
2 | "properties": {
3 | "nestedDetails.age": "Person age",
4 | "nestedDetails.height": "Person height",
5 | "nestedDetails.nested.extremelyNested": "This nesting is crazy"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/servers/nestjs/database.providers.ts:
--------------------------------------------------------------------------------
1 | import { sequelize } from '../../sources/sequelize/index.js';
2 |
3 | export const databaseProviders = [
4 | {
5 | provide: 'SEQUELIZE',
6 | useFactory: () => sequelize,
7 | },
8 | ];
9 |
--------------------------------------------------------------------------------
/src/sources/sequelize/models/index.ts:
--------------------------------------------------------------------------------
1 | export { ProductModel } from './product.model.js';
2 | export { CategoryModel } from './category.model.js';
3 | export { OrderModel } from './order.model.js';
4 | export { CartModel } from './cart.model.js';
5 |
--------------------------------------------------------------------------------
/src/sources/prisma/config.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from '@prisma/client';
2 | import { DMMFClass } from '@prisma/client/runtime';
3 |
4 | export const client = new PrismaClient();
5 |
6 | export const dmmf = (client as any)._baseDmmf as DMMFClass;
7 |
--------------------------------------------------------------------------------
/src/themes/custom-theme/index.ts:
--------------------------------------------------------------------------------
1 | import type { ThemeConfig } from 'adminjs';
2 | import { overrides } from './overrides.js';
3 |
4 | export const themeConfig: ThemeConfig = {
5 | id: 'custom-theme',
6 | name: 'custom theme',
7 | overrides,
8 | };
9 |
--------------------------------------------------------------------------------
/src/sources/sequelize/seeds/data/index.ts:
--------------------------------------------------------------------------------
1 | export { default as carts } from './carts.js';
2 | export { default as categories } from './categories.js';
3 | export { default as orders } from './orders.js';
4 | export { default as products } from './products.js';
5 |
--------------------------------------------------------------------------------
/src/sources/mongoose/seeds/data/index.ts:
--------------------------------------------------------------------------------
1 | export { default as articles } from './articles.js';
2 | export { default as categories } from './categories.js';
3 | export { default as comments } from './comments.js';
4 | export { default as users } from './users.js';
5 |
--------------------------------------------------------------------------------
/src/sources/sequelize/resources/index.ts:
--------------------------------------------------------------------------------
1 | export { CreateProductResource } from './product.resource.js';
2 | export { CreateCategoryResource } from './category.resource.js';
3 | export { CreateCartResource } from './cart.resource.js';
4 | export { CreateOrderResource } from './order.resource.js';
5 |
--------------------------------------------------------------------------------
/src/sources/prisma/seeds/data/profiles.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 |
3 | const profiles = (count: number, { publisherId }) =>
4 | Array.from({ length: count }, () => ({
5 | bio: faker.lorem.sentence(4),
6 | publisherId,
7 | }));
8 |
9 | export default profiles;
10 |
--------------------------------------------------------------------------------
/src/sources/prisma/seeds/data/publishers.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 |
3 | const publishers = (count: number) =>
4 | Array.from({ length: count }, () => ({
5 | name: faker.company.name(),
6 | email: faker.internet.email(),
7 | }));
8 |
9 | export default publishers;
10 |
--------------------------------------------------------------------------------
/src/admin/types/index.ts:
--------------------------------------------------------------------------------
1 | import type { ResourceOptions, FeatureType } from 'adminjs';
2 |
3 | export type CreateResourceResult = {
4 | resource: T;
5 | options: ResourceOptions;
6 | features?: Array;
7 | };
8 |
9 | export type ResourceFunction = () => CreateResourceResult;
10 |
--------------------------------------------------------------------------------
/src/admin/locale/de/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "messages": {
3 | "CustomPage": {
4 | "message": "Ich wurde angeklickt !!!",
5 | "messageWithInterpolation": "Übersetzte Nachrichten mit Interpolation: {{someParams}}"
6 | },
7 | "I am fetched from the backend": "Ich werde aus dem Backend geholt"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/sources/mikroorm/seeds/data/sellers.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 |
3 | import { ISeller } from '../../models/seller.model.js';
4 |
5 | const sellers = (count: number): ISeller[] =>
6 | Array.from({ length: count }, () => ({
7 | name: faker.company.name(),
8 | }));
9 |
10 | export default sellers;
11 |
--------------------------------------------------------------------------------
/src/sources/sequelize/interfaces.ts:
--------------------------------------------------------------------------------
1 | export interface ProductListInterface {
2 | products: {
3 | quantity: number;
4 | product: {
5 | id: number;
6 | name: string;
7 | price: number;
8 | };
9 | }[];
10 | }
11 |
12 | export interface ProductSumInterface {
13 | id: number;
14 | sum: number;
15 | }
16 |
--------------------------------------------------------------------------------
/src/sources/mongoose/models/index.ts:
--------------------------------------------------------------------------------
1 | export { UserModel } from './user.model.js';
2 | export { AdminModel } from './admin.model.js';
3 | export { ArticleModel } from './article.model.js';
4 | export { CategoryModel } from './category.model.js';
5 | export { CommentModel } from './comment.model.js';
6 | export { ComplicatedModel } from './complicated.model.js';
7 |
--------------------------------------------------------------------------------
/src/admin/locale/en/person.json:
--------------------------------------------------------------------------------
1 | {
2 | "labels": {
3 | "Person": "Staff"
4 | },
5 | "actions": {
6 | "dontTouchThis": "Don't touch this!!!"
7 | },
8 | "properties": {
9 | "organizationId": "Organization"
10 | },
11 | "messages": {
12 | "youCanSetupGuards": "You can setup guards before an action - just in case."
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/admin/locale/de/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "components": {
3 | "CustomPage": {
4 | "header": "Hier können Sie eine vollständig benutzerdefinierte Seite angeben",
5 | "introduction": "Mit einigen Daten, die vom Backend abgerufen wurden:",
6 | "ending": "und andere Interaktionen wie Toast :)",
7 | "button": "Klick mich"
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/sources/mongoose/models/admin.model.ts:
--------------------------------------------------------------------------------
1 | import { model, Schema } from 'mongoose';
2 |
3 | export interface Admin {
4 | email: string;
5 | password: string;
6 | }
7 |
8 | export const AdminSchema = new Schema({
9 | email: { type: 'String', required: true },
10 | password: { type: 'String', required: true },
11 | });
12 |
13 | export const AdminModel = model('Admin', AdminSchema);
14 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:16
2 |
3 | WORKDIR /admin
4 |
5 | ENV NODE_ENV="production"
6 | ENV TZ="UTC"
7 |
8 | COPY package.json ./
9 | COPY yarn.lock ./
10 |
11 | RUN yarn install --frozen-lockfile --production
12 | COPY . .
13 |
14 | RUN npm i -g typescript
15 | RUN yarn build
16 | RUN npx prisma generate
17 | RUN rm -rf src
18 |
19 | ENV ADMIN_JS_SKIP_BUNDLE="true"
20 |
21 | EXPOSE 3000
22 | CMD yarn start
23 |
--------------------------------------------------------------------------------
/src/sources/sequelize/config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | development: {
3 | use_env_variable: 'POSTGRES_DATABASE_URL',
4 | dialect: 'postgres',
5 | },
6 | production: {
7 | use_env_variable: 'POSTGRES_DATABASE_URL',
8 | dialect: 'postgres',
9 | dialectOptions: {
10 | ssl: {
11 | require: true,
12 | rejectUnauthorized: false,
13 | },
14 | },
15 | },
16 | };
17 |
--------------------------------------------------------------------------------
/.sequelizerc:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | require('dotenv').config();
3 |
4 | module.exports = {
5 | 'config': path.resolve('src', 'sources', 'sequelize', 'config.js'),
6 | 'models-path': path.resolve('src', 'sources', 'sequelize', 'models'),
7 | 'seeders-path': path.resolve('src', 'sources', 'sequelize', 'seeders'),
8 | 'migrations-path': path.resolve('src', 'sources', 'sequelize', 'migrations')
9 | }
10 |
--------------------------------------------------------------------------------
/src/admin/locale/de/index.ts:
--------------------------------------------------------------------------------
1 | import type { LocaleTranslations } from 'adminjs';
2 |
3 | import common from './common.json' assert { type: 'json' };
4 | import components from './components.json' assert { type: 'json' };
5 | import pages from './pages.json' assert { type: 'json' };
6 |
7 | const deLocale: LocaleTranslations = {
8 | ...common,
9 | ...components,
10 | ...pages,
11 | };
12 |
13 | export default deLocale;
14 |
--------------------------------------------------------------------------------
/src/sources/mongoose/resources/index.ts:
--------------------------------------------------------------------------------
1 | export { CreateAdminResource } from './admin.resource.js';
2 | export { CreateUserResource } from './user.resource.js';
3 | export { CreateArticleResource } from './article.resource.js';
4 | export { CreateCategoryResource } from './category.resource.js';
5 | export { CreateCommentResource } from './comment.resource.js';
6 | export { CreateComplicatedResource } from './complicated.resource.js';
7 |
--------------------------------------------------------------------------------
/src/admin/components/thumb.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from '@adminjs/design-system';
2 | import type { BasePropertyProps } from 'adminjs';
3 | import React, { FC } from 'react';
4 |
5 | const Thumb: FC = (props: BasePropertyProps) => {
6 | const { record, property } = props;
7 | const value = record.params[property.name];
8 |
9 | return ;
10 | };
11 |
12 | export default Thumb;
13 |
--------------------------------------------------------------------------------
/src/sources/objectionjs/models/index.ts:
--------------------------------------------------------------------------------
1 | import Knex from 'knex';
2 |
3 | import knexConfig from '../knexfile.cjs';
4 | import Office from './office.entity.js';
5 | import Manager from './manager.entity.js';
6 | import { BaseModel } from '../utils/base-model.js';
7 |
8 | const knex = BaseModel.knex(Knex.default(knexConfig[process.env.NODE_ENV ?? 'development']));
9 |
10 | export { Office, Manager };
11 | export default knex;
12 |
--------------------------------------------------------------------------------
/src/sources/sequelize/seeds/data/categories.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 |
3 | import { CategoryCreationAttributes } from '../../models/category.model.js';
4 |
5 | const categories = (count: number): CategoryCreationAttributes[] =>
6 | Array.from({ length: count }, () => ({
7 | name: faker.commerce.department(),
8 | createdAt: new Date(),
9 | updatedAt: new Date(),
10 | }));
11 |
12 | export default categories;
13 |
--------------------------------------------------------------------------------
/src/admin/locale/en/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "components": {
3 | "CustomPage": {
4 | "header": "Here you can specify a totally custom page",
5 | "introduction": "With some data fetched from the backend:",
6 | "ending": "and other interactions like toast :)",
7 | "button": "Click me"
8 | },
9 | "LanguageSelector": {
10 | "availableLanguages": {
11 | "mk": "Macedonian"
12 | }
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/sources/sequelize/index.ts:
--------------------------------------------------------------------------------
1 | import { Sequelize } from 'sequelize';
2 |
3 | export const sequelize = new Sequelize(process.env.POSTGRES_DATABASE_URL, {
4 | dialect: 'postgres',
5 | logging: process.env.DATABASE_LOGGING === 'true',
6 | dialectOptions:
7 | process.env.NODE_ENV === 'production'
8 | ? {
9 | ssl: {
10 | require: true,
11 | rejectUnauthorized: false,
12 | },
13 | }
14 | : undefined,
15 | });
16 |
--------------------------------------------------------------------------------
/src/sources/mongoose/seeds/data/comments.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 |
3 | import { Comment } from '../../models/comment.model.js';
4 |
5 | const comments = (count: number, { articleId }): Comment[] =>
6 | Array.from({ length: count }, () => ({
7 | content: faker.lorem.paragraph(3),
8 | flagged: faker.datatype.boolean(),
9 | article: articleId,
10 | createdAt: new Date(),
11 | updatedAt: new Date(),
12 | }));
13 |
14 | export default comments;
15 |
--------------------------------------------------------------------------------
/src/admin/admin.utils.ts:
--------------------------------------------------------------------------------
1 | import { ActionRequest } from 'adminjs';
2 |
3 | export const isPOSTMethod = ({ method }: ActionRequest): boolean => method.toLowerCase() === 'post';
4 |
5 | export const isGETMethod = ({ method }: ActionRequest): boolean => method.toLowerCase() === 'get';
6 |
7 | export const isNewAction = ({ params: { action } }: ActionRequest): boolean => action === 'new';
8 |
9 | export const isEditAction = ({ params: { action } }: ActionRequest): boolean => action === 'edit';
10 |
--------------------------------------------------------------------------------
/src/sources/mikroorm/seeds/data/owners.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 |
3 | import { IOwner, UserRole } from '../../models/owner.model.js';
4 |
5 | const owners = (count: number): IOwner[] =>
6 | Array.from({ length: count }, () => ({
7 | firstName: faker.name.firstName(),
8 | lastName: faker.name.lastName(),
9 | age: Number(faker.random.numeric(2)),
10 | role: faker.helpers.arrayElement(Object.values(UserRole)),
11 | }));
12 |
13 | export default owners;
14 |
--------------------------------------------------------------------------------
/src/servers/nestjs/prisma/prisma.service.ts:
--------------------------------------------------------------------------------
1 | import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common';
2 | import { PrismaClient } from '@prisma/client';
3 |
4 | @Injectable()
5 | export class PrismaService extends PrismaClient implements OnModuleInit {
6 | async onModuleInit() {
7 | await this.$connect();
8 | }
9 |
10 | async enableShutdownHooks(app: INestApplication) {
11 | this.$on('beforeExit', async () => {
12 | await app.close();
13 | });
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/sources/mongoose/seeds/data/users.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 |
3 | import { User, Gender } from '../../models/user.model.js';
4 |
5 | const users = (count: number): User[] =>
6 | Array.from({ length: count }, () => ({
7 | firstName: faker.name.firstName(),
8 | lastName: faker.name.lastName(),
9 | gender: faker.name.sex() as Gender,
10 | email: faker.internet.email(),
11 | isMyFavourite: faker.datatype.boolean(),
12 | }));
13 |
14 | export default users;
15 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import dotenv from 'dotenv';
2 | dotenv.config({
3 | path: `${process.cwd()}/.env`,
4 | });
5 |
6 | switch (process.env.SERVER) {
7 | default:
8 | case 'EXPRESS':
9 | await import('./servers/express/index.js');
10 | break;
11 | case 'HAPIJS':
12 | await import('./servers/hapijs.js');
13 | break;
14 | case 'FASTIFY':
15 | await import('./servers/fastify.js');
16 | break;
17 | case 'NESTJS':
18 | await import('./servers/nestjs/index.js');
19 | break;
20 | }
21 |
--------------------------------------------------------------------------------
/src/sources/sequelize/seeds/data/carts.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 |
3 | import { CartCreationAttributes } from '../../models/cart.model.js';
4 |
5 | const carts = (count: number, { productId, orderId }): CartCreationAttributes[] =>
6 | Array.from({ length: count }, () => ({
7 | quantity: Number(faker.random.numeric(1, { bannedDigits: '0' })),
8 | productId,
9 | orderId,
10 | createdAt: new Date(),
11 | updatedAt: new Date(),
12 | }));
13 |
14 | export default carts;
15 |
--------------------------------------------------------------------------------
/src/admin/pages/design-system-examples/modal-page.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Header } from '@adminjs/design-system';
2 | import React, { FC } from 'react';
3 |
4 | import { ModalExample } from '../../components/design-system-examples/index.js';
5 |
6 | const ModalPage: FC = () => (
7 |
8 |
11 |
12 |
13 |
14 |
15 | );
16 |
17 | export default ModalPage;
18 |
--------------------------------------------------------------------------------
/src/sources/sequelize/seeds/data/products.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 |
3 | import { ProductCreationAttributes } from '../../models/product.model.js';
4 |
5 | const products = (count: number, { categoryId }): ProductCreationAttributes[] =>
6 | Array.from({ length: count }, () => ({
7 | name: faker.commerce.productName(),
8 | price: Number(faker.commerce.price(1, 100, 2)),
9 | categoryId,
10 | createdAt: new Date(),
11 | updatedAt: new Date(),
12 | }));
13 |
14 | export default products;
15 |
--------------------------------------------------------------------------------
/src/sources/typeorm/seeds/data/persons.ts:
--------------------------------------------------------------------------------
1 | import { DeepPartial } from 'typeorm';
2 | import { faker } from '@faker-js/faker';
3 |
4 | import { Person } from '../../models/index.js';
5 |
6 | const persons = (count: number, { organizationId }): DeepPartial[] =>
7 | Array.from({ length: count }, () => ({
8 | email: faker.internet.email(),
9 | firstName: faker.name.firstName(),
10 | lastName: faker.name.lastName(),
11 | phone: faker.phone.number(),
12 | organizationId,
13 | }));
14 |
15 | export default persons;
16 |
--------------------------------------------------------------------------------
/src/themes/index.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import url from 'url';
3 |
4 | import type { ThemeConfig } from 'adminjs';
5 | import { themeConfig } from './custom-theme/index.js';
6 |
7 | const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
8 | const getThemeDir = (theme: string) => path.join(__dirname, `./${theme}`);
9 |
10 | export const customTheme: ThemeConfig = {
11 | ...themeConfig,
12 | bundlePath: `${getThemeDir(themeConfig.id)}/theme.bundle.js`,
13 | stylePath: `${getThemeDir(themeConfig.id)}/style.css`,
14 | };
15 |
--------------------------------------------------------------------------------
/src/admin/components/detailed-stats.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { H5, Text, DrawerContent } from '@adminjs/design-system';
3 | import { ActionHeader, ActionHeaderProps } from 'adminjs';
4 |
5 | const DetailedStats = (props: ActionHeaderProps) => {
6 | return (
7 |
8 |
9 | Custom action example
10 | Where you can do whatever you like...
11 |
12 | );
13 | };
14 |
15 | export default DetailedStats;
16 |
--------------------------------------------------------------------------------
/src/admin/features/useEnvironmentVariableToDisableActions.ts:
--------------------------------------------------------------------------------
1 | import { buildFeature, FeatureType } from 'adminjs';
2 |
3 | export const useEnvironmentVariableToDisableActions = (): FeatureType => {
4 | if (process.env.DISABLE_ADMINJS_ACTIONS === 'true') {
5 | return buildFeature({
6 | actions: {
7 | edit: { isAccessible: false },
8 | delete: { isAccessible: false },
9 | bulkDelete: { isAccessible: false },
10 | new: { isAccessible: false },
11 | },
12 | });
13 | }
14 | return buildFeature({});
15 | };
16 |
--------------------------------------------------------------------------------
/src/sources/mongoose/seeds/data/categories.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 |
3 | import { Category } from '../../models/category.model.js';
4 |
5 | const categories = (count: number, { userId }): Category[] =>
6 | Array.from({ length: count }, () => ({
7 | title: faker.commerce.department(),
8 | owner: userId,
9 | nested: {
10 | field: faker.database.column(),
11 | value: faker.lorem.word(),
12 | },
13 | createdAt: new Date(),
14 | updatedAt: new Date(),
15 | }));
16 |
17 | export default categories;
18 |
--------------------------------------------------------------------------------
/src/sources/sequelize/seeds/data/orders.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 |
3 | import { OrderCreationAttributes } from '../../models/order.model.js';
4 |
5 | const orders = (count: number): OrderCreationAttributes[] =>
6 | Array.from({ length: count }, () => ({
7 | isPaid: faker.datatype.boolean(),
8 | delivery: `${faker.address.zipCode()} ${faker.address.city()}, ${faker.address.street()} ${faker.address.buildingNumber()}`,
9 | createdAt: new Date(),
10 | updatedAt: new Date(),
11 | }));
12 |
13 | export default orders;
14 |
--------------------------------------------------------------------------------
/src/sources/mongoose/models/comment.model.ts:
--------------------------------------------------------------------------------
1 | import { model, Schema, Types } from 'mongoose';
2 |
3 | export interface Comment {
4 | content: string;
5 | flagged: boolean;
6 | article: Types.ObjectId;
7 | createdAt: Date;
8 | updatedAt: Date;
9 | }
10 |
11 | export const CommentSchema = new Schema({
12 | content: { type: 'String', required: true },
13 | article: { type: Schema.Types.ObjectId, ref: 'Article', required: true },
14 | flagged: { type: 'Boolean' },
15 | });
16 |
17 | export const CommentModel = model('Comment', CommentSchema);
18 |
--------------------------------------------------------------------------------
/src/sources/mikroorm/models/seller.model.ts:
--------------------------------------------------------------------------------
1 | import { Entity, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
2 | import { v4 } from 'uuid';
3 |
4 | import type { Car } from './car.model.js';
5 |
6 | export interface ISeller {
7 | name: string;
8 | }
9 |
10 | @Entity({ tableName: 'sellers' })
11 | export class Seller implements ISeller {
12 | @PrimaryKey({ columnType: 'uuid' })
13 | public id = v4();
14 |
15 | @Property({ fieldName: 'name', columnType: 'text' })
16 | name: string;
17 |
18 | @OneToMany('Car', (car: Car) => car.seller)
19 | cars: Car[];
20 | }
21 |
--------------------------------------------------------------------------------
/src/sources/mikroorm/seeds/data/cars.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 | import { Owner, Seller } from '../../models/index.js';
3 |
4 | const cars = (count: number, { owners, sellers }) =>
5 | Array.from({ length: count }, () => ({
6 | name: `${faker.vehicle.manufacturer()} ${faker.vehicle.model()}`,
7 | owner: faker.helpers.arrayElement(owners).id,
8 | seller: faker.helpers.arrayElement(sellers).id,
9 | meta: {
10 | title: faker.lorem.slug(),
11 | description: faker.lorem.sentences(3),
12 | },
13 | }));
14 |
15 | export default cars;
16 |
--------------------------------------------------------------------------------
/src/sources/typeorm/resources/organization.resource.ts:
--------------------------------------------------------------------------------
1 | import { menu } from '../../../admin/index.js';
2 | import { useEnvironmentVariableToDisableActions } from '../../../admin/features/useEnvironmentVariableToDisableActions.js';
3 | import { ResourceFunction } from '../../../admin/types/index.js';
4 | import { Organization } from '../models/index.js';
5 |
6 | export const CreateOrganizationResource: ResourceFunction = () => ({
7 | resource: Organization,
8 | features: [useEnvironmentVariableToDisableActions()],
9 | options: {
10 | navigation: menu.typeorm,
11 | },
12 | });
13 |
--------------------------------------------------------------------------------
/src/sources/mongoose/seeds/data/articles.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 |
3 | import { Article } from '../../models/article.model.js';
4 |
5 | const articles = (count: number, { authorId, categoryId }): Article[] =>
6 | Array.from({ length: count }, () => ({
7 | title: faker.lorem.sentence(4),
8 | content: faker.lorem.paragraphs(3),
9 | photo: faker.image.imageUrl(80, 80),
10 | published: faker.datatype.boolean(),
11 | author: authorId,
12 | category: categoryId,
13 | createdAt: new Date(),
14 | updatedAt: new Date(),
15 | }));
16 |
17 | export default articles;
18 |
--------------------------------------------------------------------------------
/src/admin/locale/en/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "messages": {
3 | "loginWelcome": "to the demo application made with AdminJS - the best admin framework for Node.js apps, based on React.",
4 | "Has to be filled": "Has to be filled",
5 | "CustomPage": {
6 | "message": "I was clicked !!!",
7 | "messageWithInterpolation": "Translated messages with interpolation {{someParams}}"
8 | }
9 | },
10 | "properties": {
11 | "id": "#",
12 | "Products": {
13 | "name": "Product's name",
14 | "price": "Price"
15 | }
16 | },
17 | "labels.navigation": "",
18 | "labels.dashboard": "Home"
19 | }
20 |
--------------------------------------------------------------------------------
/src/admin/pages/index.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unused-vars */
2 | import { AdminJSOptions } from 'adminjs';
3 |
4 | import { CUSTOM_PAGE, DESIGN_SYSTEM_PAGE } from '../components.bundler.js';
5 |
6 | const pages: AdminJSOptions['pages'] = {
7 | customPage: {
8 | component: CUSTOM_PAGE,
9 | icon: 'File',
10 | handler: async (request, response, context) => {
11 | return {
12 | text: 'I am fetched from the backend',
13 | };
14 | },
15 | },
16 | designSystemExamples: {
17 | component: DESIGN_SYSTEM_PAGE,
18 | icon: 'Layout',
19 | },
20 | };
21 |
22 | export default pages;
23 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | PORT=3000
2 | SERVER="EXPRESS" # EXPRESS HAPIJS FASTIFY NESTJS
3 | SESSION_SECRET=s3sS10n_secr3t
4 | LOCALE=en
5 |
6 | MONGO_DATABASE_URL=mongodb://localhost:27017/adminjs-example-app
7 | POSTGRES_DATABASE_URL=postgres://adminjs:adminjs@localhost:5435/adminjs
8 | MYSQL_DATABASE_URL=mysql://root:adminjs@localhost:3308/adminjs?schema=public
9 | DATABASE_LOGGING=true
10 | DATABASE_SYNC=false
11 | DISABLE_ADMINJS_ACTIONS=false
12 |
13 | GITHUB_URL=https://github.com/SoftwareBrothers/adminjs/issues
14 | SLACK_URL=https://adminjs.page.link/slack
15 | DOCUMENTATION_URL=https://adminjs.co
16 | STORYBOOK_URL=https://adminjs-storybook-beta.web.app/
17 |
--------------------------------------------------------------------------------
/src/sources/typeorm/seeds/data/organizations.ts:
--------------------------------------------------------------------------------
1 | import { DeepPartial } from 'typeorm';
2 | import { faker } from '@faker-js/faker';
3 |
4 | import { Organization } from '../../models/index.js';
5 | import { CountryEnum } from './../../enums/country.enum.js';
6 |
7 | const organizations = (count: number): DeepPartial[] =>
8 | Array.from({ length: count }, () => ({
9 | name: faker.company.name(),
10 | city: faker.address.city(),
11 | address: `${faker.address.street()} ${faker.address.buildingNumber()}`,
12 | postalCode: faker.address.zipCode(),
13 | country: CountryEnum.GreatBritain,
14 | }));
15 |
16 | export default organizations;
17 |
--------------------------------------------------------------------------------
/src/sources/mongoose/resources/user.resource.ts:
--------------------------------------------------------------------------------
1 | import { menu } from '../../../admin/index.js';
2 | import { useEnvironmentVariableToDisableActions } from '../../../admin/features/useEnvironmentVariableToDisableActions.js';
3 | import { ResourceFunction } from '../../../admin/types/index.js';
4 | import { UserModel } from '../models/index.js';
5 |
6 | export const CreateUserResource: ResourceFunction = () => ({
7 | resource: UserModel,
8 | features: [useEnvironmentVariableToDisableActions()],
9 | options: {
10 | navigation: menu.mongoose,
11 | properties: {
12 | _id: {
13 | isTitle: true,
14 | },
15 | },
16 | },
17 | });
18 |
--------------------------------------------------------------------------------
/src/sources/mongoose/models/category.model.ts:
--------------------------------------------------------------------------------
1 | import { model, Schema, Types } from 'mongoose';
2 |
3 | export interface Category {
4 | title: string;
5 | owner: Types.ObjectId;
6 | nested: {
7 | field: string;
8 | value: string;
9 | };
10 | createdAt: Date;
11 | updatedAt: Date;
12 | }
13 |
14 | export const CategorySchema = new Schema(
15 | {
16 | title: { type: 'String', required: true },
17 | owner: { type: Schema.Types.ObjectId, ref: 'User' },
18 | nested: new Schema({ field: 'String', value: 'String' }),
19 | },
20 | { timestamps: true },
21 | );
22 |
23 | export const CategoryModel = model('Category', CategorySchema);
24 |
--------------------------------------------------------------------------------
/src/sources/mikroorm/resources/seller.resource.ts:
--------------------------------------------------------------------------------
1 | import { menu } from '../../../admin/index.js';
2 | import { useEnvironmentVariableToDisableActions } from '../../../admin/features/useEnvironmentVariableToDisableActions.js';
3 | import { ResourceFunction } from '../../../admin/types/index.js';
4 | import { orm } from '../config.js';
5 | import { Seller } from '../models/index.js';
6 |
7 | export const CreateSellerResource: ResourceFunction<{ model: typeof Seller; orm: typeof orm }> = () => ({
8 | resource: {
9 | model: Seller,
10 | orm,
11 | },
12 | features: [useEnvironmentVariableToDisableActions()],
13 | options: {
14 | navigation: menu.mikroorm,
15 | },
16 | });
17 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react",
4 | "module": "nodenext",
5 | "moduleResolution": "nodenext",
6 | "declaration": false,
7 | "removeComments": true,
8 | "emitDecoratorMetadata": true,
9 | "experimentalDecorators": true,
10 | "allowSyntheticDefaultImports": true,
11 | "allowJs": true,
12 | "target": "ESNext",
13 | "sourceMap": false,
14 | "outDir": "./dist",
15 | "baseUrl": ".",
16 | "incremental": true,
17 | "skipLibCheck": true,
18 | "resolveJsonModule": true,
19 | "esModuleInterop": true
20 | },
21 | "exclude": ["node_modules", "src/**/theme.bundle.js"],
22 | "include": ["src"]
23 | }
24 |
--------------------------------------------------------------------------------
/src/servers/nestjs/admin/admin.setup.ts:
--------------------------------------------------------------------------------
1 | import { INestApplication } from '@nestjs/common';
2 | import AdminJS from 'adminjs';
3 | import { AbstractHttpAdapter, HttpAdapterHost } from '@nestjs/core';
4 |
5 | import { generateAdminJSConfig } from '../../../admin/index.js';
6 | import { expressAuthenticatedRouter } from '../../../admin/router.js';
7 |
8 | export const setupAdminJS = async (app: INestApplication): Promise => {
9 | const expressApp: AbstractHttpAdapter = app.get(HttpAdapterHost).httpAdapter;
10 | const config = generateAdminJSConfig();
11 | const adminJS = new AdminJS(config);
12 | expressApp.use(adminJS.options.rootPath, expressAuthenticatedRouter(adminJS));
13 | };
14 |
--------------------------------------------------------------------------------
/src/servers/nestjs/app.controller.ts:
--------------------------------------------------------------------------------
1 | import { Body, Controller, Get, Post } from '@nestjs/common';
2 | import { IsString } from 'class-validator';
3 | import { Expose } from 'class-transformer';
4 |
5 | import { AppService } from './app.service.js';
6 |
7 | export class Hello {
8 | @Expose()
9 | @IsString()
10 | public hello!: string;
11 | }
12 |
13 | @Controller()
14 | export class AppController {
15 | constructor(private readonly appService: AppService) {}
16 |
17 | @Get()
18 | public getHello(): string {
19 | return this.appService.getHello();
20 | }
21 |
22 | @Post()
23 | public postHello(@Body() testBody: Hello): string {
24 | return testBody.hello;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/sources/objectionjs/knexfile.cjs:
--------------------------------------------------------------------------------
1 | const config = {
2 | development: {
3 | client: 'pg',
4 | useNullAsDefault: true,
5 | connection: process.env.POSTGRES_DATABASE_URL,
6 | migrations: {
7 | tableName: 'knex_migrations',
8 | },
9 | },
10 | production: {
11 | client: 'pg',
12 | connection: {
13 | connectionString: process.env.POSTGRES_DATABASE_URL,
14 | ssl: {
15 | rejectUnauthorized: false,
16 | require: true,
17 | },
18 | },
19 | pool: {
20 | min: 2,
21 | max: 10,
22 | },
23 | migrations: {
24 | tableName: 'knex_migrations',
25 | },
26 | },
27 | };
28 |
29 | module.exports = config;
30 |
--------------------------------------------------------------------------------
/src/sources/prisma/resources/profile.resource.ts:
--------------------------------------------------------------------------------
1 | import { menu } from '../../../admin/index.js';
2 | import { useEnvironmentVariableToDisableActions } from '../../../admin/features/useEnvironmentVariableToDisableActions.js';
3 | import { ResourceFunction } from '../../../admin/types/index.js';
4 | import { client, dmmf } from '../config.js';
5 |
6 | export const CreateProfileResource: ResourceFunction<{
7 | model: typeof dmmf.modelMap.Profile;
8 | client: typeof client;
9 | }> = () => ({
10 | resource: {
11 | model: dmmf.modelMap.Profile,
12 | client,
13 | },
14 | features: [useEnvironmentVariableToDisableActions()],
15 | options: {
16 | navigation: menu.prisma,
17 | },
18 | });
19 |
--------------------------------------------------------------------------------
/src/sources/mongoose/resources/complicated.resource.ts:
--------------------------------------------------------------------------------
1 | import { menu } from '../../../admin/index.js';
2 | import { useEnvironmentVariableToDisableActions } from '../../../admin/features/useEnvironmentVariableToDisableActions.js';
3 | import { ResourceFunction } from '../../../admin/types/index.js';
4 | import { ComplicatedModel } from '../models/index.js';
5 |
6 | export const CreateComplicatedResource: ResourceFunction = () => ({
7 | resource: ComplicatedModel,
8 | features: [useEnvironmentVariableToDisableActions()],
9 | options: {
10 | navigation: menu.mongoose,
11 | properties: {
12 | _id: {
13 | isTitle: true,
14 | },
15 | },
16 | },
17 | });
18 |
--------------------------------------------------------------------------------
/src/sources/prisma/resources/publisher.resource.ts:
--------------------------------------------------------------------------------
1 | import { menu } from '../../../admin/index.js';
2 | import { useEnvironmentVariableToDisableActions } from '../../../admin/features/useEnvironmentVariableToDisableActions.js';
3 | import { ResourceFunction } from '../../../admin/types/index.js';
4 | import { client, dmmf } from '../config.js';
5 |
6 | export const CreatePublisherResource: ResourceFunction<{
7 | model: typeof dmmf.modelMap.Publisher;
8 | client: typeof client;
9 | }> = () => ({
10 | resource: {
11 | model: dmmf.modelMap.Publisher,
12 | client,
13 | },
14 | features: [useEnvironmentVariableToDisableActions()],
15 | options: {
16 | navigation: menu.prisma,
17 | },
18 | });
19 |
--------------------------------------------------------------------------------
/src/sources/typeorm/models/organization.entity.ts:
--------------------------------------------------------------------------------
1 | import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
2 | import { CountryEnum } from '../enums/country.enum.js';
3 |
4 | @Entity({ name: 'organizations' })
5 | export class Organization extends BaseEntity {
6 | @PrimaryGeneratedColumn()
7 | public id: number;
8 |
9 | @Column()
10 | public name: string;
11 |
12 | @Column()
13 | public address: string;
14 |
15 | @Column()
16 | public city: string;
17 |
18 | @Column({ name: 'postal_code' })
19 | public postalCode: string;
20 |
21 | @Column({
22 | type: 'enum',
23 | enum: CountryEnum,
24 | nullable: false,
25 | })
26 | public country: CountryEnum;
27 | }
28 |
--------------------------------------------------------------------------------
/src/sources/sequelize/functions/get-sum.function.ts:
--------------------------------------------------------------------------------
1 | import { CartModel, OrderModel, ProductModel } from '../models/index.js';
2 |
3 | export const getSum = async (id: number): Promise => {
4 | const order = await OrderModel.findByPk(id, {
5 | include: [
6 | {
7 | model: CartModel,
8 | as: 'carts',
9 | include: [
10 | {
11 | model: ProductModel,
12 | as: 'product',
13 | },
14 | ],
15 | },
16 | ],
17 | });
18 |
19 | return (
20 | order.carts.reduce((curr, next) => {
21 | const price = parseInt(next.product.price, 10) * next.quantity;
22 | return curr + price;
23 | }, 0) / 100
24 | );
25 | };
26 |
--------------------------------------------------------------------------------
/src/themes/custom-theme/theme.bundle.js:
--------------------------------------------------------------------------------
1 | var THEME_COMPONENTS=function(e,t,l,r,n){"use strict";const a=new r.ViewHelpers;return e.LoggedIn=()=>l.createElement(t.Box,{mr:"lg"}),e.SidebarFooter=()=>{const e=n.useSelector((e=>e.session)),{title:o,email:i,avatarUrl:c}=e,{tb:m}=r.useTranslation();return e?l.createElement(t.Box,{mt:"lg",mb:"md"},l.createElement(t.Box,{flex:!0,flexDirection:"row",alignItems:"center",px:"xl",mb:"lg"},l.createElement(t.Avatar,{src:c,alt:i,mr:"lg"},i.slice(0,1).toUpperCase()),l.createElement(t.Tooltip,{direction:"right",title:m("logout")},l.createElement(t.Box,{as:"a",href:a.logoutUrl()},l.createElement(t.Title,null,i),o&&l.createElement(t.SmallText,null,o))))):null},e}({},AdminJSDesignSystem,React,AdminJS,ReactRedux);
2 |
--------------------------------------------------------------------------------
/src/admin/locale/en/index.ts:
--------------------------------------------------------------------------------
1 | import type { LocaleTranslations } from 'adminjs';
2 |
3 | import common from './common.json' assert { type: 'json' };
4 | import Complicated from './complicated.json' assert { type: 'json' };
5 | import components from './components.json' assert { type: 'json' };
6 | import pages from './pages.json' assert { type: 'json' };
7 | import Person from './person.json' assert { type: 'json' };
8 |
9 | const enLocale: LocaleTranslations = {
10 | ...common,
11 | ...components,
12 | ...pages,
13 | resources: {
14 | Complicated,
15 | Person,
16 | products: {
17 | properties: {
18 | categoryId: 'Category',
19 | },
20 | },
21 | },
22 | };
23 |
24 | export default enLocale;
25 |
--------------------------------------------------------------------------------
/src/sources/mongoose/models/user.model.ts:
--------------------------------------------------------------------------------
1 | import { model, Schema } from 'mongoose';
2 |
3 | export enum Gender {
4 | Male = 'male',
5 | Female = 'female',
6 | }
7 |
8 | export interface User {
9 | firstName: string;
10 | lastName: string;
11 | gender: Gender;
12 | email: string;
13 | isMyFavourite: boolean;
14 | }
15 |
16 | export const UserSchema = new Schema({
17 | firstName: { type: 'String', required: true },
18 | lastName: { type: 'String', required: true },
19 | gender: { type: 'String', required: true, enum: Gender },
20 | email: { type: 'String', required: true },
21 | isMyFavourite: { type: 'Boolean', required: true },
22 | });
23 |
24 | export const UserModel = model('User', UserSchema);
25 |
--------------------------------------------------------------------------------
/src/sources/prisma/seeds/data/posts.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 |
3 | const publishers = (count: number, { publishers }) =>
4 | Array.from({ length: count }, () => ({
5 | title: faker.lorem.sentence(3),
6 | content: faker.lorem.sentence(8),
7 | status: faker.helpers.arrayElement(['ACTIVE', 'REMOVED']) as any,
8 | published: faker.datatype.boolean(),
9 | someJson: [
10 | {
11 | number: faker.random.numeric(8),
12 | string: faker.lorem.words(3),
13 | boolean: faker.datatype.boolean(),
14 | date: faker.date.recent().toISOString(),
15 | },
16 | ],
17 | publisherId: faker.helpers.arrayElement(publishers).id,
18 | }));
19 |
20 | export default publishers;
21 |
--------------------------------------------------------------------------------
/src/sources/mongoose/models/article.model.ts:
--------------------------------------------------------------------------------
1 | import { model, Schema, Types } from 'mongoose';
2 |
3 | export interface Article {
4 | title: string;
5 | content: string;
6 | photo: string;
7 | author: Types.ObjectId;
8 | category: Types.ObjectId;
9 | published: boolean;
10 | }
11 |
12 | export const ArticleSchema = new Schema({
13 | title: { type: 'String', required: true },
14 | content: { type: 'String', required: true },
15 | photo: { type: 'String' },
16 | author: { type: Schema.Types.ObjectId, ref: 'User' },
17 | category: { type: Schema.Types.ObjectId, ref: 'Category', required: true },
18 | published: { type: 'Boolean' },
19 | });
20 |
21 | export const ArticleModel = model('Article', ArticleSchema);
22 |
--------------------------------------------------------------------------------
/src/sources/mikroorm/resources/owner.resource.ts:
--------------------------------------------------------------------------------
1 | import { menu } from '../../../admin/index.js';
2 | import { useEnvironmentVariableToDisableActions } from '../../../admin/features/useEnvironmentVariableToDisableActions.js';
3 | import { ResourceFunction } from '../../../admin/types/index.js';
4 | import { orm } from '../config.js';
5 | import { Owner } from '../models/index.js';
6 |
7 | export const CreateOwnerResource: ResourceFunction<{ model: typeof Owner; orm: typeof orm }> = () => ({
8 | resource: {
9 | model: Owner,
10 | orm,
11 | },
12 | features: [useEnvironmentVariableToDisableActions()],
13 | options: {
14 | navigation: menu.mikroorm,
15 | properties: {
16 | lastName: { isTitle: true },
17 | },
18 | },
19 | });
20 |
--------------------------------------------------------------------------------
/src/sources/objectionjs/migrations/20220826123456_initial_schema.cjs:
--------------------------------------------------------------------------------
1 | exports.up = (knex) =>
2 | knex.schema
3 | .createTable('offices', (table) => {
4 | table.increments('id').primary();
5 | table.string('name');
6 | table.timestamps(true, true, true);
7 | table.jsonb('address');
8 | })
9 | .createTable('managers', (table) => {
10 | table.increments('id').primary();
11 |
12 | table.string('firstName');
13 | table.string('lastName');
14 | table.integer('age');
15 | table.timestamps(true, true, true);
16 | table.integer('officeId').unsigned().references('id').inTable('offices').onDelete('CASCADE').index();
17 | });
18 |
19 | exports.down = (knex) => knex.schema.dropTableIfExists('managers').dropTableIfExists('offices');
20 |
--------------------------------------------------------------------------------
/src/sources/objectionjs/resources/office.resource.ts:
--------------------------------------------------------------------------------
1 | import { menu } from '../../../admin/index.js';
2 | import { useEnvironmentVariableToDisableActions } from '../../../admin/features/useEnvironmentVariableToDisableActions.js';
3 | import { ResourceFunction } from '../../../admin/types/index.js';
4 | import { Office } from '../models/index.js';
5 |
6 | export const CreateOfficeResource: ResourceFunction = () => ({
7 | resource: Office,
8 | features: [useEnvironmentVariableToDisableActions()],
9 | options: {
10 | navigation: menu.objection,
11 | properties: {
12 | createdAt: { isVisible: { edit: false, list: true, show: true, filter: true } },
13 | updatedAt: { isVisible: { edit: false, list: true, show: true, filter: true } },
14 | },
15 | },
16 | });
17 |
--------------------------------------------------------------------------------
/src/admin/constants/authUsers.ts:
--------------------------------------------------------------------------------
1 | export type AuthUser = {
2 | email: string;
3 | password: string;
4 | title: string;
5 | theme: string;
6 | };
7 |
8 | export const AuthUsers: AuthUser[] = [
9 | {
10 | email: 'admin@example.com',
11 | password: 'password',
12 | title: 'Admin',
13 | theme: 'light',
14 | },
15 | {
16 | email: 'dark@example.com',
17 | password: 'password',
18 | title: 'AdminJS dark theme',
19 | theme: 'dark',
20 | },
21 | {
22 | email: 'no-sidebar@example.com',
23 | password: 'password',
24 | title: 'AdminJS no-sidebar theme',
25 | theme: 'no-sidebar',
26 | },
27 | {
28 | email: 'custom@example.com',
29 | password: 'password',
30 | title: 'AdminJS custom theme',
31 | theme: 'custom-theme',
32 | },
33 | ];
34 |
--------------------------------------------------------------------------------
/src/sources/objectionjs/resources/manager.resource.ts:
--------------------------------------------------------------------------------
1 | import { menu } from '../../../admin/index.js';
2 | import { useEnvironmentVariableToDisableActions } from '../../../admin/features/useEnvironmentVariableToDisableActions.js';
3 | import { ResourceFunction } from '../../../admin/types/index.js';
4 | import { Manager } from '../models/index.js';
5 |
6 | export const CreateManagerResource: ResourceFunction = () => ({
7 | resource: Manager,
8 | features: [useEnvironmentVariableToDisableActions()],
9 | options: {
10 | navigation: menu.objection,
11 | properties: {
12 | createdAt: { isVisible: { edit: false, list: true, show: true, filter: true } },
13 | updatedAt: { isVisible: { edit: false, list: true, show: true, filter: true } },
14 | },
15 | },
16 | });
17 |
--------------------------------------------------------------------------------
/.github/workflows/push.yml:
--------------------------------------------------------------------------------
1 | name: CI/CD
2 | on: push
3 |
4 | jobs:
5 | test:
6 | name: test
7 | runs-on: ubuntu-latest
8 | steps:
9 | - name: Checkout
10 | uses: actions/checkout@v2
11 | - name: Setup
12 | uses: actions/setup-node@v2
13 | with:
14 | node-version: "18.x"
15 | - uses: actions/cache@v2
16 | id: yarn-cache
17 | with:
18 | path: node_modules
19 | key: ${{ runner.os }}-node_modules-${{ hashFiles('**/yarn.lock') }}
20 | restore-keys: |
21 | ${{ runner.os }}-node_modules-
22 | - name: Install
23 | if: steps.yarn-cache.outputs.cache-hit != 'true'
24 | run: yarn install
25 | - name: Lint
26 | run: yarn lint
27 | - name: Build
28 | run: yarn build
29 |
--------------------------------------------------------------------------------
/src/servers/nestjs/index.ts:
--------------------------------------------------------------------------------
1 | import { NestFactory } from '@nestjs/core';
2 | import { ValidationPipe } from '@nestjs/common';
3 | import mongoose from 'mongoose';
4 |
5 | import { AppModule } from './app.module.js';
6 | import { setupAdminJS } from './admin/admin.setup.js';
7 | import { init } from '../../sources/mikroorm/config.js';
8 |
9 | const bootstrap = async () => {
10 | const app = await NestFactory.create(AppModule);
11 |
12 | app.useGlobalPipes(
13 | new ValidationPipe({
14 | transform: true,
15 | whitelist: true,
16 | }),
17 | );
18 |
19 | await mongoose.connect(process.env.MONGO_DATABASE_URL);
20 | await init();
21 | await setupAdminJS(app);
22 |
23 | await app.listen(process.env.PORT);
24 | console.log(`AdminJS is under http://localhost:${process.env.PORT}/admin`);
25 | };
26 |
27 | bootstrap();
28 |
--------------------------------------------------------------------------------
/src/sources/mikroorm/config.ts:
--------------------------------------------------------------------------------
1 | import { MikroORM, Options } from '@mikro-orm/core';
2 |
3 | import { Owner, Car, Seller } from './models/index.js';
4 |
5 | const driverOptions: Options['driverOptions'] = {};
6 |
7 | if (process.env.NODE_ENV === 'production') {
8 | driverOptions.connection = { ssl: true };
9 | }
10 |
11 | const config: Options = {
12 | entities: [Owner, Car, Seller],
13 | type: 'postgresql' as const,
14 | clientUrl: process.env.POSTGRES_DATABASE_URL,
15 | migrations: {
16 | path: 'src/sources/mikroorm/migrations',
17 | emit: 'ts',
18 | disableForeignKeys: false,
19 | },
20 | driverOptions,
21 | debug: process.env.DATABASE_LOGGING === 'true',
22 | };
23 |
24 | export const init = async () => {
25 | orm = await MikroORM.init(config);
26 | };
27 |
28 | export default config;
29 | export let orm: MikroORM;
30 |
--------------------------------------------------------------------------------
/src/sources/typeorm/handlers/validate-email.handler.ts:
--------------------------------------------------------------------------------
1 | import { flat, PropertyErrors, ValidationError, ActionRequest, ActionContext, Before } from 'adminjs';
2 |
3 | import { isPOSTMethod } from '../../../admin/admin.utils.js';
4 | import { PostPayload } from '../interfaces.js';
5 |
6 | export const validateEmail: Before = async (request: ActionRequest, context: ActionContext): Promise => {
7 | if (!isPOSTMethod(request)) {
8 | return request;
9 | }
10 |
11 | const payload = flat.unflatten(request.payload);
12 | const errors: PropertyErrors = {};
13 |
14 | if (!payload.email?.trim()?.length) {
15 | errors.email = { message: context.translateMessage('Has to be filled') };
16 | }
17 |
18 | if (Object.keys(errors).length) {
19 | throw new ValidationError(errors);
20 | }
21 |
22 | return request;
23 | };
24 |
--------------------------------------------------------------------------------
/src/servers/nestjs/app.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { MikroOrmModule } from '@mikro-orm/nestjs';
3 | import { TypeOrmModule } from '@nestjs/typeorm';
4 |
5 | import { AppController } from './app.controller.js';
6 | import { AppService } from './app.service.js';
7 | import { PrismaService } from './prisma/prisma.service.js';
8 | import { params } from '../../sources/typeorm/config.js';
9 | import { MongooseSchemasModule } from './mongoose/mongoose.module.js';
10 | import config from '../../sources/mikroorm/config.js';
11 | import { databaseProviders } from './database.providers.js';
12 |
13 | @Module({
14 | imports: [MikroOrmModule.forRoot(config), TypeOrmModule.forRoot(params), MongooseSchemasModule],
15 | controllers: [AppController],
16 | providers: [AppService, PrismaService, ...databaseProviders],
17 | })
18 | export class AppModule {}
19 |
--------------------------------------------------------------------------------
/src/sources/sequelize/resources/cart.resource.ts:
--------------------------------------------------------------------------------
1 | import { menu } from '../../../admin/index.js';
2 | import { useEnvironmentVariableToDisableActions } from '../../../admin/features/useEnvironmentVariableToDisableActions.js';
3 | import { ResourceFunction } from '../../../admin/types/index.js';
4 | import { CartModel } from '../models/index.js';
5 |
6 | export const CreateCartResource: ResourceFunction = () => ({
7 | resource: CartModel,
8 | features: [useEnvironmentVariableToDisableActions()],
9 | options: {
10 | navigation: menu.sequelize,
11 | properties: {
12 | createdAt: {
13 | isVisible: {
14 | show: true,
15 | edit: false,
16 | },
17 | },
18 | updatedAt: {
19 | isVisible: {
20 | show: true,
21 | edit: false,
22 | },
23 | },
24 | },
25 | },
26 | });
27 |
--------------------------------------------------------------------------------
/src/sources/mikroorm/resources/car.resource.ts:
--------------------------------------------------------------------------------
1 | import { menu } from '../../../admin/index.js';
2 | import { useEnvironmentVariableToDisableActions } from '../../../admin/features/useEnvironmentVariableToDisableActions.js';
3 | import { ResourceFunction } from '../../../admin/types/index.js';
4 | import { orm } from '../config.js';
5 | import { Car } from '../models/index.js';
6 |
7 | export const CreateCarResource: ResourceFunction<{ model: typeof Car; orm: typeof orm }> = () => ({
8 | resource: {
9 | model: Car,
10 | orm,
11 | },
12 | features: [useEnvironmentVariableToDisableActions()],
13 | options: {
14 | navigation: menu.mikroorm,
15 | properties: {
16 | meta: { type: 'mixed' },
17 | 'meta.title': {
18 | type: 'string',
19 | },
20 | 'meta.description': {
21 | type: 'string',
22 | },
23 | },
24 | },
25 | });
26 |
--------------------------------------------------------------------------------
/src/sources/typeorm/config.ts:
--------------------------------------------------------------------------------
1 | import dotenv from 'dotenv';
2 | dotenv.config();
3 |
4 | import { DataSource, DataSourceOptions } from 'typeorm';
5 | import { init1644569575919 } from './migrations/1644569575919-init.js';
6 | import { Organization, Person } from './models/index.js';
7 |
8 | export const params: DataSourceOptions = {
9 | type: 'postgres' as const,
10 | url: process.env.POSTGRES_DATABASE_URL,
11 | synchronize: process.env.DATABASE_SYNC === 'true',
12 | logging: process.env.DATABASE_LOGGING === 'true',
13 | entities: [Organization, Person],
14 | migrations: [init1644569575919],
15 | migrationsRun: true,
16 | subscribers: [],
17 | extra:
18 | process.env.NODE_ENV === 'production'
19 | ? {
20 | ssl: true,
21 | }
22 | : undefined,
23 | };
24 |
25 | const dataSource = new DataSource(params);
26 |
27 | export default dataSource;
28 |
--------------------------------------------------------------------------------
/src/admin/pages/design-system-examples/tabs-page.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Tab, Tabs, Header } from '@adminjs/design-system';
2 | import React, { FC, useState } from 'react';
3 |
4 | const TabsPage: FC = () => {
5 | const [selectedTab, setSelectedTab] = useState('first');
6 |
7 | return (
8 |
9 |
12 |
13 |
14 |
15 | First
16 |
17 |
18 | Second
19 |
20 |
21 | Third
22 |
23 |
24 |
25 |
26 | );
27 | };
28 |
29 | export default TabsPage;
30 |
--------------------------------------------------------------------------------
/src/admin/pages/design-system-examples/icons-page.tsx:
--------------------------------------------------------------------------------
1 | import { Box, H6, Header, Icon } from '@adminjs/design-system';
2 | import React from 'react';
3 | import * as FeatherIcons from 'react-feather';
4 |
5 | const IconsPage = () => {
6 | const IconsSet = Object.keys(FeatherIcons)
7 | .filter((name) => name !== 'default')
8 | .map((iconName) => (
9 |
10 | {iconName}
11 |
12 |
13 | ));
14 |
15 | return (
16 |
17 |
20 |
21 | {IconsSet}
22 |
23 |
24 | );
25 | };
26 |
27 | export default IconsPage;
28 |
--------------------------------------------------------------------------------
/src/sources/mongoose/resources/comment.resource.ts:
--------------------------------------------------------------------------------
1 | import { menu } from '../../../admin/index.js';
2 | import { useEnvironmentVariableToDisableActions } from '../../../admin/features/useEnvironmentVariableToDisableActions.js';
3 | import { ResourceFunction } from '../../../admin/types/index.js';
4 | import { CommentModel } from '../models/index.js';
5 |
6 | export const CreateCommentResource: ResourceFunction = () => ({
7 | resource: CommentModel,
8 | features: [useEnvironmentVariableToDisableActions()],
9 | options: {
10 | navigation: menu.mongoose,
11 | actions: {
12 | show: {
13 | isAccessible: false,
14 | },
15 | edit: {
16 | showInDrawer: true,
17 | },
18 | },
19 | properties: {
20 | _id: {
21 | isTitle: true,
22 | },
23 | content: {
24 | type: 'textarea',
25 | },
26 | },
27 | },
28 | });
29 |
--------------------------------------------------------------------------------
/src/sources/mongoose/resources/category.resource.ts:
--------------------------------------------------------------------------------
1 | import { menu } from '../../../admin/index.js';
2 | import { useEnvironmentVariableToDisableActions } from '../../../admin/features/useEnvironmentVariableToDisableActions.js';
3 | import { ResourceFunction } from '../../../admin/types/index.js';
4 | import { CategoryModel } from '../models/index.js';
5 |
6 | export const CreateCategoryResource: ResourceFunction = () => ({
7 | resource: CategoryModel,
8 | features: [useEnvironmentVariableToDisableActions()],
9 | options: {
10 | navigation: menu.mongoose,
11 | actions: {
12 | show: {
13 | showInDrawer: true,
14 | },
15 | edit: {
16 | showInDrawer: true,
17 | },
18 | new: {
19 | showInDrawer: true,
20 | },
21 | },
22 | properties: {
23 | _id: {
24 | isTitle: true,
25 | },
26 | },
27 | },
28 | });
29 |
--------------------------------------------------------------------------------
/src/sources/mongoose/resources/article.resource.ts:
--------------------------------------------------------------------------------
1 | import { menu } from '../../../admin/index.js';
2 | import { THUMB } from '../../../admin/components.bundler.js';
3 | import { useEnvironmentVariableToDisableActions } from '../../../admin/features/useEnvironmentVariableToDisableActions.js';
4 | import { ResourceFunction } from '../../../admin/types/index.js';
5 | import { ArticleModel } from '../models/index.js';
6 |
7 | export const CreateArticleResource: ResourceFunction = () => ({
8 | resource: ArticleModel,
9 | features: [useEnvironmentVariableToDisableActions()],
10 | options: {
11 | navigation: menu.mongoose,
12 | properties: {
13 | _id: {
14 | isTitle: true,
15 | },
16 | content: {
17 | type: 'richtext',
18 | },
19 | published: {
20 | components: {
21 | list: THUMB,
22 | },
23 | },
24 | },
25 | },
26 | });
27 |
--------------------------------------------------------------------------------
/src/sources/sequelize/resources/category.resource.ts:
--------------------------------------------------------------------------------
1 | import { menu } from '../../../admin/index.js';
2 | import { useEnvironmentVariableToDisableActions } from '../../../admin/features/useEnvironmentVariableToDisableActions.js';
3 | import { ResourceFunction } from '../../../admin/types/index.js';
4 | import { CategoryModel } from '../models/index.js';
5 |
6 | export const CreateCategoryResource: ResourceFunction = () => ({
7 | resource: CategoryModel,
8 | features: [useEnvironmentVariableToDisableActions()],
9 | options: {
10 | navigation: menu.sequelize,
11 | properties: {
12 | name: {
13 | isTitle: true,
14 | },
15 | createdAt: {
16 | isVisible: {
17 | show: true,
18 | edit: false,
19 | },
20 | },
21 | updatedAt: {
22 | isVisible: {
23 | show: true,
24 | edit: false,
25 | },
26 | },
27 | },
28 | },
29 | });
30 |
--------------------------------------------------------------------------------
/src/scripts/truncate-postgres.ts:
--------------------------------------------------------------------------------
1 | import dotenv from 'dotenv';
2 | dotenv.config({
3 | path: `${process.cwd()}/.env`,
4 | });
5 |
6 | import { sequelize } from '../sources/sequelize/index.js';
7 |
8 | const truncatePostgres = async () => {
9 | await sequelize.query(`
10 | CREATE OR REPLACE FUNCTION truncate_tables() RETURNS void AS $$
11 | DECLARE
12 | row record;
13 | BEGIN
14 | FOR row IN
15 | SELECT
16 | tablename
17 | FROM
18 | pg_catalog.pg_tables
19 | WHERE
20 | schemaname = 'public'
21 | AND
22 | tablename NOT IN ('SequelizeMeta', 'migrations', 'mikro_orm_migrations')
23 | LOOP
24 | EXECUTE format('TRUNCATE %I RESTART IDENTITY CASCADE', row.tablename);
25 | END LOOP;
26 | END;
27 | $$ LANGUAGE plpgsql;
28 |
29 | SELECT truncate_tables();
30 | `);
31 | };
32 |
33 | truncatePostgres()
34 | .then(() => process.exit(0))
35 | .catch((e) => {
36 | console.log(e);
37 | process.exit(1);
38 | });
39 |
--------------------------------------------------------------------------------
/src/admin/components/design-system-examples/modal-example.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Button, Modal, ModalProps } from '@adminjs/design-system';
2 | import React, { FC, useCallback, useState } from 'react';
3 |
4 | const ModalExample: FC = () => {
5 | const [showModal, setShowModal] = useState(false);
6 | const handleButtonClick = useCallback(() => setShowModal(true), []);
7 |
8 | const modalProps: ModalProps = {
9 | variant: 'primary',
10 | label: 'Modal header',
11 | icon: 'Bookmark',
12 | title: 'Modal title',
13 | subTitle: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
14 | buttons: [{ label: 'Cancel' }, { label: 'Delete', color: 'danger' }],
15 | onClose: () => setShowModal(false),
16 | onOverlayClick: () => setShowModal(false),
17 | };
18 |
19 | return (
20 |
21 |
22 | {showModal && }
23 |
24 | );
25 | };
26 |
27 | export default ModalExample;
28 |
--------------------------------------------------------------------------------
/src/admin/locale/index.ts:
--------------------------------------------------------------------------------
1 | import { Locale, locales as AdminJSLocales } from 'adminjs';
2 |
3 | import de from './de/index.js';
4 | import en from './en/index.js';
5 |
6 | const localeKey = process.env.LOCALE || 'en';
7 | const customLanguage = 'mk';
8 |
9 | export const locale: Locale = {
10 | language: localeKey,
11 | availableLanguages: [...Object.keys(AdminJSLocales), customLanguage].sort(),
12 | localeDetection: true,
13 | withBackend: true,
14 | translations: {
15 | de,
16 | en,
17 | [customLanguage]: {
18 | components: {
19 | LanguageSelector: {
20 | availableLanguages: {
21 | de: 'германски',
22 | en: 'Англиски',
23 | es: 'шпански',
24 | it: 'италијански',
25 | pl: 'полски',
26 | mk: 'македонски',
27 | 'pt-BR': 'португалски (Бразил)',
28 | ua: 'украински',
29 | 'zh-CN': 'кинески',
30 | },
31 | },
32 | },
33 | },
34 | },
35 | };
36 |
--------------------------------------------------------------------------------
/src/scripts/truncate-mongodb.ts:
--------------------------------------------------------------------------------
1 | import dotenv from 'dotenv';
2 | dotenv.config({
3 | path: `${process.cwd()}/.env`,
4 | });
5 |
6 | import mongoose from 'mongoose';
7 | import { AuthUsers } from '../admin/constants/authUsers.js';
8 | import {
9 | AdminModel,
10 | UserModel,
11 | ArticleModel,
12 | CommentModel,
13 | CategoryModel,
14 | ComplicatedModel,
15 | } from '../sources/mongoose/models/index.js';
16 |
17 | async function truncateMongodb() {
18 | await mongoose.connect(process.env.MONGO_DATABASE_URL);
19 | await UserModel.deleteMany({});
20 | await ArticleModel.deleteMany({});
21 | await CommentModel.deleteMany({});
22 | await CategoryModel.deleteMany({});
23 | await ComplicatedModel.deleteMany({});
24 | await AdminModel.deleteMany({
25 | email: {
26 | $nin: AuthUsers.map((user) => user.email),
27 | },
28 | });
29 | }
30 |
31 | truncateMongodb()
32 | .then(() => process.exit(0))
33 | .catch((e) => {
34 | console.log(e);
35 | process.exit(1);
36 | });
37 |
--------------------------------------------------------------------------------
/src/sources/prisma/resources/post.resource.ts:
--------------------------------------------------------------------------------
1 | import { menu } from '../../../admin/index.js';
2 | import { useEnvironmentVariableToDisableActions } from '../../../admin/features/useEnvironmentVariableToDisableActions.js';
3 | import { ResourceFunction } from '../../../admin/types/index.js';
4 | import { client, dmmf } from '../config.js';
5 |
6 | export const CreatePostResource: ResourceFunction<{
7 | model: typeof dmmf.modelMap.Post;
8 | client: typeof client;
9 | }> = () => ({
10 | resource: {
11 | model: dmmf.modelMap.Post,
12 | client,
13 | },
14 | features: [useEnvironmentVariableToDisableActions()],
15 | options: {
16 | navigation: menu.prisma,
17 | properties: {
18 | content: { type: 'richtext' },
19 | someJson: { type: 'mixed', isArray: true },
20 | 'someJson.number': { type: 'number' },
21 | 'someJson.string': { type: 'string' },
22 | 'someJson.boolean': { type: 'boolean' },
23 | 'someJson.date': { type: 'datetime' },
24 | },
25 | },
26 | });
27 |
--------------------------------------------------------------------------------
/src/sources/prisma/seeds/run.ts:
--------------------------------------------------------------------------------
1 | import dotenv from 'dotenv';
2 | dotenv.config({
3 | path: `${process.cwd()}/.env`,
4 | });
5 |
6 | import { PrismaClient } from '@prisma/client';
7 | import { publishers, posts, profiles } from './data/index.js';
8 |
9 | const publishersCount = 5;
10 | const postsCount = 20;
11 |
12 | const run = async () => {
13 | const prisma = new PrismaClient();
14 | await prisma.$connect();
15 |
16 | const createdPublishers = await prisma.$transaction(
17 | publishers(publishersCount).map((publisher) => prisma.publisher.create({ data: publisher })),
18 | );
19 | await prisma.$transaction(
20 | createdPublishers.map((publisher) =>
21 | prisma.profile.create({ data: profiles(1, { publisherId: publisher.id })[0] }),
22 | ),
23 | );
24 | await prisma.post.createMany({ data: posts(postsCount, { publishers: createdPublishers }) });
25 | };
26 |
27 | run()
28 | .then(() => process.exit(0))
29 | .catch((e) => {
30 | console.log(e);
31 | process.exit(1);
32 | });
33 |
--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | version: '3.7'
2 |
3 | services:
4 | mongo_db:
5 | container_name: adminjs-example-mongo
6 | image: mongo
7 | ports:
8 | - "27017:27017"
9 | volumes:
10 | - mongo_db_example_app:/data/db
11 |
12 | postgres_db:
13 | container_name: adminjs-example-postgres
14 | image: postgres
15 | environment:
16 | - POSTGRES_DB=adminjs
17 | - POSTGRES_USER=adminjs
18 | - POSTGRES_PASSWORD=adminjs
19 | ports:
20 | - "5435:5432"
21 | volumes:
22 | - postgres_db_example_app:/var/lib/postgresql/data
23 |
24 | mysql_db:
25 | container_name: adminjs-example-mysql
26 | image: mysql
27 | environment:
28 | MYSQL_USER: adminjs
29 | MYSQL_PASSWORD: adminjs
30 | MYSQL_ROOT_PASSWORD: adminjs
31 | MYSQL_DATABASE: adminjs
32 | ports:
33 | - "3308:3306"
34 | volumes:
35 | - mysql_db_example_app:/var/lib/mysql
36 |
37 | volumes:
38 | mongo_db_example_app:
39 | postgres_db_example_app:
40 | mysql_db_example_app:
41 |
--------------------------------------------------------------------------------
/src/sources/typeorm/models/person.entity.ts:
--------------------------------------------------------------------------------
1 | import { BaseEntity, Column, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn, RelationId } from 'typeorm';
2 | import { Organization } from './organization.entity.js';
3 |
4 | @Entity({ name: 'persons' })
5 | export class Person extends BaseEntity {
6 | @PrimaryGeneratedColumn()
7 | public id: number;
8 |
9 | @Column({ name: 'email', nullable: false })
10 | public email: string;
11 |
12 | @Column({ name: 'first_name' })
13 | public firstName: string;
14 |
15 | @Column({ name: 'last_name' })
16 | public lastName: string;
17 |
18 | @Column({ name: 'phone' })
19 | public phone: string;
20 |
21 | @ManyToOne(() => Organization)
22 | @JoinColumn({ name: 'organization_id' })
23 | public organization: Organization;
24 |
25 | @RelationId((person: Person) => person.organization)
26 | @Column({ name: 'organization_id' })
27 | public organizationId: number;
28 |
29 | public getFullName(): string {
30 | return `${this.firstName} ${this.lastName}`;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2018 SoftwareBrothers.co
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/src/sources/sequelize/models/category.model.ts:
--------------------------------------------------------------------------------
1 | import { DataTypes, Model, Optional } from 'sequelize';
2 |
3 | import { sequelize } from '../index.js';
4 |
5 | type Category = {
6 | id: number;
7 | name: string;
8 | createdAt: Date;
9 | updatedAt: Date;
10 | };
11 |
12 | export type CategoryCreationAttributes = Optional;
13 |
14 | export class CategoryModel extends Model {
15 | declare id: number;
16 | declare name: string;
17 | declare createdAt: Date;
18 | declare updatedAt: Date;
19 | }
20 |
21 | CategoryModel.init(
22 | {
23 | id: {
24 | type: DataTypes.INTEGER,
25 | autoIncrement: true,
26 | primaryKey: true,
27 | },
28 | name: {
29 | type: new DataTypes.STRING(128),
30 | allowNull: false,
31 | },
32 | createdAt: {
33 | type: DataTypes.DATE,
34 | },
35 | updatedAt: {
36 | type: DataTypes.DATE,
37 | },
38 | },
39 | {
40 | sequelize,
41 | tableName: 'categories',
42 | modelName: 'category',
43 | },
44 | );
45 |
--------------------------------------------------------------------------------
/src/sources/typeorm/seeds/run.ts:
--------------------------------------------------------------------------------
1 | import dotenv from 'dotenv';
2 | dotenv.config({
3 | path: `${process.cwd()}/.env`,
4 | });
5 |
6 | import dataSource from '../config.js';
7 | import { Organization, Person } from '../models/index.js';
8 | import { organizations, persons } from './data/index.js';
9 |
10 | const organizationsCount = 3;
11 | const personsPerOrganizationCount = 2;
12 |
13 | const run = async () => {
14 | await dataSource.initialize();
15 | const em = dataSource.manager;
16 | const organizationRepository = em.getRepository(Organization);
17 | const personRepository = em.getRepository(Person);
18 |
19 | const createdOrganizations = await organizationRepository.insert(organizations(organizationsCount));
20 |
21 | await Promise.all(
22 | createdOrganizations.identifiers.map(({ id: organizationId }) =>
23 | personRepository.insert(persons(personsPerOrganizationCount, { organizationId })),
24 | ),
25 | );
26 | };
27 |
28 | run()
29 | .then(() => process.exit(0))
30 | .catch((e) => {
31 | console.log(e);
32 | process.exit(1);
33 | });
34 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "es6": true,
4 | "jest": true,
5 | "node": true
6 | },
7 | "parser": "@typescript-eslint/parser",
8 | "parserOptions": {
9 | "project": "tsconfig.json",
10 | "sourceType": "module",
11 | "ecmaFeatures": {
12 | "jsx": true
13 | },
14 | "ecmaVersion": 2022
15 | },
16 | "plugins": [
17 | "@typescript-eslint",
18 | "react",
19 | "prettier"
20 | ],
21 | "extends": [
22 | "eslint:recommended",
23 | "plugin:@typescript-eslint/eslint-recommended",
24 | "plugin:@typescript-eslint/recommended",
25 | "plugin:react/recommended",
26 | "plugin:react/jsx-runtime",
27 | "plugin:react-hooks/recommended",
28 | "prettier"
29 | ],
30 | "rules": {
31 | "prettier/prettier": "error",
32 | "@typescript-eslint/no-var-requires": "off",
33 | "@typescript-eslint/ban-ts-comment": "off",
34 | "@typescript-eslint/no-explicit-any": "off",
35 | "react/jsx-uses-react": "error"
36 | },
37 | "settings": {
38 | "react": {
39 | "version": "detect"
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/admin/components/dont-touch-this-action.tsx:
--------------------------------------------------------------------------------
1 | import { Box, H3, Text } from '@adminjs/design-system';
2 | import { BasePropertyProps } from 'adminjs';
3 | import React from 'react';
4 |
5 | const DontTouchThis = (props: BasePropertyProps) => {
6 | const { record } = props;
7 |
8 | return (
9 |
10 |
11 | Example of a simple page
12 | Where you can put almost everything like this:
13 |
14 |
15 |
16 |
17 |
18 | Or (more likely), operate on a returned record:
19 |
20 | {JSON.stringify(record, null, 2)}
21 |
22 |
23 |
24 | );
25 | };
26 |
27 | export default DontTouchThis;
28 |
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | datasource db {
2 | provider = "mysql"
3 | url = env("MYSQL_DATABASE_URL")
4 | }
5 |
6 | generator client {
7 | provider = "prisma-client-js"
8 | }
9 |
10 | enum Status {
11 | ACTIVE
12 | REMOVED
13 | }
14 |
15 | model Post {
16 | id Int @id @default(autoincrement())
17 | createdAt DateTime @default(now())
18 | updatedAt DateTime @updatedAt
19 | title String @db.VarChar(255)
20 | content String?
21 | someJson Json? @db.Json
22 | status Status @default(ACTIVE)
23 | published Boolean @default(false)
24 | publisher Publisher @relation(fields: [publisherId], references: [id])
25 | publisherId Int
26 | }
27 |
28 | model Profile {
29 | id Int @id @default(autoincrement())
30 | bio String?
31 | publisher Publisher @relation(fields: [publisherId], references: [id])
32 | publisherId Int @unique
33 | }
34 |
35 | model Publisher {
36 | id Int @id @default(autoincrement())
37 | email String @unique
38 | name String?
39 | posts Post[]
40 | profile Profile?
41 | }
42 |
--------------------------------------------------------------------------------
/src/admin/components/sidebar-resource-section.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/jsx-one-expression-per-line */
2 | import React, { FC } from 'react';
3 | import { Navigation } from '@adminjs/design-system';
4 | import { useTranslation, type SidebarResourceSectionProps, useNavigationResources } from 'adminjs';
5 |
6 | const SidebarResourceSection: FC = ({ resources }) => {
7 | const elements = useNavigationResources(resources);
8 | const { translateLabel } = useTranslation();
9 |
10 | const openUrl = (url: string) => () => {
11 | window.open(url, '_blank');
12 | };
13 |
14 | elements.unshift({
15 | icon: 'Truck',
16 | label: translateLabel('kanbanBoard'),
17 | onClick: openUrl('https://github.com/orgs/SoftwareBrothers/projects/5'),
18 | });
19 |
20 | elements.unshift({
21 | icon: 'PieChart',
22 | label: translateLabel('stats'),
23 | onClick: openUrl('https://stats.adminjs.co'),
24 | });
25 |
26 | return ;
27 | };
28 |
29 | export default SidebarResourceSection;
30 |
--------------------------------------------------------------------------------
/public/gtm.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | const headGtm = `
3 |
4 | (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
5 | new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
6 | j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
7 | 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
8 | })(window,document,'script','dataLayer','GTM-K6R3HZ4');
9 |
10 | `;
11 | const bodyGtm = `
12 |
13 |
15 |
16 | `;
17 | const headScript = window.document.createElement('script');
18 | headScript.type = 'text/javascript';
19 | headScript.text = headGtm;
20 | const bodyScript = window.document.createElement('noscript');
21 | bodyScript.text = bodyGtm;
22 |
23 | window.document.head.appendChild(headScript);
24 | window.document.body.appendChild(bodyScript);
25 | })();
26 |
--------------------------------------------------------------------------------
/src/sources/sequelize/hooks/get-sum.hook.ts:
--------------------------------------------------------------------------------
1 | import { ActionRequest, ActionResponse, After, flat } from 'adminjs';
2 |
3 | import { isGETMethod } from '../../../admin/admin.utils.js';
4 | import { getSum } from '../functions/get-sum.function.js';
5 | import { ProductSumInterface } from '../interfaces.js';
6 |
7 | export const getSumForOrder =
8 | (): After =>
9 | async (response: ActionResponse, request: ActionRequest): Promise => {
10 | if (!isGETMethod(request)) {
11 | return response;
12 | }
13 |
14 | if (response.records) {
15 | await Promise.all(
16 | response.records.map(async (record) => {
17 | const params = flat.unflatten(record.params);
18 | params.sum = await getSum(params.id);
19 | record.params = flat.flatten(params);
20 | }),
21 | );
22 | } else {
23 | const params = flat.unflatten(response.record.params);
24 | params.sum = await getSum(params.id);
25 | response.record.params = flat.flatten(params);
26 | }
27 |
28 | return response;
29 | };
30 |
--------------------------------------------------------------------------------
/src/themes/custom-theme/components/SidebarFooter.tsx:
--------------------------------------------------------------------------------
1 | import { Avatar, Box, SmallText, Title, Tooltip } from '@adminjs/design-system';
2 | import { ReduxState, ViewHelpers, useTranslation } from 'adminjs';
3 | import React from 'react';
4 | import { useSelector } from 'react-redux';
5 |
6 | const h = new ViewHelpers();
7 |
8 | const SidebarFooter = () => {
9 | const session = useSelector((state: ReduxState) => state.session);
10 | const { title, email, avatarUrl } = session;
11 | const { tb } = useTranslation();
12 |
13 | if (!session) return null;
14 |
15 | return (
16 |
17 |
18 |
19 | {email.slice(0, 1).toUpperCase()}
20 |
21 |
22 |
23 | {email}
24 | {title && {title}}
25 |
26 |
27 |
28 |
29 | );
30 | };
31 |
32 | export default SidebarFooter;
33 |
--------------------------------------------------------------------------------
/src/sources/mikroorm/models/car.model.ts:
--------------------------------------------------------------------------------
1 | import { BaseEntity, Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core';
2 |
3 | import type { Seller } from './seller.model.js';
4 | import type { Owner } from './owner.model.js';
5 |
6 | export interface ICar {
7 | name: string;
8 | meta: Record;
9 | owner: Owner;
10 | seller: Seller;
11 | }
12 |
13 | @Entity({ tableName: 'cars' })
14 | export class Car extends BaseEntity implements ICar {
15 | @PrimaryKey()
16 | id: number;
17 |
18 | @Property({ fieldName: 'name', columnType: 'text' })
19 | name: string;
20 |
21 | @Property({ fieldName: 'meta', columnType: 'jsonb', default: '{}' })
22 | meta: Record;
23 |
24 | @Property({ fieldName: 'created_at', columnType: 'timestamptz' })
25 | createdAt: Date = new Date();
26 |
27 | @Property({
28 | fieldName: 'updated_at',
29 | columnType: 'timestamptz',
30 | onUpdate: () => new Date(),
31 | })
32 | updatedAt: Date = new Date();
33 |
34 | @ManyToOne('Owner', { mapToPk: true })
35 | owner: Owner;
36 |
37 | @ManyToOne('Seller', { mapToPk: true })
38 | seller: Seller;
39 | }
40 |
--------------------------------------------------------------------------------
/src/admin/pages/design-system-examples/illustrations-page.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Header, Illustration, Text } from '@adminjs/design-system';
2 | import React, { FC } from 'react';
3 |
4 | const variants = [
5 | 'Accept',
6 | 'Cup',
7 | 'Bag',
8 | 'Beware',
9 | 'Notebook',
10 | 'NotFound',
11 | 'Padlock',
12 | 'Photos',
13 | 'Plug',
14 | 'RocketNew',
15 | 'Tags',
16 | 'Folder',
17 | 'Box',
18 | 'Calendar',
19 | 'Cancel',
20 | 'Cards',
21 | 'Clip',
22 | 'Cloud',
23 | 'Details',
24 | 'Docs',
25 | 'Drawer',
26 | 'IdentityCard',
27 | ] as const;
28 |
29 | const IllustrationPage: FC = () => (
30 |
31 |
34 |
35 | {variants.map((variant) => (
36 |
37 |
38 | {variant}
39 |
40 | ))}
41 |
42 |
43 | );
44 |
45 | export default IllustrationPage;
46 |
--------------------------------------------------------------------------------
/src/sources/sequelize/resources/product.resource.ts:
--------------------------------------------------------------------------------
1 | import { menu } from '../../../admin/index.js';
2 | import { useEnvironmentVariableToDisableActions } from '../../../admin/features/useEnvironmentVariableToDisableActions.js';
3 | import { ResourceFunction } from '../../../admin/types/index.js';
4 | import { ProductModel } from '../models/index.js';
5 |
6 | export const CreateProductResource: ResourceFunction = () => ({
7 | resource: ProductModel,
8 | features: [useEnvironmentVariableToDisableActions()],
9 | options: {
10 | navigation: menu.sequelize,
11 | properties: {
12 | price: {
13 | type: 'currency',
14 | props: {
15 | decimalSeparator: '.',
16 | disableGroupSeparators: true,
17 | prefix: '$',
18 | fixedDecimalLength: 2,
19 | },
20 | },
21 | name: {
22 | isTitle: true,
23 | },
24 | createdAt: {
25 | isVisible: {
26 | show: true,
27 | edit: false,
28 | },
29 | },
30 | updatedAt: {
31 | isVisible: {
32 | show: true,
33 | edit: false,
34 | },
35 | },
36 | },
37 | },
38 | });
39 |
--------------------------------------------------------------------------------
/src/servers/nestjs/mongoose/mongoose.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { MongooseModule } from '@nestjs/mongoose';
3 |
4 | import { AdminSchema } from '../../../sources/mongoose/models/admin.model.js';
5 | import { UserSchema } from '../../../sources/mongoose/models/user.model.js';
6 | import { ArticleSchema } from '../../../sources/mongoose/models/article.model.js';
7 | import { CategorySchema } from '../../../sources/mongoose/models/category.model.js';
8 | import { CommentSchema } from '../../../sources/mongoose/models/comment.model.js';
9 | import { ComplicatedSchema } from '../../../sources/mongoose/models/complicated.model.js';
10 |
11 | @Module({
12 | imports: [
13 | MongooseModule.forRoot(process.env.MONGO_DATABASE_URL),
14 | MongooseModule.forFeature([
15 | { name: 'Admin', schema: AdminSchema },
16 | { name: 'User', schema: UserSchema },
17 | { name: 'Article', schema: ArticleSchema },
18 | { name: 'Category', schema: CategorySchema },
19 | { name: 'Comment', schema: CommentSchema },
20 | { name: 'Complicated', schema: ComplicatedSchema },
21 | ]),
22 | ],
23 | exports: [MongooseModule],
24 | })
25 | export class MongooseSchemasModule {}
26 |
--------------------------------------------------------------------------------
/src/sources/objectionjs/utils/base-model.ts:
--------------------------------------------------------------------------------
1 | import addFormatsPlugin from 'ajv-formats';
2 | import { AjvValidator, Model } from 'objection';
3 |
4 | const addFormats = addFormatsPlugin.default;
5 |
6 | // https://github.com/Vincit/objection.js/issues/2249#issuecomment-1075898552
7 | // There is no support for Date type in JSONSchema, so this class does two things for us:
8 | // 1. It adds support to formats such as `format: "date-format"` in your "jsonSchema"
9 | // 2. It adds $beforeInsert and $beforeUpdate hooks to update your timestamps
10 | // AdminJS can not load your models properly without it.
11 | export abstract class BaseModel extends Model {
12 | createdAt: string;
13 |
14 | updatedAt: string;
15 |
16 | static createValidator(): AjvValidator {
17 | return new AjvValidator({
18 | onCreateAjv: (ajv) => {
19 | addFormats(ajv);
20 | },
21 | options: {
22 | allErrors: true,
23 | validateSchema: false,
24 | ownProperties: true,
25 | },
26 | });
27 | }
28 |
29 | $beforeInsert(): void {
30 | this.createdAt = new Date().toISOString();
31 | }
32 |
33 | $beforeUpdate(): void {
34 | this.updatedAt = new Date().toISOString();
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/admin/components/top-bar.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Button, Icon, Text } from '@adminjs/design-system';
2 | import { ReduxState } from 'adminjs';
3 | import React, { FC } from 'react';
4 | import { useSelector } from 'react-redux';
5 |
6 | const TopBar: FC = () => {
7 | const versions = useSelector((state: ReduxState) => state.versions);
8 | const GITHUB_URL = (window as any).AdminJS.env.GITHUB_URL;
9 | const SLACK_URL = (window as any).AdminJS.env.SLACK_URL;
10 | const DOCUMENTATION_URL = (window as any).AdminJS.env.DOCUMENTATION_URL;
11 |
12 | return (
13 |
14 |
15 | {versions.admin}
16 |
17 |
21 |
25 |
29 |
30 | );
31 | };
32 |
33 | export { TopBar };
34 | export default TopBar;
35 |
--------------------------------------------------------------------------------
/src/sources/objectionjs/models/manager.entity.ts:
--------------------------------------------------------------------------------
1 | import { BaseModel } from '../utils/base-model.js';
2 |
3 | import Office from './office.entity.js';
4 |
5 | class Manager extends BaseModel {
6 | id: number;
7 |
8 | firstName: string;
9 |
10 | lastName: string;
11 |
12 | age: number;
13 |
14 | officeId?: number;
15 |
16 | office?: Office;
17 |
18 | static tableName = 'managers';
19 |
20 | static jsonSchema = {
21 | type: 'object',
22 | required: ['firstName', 'lastName'],
23 |
24 | properties: {
25 | id: { type: 'integer' },
26 | officeId: { type: 'integer' },
27 | firstName: { type: 'string', minLength: 1, maxLength: 255 },
28 | lastName: { type: 'string', minLength: 1, maxLength: 255 },
29 | age: { type: 'number' },
30 | createdAt: { type: 'string', format: 'date-time', readOnly: true },
31 | updatedAt: { type: 'string', format: 'date-time', readOnly: true },
32 | },
33 | };
34 |
35 | static relationMappings = () => ({
36 | office: {
37 | relation: BaseModel.BelongsToOneRelation,
38 | modelClass: Office,
39 | join: {
40 | from: 'managers.officeId',
41 | to: 'offices.id',
42 | },
43 | },
44 | });
45 | }
46 |
47 | export default Manager;
48 |
--------------------------------------------------------------------------------
/src/servers/hapijs.ts:
--------------------------------------------------------------------------------
1 | import { init } from '../sources/mikroorm/config.js';
2 | import dataSource from '../sources/typeorm/config.js';
3 | import { generateAdminJSConfig } from '../admin/index.js';
4 | import { expressAuthenticatedRouter } from '../admin/router.js';
5 | import AdminJS from 'adminjs';
6 |
7 | import Hapi, { ServerRegisterPluginObjectDirect } from '@hapi/hapi';
8 | import mongoose from 'mongoose';
9 | import AdminJSPlugin from '@adminjs/hapi';
10 |
11 | const start = async () => {
12 | try {
13 | const server = Hapi.server({
14 | port: process.env.PORT || 3000,
15 | });
16 |
17 | await mongoose.connect(process.env.MONGO_DATABASE_URL);
18 | await init();
19 | await dataSource.initialize();
20 |
21 | const adminJS = generateAdminJSConfig();
22 |
23 | await server.register({
24 | plugin: AdminJSPlugin,
25 | options: {
26 | ...adminJS,
27 | auth: expressAuthenticatedRouter(new AdminJS(adminJS)),
28 | },
29 | } as ServerRegisterPluginObjectDirect);
30 |
31 | await server.start();
32 | console.log(`AdminJS is under http://localhost:${process.env.PORT}/admin`);
33 | } catch (error) {
34 | console.log(error);
35 | process.exit(1);
36 | }
37 | };
38 |
39 | start();
40 |
--------------------------------------------------------------------------------
/src/sources/sequelize/seeds/run.ts:
--------------------------------------------------------------------------------
1 | import dotenv from 'dotenv';
2 | dotenv.config({
3 | path: `${process.cwd()}/.env`,
4 | });
5 |
6 | import flatten from 'lodash/flatten.js';
7 | import { CategoryModel, CartModel, ProductModel, OrderModel } from '../models/index.js';
8 | import { categories, carts, products, orders } from './data/index.js';
9 |
10 | const categoriesCount = 2;
11 | const productsPerCategory = 2;
12 | const cartsPerProduct = 1;
13 |
14 | const run = async () => {
15 | const createdCategories = await CategoryModel.bulkCreate(categories(categoriesCount));
16 | const createdProducts = flatten(
17 | await Promise.all(
18 | createdCategories.map((cat) =>
19 | ProductModel.bulkCreate(products(productsPerCategory, { categoryId: cat.get('id') })),
20 | ),
21 | ),
22 | );
23 | // TODO: Look into Order/Cart relationship, it's a bit odd...
24 | const createdOrder = await OrderModel.create(orders(1)[0]);
25 | await Promise.all(
26 | createdProducts.map((p) =>
27 | CartModel.bulkCreate(carts(cartsPerProduct, { orderId: createdOrder.get('id'), productId: p.get('id') })),
28 | ),
29 | );
30 | };
31 |
32 | run()
33 | .then(() => process.exit(0))
34 | .catch((e) => {
35 | console.log(e);
36 | process.exit(1);
37 | });
38 |
--------------------------------------------------------------------------------
/src/sources/mongoose/resources/admin.resource.ts:
--------------------------------------------------------------------------------
1 | import passwordsFeature from '@adminjs/passwords';
2 | import argon2 from 'argon2';
3 |
4 | import { componentLoader } from '../../../admin/components.bundler.js';
5 | import { menu } from '../../../admin/index.js';
6 | import { ResourceFunction } from '../../../admin/types/index.js';
7 | import { AdminModel } from '../models/index.js';
8 |
9 | export const CreateAdminResource: ResourceFunction = () => ({
10 | resource: AdminModel,
11 | features: [
12 | (options): object => ({
13 | ...options,
14 | listProperties: ['_id', 'email'],
15 | showProperties: ['_id', 'email'],
16 | editProperties: ['email', 'newPassword'],
17 | }),
18 | passwordsFeature({
19 | properties: {
20 | password: 'newPassword',
21 | encryptedPassword: 'password',
22 | },
23 | hash: argon2.hash,
24 | componentLoader,
25 | }),
26 | ],
27 | options: {
28 | navigation: menu.mongoose,
29 | sort: {
30 | direction: 'asc',
31 | sortBy: 'email',
32 | },
33 | actions: {
34 | delete: { isAccessible: false },
35 | bulkDelete: { isAccessible: false },
36 | edit: { isAccessible: false },
37 | new: { isAccessible: false },
38 | },
39 | },
40 | });
41 |
--------------------------------------------------------------------------------
/src/sources/mikroorm/models/owner.model.ts:
--------------------------------------------------------------------------------
1 | import { v4 } from 'uuid';
2 | import { BaseEntity, Entity, Enum, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
3 |
4 | import type { Car } from './car.model.js';
5 |
6 | export enum UserRole {
7 | ADMIN = 'admin',
8 | CLIENT = 'client',
9 | }
10 |
11 | export interface IOwner {
12 | firstName: string;
13 | lastName: string;
14 | age: number;
15 | role: UserRole;
16 | }
17 |
18 | @Entity({ tableName: 'owners' })
19 | export class Owner extends BaseEntity implements IOwner {
20 | @PrimaryKey({ columnType: 'uuid' })
21 | id = v4();
22 |
23 | @Property({ fieldName: 'first_name', columnType: 'text' })
24 | firstName: string;
25 |
26 | @Property({ fieldName: 'last_name', columnType: 'text' })
27 | lastName: string;
28 |
29 | @Property({ fieldName: 'age', columnType: 'integer' })
30 | age: number;
31 |
32 | @Enum(() => UserRole)
33 | role: UserRole;
34 |
35 | @Property({ fieldName: 'created_at', columnType: 'timestamptz' })
36 | createdAt: Date = new Date();
37 |
38 | @Property({
39 | fieldName: 'updated_at',
40 | columnType: 'timestamptz',
41 | onUpdate: () => new Date(),
42 | })
43 | updatedAt: Date = new Date();
44 |
45 | @OneToMany('Car', (car: Car) => car.owner)
46 | cars: Car[];
47 | }
48 |
--------------------------------------------------------------------------------
/src/admin/pages/design-system-examples/messages-page.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Header, MessageBox, MessageBoxProps, Text } from '@adminjs/design-system';
2 | import React, { FC } from 'react';
3 |
4 | const MessagesPage: FC = () => {
5 | const variants: MessageBoxProps['variant'][] = ['info', 'danger', 'success', 'warning'];
6 |
7 | return (
8 |
9 |
12 |
13 | {variants.map((variant) => (
14 | // eslint-disable-next-line @typescript-eslint/no-empty-function
15 | {}} />
16 | ))}
17 | With extra body
18 | {variants.map((variant) => (
19 | // eslint-disable-next-line @typescript-eslint/no-empty-function
20 | {}}>
21 | Lorem ipsum dolor sit, amet consectetur adipisicing elit. Fuga itaque quaerat quia eum ratione ipsum
22 | deleniti. Officiis nisi non necessitatibus laudantium blanditiis inventore.
23 |
24 | ))}
25 |
26 |
27 | );
28 | };
29 |
30 | export default MessagesPage;
31 |
--------------------------------------------------------------------------------
/src/sources/objectionjs/models/office.entity.ts:
--------------------------------------------------------------------------------
1 | import { BaseModel } from '../utils/base-model.js';
2 |
3 | import Manager from './manager.entity.js';
4 |
5 | export interface OfficeAddress {
6 | street: string;
7 | city: string;
8 | zipCode: string;
9 | }
10 |
11 | class Office extends BaseModel {
12 | id: number;
13 |
14 | name: string;
15 |
16 | address?: OfficeAddress;
17 |
18 | managers?: Manager[];
19 |
20 | static tableName = 'offices';
21 |
22 | static jsonSchema = {
23 | type: 'object',
24 | required: ['name'],
25 | properties: {
26 | id: { type: 'integer' },
27 | name: { type: 'string', minLength: 1, maxLength: 255 },
28 | address: {
29 | type: 'object',
30 | properties: {
31 | street: { type: 'string' },
32 | city: { type: 'string' },
33 | zipCode: { type: 'string' },
34 | },
35 | },
36 | createdAt: { type: 'string', format: 'date-time' },
37 | updatedAt: { type: 'string', format: 'date-time' },
38 | },
39 | };
40 |
41 | static relationMappings = () => ({
42 | managers: {
43 | relation: BaseModel.HasManyRelation,
44 | modelClass: Manager,
45 | join: {
46 | from: 'offices.id',
47 | to: 'managers.officeId',
48 | },
49 | },
50 | });
51 | }
52 |
53 | export default Office;
54 |
--------------------------------------------------------------------------------
/src/admin/components.bundler.ts:
--------------------------------------------------------------------------------
1 | import { ComponentLoader, OverridableComponent } from 'adminjs';
2 | import path from 'path';
3 | import * as url from 'url';
4 |
5 | const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
6 | export const componentLoader = new ComponentLoader();
7 |
8 | export const add = (url: string, componentName: string): string =>
9 | componentLoader.add(componentName, path.join(__dirname, url));
10 |
11 | export const override = (url: string, componentName: OverridableComponent): string =>
12 | componentLoader.override(componentName, path.join(__dirname, url));
13 |
14 | /**
15 | * Overridable components
16 | */
17 | override('components/top-bar', 'Version');
18 | override('components/login', 'Login');
19 | override('components/sidebar-resource-section', 'SidebarResourceSection');
20 |
21 | /**
22 | * Common components
23 | */
24 | export const PRODUCTS_LIST = add('components/products-list', 'ProductList');
25 | export const DONT_TOUCH_THIS_ACTION = add('components/dont-touch-this-action', 'CustomAction');
26 | export const DETAILED_STATS = add('components/detailed-stats', 'DetailedStats');
27 | export const THUMB = add('components/thumb', 'Thumb');
28 |
29 | /**
30 | * Pages
31 | */
32 | export const CUSTOM_PAGE = add('pages/custom-page', 'CustomPage');
33 | export const DESIGN_SYSTEM_PAGE = add('pages/design-system-examples/index', 'DesignSystemPage');
34 |
--------------------------------------------------------------------------------
/src/sources/mikroorm/migrations/Migration20220714094312.ts:
--------------------------------------------------------------------------------
1 | import { Migration } from '@mikro-orm/migrations';
2 |
3 | export class Migration20220714094312 extends Migration {
4 | async up(): Promise {
5 | this.addSql('create table "sellers" ("id" uuid not null, "name" text not null);');
6 | this.addSql('alter table "sellers" add constraint "sellers_pkey" primary key ("id");');
7 |
8 | this.addSql(
9 | 'create table "owners" ("id" uuid not null, "first_name" text not null, "last_name" text not null, "age" integer not null, "role" text check ("role" in (\'admin\', \'client\')) not null, "created_at" timestamptz not null, "updated_at" timestamptz not null);',
10 | );
11 | this.addSql('alter table "owners" add constraint "owners_pkey" primary key ("id");');
12 |
13 | this.addSql(
14 | 'create table "cars" ("id" serial primary key, "name" text not null, "meta" jsonb not null default \'{}\', "created_at" timestamptz not null, "updated_at" timestamptz not null, "owner_id" uuid not null, "seller_id" uuid not null);',
15 | );
16 |
17 | this.addSql(
18 | 'alter table "cars" add constraint "cars_owner_id_foreign" foreign key ("owner_id") references "owners" ("id") on update cascade;',
19 | );
20 | this.addSql(
21 | 'alter table "cars" add constraint "cars_seller_id_foreign" foreign key ("seller_id") references "sellers" ("id") on update cascade;',
22 | );
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/sources/sequelize/hooks/get-products.hook.ts:
--------------------------------------------------------------------------------
1 | import { ActionRequest, ActionResponse, After, flat } from 'adminjs';
2 |
3 | import { isGETMethod } from '../../../admin/admin.utils.js';
4 | import { CartModel, OrderModel, ProductModel } from '../models/index.js';
5 | import { ProductListInterface } from '../interfaces.js';
6 |
7 | export const getProducts =
8 | (): After =>
9 | async (response: ActionResponse, request: ActionRequest): Promise => {
10 | if (!isGETMethod(request)) {
11 | return response;
12 | }
13 |
14 | const id = parseInt(request.params.recordId, 10);
15 |
16 | const order = await OrderModel.findByPk(id, {
17 | include: [
18 | {
19 | model: CartModel,
20 | as: 'carts',
21 | include: [
22 | {
23 | model: ProductModel,
24 | as: 'product',
25 | },
26 | ],
27 | },
28 | ],
29 | });
30 |
31 | const params = flat.unflatten(response.record.params);
32 | params.products = order.carts?.map((cart) => ({
33 | quantity: cart.quantity,
34 | product: {
35 | id: cart.product?.id,
36 | name: cart.product?.name,
37 | price: parseFloat(cart.product?.price),
38 | },
39 | }));
40 |
41 | response.record.params = flat.flatten(params);
42 |
43 | return response;
44 | };
45 |
--------------------------------------------------------------------------------
/src/servers/fastify.ts:
--------------------------------------------------------------------------------
1 | import fastify from 'fastify';
2 | import mongoose from 'mongoose';
3 | import AdminJS from 'adminjs';
4 | import fastifyStatic from 'fastify-static';
5 | import path from 'path';
6 | import * as url from 'url';
7 |
8 | import { createAuthUsers, generateAdminJSConfig } from '../admin/index.js';
9 | import { init } from '../sources/mikroorm/config.js';
10 | import dataSource from '../sources/typeorm/config.js';
11 | import { fastifyAuthenticatedRouter } from '../admin/router.js';
12 |
13 | const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
14 |
15 | const app = fastify();
16 |
17 | const attachAdminJS = async () => {
18 | const config = generateAdminJSConfig();
19 | const adminJS = new AdminJS(config);
20 | await fastifyAuthenticatedRouter(adminJS, app);
21 | await createAuthUsers();
22 | };
23 |
24 | const run = async (): Promise => {
25 | try {
26 | await mongoose.connect(process.env.MONGO_DATABASE_URL);
27 | await init();
28 | await dataSource.initialize();
29 |
30 | app.register(fastifyStatic, {
31 | root: path.join(__dirname, '../assets'),
32 | prefix: '/assets/',
33 | });
34 |
35 | await attachAdminJS();
36 |
37 | await app.listen(process.env.PORT);
38 | console.log(`AdminJS is under http://localhost:${process.env.PORT}/admin`);
39 | } catch (err) {
40 | app.log.error(err);
41 | process.exit(1);
42 | }
43 | };
44 |
45 | run();
46 |
--------------------------------------------------------------------------------
/src/sources/mikroorm/seeds/run.ts:
--------------------------------------------------------------------------------
1 | import dotenv from 'dotenv';
2 | dotenv.config({
3 | path: `${process.cwd()}/.env`,
4 | });
5 |
6 | import { init, orm } from '../config.js';
7 | import { Car, Owner, Seller } from '../models/index.js';
8 | import { cars, owners, sellers } from './data/index.js';
9 |
10 | const ownersCount = 4;
11 | const sellersCount = 4;
12 | const carsCount = 15;
13 |
14 | const run = async () => {
15 | await init();
16 | const ownerRepository = orm.em.getRepository(Owner);
17 | const createdOwners = owners(ownersCount).map((o) => {
18 | const owner = ownerRepository.create(o);
19 | orm.em.persist(owner);
20 |
21 | return owner;
22 | });
23 | await orm.em.flush();
24 |
25 | const sellerRepository = orm.em.getRepository(Seller);
26 | const createdSellers = sellers(sellersCount).map((s) => {
27 | const seller = sellerRepository.create(s);
28 | orm.em.persist(seller);
29 |
30 | return seller;
31 | });
32 | await orm.em.flush();
33 |
34 | const carRepository = orm.em.getRepository(Car);
35 | cars(carsCount, {
36 | owners: createdOwners,
37 | sellers: createdSellers,
38 | }).map((c) => {
39 | const car = carRepository.create(c);
40 | orm.em.persist(car);
41 |
42 | return car;
43 | });
44 | await orm.em.flush();
45 | };
46 |
47 | run()
48 | .then(() => process.exit(0))
49 | .catch((e) => {
50 | console.log(e);
51 | process.exit(1);
52 | });
53 |
--------------------------------------------------------------------------------
/src/sources/sequelize/models/order.model.ts:
--------------------------------------------------------------------------------
1 | import { DataTypes, Model, Optional, Association, NonAttribute, HasManyGetAssociationsMixin } from 'sequelize';
2 |
3 | import { sequelize } from '../index.js';
4 | import { CartModel } from './cart.model.js';
5 |
6 | type Order = {
7 | id: number;
8 | isPaid: boolean;
9 | delivery: string;
10 | createdAt: Date;
11 | updatedAt: Date;
12 | };
13 |
14 | export type OrderCreationAttributes = Optional;
15 |
16 | export class OrderModel extends Model {
17 | declare id: number;
18 | declare isPaid: boolean;
19 | declare delivery: string;
20 | declare createdAt: Date;
21 | declare updatedAt: Date;
22 |
23 | declare carts?: NonAttribute;
24 |
25 | declare getCarts: HasManyGetAssociationsMixin;
26 |
27 | declare static associations: {
28 | carts: Association;
29 | };
30 | }
31 |
32 | OrderModel.init(
33 | {
34 | id: {
35 | type: DataTypes.INTEGER,
36 | autoIncrement: true,
37 | primaryKey: true,
38 | },
39 | isPaid: {
40 | type: DataTypes.BOOLEAN,
41 | defaultValue: false,
42 | },
43 | delivery: {
44 | type: DataTypes.STRING,
45 | },
46 | createdAt: {
47 | type: DataTypes.DATE,
48 | },
49 | updatedAt: {
50 | type: DataTypes.DATE,
51 | },
52 | },
53 | {
54 | sequelize,
55 | tableName: 'orders',
56 | modelName: 'order',
57 | },
58 | );
59 |
--------------------------------------------------------------------------------
/prisma/migrations/20220713101214_init/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE `Post` (
3 | `id` INTEGER NOT NULL AUTO_INCREMENT,
4 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
5 | `updatedAt` DATETIME(3) NOT NULL,
6 | `title` VARCHAR(255) NOT NULL,
7 | `content` VARCHAR(191) NULL,
8 | `someJson` JSON NULL,
9 | `status` ENUM('ACTIVE', 'REMOVED') NOT NULL DEFAULT 'ACTIVE',
10 | `published` BOOLEAN NOT NULL DEFAULT false,
11 | `publisherId` INTEGER NOT NULL,
12 |
13 | PRIMARY KEY (`id`)
14 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
15 |
16 | -- CreateTable
17 | CREATE TABLE `Profile` (
18 | `id` INTEGER NOT NULL AUTO_INCREMENT,
19 | `bio` VARCHAR(191) NULL,
20 | `publisherId` INTEGER NOT NULL,
21 |
22 | UNIQUE INDEX `Profile_publisherId_key`(`publisherId`),
23 | PRIMARY KEY (`id`)
24 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
25 |
26 | -- CreateTable
27 | CREATE TABLE `Publisher` (
28 | `id` INTEGER NOT NULL AUTO_INCREMENT,
29 | `email` VARCHAR(191) NOT NULL,
30 | `name` VARCHAR(191) NULL,
31 |
32 | UNIQUE INDEX `Publisher_email_key`(`email`),
33 | PRIMARY KEY (`id`)
34 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
35 |
36 | -- AddForeignKey
37 | ALTER TABLE `Post` ADD CONSTRAINT `Post_publisherId_fkey` FOREIGN KEY (`publisherId`) REFERENCES `Publisher`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
38 |
39 | -- AddForeignKey
40 | ALTER TABLE `Profile` ADD CONSTRAINT `Profile_publisherId_fkey` FOREIGN KEY (`publisherId`) REFERENCES `Publisher`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
41 |
--------------------------------------------------------------------------------
/src/sources/prisma/migrations/20220218111814_init/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE `Post` (
3 | `id` INTEGER NOT NULL AUTO_INCREMENT,
4 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
5 | `updatedAt` DATETIME(3) NOT NULL,
6 | `title` VARCHAR(255) NOT NULL,
7 | `content` VARCHAR(191) NULL,
8 | `someJson` JSON NULL,
9 | `status` ENUM('ACTIVE', 'REMOVED') NOT NULL DEFAULT 'ACTIVE',
10 | `published` BOOLEAN NOT NULL DEFAULT false,
11 | `publisherId` INTEGER NOT NULL,
12 |
13 | PRIMARY KEY (`id`)
14 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
15 |
16 | -- CreateTable
17 | CREATE TABLE `Profile` (
18 | `id` INTEGER NOT NULL AUTO_INCREMENT,
19 | `bio` VARCHAR(191) NULL,
20 | `publisherId` INTEGER NOT NULL,
21 |
22 | UNIQUE INDEX `Profile_publisherId_key`(`publisherId`),
23 | PRIMARY KEY (`id`)
24 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
25 |
26 | -- CreateTable
27 | CREATE TABLE `Publisher` (
28 | `id` INTEGER NOT NULL AUTO_INCREMENT,
29 | `email` VARCHAR(191) NOT NULL,
30 | `name` VARCHAR(191) NULL,
31 |
32 | UNIQUE INDEX `Publisher_email_key`(`email`),
33 | PRIMARY KEY (`id`)
34 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
35 |
36 | -- AddForeignKey
37 | ALTER TABLE `Post` ADD CONSTRAINT `Post_publisherId_fkey` FOREIGN KEY (`publisherId`) REFERENCES `Publisher`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
38 |
39 | -- AddForeignKey
40 | ALTER TABLE `Profile` ADD CONSTRAINT `Profile_publisherId_fkey` FOREIGN KEY (`publisherId`) REFERENCES `Publisher`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
41 |
--------------------------------------------------------------------------------
/src/servers/express/index.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import mongoose from 'mongoose';
3 | import express, { Express } from 'express';
4 | import cors from 'cors';
5 | import AdminJS from 'adminjs';
6 | import * as url from 'url';
7 |
8 | import { createAuthUsers, generateAdminJSConfig } from '../../admin/index.js';
9 | import { expressAuthenticatedRouter } from '../../admin/router.js';
10 | import { init } from '../../sources/mikroorm/config.js';
11 | import dataSource from '../../sources/typeorm/config.js';
12 |
13 | const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
14 |
15 | const attachAdminJS = async (app: Express) => {
16 | const config = generateAdminJSConfig();
17 | const adminJS = new AdminJS(config);
18 |
19 | if (process.env.NODE_ENV === 'production') await adminJS.initialize();
20 | else adminJS.watch();
21 |
22 | const adminRouter = expressAuthenticatedRouter(adminJS);
23 |
24 | app.use(adminJS.options.rootPath, adminRouter);
25 | app.get('/', (req, res) => res.redirect(adminJS.options.rootPath));
26 | app.use(express.static(path.join(__dirname, '../../../public')));
27 |
28 | await createAuthUsers();
29 | };
30 |
31 | const start = async () => {
32 | const app = express();
33 | app.enable('trust proxy');
34 | app.use(cors({ credentials: true, origin: true }));
35 |
36 | await mongoose.connect(process.env.MONGO_DATABASE_URL);
37 | await init();
38 | await dataSource.initialize();
39 |
40 | await attachAdminJS(app);
41 |
42 | app.listen(process.env.PORT, async () => {
43 | console.log(`AdminJS is under http://localhost:${process.env.PORT}`);
44 | });
45 | };
46 |
47 | start();
48 |
--------------------------------------------------------------------------------
/src/sources/typeorm/resources/person.resource.ts:
--------------------------------------------------------------------------------
1 | import { menu } from '../../../admin/index.js';
2 | import { DETAILED_STATS, DONT_TOUCH_THIS_ACTION } from '../../../admin/components.bundler.js';
3 | import { useEnvironmentVariableToDisableActions } from '../../../admin/features/useEnvironmentVariableToDisableActions.js';
4 | import { ResourceFunction } from '../../../admin/types/index.js';
5 | import { validateEmail } from '../handlers/validate-email.handler.js';
6 | import { Person } from '../models/index.js';
7 |
8 | export const CreatePersonResource: ResourceFunction = () => ({
9 | resource: Person,
10 | features: [useEnvironmentVariableToDisableActions()],
11 | options: {
12 | navigation: menu.typeorm,
13 | properties: {
14 | phone: {
15 | type: 'phone',
16 | },
17 | },
18 | actions: {
19 | new: {
20 | before: [validateEmail],
21 | },
22 | edit: {
23 | before: [validateEmail],
24 | },
25 | detailedStats: {
26 | actionType: 'resource',
27 | icon: 'BarChart2',
28 | name: 'Resource statistics',
29 | component: DETAILED_STATS,
30 | handler: async () => {
31 | return { true: 'ueas' };
32 | },
33 | showInDrawer: true,
34 | },
35 | dontTouchThis: {
36 | actionType: 'record',
37 | icon: 'Exit',
38 | guard: 'youCanSetupGuards',
39 | component: DONT_TOUCH_THIS_ACTION,
40 | handler: async (request, response, context) => {
41 | return {
42 | record: context.record.toJSON(),
43 | };
44 | },
45 | },
46 | },
47 | },
48 | });
49 |
--------------------------------------------------------------------------------
/src/sources/typeorm/migrations/1644569575919-init.ts:
--------------------------------------------------------------------------------
1 | import { MigrationInterface, QueryRunner } from 'typeorm';
2 |
3 | export class init1644569575919 implements MigrationInterface {
4 | name = 'init1644569575919';
5 |
6 | public async up(queryRunner: QueryRunner): Promise {
7 | await queryRunner.query(`CREATE TYPE "public"."organizations_country_enum" AS ENUM('PL', 'GB', 'DE')`);
8 | await queryRunner.query(
9 | `CREATE TABLE "organizations" ("id" SERIAL NOT NULL, "name" character varying NOT NULL, "address" character varying NOT NULL, "city" character varying NOT NULL, "postal_code" character varying NOT NULL, "country" "public"."organizations_country_enum" NOT NULL, CONSTRAINT "PK_6b031fcd0863e3f6b44230163f9" PRIMARY KEY ("id"))`,
10 | );
11 | await queryRunner.query(
12 | `CREATE TABLE "persons" ("id" SERIAL NOT NULL, "email" character varying NOT NULL, "first_name" character varying NOT NULL, "last_name" character varying NOT NULL, "phone" character varying NOT NULL, "organization_id" integer NOT NULL, CONSTRAINT "PK_74278d8812a049233ce41440ac7" PRIMARY KEY ("id"))`,
13 | );
14 | await queryRunner.query(
15 | `ALTER TABLE "persons" ADD CONSTRAINT "FK_e1929e2e66a63922e776334f77b" FOREIGN KEY ("organization_id") REFERENCES "organizations"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
16 | );
17 | }
18 |
19 | public async down(queryRunner: QueryRunner): Promise {
20 | await queryRunner.query(`ALTER TABLE "persons" DROP CONSTRAINT "FK_e1929e2e66a63922e776334f77b"`);
21 | await queryRunner.query(`DROP TABLE "persons"`);
22 | await queryRunner.query(`DROP TABLE "organizations"`);
23 | await queryRunner.query(`DROP TYPE "public"."organizations_country_enum"`);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/sources/mongoose/seeds/run.ts:
--------------------------------------------------------------------------------
1 | import dotenv from 'dotenv';
2 | dotenv.config({
3 | path: `${process.cwd()}/.env`,
4 | });
5 |
6 | import flatten from 'lodash/flatten.js';
7 | import mongoose from 'mongoose';
8 | import { ArticleModel, CategoryModel, CommentModel, UserModel } from '../models/index.js';
9 | import { articles, categories, comments, users } from './data/index.js';
10 |
11 | const usersCount = 3;
12 | const categoriesPerUserCount = 1;
13 | const articlesPerCategoryCount = 1;
14 | const commentsPerArticleCount = 5;
15 |
16 | const run = async () => {
17 | await mongoose.connect(process.env.MONGO_DATABASE_URL);
18 |
19 | const createdUsers = await Promise.all(users(usersCount).map((u) => new UserModel(u).save()));
20 | const createdCategories: Record[] = flatten(
21 | await Promise.all(
22 | createdUsers.map((u) =>
23 | Promise.all(categories(categoriesPerUserCount, { userId: u._id }).map((c) => new CategoryModel(c).save())),
24 | ),
25 | ),
26 | );
27 | const createdArticles: Record[] = flatten(
28 | await Promise.all(
29 | createdUsers.map((u, idx) =>
30 | Promise.all(
31 | articles(articlesPerCategoryCount, { authorId: u._id, categoryId: createdCategories[idx]._id }).map((a) =>
32 | new ArticleModel(a).save(),
33 | ),
34 | ),
35 | ),
36 | ),
37 | );
38 | await Promise.all(
39 | createdArticles.map((a) =>
40 | Promise.all(comments(commentsPerArticleCount, { articleId: a._id }).map((c) => new CommentModel(c).save())),
41 | ),
42 | );
43 | // TODO: Add ComplicatedModel seeds
44 | };
45 |
46 | run()
47 | .then(() => process.exit(0))
48 | .catch((e) => {
49 | console.log(e);
50 | process.exit(1);
51 | });
52 |
--------------------------------------------------------------------------------
/src/sources/sequelize/resources/order.resource.ts:
--------------------------------------------------------------------------------
1 | import { menu } from '../../../admin/index.js';
2 | import { PRODUCTS_LIST } from '../../../admin/components.bundler.js';
3 | import { useEnvironmentVariableToDisableActions } from '../../../admin/features/useEnvironmentVariableToDisableActions.js';
4 | import { ResourceFunction } from '../../../admin/types/index.js';
5 | import { getProducts } from '../hooks/get-products.hook.js';
6 | import { getSumForOrder } from '../hooks/get-sum.hook.js';
7 | import { OrderModel } from '../models/index.js';
8 |
9 | export const CreateOrderResource: ResourceFunction = () => ({
10 | resource: OrderModel,
11 | features: [useEnvironmentVariableToDisableActions()],
12 | options: {
13 | navigation: menu.sequelize,
14 | actions: {
15 | list: {
16 | after: [getSumForOrder()],
17 | },
18 | show: {
19 | after: [getProducts(), getSumForOrder()],
20 | },
21 | },
22 | properties: {
23 | sum: {
24 | isVisible: {
25 | edit: false,
26 | list: true,
27 | filter: false,
28 | show: true,
29 | },
30 | position: 998,
31 | },
32 | productsList: {
33 | isVisible: {
34 | edit: false,
35 | list: false,
36 | filter: false,
37 | show: true,
38 | },
39 | components: {
40 | show: PRODUCTS_LIST,
41 | },
42 | position: 999,
43 | },
44 | createdAt: {
45 | isVisible: {
46 | show: true,
47 | edit: false,
48 | },
49 | },
50 | updatedAt: {
51 | isVisible: {
52 | show: true,
53 | edit: false,
54 | },
55 | },
56 | },
57 | },
58 | });
59 |
--------------------------------------------------------------------------------
/src/sources/sequelize/models/product.model.ts:
--------------------------------------------------------------------------------
1 | import { DataTypes, Model, Optional, Association, HasOneGetAssociationMixin, NonAttribute } from 'sequelize';
2 |
3 | import { sequelize } from '../index.js';
4 | import { CategoryModel } from './category.model.js';
5 |
6 | type Product = {
7 | id: number;
8 | name: string;
9 | price: number;
10 | categoryId: number;
11 | createdAt: Date;
12 | updatedAt: Date;
13 | };
14 |
15 | export type ProductCreationAttributes = Optional;
16 |
17 | export class ProductModel extends Model {
18 | declare id: number;
19 | declare name: string;
20 | declare price: string;
21 | declare categoryId: number;
22 | declare createdAt: Date;
23 | declare updatedAt: Date;
24 |
25 | declare category?: NonAttribute;
26 |
27 | declare getCategory: HasOneGetAssociationMixin;
28 |
29 | declare static associations: {
30 | category: Association;
31 | };
32 | }
33 |
34 | ProductModel.init(
35 | {
36 | id: {
37 | type: DataTypes.INTEGER,
38 | autoIncrement: true,
39 | primaryKey: true,
40 | },
41 | name: {
42 | type: new DataTypes.STRING(128),
43 | allowNull: false,
44 | },
45 | price: {
46 | type: new DataTypes.DECIMAL(15, 6),
47 | allowNull: false,
48 | },
49 | createdAt: {
50 | type: DataTypes.DATE,
51 | },
52 | updatedAt: {
53 | type: DataTypes.DATE,
54 | },
55 | categoryId: {
56 | type: DataTypes.INTEGER,
57 | references: {
58 | model: 'categories',
59 | key: 'id',
60 | },
61 | allowNull: false,
62 | },
63 | },
64 | {
65 | sequelize,
66 | tableName: 'products',
67 | modelName: 'product',
68 | },
69 | );
70 |
71 | ProductModel.belongsTo(CategoryModel, {
72 | foreignKey: 'categoryId',
73 | targetKey: 'id',
74 | as: 'category',
75 | });
76 | CategoryModel.hasMany(ProductModel, { sourceKey: 'id' });
77 |
--------------------------------------------------------------------------------
/src/admin/pages/design-system-examples/blog-page.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Box,
3 | Button,
4 | Drawer,
5 | DrawerContent,
6 | DropZone,
7 | Header,
8 | Icon,
9 | Input,
10 | Label,
11 | RichTextEditor,
12 | } from '@adminjs/design-system';
13 | import React, { useState } from 'react';
14 |
15 | const BlogPage: React.FC = () => {
16 | const [isDrawerVisible, setIsDrawerVisible] = useState(false);
17 |
18 | const handler = (html) => {
19 | console.log(html);
20 | };
21 |
22 | return (
23 |
24 |
27 |
28 | {isDrawerVisible && (
29 |
30 |
31 |
32 |
35 | Article settings
36 |
37 |
38 |
39 |
40 |
41 | )}
42 |
43 |
44 |
47 |
51 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | );
65 | };
66 |
67 | export default BlogPage;
68 |
--------------------------------------------------------------------------------
/src/admin/components/products-list.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Box, FormGroup, Label, Table, TableBody, TableCell, TableHead, TableRow } from '@adminjs/design-system';
3 | import { flat, useTranslation, BasePropertyProps } from 'adminjs';
4 |
5 | import { ProductListInterface } from '../../sources/sequelize/interfaces.js';
6 |
7 | const ProductsList = (props: BasePropertyProps) => {
8 | const { translateLabel } = useTranslation();
9 | const params = flat.unflatten(props.record.params);
10 |
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 | {translateLabel('ID')}
19 | {translateLabel('Name')}
20 | {translateLabel('Quantity')}
21 | {translateLabel('Unit price')}
22 | {translateLabel('Sum')}
23 |
24 |
25 |
26 | {!params.products.length && (
27 |
28 |
29 | No records
30 |
31 |
32 | )}
33 | {params.products.length > 0 &&
34 | params.products.map(({ quantity, product }) => (
35 |
36 | {product.id}
37 | {product.name}
38 | {quantity}
39 | {product.price / 100}
40 | {(product.price * quantity) / 100}
41 |
42 | ))}
43 |
44 |
45 |
46 |
47 | );
48 | };
49 |
50 | export default ProductsList;
51 |
--------------------------------------------------------------------------------
/src/sources/mongoose/models/complicated.model.ts:
--------------------------------------------------------------------------------
1 | import { model, Schema, Types } from 'mongoose';
2 |
3 | export interface Complicated {
4 | name: string;
5 | stringArray: string[];
6 | authors: Types.ObjectId[];
7 | nestedDetails: {
8 | age: number;
9 | height: number;
10 | placeOfBirth: string;
11 | nested: {
12 | extremelyNested: string;
13 | };
14 | };
15 | parents: Types.ObjectId[];
16 | items: {
17 | imageVariants: {
18 | imageUrl: string;
19 | isApproved: boolean;
20 | dateCreated: Date;
21 | isDeleted: boolean;
22 | }[];
23 | }[];
24 | }
25 |
26 | export const ComplicatedSchema = new Schema({
27 | name: { type: 'String', required: true },
28 | stringArray: { type: ['String'] },
29 | authors: [{ type: Types.ObjectId, ref: 'User' }],
30 | nestedDetails: new Schema(
31 | {
32 | age: { type: 'Number', required: true },
33 | height: { type: 'Number', required: true },
34 | placeOfBirth: { type: 'String', required: true },
35 | nested: new Schema(
36 | {
37 | extremelyNested: { type: 'String', required: true },
38 | },
39 | { _id: false },
40 | ),
41 | },
42 | { _id: false },
43 | ),
44 | parents: [
45 | new Schema(
46 | {
47 | firstName: { type: 'String', required: true },
48 | lastName: { type: 'String', required: true },
49 | },
50 | { _id: false },
51 | ),
52 | ],
53 | items: [
54 | new Schema(
55 | {
56 | imageVariants: [
57 | new Schema(
58 | {
59 | imageUrl: { type: 'String', required: true },
60 | isApproved: { type: 'Boolean' },
61 | isDeleted: { type: 'Boolean' },
62 | dateCreated: { type: 'Date', default: Date.now },
63 | },
64 | { _id: false },
65 | ),
66 | ],
67 | },
68 | { _id: false },
69 | ),
70 | ],
71 | });
72 |
73 | export const ComplicatedModel = model('Complicated', ComplicatedSchema);
74 |
--------------------------------------------------------------------------------
/src/admin/pages/design-system-examples/index.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Header, Link, Placeholder, Text } from '@adminjs/design-system';
2 | import React, { FC, lazy, Suspense } from 'react';
3 |
4 | const BlogPage = lazy(() => import('./blog-page.js'));
5 | const ButtonsPage = lazy(() => import('./buttons-page.js'));
6 | const FormPage = lazy(() => import('./form-page.js'));
7 | const IconsPage = lazy(() => import('./icons-page.js'));
8 | const IllustrationPage = lazy(() => import('./illustrations-page.js'));
9 | const MessagesPage = lazy(() => import('./messages-page.js'));
10 | const ModalPage = lazy(() => import('./modal-page.js'));
11 | const TabsPage = lazy(() => import('./tabs-page.js'));
12 | const TypographyPage = lazy(() => import('./typography-page.js'));
13 |
14 | const DesignSystemPage: FC = () => {
15 | const STORYBOOK_URL = (window as any).AdminJS.env.STORYBOOK_URL;
16 | return (
17 | <>
18 |
19 |
22 |
23 |
24 | For more examples visit our Storybook
25 |
26 | {STORYBOOK_URL}
27 |
28 |
29 |
30 |
31 | }>
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | >
43 | );
44 | };
45 |
46 | const DesignSytemPagePlaceholder = () => (
47 | <>
48 | {Array.from({ length: 3 }).map((_, index) => (
49 |
50 |
51 |
52 |
53 |
54 |
55 | ))}
56 | >
57 | );
58 |
59 | export default DesignSystemPage;
60 |
--------------------------------------------------------------------------------
/src/admin/router.ts:
--------------------------------------------------------------------------------
1 | import AdminJSExpress from '@adminjs/express';
2 | import AdminJSFastify from '@adminjs/fastify';
3 | import AdminJS from 'adminjs';
4 | import argon2 from 'argon2';
5 | import { FastifyInstance } from 'fastify';
6 | import ConnectPgSimple from 'connect-pg-simple';
7 | import session from 'express-session';
8 | import { Router } from 'express';
9 |
10 | import { AdminModel } from '../sources/mongoose/models/index.js';
11 | import { AuthUsers } from './constants/authUsers.js';
12 |
13 | export const authenticateUser = async (email, password) => {
14 | const user = await AdminModel.findOne({ email });
15 | if (user && (await argon2.verify(user.password, password))) {
16 | const userData = AuthUsers.find((au) => email === au.email);
17 | return { ...userData, ...user.toObject() };
18 | }
19 | return null;
20 | };
21 |
22 | export const expressAuthenticatedRouter = (adminJs: AdminJS, router: Router | null = null) => {
23 | const ConnectSession = ConnectPgSimple(session);
24 |
25 | const sessionStore = new ConnectSession({
26 | conObject: {
27 | connectionString: process.env.POSTGRES_DATABASE_URL,
28 | ssl: process.env.NODE_ENV === 'production',
29 | },
30 | tableName: 'session',
31 | createTableIfMissing: true,
32 | });
33 |
34 | return AdminJSExpress.buildAuthenticatedRouter(
35 | adminJs,
36 | {
37 | authenticate: authenticateUser,
38 | cookieName: 'adminjs',
39 | cookiePassword: process.env.SESSION_SECRET ?? 'sessionsecret',
40 | },
41 | router,
42 | {
43 | store: sessionStore,
44 | resave: true,
45 | saveUninitialized: true,
46 | secret: process.env.SESSION_SECRET ?? 'sessionsecret',
47 | cookie: {
48 | httpOnly: true,
49 | secure: process.env.NODE_ENV === 'production',
50 | },
51 | name: 'adminjs',
52 | },
53 | );
54 | };
55 |
56 | export const fastifyAuthenticatedRouter = (adminJs: AdminJS, app: FastifyInstance) =>
57 | AdminJSFastify.buildAuthenticatedRouter(
58 | adminJs,
59 | {
60 | cookiePassword: 'secretsecretsecretsecretsecretsecretsecretsecret',
61 | authenticate: authenticateUser,
62 | },
63 | app,
64 | );
65 |
--------------------------------------------------------------------------------
/src/admin/pages/custom-page.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Button, H3, Link, Placeholder, Text } from '@adminjs/design-system';
2 | import { ApiClient, useNotice, useTranslation } from 'adminjs';
3 | import React, { FC, useEffect, useState } from 'react';
4 |
5 | const api = new ApiClient();
6 |
7 | type ApiGetPageResponse = { text: string };
8 |
9 | const CustomPage: FC = () => {
10 | const [text, setText] = useState();
11 | const addNotice = useNotice();
12 | const {
13 | tc,
14 | tm,
15 | i18n: { language },
16 | } = useTranslation();
17 |
18 | useEffect(() => {
19 | api.getPage({ pageName: 'customPage' }).then((res) => {
20 | setText(tm(res.data.text, { defaultValue: res.data.text }));
21 | });
22 | // eslint-disable-next-line react-hooks/exhaustive-deps
23 | }, [language]);
24 |
25 | const sendSimpleNotice = () =>
26 | addNotice({
27 | message: 'CustomPage.message',
28 | type: 'success',
29 | });
30 |
31 | const sendTranslatedNotice = () =>
32 | addNotice({
33 | message: 'CustomPage.messageWithInterpolation',
34 | options: {
35 | someParams: ['param 1', 'param2'].join(', '),
36 | },
37 | body: (
38 | <>
39 | {tm('CustomPage.message')} {tc('CustomPage.button')}
40 | >
41 | ),
42 | } as any);
43 |
44 | return (
45 |
46 |
47 | {tc('CustomPage.header')}
48 |
49 | {tc('CustomPage.introduction')}
50 | {text ? JSON.stringify(text, null, 2) : }
51 | {tc('CustomPage.ending')}
52 |
53 |
54 |
57 |
60 |
61 |
62 |
63 | );
64 | };
65 |
66 | export default CustomPage;
67 |
--------------------------------------------------------------------------------
/src/sources/sequelize/models/cart.model.ts:
--------------------------------------------------------------------------------
1 | import { DataTypes, Model, Optional, Association, HasOneGetAssociationMixin, NonAttribute } from 'sequelize';
2 |
3 | import { sequelize } from '../index.js';
4 | import { ProductModel } from './product.model.js';
5 | import { OrderModel } from './order.model.js';
6 |
7 | type Cart = {
8 | id: number;
9 | quantity: number;
10 | orderId: number;
11 | productId: number;
12 | createdAt: Date;
13 | updatedAt: Date;
14 | };
15 |
16 | export type CartCreationAttributes = Optional;
17 |
18 | export class CartModel extends Model {
19 | declare id: number;
20 | declare quantity: number;
21 | declare createdAt: Date;
22 | declare updatedAt: Date;
23 |
24 | declare product?: NonAttribute;
25 | declare order?: NonAttribute;
26 |
27 | declare getProduct: HasOneGetAssociationMixin;
28 | declare getOrder: HasOneGetAssociationMixin;
29 |
30 | declare static associations: {
31 | product: Association;
32 | order: Association;
33 | };
34 | }
35 |
36 | CartModel.init(
37 | {
38 | id: {
39 | type: DataTypes.INTEGER,
40 | autoIncrement: true,
41 | primaryKey: true,
42 | },
43 | quantity: {
44 | type: DataTypes.INTEGER,
45 | allowNull: false,
46 | },
47 | createdAt: {
48 | type: DataTypes.DATE,
49 | },
50 | updatedAt: {
51 | type: DataTypes.DATE,
52 | },
53 | productId: {
54 | type: DataTypes.INTEGER,
55 | references: {
56 | model: 'products',
57 | key: 'id',
58 | },
59 | allowNull: false,
60 | },
61 | orderId: {
62 | type: DataTypes.INTEGER,
63 | references: {
64 | model: 'orders',
65 | key: 'id',
66 | },
67 | allowNull: false,
68 | },
69 | },
70 | {
71 | sequelize,
72 | tableName: 'cart_products',
73 | modelName: 'cart',
74 | },
75 | );
76 |
77 | CartModel.belongsTo(ProductModel, {
78 | targetKey: 'id',
79 | as: 'product',
80 | foreignKey: 'productId',
81 | });
82 |
83 | CartModel.belongsTo(OrderModel, {
84 | targetKey: 'id',
85 | as: 'order',
86 | foreignKey: 'orderId',
87 | });
88 | OrderModel.hasMany(CartModel, {
89 | sourceKey: 'id',
90 | as: 'carts',
91 | foreignKey: 'orderId',
92 | });
93 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # Snowpack dependency directory (https://snowpack.dev/)
45 | web_modules/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # parcel-bundler cache (https://parceljs.org/)
72 | .cache
73 | .parcel-cache
74 |
75 | # Next.js build output
76 | .next
77 | out
78 |
79 | # Nuxt.js build / generate output
80 | .nuxt
81 | dist
82 |
83 | # Gatsby files
84 | .cache/
85 | # Comment in the public line in if your project uses Gatsby and not Next.js
86 | # https://nextjs.org/blog/next-9-1#public-directory-support
87 | # public
88 |
89 | # vuepress build output
90 | .vuepress/dist
91 |
92 | # Serverless directories
93 | .serverless/
94 |
95 | # FuseBox cache
96 | .fusebox/
97 |
98 | # DynamoDB Local files
99 | .dynamodb/
100 |
101 | # TernJS port file
102 | .tern-port
103 |
104 | # Stores VSCode versions used for testing VSCode extensions
105 | .vscode-test
106 |
107 | # yarn v2
108 | .yarn/cache
109 | .yarn/unplugged
110 | .yarn/build-state.yml
111 | .yarn/install-state.gz
112 | .pnp.*
113 |
114 | node_modules
115 | .nyc_output
116 | coverage
117 | .DS_store
118 | .adminjs
119 | .adminbro
120 | .idea
121 |
--------------------------------------------------------------------------------
/src/admin/pages/design-system-examples/form-page.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Box,
3 | CheckBox,
4 | CurrencyInput,
5 | DatePicker,
6 | DropZone,
7 | DropZoneProps,
8 | Header,
9 | Input,
10 | Label,
11 | PhoneInput,
12 | Select,
13 | TextArea,
14 | } from '@adminjs/design-system';
15 | import { useTranslation } from 'adminjs';
16 | import React, { useState } from 'react';
17 |
18 | const FormPage = () => {
19 | const [value, setValue] = useState();
20 | const [date, setDate] = useState('2021-06-17');
21 | const options = [
22 | { value: '1', label: 'Office 1' },
23 | { value: '2', label: 'Office 2' },
24 | ];
25 | const { translateComponent } = useTranslation();
26 |
27 | return (
28 |
29 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
73 |
74 |
75 |
76 |
77 |
81 |
82 |
83 |
84 | );
85 | };
86 |
87 | export default FormPage;
88 |
--------------------------------------------------------------------------------
/src/admin/pages/design-system-examples/typography-page.tsx:
--------------------------------------------------------------------------------
1 | import { Box, H1, H2, H3, H4, H5, H6, Header, Text } from '@adminjs/design-system';
2 | import React from 'react';
3 |
4 | const TypographyPage = () => (
5 |
6 |
9 |
10 | This is H1 header
11 | This is H2 header
12 | This is H3 header
13 | This is H4 header
14 | This is H5 header
15 | This is H6 header
16 |
17 |
18 |
19 | Sed tempus tempor dictum. Integer in lacus lacus. Curabitur sit amet ante eget ipsum finibus gravida. Donec
20 | viverra aliquet libero. Integer a nisl ac neque tempor pharetra. Donec sapien tortor, fermentum eu justo sed,
21 | egestas ultricies lacus. Pellentesque eget tincidunt nibh. Ut non lectus varius, semper lorem vel, porta orci.
22 | Duis id risus eu arcu efficitur bibendum. In eget eros ex. Integer et malesuada tellus.
23 |
24 |
25 |
26 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed venenatis semper massa a rhoncus. Praesent eu
27 | rutrum leo. Donec malesuada quis metus vel pulvinar. Quisque in vehicula nulla. Nam vestibulum facilisis lorem,
28 | ac mattis odio aliquet et. Suspendisse enim ligula, ultricies pellentesque ligula id, molestie sodales quam.
29 | Etiam in lectus ut nibh laoreet consequat.
30 |
31 |
32 |
33 | Suspendisse efficitur, urna sit amet tempor dignissim, ex est feugiat ex, sed molestie ante erat eget dolor.
34 | Duis purus orci, commodo non semper sed, laoreet quis nisl. Donec a bibendum arcu. Donec eget justo nunc. Nunc
35 | elementum augue et bibendum molestie. Duis tincidunt pellentesque enim ac mattis. Nunc congue, ante id efficitur
36 | gravida, turpis velit porta nunc, at egestas urna odio eget arcu. Ut enim metus, fringilla eu risus eu, sodales
37 | venenatis lacus.
38 |
39 |
40 |
41 | Donec vel malesuada turpis. Curabitur ultricies neque a sapien ullamcorper, quis faucibus felis porta. Etiam
42 | fermentum odio at rutrum pretium. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere
43 | cubilia curae; Suspendisse aliquet suscipit turpis at placerat. Cras quis sem vitae velit vehicula congue et nec
44 | libero. Curabitur aliquam, est id dapibus egestas, felis augue suscipit est, eu venenatis leo justo vel sapien.
45 | Donec id dignissim diam. Nulla feugiat ex sit amet augue sollicitudin pulvinar. Pellentesque id felis rhoncus,
46 | varius sem et, condimentum ligula. Proin accumsan sit amet nisl ac vehicula. Nulla facilisi. Ut pharetra vel
47 | tortor vel lacinia. Nulla sit amet neque lectus. Mauris tristique justo in sem tempus, at sagittis lectus
48 | molestie. Phasellus sit amet nulla id mi rutrum varius in eu nisi.
49 |
50 |
51 |
52 | );
53 |
54 | export default TypographyPage;
55 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## AdminJS-example-app
2 |
3 | Example application using [adminjs](https://github.com/SoftwareBrothers/adminjs)
4 |
5 | ## Demo
6 |
7 | You can check out the current demo version at: https://demo.adminjs.co
8 |
9 | Login: admin@example.com
10 | Password: password
11 |
12 | ## Prerequisites
13 |
14 | Install Docker if you don't have it: https://docs.docker.com/desktop/#download-and-install
15 |
16 | Run:
17 | ```bash
18 | $ docker-compose up -d
19 | ```
20 | to setup databases.
21 |
22 | Make sure your `.env` file is configured. If you didn't do any changes to `docker-compose.yml` file,
23 | the default contents of the `.env` file should work for you.
24 |
25 | ## Starting the app
26 |
27 | First, install all dependencies
28 |
29 | ```bash
30 | yarn install --frozen-lockfile
31 | ```
32 |
33 | Make sure you have all environmental variables set up (read the previous paragraph).
34 |
35 | Then create postgres database and run migrations:
36 |
37 | ```bash
38 | $ npx prisma generate # # this sets up Prisma Client in your node_modules
39 | $ yarn migration:up
40 | ```
41 |
42 | Note: If you see the error below when Prisma MySQL migration is run:
43 | ```
44 | Error: P1017: Server has closed the connection.
45 | ```
46 | Please wait a minute or two for the MySQL server to start and retry.
47 |
48 | In the end, you can launch the app
49 |
50 | ```bash
51 | $ yarn build:watch # keep it running if developing
52 | $ yarn start:dev # in a separate terminal tab, concurrently
53 | ```
54 |
55 | By default the app will be available under: `http://localhost:3000/admin`
56 |
57 | ## Developing the app
58 |
59 | The best way of developing the app is to do this via https://github.com/SoftwareBrothers/adminjs-dev.
60 |
61 | Alternatively, you can fork and clone each repository separately and link them using:
62 |
63 | * `yarn link`
64 | * `npm link`
65 |
66 | to see your local changes.
67 |
68 | #### Sequelize
69 | ##### migrations
70 | - `yarn sequelize migration:generate --name init`
71 | - `yarn sequelize db:migrate`
72 | - `yarn sequelize db:migrate:undo`
73 |
74 | #### Typeorm
75 | ##### migrations
76 | - `yarn typeorm migration:generate -n init`
77 | - `yarn typeorm migration:run`
78 | - `yarn typeorm migration:revert`
79 |
80 |
81 | #### mikro-orm
82 | ##### migrations
83 | - `yarn mikro-orm migration:create`
84 | - `yarn mikro-orm migration:up`
85 | - `yarn mikro-orm migration:down`
86 |
87 | #### prisma
88 | - `npx prisma migrate dev --schema prisma/schema.prisma`
89 |
90 | ## License
91 |
92 | AdminJS is copyrighted © 2023 rst.software. It is a free software, and may be redistributed under the terms specified in the [LICENSE](LICENSE.md) file.
93 |
94 | ## About rst.software
95 |
96 |
97 |
98 | We’re an open, friendly team that helps clients from all over the world to transform their businesses and create astonishing products.
99 |
100 | * We are available for [hire](https://www.rst.software/estimate-your-project).
101 | * If you want to work for us - check out the [career page](https://www.rst.software/join-us).
102 |
--------------------------------------------------------------------------------
/src/sources/sequelize/migrations/20220214101918-init.cjs:
--------------------------------------------------------------------------------
1 | const { DataTypes } = require('sequelize');
2 |
3 | module.exports = {
4 | async up(queryInterface, Sequelize) {
5 | await queryInterface.createTable('categories', {
6 | id: {
7 | type: DataTypes.INTEGER,
8 | autoIncrement: true,
9 | primaryKey: true,
10 | },
11 | name: {
12 | type: new DataTypes.STRING(),
13 | allowNull: false,
14 | },
15 | createdAt: {
16 | type: DataTypes.DATE,
17 | default: Date.now,
18 | },
19 | updatedAt: {
20 | type: DataTypes.DATE,
21 | default: Date.now,
22 | },
23 | });
24 |
25 | await queryInterface.createTable('products', {
26 | id: {
27 | type: DataTypes.INTEGER,
28 | autoIncrement: true,
29 | primaryKey: true,
30 | },
31 | name: {
32 | type: new DataTypes.STRING(),
33 | allowNull: false,
34 | },
35 | price: {
36 | type: new DataTypes.DECIMAL(15, 6),
37 | allowNull: false,
38 | },
39 | createdAt: {
40 | type: DataTypes.DATE,
41 | default: Date.now,
42 | },
43 | updatedAt: {
44 | type: DataTypes.DATE,
45 | default: Date.now,
46 | },
47 | categoryId: {
48 | type: DataTypes.INTEGER,
49 | references: {
50 | model: 'categories',
51 | key: 'id',
52 | },
53 | allowNull: false,
54 | },
55 | });
56 |
57 | await queryInterface.createTable('orders', {
58 | id: {
59 | type: DataTypes.INTEGER,
60 | autoIncrement: true,
61 | primaryKey: true,
62 | },
63 | isPaid: {
64 | type: DataTypes.BOOLEAN,
65 | defaultValue: false,
66 | },
67 | delivery: {
68 | type: DataTypes.STRING,
69 | },
70 | createdAt: {
71 | type: DataTypes.DATE,
72 | },
73 | updatedAt: {
74 | type: DataTypes.DATE,
75 | },
76 | });
77 |
78 | await queryInterface.createTable('cart_products', {
79 | id: {
80 | type: DataTypes.INTEGER,
81 | autoIncrement: true,
82 | primaryKey: true,
83 | },
84 | quantity: {
85 | type: DataTypes.INTEGER,
86 | allowNull: false,
87 | },
88 | createdAt: {
89 | type: DataTypes.DATE,
90 | default: Date.now,
91 | },
92 | updatedAt: {
93 | type: DataTypes.DATE,
94 | default: Date.now,
95 | },
96 | orderId: {
97 | type: DataTypes.INTEGER,
98 | references: {
99 | model: 'orders',
100 | key: 'id',
101 | },
102 | allowNull: false,
103 | },
104 | productId: {
105 | type: DataTypes.INTEGER,
106 | references: {
107 | model: 'products',
108 | key: 'id',
109 | },
110 | allowNull: false,
111 | },
112 | });
113 | },
114 |
115 | async down(queryInterface, Sequelize) {
116 | await queryInterface.dropTable('cart_products');
117 | await queryInterface.dropTable('orders');
118 | await queryInterface.dropTable('products');
119 | await queryInterface.dropTable('categories');
120 | },
121 | };
122 |
--------------------------------------------------------------------------------
/src/sources/rest/crypto-database.ts:
--------------------------------------------------------------------------------
1 | import AdminJS, { BaseResource, BaseProperty, BaseRecord, ResourceOptions } from 'adminjs';
2 | import axios from 'axios';
3 |
4 | import { menu } from '../../admin/index.js';
5 |
6 | export class CryptoDatabase extends BaseResource {
7 | public totalCount = 0;
8 |
9 | private readonly _properties: BaseProperty[] = [
10 | new BaseProperty({
11 | path: 'website_slug',
12 | type: 'string',
13 | isId: true,
14 | isSortable: false,
15 | position: 1,
16 | }),
17 | new BaseProperty({
18 | path: 'name',
19 | type: 'string',
20 | isSortable: false,
21 | position: 2,
22 | }),
23 | new BaseProperty({
24 | path: 'symbol',
25 | type: 'string',
26 | isSortable: false,
27 | position: 3,
28 | }),
29 | new BaseProperty({
30 | path: 'quotes.USD.price',
31 | type: 'string',
32 | isSortable: false,
33 | position: 4,
34 | }),
35 | new BaseProperty({
36 | path: 'quotes.USD.percent_change_1h',
37 | type: 'string',
38 | isSortable: false,
39 | position: 5,
40 | }),
41 | new BaseProperty({
42 | path: 'quotes.USD.percent_change_24h',
43 | type: 'string',
44 | isSortable: false,
45 | position: 6,
46 | }),
47 | new BaseProperty({
48 | path: 'quotes.USD.percent_change_7d',
49 | type: 'string',
50 | isSortable: false,
51 | position: 7,
52 | }),
53 | new BaseProperty({
54 | path: 'last_updated',
55 | type: 'date',
56 | isSortable: false,
57 | position: 8,
58 | }),
59 | ];
60 |
61 | public assignDecorator(admin: AdminJS, options?: ResourceOptions) {
62 | super.assignDecorator(admin, {
63 | ...options,
64 | navigation: menu.rest,
65 | listProperties: ['website_slug', 'name', 'symbol', 'quotes.USD.price'],
66 | actions: {
67 | list: {
68 | showFilter: false,
69 | },
70 | show: {
71 | showInDrawer: true,
72 | },
73 | delete: {
74 | isAccessible: false,
75 | },
76 | bulkDelete: {
77 | isAccessible: false,
78 | },
79 | new: {
80 | isAccessible: false,
81 | },
82 | edit: {
83 | isAccessible: false,
84 | },
85 | },
86 | });
87 | }
88 |
89 | public id(): string {
90 | return 'crypto-database';
91 | }
92 |
93 | public name(): string {
94 | return 'Crypto Database';
95 | }
96 |
97 | public properties(): BaseProperty[] {
98 | return this._properties;
99 | }
100 |
101 | public property(path): BaseProperty {
102 | return this._properties.find((p) => p.path === path);
103 | }
104 |
105 | async count(): Promise {
106 | return this.totalCount;
107 | }
108 |
109 | async find(query, { limit = 20, offset = 0 }): Promise {
110 | const { data } = await axios.get('https://api.alternative.me/v2/ticker/', {
111 | params: { start: offset / limit, limit, structure: 'array' },
112 | });
113 | this.totalCount = data.metadata.num_cryptocurrencies;
114 |
115 | return data.data.map((obj) => new BaseRecord(obj, this));
116 | }
117 |
118 | async findOne(id): Promise {
119 | const { data } = await axios.get(`https://api.alternative.me/v2/ticker/${id}/`, {
120 | params: { structure: 'array' },
121 | });
122 | const record = data.data[0] as { last_updated: number };
123 |
124 | return new BaseRecord(
125 | {
126 | ...record,
127 | last_updated: record.last_updated ? new Date(record.last_updated * 1000) : null,
128 | },
129 | this,
130 | );
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/src/admin/pages/design-system-examples/buttons-page.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Button, Header, Icon, Text } from '@adminjs/design-system';
2 | import React, { FC, PropsWithChildren } from 'react';
3 |
4 | const Wrapper: FC = ({ children, title }) => (
5 |
6 | {title}
7 |
8 | {children}
9 |
10 |
11 | );
12 |
13 | const colors = ['primary', 'secondary', 'success', 'info', 'danger', 'text'] as const;
14 | const [primary] = colors;
15 |
16 | const ButtonsPage = () => (
17 |
18 |
21 |
22 |
23 | {colors.map((color) => (
24 |
27 | ))}
28 |
29 |
30 | {colors.map((color) => (
31 |
34 | ))}
35 |
36 |
37 | {colors.map((color) => (
38 |
41 | ))}
42 |
43 |
44 | {colors.map((color) => (
45 |
48 | ))}
49 |
50 |
51 | {colors.map((color) => (
52 |
55 | ))}
56 |
57 |
58 | {colors.map((color) => (
59 |
62 | ))}
63 |
64 |
65 | {colors.map((color) => (
66 |
69 | ))}
70 |
71 |
72 | {colors.map((color) => (
73 |
76 | ))}
77 |
78 |
79 |
82 |
85 |
88 |
89 |
90 | {colors.map((color) => (
91 |
95 | ))}
96 |
97 |
98 | {colors.map((color) => (
99 |
102 | ))}
103 |
104 |
105 | {colors.map((color) => (
106 |
109 | ))}
110 |
111 |
112 |
113 | );
114 |
115 | export default ButtonsPage;
116 |
--------------------------------------------------------------------------------
/src/admin/components/login.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { useSelector } from 'react-redux';
4 | import {
5 | Box,
6 | BoxProps,
7 | H5,
8 | H2,
9 | Label,
10 | Illustration,
11 | Input,
12 | FormGroup,
13 | Button,
14 | Text,
15 | MessageBox,
16 | MadeWithLove,
17 | themeGet,
18 | } from '@adminjs/design-system';
19 | import { styled } from '@adminjs/design-system/styled-components';
20 | import { ReduxState, useTranslation } from 'adminjs';
21 | import { AuthUser, AuthUsers } from '../constants/authUsers.js';
22 |
23 | const Wrapper = styled(Box)`
24 | align-items: center;
25 | justify-content: center;
26 | flex-direction: column;
27 | height: 100%;
28 | `;
29 |
30 | const StyledLogo = styled.img`
31 | max-width: 200px;
32 | margin: ${themeGet('space', 'md')} 0;
33 | `;
34 |
35 | const IllustrationsWrapper = styled(Box)`
36 | display: flex;
37 | flex-wrap: wrap;
38 | align-items: center;
39 | justify-content: center;
40 | & svg [stroke='#3B3552'] {
41 | stroke: rgba(255, 255, 255, 0.5);
42 | }
43 | & svg [fill='#3040D6'] {
44 | fill: rgba(255, 255, 255, 1);
45 | }
46 | `;
47 |
48 | export type LoginProps = {
49 | credentials: Pick;
50 | action: string;
51 | errorMessage?: string;
52 | children?: any;
53 | };
54 |
55 | export const Login: React.FC = (props) => {
56 | const { action, errorMessage } = props;
57 | const { translateComponent, translateMessage } = useTranslation();
58 | const [defaultUser] = AuthUsers;
59 | const branding = useSelector((state: ReduxState) => state.branding);
60 | const message = `Email: ${defaultUser.email}\nPassword: ${defaultUser.password}`;
61 |
62 | return (
63 |
64 |
65 |
66 |
75 | {translateComponent('Login.welcomeHeader')}
76 |
77 | {translateComponent('Login.welcomeMessage')}
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 | {branding.logo ? : branding.companyName}
94 |
95 |
96 | {errorMessage && (
97 | 1 ? errorMessage : translateMessage(errorMessage)}
100 | variant="danger"
101 | />
102 | )}
103 |
104 |
105 |
110 |
111 |
112 |
113 |
120 |
121 |
122 |
123 |
124 |
125 |
126 | {branding.withMadeWithLove ? (
127 |
128 |
129 |
130 | ) : null}
131 |
132 |
133 | );
134 | };
135 |
136 | export default Login;
137 |
--------------------------------------------------------------------------------
/src/admin/index.ts:
--------------------------------------------------------------------------------
1 | // Adapters
2 | import { Database as MikroormDatabase, Resource as MikroormResource } from '@adminjs/mikroorm';
3 | import { Database as MongooseDatabase, Resource as MongooseResource } from '@adminjs/mongoose';
4 | import { Database as ObjectionDatabase, Resource as ObjectionResource } from '@adminjs/objection';
5 | import { Database as PrismaDatabase, Resource as PrismaResource } from '@adminjs/prisma';
6 | import { Database as SequelizeDatabase, Resource as SequelizeResource } from '@adminjs/sequelize';
7 | import { dark, light, noSidebar } from '@adminjs/themes';
8 | import { Database as TypeormDatabase, Resource as TypeormResource } from '@adminjs/typeorm';
9 |
10 | import AdminJS, { AdminJSOptions, ResourceOptions } from 'adminjs';
11 | import argon2 from 'argon2';
12 | import { CreateCarResource, CreateOwnerResource, CreateSellerResource } from '../sources/mikroorm/resources/index.js';
13 | import { AdminModel } from '../sources/mongoose/models/index.js';
14 | import {
15 | CreateAdminResource,
16 | CreateArticleResource,
17 | CreateCategoryResource,
18 | CreateCommentResource,
19 | CreateComplicatedResource,
20 | CreateUserResource,
21 | } from '../sources/mongoose/resources/index.js';
22 | import { CreateManagerResource, CreateOfficeResource } from '../sources/objectionjs/resources/index.js';
23 | import {
24 | CreatePostResource,
25 | CreateProfileResource,
26 | CreatePublisherResource,
27 | } from '../sources/prisma/resources/index.js';
28 | import { CryptoDatabase } from '../sources/rest/crypto-database.js';
29 | import {
30 | CreateCartResource,
31 | CreateOrderResource,
32 | CreateProductResource,
33 | CreateCategoryResource as CreateSequelizeCategoryResource,
34 | } from '../sources/sequelize/resources/index.js';
35 | import { CreateOrganizationResource, CreatePersonResource } from '../sources/typeorm/resources/index.js';
36 | import './components.bundler.js';
37 | import { componentLoader } from './components.bundler.js';
38 | import { AuthUsers } from './constants/authUsers.js';
39 | import { locale } from './locale/index.js';
40 | import pages from './pages/index.js';
41 | import { customTheme } from '../themes/index.js';
42 |
43 | AdminJS.registerAdapter({ Database: MikroormDatabase, Resource: MikroormResource });
44 | AdminJS.registerAdapter({ Database: MongooseDatabase, Resource: MongooseResource });
45 | AdminJS.registerAdapter({ Database: ObjectionDatabase, Resource: ObjectionResource });
46 | AdminJS.registerAdapter({ Database: PrismaDatabase, Resource: PrismaResource });
47 | AdminJS.registerAdapter({ Database: SequelizeDatabase, Resource: SequelizeResource });
48 | AdminJS.registerAdapter({ Database: TypeormDatabase, Resource: TypeormResource });
49 |
50 | export const menu: Record = {
51 | mongoose: { name: 'Mongoose', icon: 'Folder' },
52 | sequelize: { name: 'Sequelize', icon: 'Folder' },
53 | typeorm: { name: 'Typeorm', icon: 'Folder' },
54 | mikroorm: { name: 'Mikroorm', icon: 'Folder' },
55 | prisma: { name: 'Prisma', icon: 'Folder' },
56 | objection: { name: 'Objection', icon: 'Folder' },
57 | rest: { name: 'REST', icon: 'Link' },
58 | };
59 |
60 | export const generateAdminJSConfig: () => AdminJSOptions = () => ({
61 | version: { admin: true, app: '2.0.0' },
62 | rootPath: '/admin',
63 | locale,
64 | assets: {
65 | styles: ['/custom.css'],
66 | scripts: process.env.NODE_ENV === 'production' ? ['/gtm.js'] : [],
67 | },
68 | branding: {
69 | companyName: 'AdminJS demo page',
70 | favicon: '/favicon.ico',
71 | theme: {
72 | colors: { primary100: '#4D70EB' },
73 | },
74 | },
75 | defaultTheme: 'light',
76 | availableThemes: [light, dark, noSidebar, customTheme],
77 | componentLoader,
78 | pages,
79 | env: {
80 | STORYBOOK_URL: process.env.STORYBOOK_URL,
81 | GITHUB_URL: process.env.GITHUB_URL,
82 | SLACK_URL: process.env.SLACK_URL,
83 | DOCUMENTATION_URL: process.env.DOCUMENTATION_URL,
84 | },
85 | resources: [
86 | // mongo
87 | CreateAdminResource(),
88 | CreateUserResource(),
89 | CreateCategoryResource(),
90 | CreateArticleResource(),
91 | CreateCommentResource(),
92 | CreateComplicatedResource(),
93 | // sequelize
94 | CreateSequelizeCategoryResource(),
95 | CreateProductResource(),
96 | CreateOrderResource(),
97 | CreateCartResource(),
98 | // typeorm
99 | CreateOrganizationResource(),
100 | CreatePersonResource(),
101 | // mikroorm
102 | CreateCarResource(),
103 | CreateSellerResource(),
104 | CreateOwnerResource(),
105 | // prisma
106 | CreatePublisherResource(),
107 | CreateProfileResource(),
108 | CreatePostResource(),
109 | // objectionjs
110 | CreateOfficeResource(),
111 | CreateManagerResource(),
112 | // custom
113 | new CryptoDatabase(),
114 | ],
115 | });
116 |
117 | export const createAuthUsers = async () =>
118 | Promise.all(
119 | AuthUsers.map(async ({ email, password }) => {
120 | const admin = await AdminModel.findOne({ email });
121 | if (!admin) {
122 | await AdminModel.create({ email, password: await argon2.hash(password) });
123 | }
124 | }),
125 | );
126 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "version": "1.0.0",
4 | "description": "",
5 | "author": "",
6 | "type": "module",
7 | "license": "ISC",
8 | "engines": {
9 | "node": ">=18"
10 | },
11 | "scripts": {
12 | "lint": "eslint src --ext .ts,.tsx",
13 | "typeorm": "./node_modules/typeorm/cli.js --dataSource dist/sources/typeorm/config.js",
14 | "sequelize": "yarn sequelize-cli --config src/sources/sequelize/config.js",
15 | "knex": "dotenv -c '.env' -- npx knex --knexpath dist/sources/objectionjs/index.js --knexfile dist/sources/objectionjs/knexfile.cjs",
16 | "mikroorm": "node --experimental-specifier-resolution=node --loader ts-node/esm ./node_modules/.bin/mikro-orm",
17 | "migration:up": "yarn sequelize db:migrate && yarn typeorm migration:run && yarn mikroorm migration:up && yarn prisma migrate dev --schema prisma/schema.prisma && yarn knex migrate:latest",
18 | "migration:up:production": "yarn sequelize db:migrate && yarn typeorm migration:run && yarn mikro-orm migration:up && yarn prisma migrate deploy --schema prisma/schema.prisma",
19 | "docker:up": "docker-compose -f docker-compose.yaml up -d",
20 | "start:dev": "nodemon",
21 | "start": "node dist/index.js",
22 | "build": "yarn clean && tsc && yarn copy-theme",
23 | "build:watch": "tsc -w",
24 | "postgres:clean": "node dist/scripts/truncate-postgres",
25 | "mysql:clean": "prisma migrate reset --force && prisma migrate deploy",
26 | "mongodb:clean": "node dist/scripts/truncate-mongodb",
27 | "db:clean": "yarn postgres:clean && yarn mysql:clean && yarn mongodb:clean",
28 | "seed:typeorm": "node dist/sources/typeorm/seeds/run",
29 | "seed:sequelize": "node dist/sources/sequelize/seeds/run",
30 | "seed:mongoose": "node dist/sources/mongoose/seeds/run",
31 | "seed:mikro-orm": "node dist/sources/mikroorm/seeds/run",
32 | "seed:prisma": "node dist/sources/prisma/seeds/run",
33 | "seed:all": "yarn seed:typeorm && yarn seed:sequelize && yarn seed:mongoose && yarn seed:mikro-orm && yarn seed:prisma",
34 | "clean": "rimraf dist",
35 | "copy-theme": "copyfiles -u 1 src/themes/**/theme.bundle.js src/themes/**/*.css dist/"
36 | },
37 | "mikro-orm": {
38 | "useTsNode": true,
39 | "configPaths": [
40 | "./dist/sources/mikroorm/config.js",
41 | "./src/sources/mikroorm/config.ts"
42 | ]
43 | },
44 | "prisma": {
45 | "schema": "prisma/schema.prisma"
46 | },
47 | "dependencies": {
48 | "@adminjs/express": "^6.0.0",
49 | "@adminjs/fastify": "^4.0.0",
50 | "@adminjs/hapi": "^7.0.0",
51 | "@adminjs/koa": "^4.0.0",
52 | "@adminjs/mikroorm": "^3.0.0",
53 | "@adminjs/mongoose": "^4.0.0",
54 | "@adminjs/objection": "^2.0.0",
55 | "@adminjs/passwords": "^4.0.0",
56 | "@adminjs/prisma": "^4.0.0",
57 | "@adminjs/sequelize": "^4.0.0",
58 | "@adminjs/themes": "^1.0.0",
59 | "@adminjs/typeorm": "^5.0.0",
60 | "@faker-js/faker": "^7.6.0",
61 | "@hapi/boom": "^10.0.1",
62 | "@hapi/cookie": "^12.0.1",
63 | "@hapi/hapi": "^21.3.0",
64 | "@koa/router": "^12.0.0",
65 | "@mikro-orm/cli": "^5.6.15",
66 | "@mikro-orm/core": "^5.6.15",
67 | "@mikro-orm/migrations": "^5.6.15",
68 | "@mikro-orm/nestjs": "^5.1.7",
69 | "@mikro-orm/postgresql": "^5.6.15",
70 | "@nestjs/common": "^9.3.12",
71 | "@nestjs/core": "^9.3.12",
72 | "@nestjs/mongoose": "^9.2.2",
73 | "@nestjs/platform-express": "^9.3.12",
74 | "@nestjs/typeorm": "^9.0.1",
75 | "@prisma/client": "^4.11.0",
76 | "adminjs": "^7.0.9",
77 | "ajv-formats": "^2.1.1",
78 | "argon2": "^0.30.3",
79 | "class-transformer": "^0.5.1",
80 | "class-validator": "^0.14.0",
81 | "connect-pg-simple": "^8.0.0",
82 | "cors": "^2.8.5",
83 | "dotenv": "^16.0.3",
84 | "express": "^4.18.2",
85 | "express-formidable": "^1.2.0",
86 | "express-session": "^1.17.3",
87 | "fastify-static": "^4.7.0",
88 | "i18next": "^22.4.13",
89 | "knex": "^2.4.2",
90 | "koa": "^2.14.2",
91 | "koa2-formidable": "^1.0.3",
92 | "mongoose": "^7.0.2",
93 | "nodemon": "^2.0.21",
94 | "objection": "^3.0.1",
95 | "pg": "^8.10.0",
96 | "prisma": "^4.11.0",
97 | "reflect-metadata": "^0.1.13",
98 | "rxjs": "^7.8.0",
99 | "sequelize": "^6.29.3",
100 | "sequelize-cli": "^6.6.0",
101 | "tslib": "^2.5.0",
102 | "typeorm": "^0.3.12",
103 | "uuid": "^9.0.0"
104 | },
105 | "devDependencies": {
106 | "@types/argon2": "^0.15.0",
107 | "@types/connect-pg-simple": "^7.0.0",
108 | "@types/cors": "^2.8.13",
109 | "@types/express-session": "^1.17.7",
110 | "@types/lodash": "^4.14.191",
111 | "@types/node": "^18.15.5",
112 | "@types/uuid": "^9.0.1",
113 | "@typescript-eslint/eslint-plugin": "^5.56.0",
114 | "@typescript-eslint/parser": "^5.56.0",
115 | "copyfiles": "^2.4.1",
116 | "dotenv-cli": "^7.1.0",
117 | "eslint": "^8.36.0",
118 | "eslint-config-prettier": "^8.8.0",
119 | "eslint-plugin-prettier": "^4.2.1",
120 | "eslint-plugin-react": "^7.32.2",
121 | "eslint-plugin-react-hooks": "^4.6.0",
122 | "prettier": "^2.8.6",
123 | "rimraf": "^4.4.1",
124 | "ts-node": "^10.9.1",
125 | "tsconfig-paths": "^4.1.2",
126 | "typescript": "^5.0.2"
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/public/locales/mk/translation.json:
--------------------------------------------------------------------------------
1 | {
2 | "actions": {
3 | "new": "Создадете ново",
4 | "edit": "Уредување",
5 | "show": "Прикажи",
6 | "delete": "Избриши",
7 | "bulkDelete": "Избриши ги сите",
8 | "list": "Список"
9 | },
10 | "buttons": {
11 | "save": "Зачувај",
12 | "addNewItem": "Додадете нова ставка",
13 | "filter": "Филтер",
14 | "applyChanges": "Примени промени",
15 | "resetFilter": "Ресетирај",
16 | "confirmRemovalMany": "Потврдете го отстранувањето на {{count}} рекорд",
17 | "confirmRemovalMany_plural": "Потврдете го отстранувањето на {{count}} рекорди",
18 | "logout": "Одјави се",
19 | "login": "Логирај Се",
20 | "seeTheDocumentation": "Видете: <1>документацијата1>",
21 | "createFirstRecord": "Создадете прв запис",
22 | "cancel": "Откажи",
23 | "confirm": "Потврди"
24 | },
25 | "components": {
26 | "DropZone": {
27 | "placeholder": "Спуштете ја вашата датотека овде или кликнете за да пребарувате",
28 | "acceptedSize": "Максимална големина: {{maxSize}}",
29 | "acceptedType": "Поддржува: {{mimeTypes}}",
30 | "unsupportedSize": "Датотека {{fileName}} е преголем",
31 | "unsupportedType": "Датотека {{fileName}} има неподдржан тип: {{fileType}}"
32 | }
33 | },
34 | "labels": {
35 | "navigation": "Навигација",
36 | "pages": "Страници",
37 | "selectedRecords": "Избрано ({{selected}})",
38 | "filters": "Филтри",
39 | "adminVersion": "Администратор: {{version}}",
40 | "appVersion": "Апликација: {{version}}",
41 | "loginWelcome": "Добредојдовте",
42 | "dashboard": "Контролна табла"
43 | },
44 | "properties": {
45 | "length": "Должина",
46 | "from": "Од",
47 | "to": "До"
48 | },
49 | "resources": {},
50 | "messages": {
51 | "successfullyBulkDeleted": "успешно отстранети {{count}} рекорд",
52 | "successfullyBulkDeleted_plural": "успешно отстранети {{count}} рекорди",
53 | "successfullyDeleted": "Дадениот запис е успешно избришан",
54 | "successfullyUpdated": "Дадениот запис е успешно ажуриран",
55 | "thereWereValidationErrors": "Има грешки при валидација - проверете ги подолу",
56 | "forbiddenError": "Не можете да извршите дејство {{actionName}} на {{resourceId}}",
57 | "anyForbiddenError": "Не можете да извршите дадено дејство",
58 | "successfullyCreated": "Успешно создаде нов рекорд",
59 | "bulkDeleteError": "Настана грешка при бришење записи. Проверете ја конзолата за да видите повеќе информации",
60 | "errorFetchingRecords": "Настана грешка при преземањето на записите. Проверете ја конзолата за да видите повеќе информации",
61 | "errorFetchingRecord": "Настана грешка при преземањето на записот. Проверете ја конзолата за да видите повеќе информации",
62 | "noRecordsSelected": "Не избравте ниедна запис",
63 | "theseRecordsWillBeRemoved": "Следниот запис ќе биде отстранет",
64 | "theseRecordsWillBeRemoved_plural": "Следните записи ќе бидат отстранети",
65 | "pickSomeFirstToRemove": "За да ги отстраните записите, прво треба да ги изберете",
66 | "error404Resource": "Ресурс на дадениот ид: {{resourceId}} не може да се најде",
67 | "error404Action": "Ресурс на дадениот ид: {{resourceId}} нема дејство со име: {{actionName}} или не сте овластени да го користите!",
68 | "error404Record": "Ресурс на дадениот ид: {{resourceId}} нема запис со ид: {{recordId}} или не сте овластени да го користите!",
69 | "seeConsoleForMore": "Погледнете ја развојната конзола за повеќе детали...",
70 | "noActionComponent": "Мора да имплементирате акциона компонента за вашата акција",
71 | "noRecordsInResource": "Нема записи во овој ресурс",
72 | "noRecords": "Нема записи",
73 | "confirmDelete": "Дали навистина сакате да ја отстраните оваа ставка?",
74 | "welcomeOnBoard_title": "Добредојдовте на одборот!",
75 | "welcomeOnBoard_subtitle": "Сега сте еден од нас! ",
76 | "loginWelcome": "до AdminJS – водечки светски административен панел генериран автоматски со отворен код за вашата апликација Node.js кој ви овозможува да управувате со сите ваши податоци на едно место",
77 | "addingResources_title": "Додавање ресурси",
78 | "addingResources_subtitle": "Како да додадете нови ресурси на страничната лента",
79 | "customizeResources_title": "Прилагодете ги ресурсите",
80 | "customizeResources_subtitle": "Дефинирање на однесување, додавање својства и повеќе...",
81 | "customizeActions_title": "Приспособете ги акциите",
82 | "customizeActions_subtitle": "Изменување на постоечки дејства и додавање нови",
83 | "writeOwnComponents_title": "Напиши компоненти",
84 | "writeOwnComponents_subtitle": "Како да го измените изгледот и чувството на AdminJS",
85 | "customDashboard_title": "Прилагодена контролна табла",
86 | "customDashboard_subtitle": "Како да го измените овој приказ и да додадете нови страници на страничната лента",
87 | "roleBasedAccess_title": "Контрола на пристап заснована на улоги",
88 | "roleBasedAccess_subtitle": "Креирајте кориснички улоги и дозволи во AdminJS",
89 | "community_title": "Придружете се на опуштената заедница",
90 | "community_subtitle": "Разговарајте со креаторите на AdminJS и другите корисници на AdminJS",
91 | "foundBug_title": "Најдовте бубачка? ",
92 | "foundBug_subtitle": "Поставете проблем на нашето складиште на GitHub",
93 | "needMoreSolutions_title": "Ви требаат понапредни решенија?",
94 | "needMoreSolutions_subtitle": "Ние сме тука да ви обезбедиме прекрасен дизајн на UX/UI и прилагоден софтвер базиран (не само) на AdminJS",
95 | "invalidCredentials": "Погрешна е-пошта и/или лозинка",
96 | "keyPlaceholder": "КЛУЧ",
97 | "valuePlaceholder": "ВРЕДНОСТ",
98 | "initialKey": "клуч-{{number}}",
99 | "keyInUse": "Копчињата за објекти мора да бидат единствени.",
100 | "keyValuePropertyDefaultDescription": "Сите вредности се зачувани како текст. "
101 | }
102 | }
--------------------------------------------------------------------------------