feat: add docker compose support for minio (#375)

* feat: add docker compose support for minio

---------

Co-authored-by: pengap <penganpingprivte@gmail.com>
This commit is contained in:
Pengap 2024-02-26 18:57:57 +08:00 committed by GitHub
parent 3a7e95cf00
commit e8c5fe8720
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
50 changed files with 2171 additions and 1668 deletions

View File

@ -79,7 +79,7 @@
"dotenv-flow": "4.1.0",
"dotenv-flow-cli": "1.1.1",
"es-check": "7.1.1",
"eslint": "8.56.0",
"eslint": "8.57.0",
"eslint-config-next": "13.0.2",
"get-tsconfig": "4.7.2",
"npm-run-all2": "6.1.2",
@ -92,9 +92,9 @@
"typescript": "5.3.3",
"unplugin-swc": "1.4.4",
"vite-tsconfig-paths": "4.3.1",
"vitest": "1.3.0",
"vitest": "1.3.1",
"vitest-mock-extended": "1.3.1",
"webpack": "5.90.2"
"webpack": "5.90.3"
},
"dependencies": {
"@keyv/redis": "2.8.4",
@ -121,8 +121,8 @@
"@opentelemetry/resources": "1.21.0",
"@opentelemetry/sdk-node": "0.48.0",
"@opentelemetry/semantic-conventions": "1.21.0",
"@prisma/client": "5.9.1",
"@prisma/instrumentation": "5.9.1",
"@prisma/client": "5.10.2",
"@prisma/instrumentation": "5.10.2",
"@teable/common-i18n": "workspace:^",
"@teable/core": "workspace:^",
"@teable/db-main-prisma": "workspace:^",
@ -143,7 +143,7 @@
"handlebars": "4.7.8",
"helmet": "7.1.0",
"is-port-reachable": "3.1.0",
"joi": "17.12.1",
"joi": "17.12.2",
"json-rules-engine": "6.5.0",
"jsonpath-plus": "7.2.0",
"keyv": "4.5.4",
@ -157,12 +157,12 @@
"multer": "1.4.5-lts.1",
"nanoid": "3.3.7",
"nest-knexjs": "0.0.21",
"nestjs-cls": "4.1.0",
"nestjs-cls": "4.2.0",
"nestjs-pino": "4.0.0",
"nestjs-redoc": "2.2.2",
"next": "13.0.2",
"node-fetch": "2.7.0",
"nodemailer": "6.9.9",
"nodemailer": "6.9.10",
"passport": "0.7.0",
"passport-jwt": "4.0.1",
"passport-local": "1.0.0",

View File

@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/naming-convention */
import path from 'path';
import KeyvRedis from '@keyv/redis';
import KeyvSqlite from '@keyv/sqlite';
import type { Provider } from '@nestjs/common';
@ -16,11 +17,20 @@ export const CacheProvider: Provider = {
useFactory: async (config: ICacheConfig) => {
const { provider, sqlite, redis } = config;
Logger.log(`[Cache Manager Adapter]: ${provider}`);
const store = match(provider)
.with('memory', () => new Map())
.with('sqlite', () => {
fse.ensureFileSync(sqlite.uri);
return new KeyvSqlite(sqlite);
const uri = sqlite.uri.replace(/^sqlite:\/\//, '');
fse.ensureFileSync(uri);
Logger.log(`[Cache Manager File Path]: ${path.resolve(uri)}`);
return new KeyvSqlite({
...sqlite,
uri,
});
})
.with('redis', () => new KeyvRedis(redis, { useRedisSets: false }))
.exhaustive();
@ -30,7 +40,6 @@ export const CacheProvider: Provider = {
error && Logger.error(error, 'Cache Manager Connection Error');
});
Logger.log(`[Cache Manager Adapter]: ${provider}`);
Logger.log(`[Cache Manager Namespace]: ${keyv.opts.namespace}`);
return new CacheService(keyv);
},

View File

@ -4,7 +4,7 @@ import type { ConfigType } from '@nestjs/config';
import { registerAs } from '@nestjs/config';
export const storageConfig = registerAs('storage', () => ({
provider: process.env.BACKEND_STORAGE_PROVIDER ?? 'local',
provider: (process.env.BACKEND_STORAGE_PROVIDER ?? 'local') as 'local' | 'minio',
local: {
path: process.env.BACKEND_STORAGE_LOCAL_PATH ?? '.assets/uploads',
},
@ -12,9 +12,7 @@ export const storageConfig = registerAs('storage', () => ({
privateBucket: process.env.BACKEND_STORAGE_PRIVATE_BUCKET || '',
minio: {
endPoint: process.env.BACKEND_STORAGE_MINIO_ENDPOINT,
port: process.env.BACKEND_STORAGE_MINIO_PORT
? parseInt(process.env.BACKEND_STORAGE_MINIO_PORT)
: 9000,
port: Number(process.env.BACKEND_STORAGE_MINIO_PORT ?? 9000),
useSSL: process.env.BACKEND_STORAGE_MINIO_USE_SSL === 'true',
accessKey: process.env.BACKEND_STORAGE_MINIO_ACCESS_KEY,
secretKey: process.env.BACKEND_STORAGE_MINIO_SECRET_KEY,

View File

@ -34,7 +34,7 @@ export class AggregationFunctionPostgres extends AbstractAggregationFunction {
totalAttachmentSize(): string {
return this.knex
.raw(
`SELECT SUM(("value"::json ->> 'size')::INTEGER) AS "value" FROM ??, json_array_elements(??)`,
`SELECT SUM(("value"::json ->> 'size')::INTEGER) AS "value" FROM ??, jsonb_array_elements(??)`,
[this.dbTableName, this.tableColumnRef]
)
.toQuery();

View File

@ -58,8 +58,8 @@ export class ActionTriggerListener {
const oldColumn = oldValue as IGridColumn;
const newColumn = newValue as IGridColumn;
const shouldShow = oldColumn.hidden !== newColumn.hidden && !newColumn.hidden;
const shouldApplyStatFunc = oldColumn.statisticFunc !== newColumn.statisticFunc;
const shouldShow = !newColumn?.hidden && oldColumn?.hidden !== newColumn?.hidden;
const shouldApplyStatFunc = oldColumn?.statisticFunc !== newColumn?.statisticFunc;
if (shouldShow) {
buffer.showViewField!.push(fieldId);

View File

@ -58,8 +58,8 @@ export class MinioStorage implements StorageAdapter {
}
}
async getObject(bucket: string, path: string, token: string) {
const objectName = join(path, token);
async getObject(bucket: string, path: string, _token: string) {
const objectName = path;
const { metaData, size, etag: hash } = await this.minioClient.statObject(bucket, objectName);
const mimetype = metaData['content-type'] as string;
const url = `/${bucket}/${objectName}`;

View File

@ -11,6 +11,7 @@ import { isDate } from 'lodash';
import { InjectModel } from 'nest-knexjs';
import { ClsService } from 'nestjs-cls';
import type { IClsStore } from '../../types/cls';
import { getFullStorageUrl } from '../../utils/full-storage-url';
@Injectable()
export class CollaboratorService {
@ -113,6 +114,9 @@ export class CollaboratorService {
if (isDate(collaborator.createdTime)) {
collaborator.createdTime = collaborator.createdTime.toISOString();
}
if (collaborator.avatar) {
collaborator.avatar = getFullStorageUrl(collaborator.avatar);
}
return collaborator;
});
}

View File

@ -1,12 +1,47 @@
import type { IUserCellValue } from '@teable/core';
import { UserFieldCore } from '@teable/core';
import { UploadType } from '@teable/openapi';
import { omit } from 'lodash';
import { getFullStorageUrl } from '../../../../utils/full-storage-url';
import StorageAdapter from '../../../attachments/plugins/adapter';
import type { IFieldBase } from '../field-base';
export class UserFieldDto extends UserFieldCore implements IFieldBase {
convertCellValue2DBValue(value: unknown): unknown {
return value && JSON.stringify(value);
if (!value) {
return null;
}
this.applyTransformation<IUserCellValue>(value as IUserCellValue | IUserCellValue[], (item) =>
omit(item, ['avatarUrl'])
);
return JSON.stringify(value);
}
convertDBValue2CellValue(value: unknown): unknown {
return value == null || typeof value === 'object' ? value : JSON.parse(value as string);
if (value === null) return null;
const parsedValue: IUserCellValue | IUserCellValue[] =
typeof value === 'string' ? JSON.parse(value) : value;
return this.applyTransformation<IUserCellValue>(parsedValue, this.fullAvatarUrl);
}
private fullAvatarUrl(cellValue: IUserCellValue) {
if (cellValue?.id) {
const bucket = StorageAdapter.getBucket(UploadType.Avatar);
const path = `${StorageAdapter.getDir(UploadType.Avatar)}/${cellValue.id}`;
cellValue.avatarUrl = getFullStorageUrl(`${bucket}/${path}`);
}
return cellValue;
}
private applyTransformation<T>(value: T | T[], transform: (item: T) => void): T | T[] {
if (Array.isArray(value)) {
value.forEach(transform);
} else {
transform(value);
}
return value;
}
}

View File

@ -1,6 +1,5 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import type {
IAttachmentCellValue,
ICreateRecordsRo,
ICreateRecordsVo,
IRecord,
@ -9,10 +8,8 @@ import type {
} from '@teable/core';
import { FieldKeyType, FieldType } from '@teable/core';
import { PrismaService } from '@teable/db-main-prisma';
import { UploadType } from '@teable/openapi';
import { forEach, map } from 'lodash';
import { AttachmentsStorageService } from '../../attachments/attachments-storage.service';
import StorageAdapter from '../../attachments/plugins/adapter';
import { FieldConvertingService } from '../../field/field-calculate/field-converting.service';
import { createFieldInstanceByRaw } from '../../field/model/factory';
import { RecordCalculateService } from '../record-calculate/record-calculate.service';
@ -127,6 +124,7 @@ export class RecordOpenApiService {
prismaService: this.prismaService,
fieldConvertingService: this.fieldConvertingService,
recordService: this.recordService,
attachmentsStorageService: this.attachmentsStorageService,
},
field,
tableId,
@ -143,35 +141,6 @@ export class RecordOpenApiService {
recordField[fieldIdOrName] = newCellValues[i];
}
});
if (field.type === FieldType.Attachment) {
// attachment presignedUrl reparation
for (const recordField of newRecordsFields) {
const attachmentCellValue = recordField[fieldIdOrName] as IAttachmentCellValue;
if (!attachmentCellValue) {
continue;
}
recordField[fieldIdOrName] = await Promise.all(
attachmentCellValue.map(async (item) => {
const { path, mimetype, token } = item;
const presignedUrl = await this.attachmentsStorageService.getPreviewUrlByPath(
StorageAdapter.getBucket(UploadType.Table),
path,
token,
undefined,
{
// eslint-disable-next-line @typescript-eslint/naming-convention
'Content-Type': mimetype,
}
);
return {
...item,
presignedUrl,
};
})
);
}
}
}
return records.map((record, i) => ({

View File

@ -11,8 +11,8 @@ import type {
IExtraResult,
IFilter,
IGetRecordQuery,
IGroup,
IGetRecordsRo,
IGroup,
ILinkCellValue,
IRecord,
IRecordsVo,
@ -32,8 +32,8 @@ import {
mergeWithDefaultFilter,
mergeWithDefaultSort,
OpName,
Relationship,
parseGroup,
Relationship,
} from '@teable/core';
import type { Field, Prisma } from '@teable/db-main-prisma';
import { PrismaService } from '@teable/db-main-prisma';

View File

@ -3,6 +3,7 @@ import { Colors, FieldType } from '@teable/core';
import type { PrismaService } from '@teable/db-main-prisma';
import { vi } from 'vitest';
import { mockDeep, mockReset } from 'vitest-mock-extended';
import type { AttachmentsStorageService } from '../attachments/attachments-storage.service';
import type { FieldConvertingService } from '../field/field-calculate/field-converting.service';
import type { IFieldInstance } from '../field/model/factory';
import type { LinkFieldDto } from '../field/model/field-dto/link-field.dto';
@ -21,11 +22,13 @@ describe('TypeCastAndValidate', () => {
const prismaService = mockDeep<PrismaService>();
const fieldConvertingService = mockDeep<FieldConvertingService>();
const recordService = mockDeep<RecordService>();
const attachmentsStorageService = mockDeep<AttachmentsStorageService>();
const services = {
prismaService,
fieldConvertingService,
recordService,
attachmentsStorageService,
};
const tableId = 'tableId';

View File

@ -1,9 +1,12 @@
import { BadRequestException } from '@nestjs/common';
import type { ILinkCellValue } from '@teable/core';
import type { IAttachmentCellValue, ILinkCellValue } from '@teable/core';
import { ColorUtils, FieldType, generateChoiceId } from '@teable/core';
import type { PrismaService } from '@teable/db-main-prisma';
import { UploadType } from '@teable/openapi';
import { isUndefined, keyBy, map } from 'lodash';
import { fromZodError } from 'zod-validation-error';
import type { AttachmentsStorageService } from '../attachments/attachments-storage.service';
import StorageAdapter from '../attachments/plugins/adapter';
import type { FieldConvertingService } from '../field/field-calculate/field-converting.service';
import type { IFieldInstance } from '../field/model/factory';
import type { LinkFieldDto } from '../field/model/field-dto/link-field.dto';
@ -15,6 +18,7 @@ interface IServices {
prismaService: PrismaService;
fieldConvertingService: FieldConvertingService;
recordService: RecordService;
attachmentsStorageService: AttachmentsStorageService;
}
/**
@ -61,6 +65,10 @@ export class TypeCastAndValidate {
case FieldType.Link: {
return await this.castToLink(cellValues);
}
case FieldType.User:
return await this.castToUser(cellValues);
case FieldType.Attachment:
return await this.castToAttachment(cellValues);
default:
return this.defaultCastTo(cellValues);
}
@ -202,6 +210,45 @@ export class TypeCastAndValidate {
});
}
private async castToUser(cellValues: unknown[]): Promise<unknown[]> {
const newCellValues = this.defaultCastTo(cellValues);
return newCellValues.map((cellValues) => {
return this.field.convertDBValue2CellValue(cellValues);
});
}
private async castToAttachment(cellValues: unknown[]): Promise<unknown[]> {
const newCellValues = this.defaultCastTo(cellValues);
const allAttachmentsPromises = newCellValues.map((cellValues) => {
const attachmentCellValue = cellValues as IAttachmentCellValue;
if (!attachmentCellValue) {
return attachmentCellValue;
}
const attachmentsWithPresignedUrls = attachmentCellValue.map(async (item) => {
const { path, mimetype, token } = item;
const presignedUrl = await this.services.attachmentsStorageService.getPreviewUrlByPath(
StorageAdapter.getBucket(UploadType.Table),
path,
token,
undefined,
{
// eslint-disable-next-line @typescript-eslint/naming-convention
'Content-Type': mimetype,
}
);
return {
...item,
presignedUrl,
};
});
return Promise.all(attachmentsWithPresignedUrls);
});
return await Promise.all(allAttachmentsPromises);
}
/**
* Get the recordMap of the associated table, the format is: {[title]: [id]}.
*/

View File

@ -3,12 +3,12 @@ import { Injectable } from '@nestjs/common';
import { generateSpaceId, SpaceRole } from '@teable/core';
import type { Prisma } from '@teable/db-main-prisma';
import { PrismaService } from '@teable/db-main-prisma';
import { UploadType, type ICreateSpaceRo, type IUserNotifyMeta } from '@teable/openapi';
import { type ICreateSpaceRo, type IUserNotifyMeta, UploadType } from '@teable/openapi';
import { ClsService } from 'nestjs-cls';
import type { IClsStore } from '../../types/cls';
import { getFullStorageUrl } from '../../utils/full-storage-url';
import StorageAdapter from '../attachments/plugins/adapter';
import type { LocalStorage } from '../attachments/plugins/local';
import { LocalStorage } from '../attachments/plugins/local';
import { InjectStorageAdapter } from '../attachments/plugins/storage';
@Injectable()
@ -106,10 +106,21 @@ export class UserService {
'Content-Type': avatarFile.mimetype,
});
const localStorage = this.storageAdapter as LocalStorage;
const { size, mimetype, path: filePath } = avatarFile;
const hash = await localStorage.getHash(filePath);
const { width, height } = await localStorage.getFileMate(filePath);
let hash, width, height;
const storage = this.storageAdapter;
if (storage instanceof LocalStorage) {
hash = await storage.getHash(filePath);
const fileMate = await storage.getFileMate(filePath);
width = fileMate.width;
height = fileMate.height;
} else {
const objectMeta = await storage.getObject(bucket, path, id);
hash = objectMeta.hash;
width = objectMeta.width;
height = objectMeta.height;
}
const isExist = await this.prismaService.txClient().attachments.count({
where: {

View File

@ -84,6 +84,8 @@ const secureHeaders = createSecureHeaders({
'https://vitals.vercel-insights.com',
'https://*.sentry.io',
'https://*.teable.io',
'http://localhost:9000',
'http://127.0.0.1:9000',
],
imgSrc: ["'self'", 'https:', 'http:', 'data:'],
workerSrc: ['blob:'],

View File

@ -64,7 +64,7 @@
"@types/lodash": "4.14.202",
"@types/node": "20.9.0",
"@types/nprogress": "0.2.3",
"@types/react": "18.2.56",
"@types/react": "18.2.58",
"@types/react-dom": "18.2.19",
"@types/react-grid-layout": "1.3.5",
"@types/react-syntax-highlighter": "15.5.11",
@ -76,14 +76,14 @@
"dotenv-flow": "4.1.0",
"dotenv-flow-cli": "1.1.1",
"es-check": "7.1.1",
"eslint": "8.56.0",
"eslint": "8.57.0",
"eslint-config-next": "13.0.2",
"get-tsconfig": "4.7.2",
"happy-dom": "13.3.8",
"happy-dom": "13.6.0",
"npm-run-all2": "6.1.2",
"postcss": "8.4.35",
"postcss-flexbugs-fixes": "5.0.2",
"postcss-preset-env": "9.3.0",
"postcss-preset-env": "9.4.0",
"prettier": "3.2.5",
"rimraf": "5.0.5",
"size-limit": "11.0.2",
@ -93,7 +93,7 @@
"typescript": "5.3.3",
"vite-plugin-svgr": "4.2.0",
"vite-tsconfig-paths": "4.3.1",
"vitest": "1.3.0"
"vitest": "1.3.1"
},
"dependencies": {
"@antv/g6": "4.8.24",
@ -101,9 +101,9 @@
"@belgattitude/http-exception": "1.5.0",
"@codemirror/autocomplete": "6.12.0",
"@codemirror/commands": "6.3.3",
"@codemirror/language": "6.10.0",
"@codemirror/state": "6.4.0",
"@codemirror/view": "6.23.0",
"@codemirror/language": "6.10.1",
"@codemirror/state": "6.4.1",
"@codemirror/view": "6.24.1",
"@dnd-kit/core": "6.1.0",
"@dnd-kit/sortable": "8.0.0",
"@dnd-kit/utilities": "3.2.2",
@ -134,14 +134,14 @@
"eventsource-parser": "1.1.2",
"express": "4.18.2",
"fuse.js": "7.0.0",
"i18next": "23.5.1",
"i18next": "23.10.0",
"is-port-reachable": "3.1.0",
"knex": "3.1.0",
"lodash": "4.17.21",
"lru-cache": "10.2.0",
"lucide-react": "0.331.0",
"lucide-react": "0.341.0",
"next": "13.0.2",
"next-i18next": "14.0.3",
"next-i18next": "15.2.0",
"next-secure-headers": "2.2.0",
"next-seo": "6.5.0",
"next-transpile-modules": "10.0.1",
@ -155,7 +155,7 @@
"react-grid-layout": "1.4.4",
"react-hook-form": "7.50.1",
"react-hotkeys-hook": "4.5.0",
"react-i18next": "13.3.0",
"react-i18next": "14.0.5",
"react-markdown": "9.0.1",
"react-resizable": "3.0.5",
"react-responsive-carousel": "3.2.23",
@ -163,14 +163,14 @@
"react-syntax-highlighter": "15.5.0",
"react-textarea-autosize": "8.5.3",
"react-use": "17.5.0",
"recharts": "2.12.0",
"recharts": "2.12.1",
"reconnecting-websocket": "4.4.0",
"reflect-metadata": "0.2.1",
"remark-gfm": "4.0.0",
"sharedb": "4.1.2",
"tailwind-scrollbar": "3.1.0",
"tailwindcss": "3.4.1",
"type-fest": "4.10.2",
"type-fest": "4.10.3",
"zod": "3.22.4",
"zod-validation-error": "3.0.2",
"zustand": "4.5.1"

View File

@ -6,4 +6,8 @@ POSTGRES_USER=teable
POSTGRES_PASSWORD=teable
# Redis env
REDIS_PASSWORD=teable
REDIS_PASSWORD=teable
# Minio env
MINIO_ACCESS_KEY=teable
MINIO_SECRET_KEY=teable123

View File

@ -5,6 +5,7 @@ services:
image: redis:7.2.4
container_name: teable-cache
hostname: teable-cache
restart: always
ports:
- '6379:6379'
networks:
@ -20,7 +21,6 @@ services:
interval: 10s
timeout: 3s
retries: 3
restart: always
volumes:
cache_data:

View File

@ -1,6 +1,6 @@
# Example with teable cluster
Look into the `.env` file and update the vaiables before executing `docker-compose up -d`.
Look into the `.env` file and update the vaiables before executing `docker compose up -d`.
## Teable

View File

@ -9,29 +9,35 @@ services:
expose:
- '3000'
volumes:
- teable_data:/app/.assets:rw
- teable-data:/app/.assets:rw
environment:
- TZ=${TIMEZONE}
- NODE_OPTIONS=--max-old-space-size=1024
- PUBLIC_ORIGIN=http://127.0.0.1
- PRISMA_DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@teable_db:5432/${POSTGRES_DB}
- PRISMA_DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@teable-db:5432/${POSTGRES_DB}
- BACKEND_CACHE_PROVIDER=redis
- BACKEND_CACHE_REDIS_URI=redis://:${POSTGRES_PASSWORD}@teable_cache:6379/0
- BACKEND_CACHE_REDIS_URI=redis://:${POSTGRES_PASSWORD}@teable-cache:6379/0
networks:
- teable-cluster
depends_on:
teable_db_migrate:
teable-db-migrate:
condition: service_completed_successfully
teable_cache:
teable-cache:
condition: service_healthy
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:3000/health']
start_period: 5s
interval: 5s
timeout: 3s
retries: 3
teable_db:
teable-db:
image: postgres:15.4
restart: always
expose:
- '5432'
volumes:
- teable_db:/var/lib/postgresql/data:rw
- teable-db:/var/lib/postgresql/data:rw
environment:
- TZ=${TIMEZONE}
- POSTGRES_DB=${POSTGRES_DB}
@ -45,13 +51,13 @@ services:
timeout: 3s
retries: 3
teable_cache:
teable-cache:
image: redis:7.2.4
restart: always
expose:
- '6379'
volumes:
- teable_cache:/data:rw
- teable-cache:/data:rw
networks:
- teable-cluster
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
@ -61,37 +67,42 @@ services:
timeout: 3s
retries: 3
teable_db_migrate:
teable-db-migrate:
image: ghcr.io/teableio/teable-db-migrate:latest
environment:
- TZ=${TIMEZONE}
- PRISMA_DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@teable_db:5432/${POSTGRES_DB}
- PRISMA_DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@teable-db:5432/${POSTGRES_DB}
networks:
- teable-cluster
depends_on:
teable_db:
teable-db:
condition: service_healthy
teable_reverse_proxy:
image: nginx:alpine
teable-gateway:
image: openresty/openresty:1.25.3.1-2-bookworm-fat
restart: unless-stopped
ports:
- '80:80'
- '443:443'
volumes:
- './nginx/conf.d:/etc/nginx/conf.d'
- './gateway/conf.d:/etc/nginx/conf.d'
networks:
- teable-cluster
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost/healthcheck']
interval: 10s
timeout: 3s
retries: 3
depends_on:
teable:
condition: service_started
networks:
teable-cluster:
name: teable_cluster_network
name: teable-cluster-network
driver: bridge
volumes:
teable_data: {}
teable_db: {}
teable_cache: {}
teable-data: {}
teable-db: {}
teable-cache: {}

View File

@ -0,0 +1,55 @@
log_format json_log escape=json '{'
'"timestamp":"$time_iso8601",'
'"remote_addr":"$remote_addr",'
'"remote_user":"$remote_user",'
'"request_method":"$request_method",'
'"request_uri":"$request_uri",'
'"protocol":"$server_protocol",'
'"status":$status,'
'"body_bytes_sent":$body_bytes_sent,'
'"request_time":$request_time,'
'"http_referrer":"$http_referer",'
'"http_user_agent":"$http_user_agent",'
'"http_x_forwarded_for":"$http_x_forwarded_for",'
'"upstream_addr":"$upstream_addr",'
'"upstream_status":"$upstream_status",'
'"upstream_response_time":"$upstream_response_time",'
'"server_name":"$server_name",'
'"http_host":"$host"'
'}';
access_log /dev/stdout json_log;
server_tokens off;
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream teable {
server teable:3000;
}
server {
server_name localhost;
listen 80;
listen [::]:80;
location / {
proxy_pass http://teable;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
}
location /healthcheck {
default_type application/json;
access_log off;
return 200 '{"status":"ok"}';
}
}

View File

@ -1,26 +0,0 @@
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream teable {
server teable:3000;
}
server {
server_name localhost;
listen 80;
listen [::]:80;
location / {
proxy_pass http://teable;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
}
}

View File

@ -0,0 +1,10 @@
TIMEZONE=UTC
POSTGRES_DB=example
POSTGRES_USER=example
POSTGRES_PASSWORD=swarm_replace_password
REDIS_PASSWORD=swarm_replace_password
MINIO_ACCESS_KEY=swarm_replace_access
MINIO_SECRET_KEY=swarm_replace_secret

View File

@ -0,0 +1,22 @@
# Example with teable by Docker swarm deploy
```shell
# Init
docker swarm init
# or specify the address manually
# docker swarm init --advertise-addr 192.168.99.100
# ./deploy.sh [service_type] [stack_name]
./deploy.sh - example
# view services
docker service ls
# update app service
./deploy.sh app example
# remove service
docker stack rm example
```

View File

@ -0,0 +1,65 @@
#!/bin/sh
create_network() {
docker network create -d overlay teable-swarm || true
}
export_env_vars() {
if [ -f .env ]; then
# see https://github.com/moby/moby/issues/29133
export $(cat .env | xargs)
else
echo ".env file not found, skipping export."
fi
}
deploy_stack() {
compose_files="$1" # Compose files to use for deployment
stack_name="${2:-default_stack}" # Stack name with a default value if not provided
echo "Deploying services with stack name '$stack_name' using compose files: $compose_files"
docker stack deploy -c docker-compose.default.yml $compose_files $stack_name
}
show_help() {
echo "Usage: $0 [service_type] [stack_name]"
echo "service_type: The type of service to deploy (kit, app, gateway). Leave empty to deploy all."
echo "stack_name: The name of the stack. Optional."
echo "Examples:"
echo " $0 kit - Deploys the 'kit' service stack."
echo " $0 app default_stack - Deploys the 'app' service stack with a specific stack name."
echo " $0 - Deploys all services with default stack name."
}
deploy_service() {
service_type=$1
stack_name=$2
if [ -z "$1" ] || [ "$1" = "help" ]; then
show_help
exit 0
fi
create_network
export_env_vars
case $service_type in
"kit")
deploy_stack "-c docker-compose.kit.yml" $stack_name
;;
"app")
deploy_stack "-c docker-compose.app.yml" $stack_name
;;
"gateway")
deploy_stack "-c docker-compose.gateway.yml" $stack_name
;;
*)
# Deploy all services if no specific service type is provided
deploy_stack "-c docker-compose.kit.yml -c docker-compose.app.yml -c docker-compose.gateway.yml" $stack_name
;;
esac
}
# $1 is the service type (kit, app, gateway) or empty for all services
# $2 is the stack name, optional
deploy_service $1 $2

View File

@ -0,0 +1,38 @@
version: '3.9'
services:
teable:
image: ghcr.io/teableio/teable:latest
deploy:
replicas: 2
restart_policy:
condition: on-failure
update_config:
parallelism: 1
delay: 10s
order: start-first
expose:
- '3000'
environment:
- TZ=${TIMEZONE}
- NODE_OPTIONS=--max-old-space-size=1024
- PUBLIC_ORIGIN=http://127.0.0.1
- PRISMA_DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@teable-db:5432/${POSTGRES_DB}
- BACKEND_CACHE_PROVIDER=redis
- BACKEND_CACHE_REDIS_URI=redis://:${POSTGRES_PASSWORD}@teable-cache:6379/0
- BACKEND_STORAGE_PROVIDER=minio
- BACKEND_STORAGE_PUBLIC_BUCKET=public
- BACKEND_STORAGE_PRIVATE_BUCKET=private
- BACKEND_STORAGE_MINIO_ENDPOINT=127.0.0.1
- BACKEND_STORAGE_MINIO_PORT=9000
- BACKEND_STORAGE_MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY}
- BACKEND_STORAGE_MINIO_SECRET_KEY=${MINIO_SECRET_KEY}
- STORAGE_PREFIX=http://127.0.0.1:9000
networks:
- teable-swarm
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:3000/health']
start_period: 5s
interval: 5s
timeout: 3s
retries: 3

View File

@ -0,0 +1,10 @@
version: '3.9'
networks:
teable-swarm:
external: true
volumes:
teable-db:
teable-cache:
teable-storage:

View File

@ -0,0 +1,19 @@
version: '3.9'
services:
teable-gateway:
image: openresty/openresty:1.25.3.1-2-bookworm-fat
ports:
- '80:80'
- '443:443'
- '9000:9000'
- '9001:9001'
volumes:
- ./gateway/conf.d:/etc/nginx/conf.d
networks:
- teable-swarm
healthcheck:
test: ['CMD', 'curl', '-f', 'http://127.0.0.1/healthcheck']
interval: 10s
timeout: 3s
retries: 3

View File

@ -0,0 +1,77 @@
version: '3.9'
services:
teable-db:
image: postgres:15.4
expose:
- '5432'
volumes:
- teable-db:/var/lib/postgresql/data:rw
environment:
- TZ=${TIMEZONE}
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
networks:
- teable-swarm
healthcheck:
test: ['CMD-SHELL', "sh -c 'pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}'"]
interval: 10s
timeout: 3s
retries: 3
teable-db-migrate:
image: ghcr.io/teableio/teable-db-migrate:latest
deploy:
restart_policy:
condition: on-failure
environment:
- TZ=${TIMEZONE}
- PRISMA_DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@teable-db:5432/${POSTGRES_DB}
networks:
- teable-swarm
teable-cache:
image: redis:7.2.4
expose:
- '6379'
volumes:
- teable-cache:/data:rw
networks:
- teable-swarm
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
healthcheck:
test: ['CMD', 'redis-cli', '--raw', 'incr', 'ping']
interval: 10s
timeout: 3s
retries: 3
teable-storage:
image: minio/minio:RELEASE.2024-02-17T01-15-57Z
expose:
- '9000'
- '9001'
environment:
- MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY}
- MINIO_SECRET_KEY=${MINIO_SECRET_KEY}
volumes:
- teable-storage:/data:rw
networks:
- teable-swarm
command: server /data --console-address ":9001"
createbuckets:
image: minio/mc
deploy:
restart_policy:
condition: on-failure
networks:
- teable-swarm
entrypoint: >
/bin/sh -c "
/usr/bin/mc alias set teable-storage http://teable-storage:9000 ${MINIO_ACCESS_KEY} ${MINIO_SECRET_KEY};
/usr/bin/mc mb teable-storage/public;
/usr/bin/mc anonymous set public teable-storage/public;
/usr/bin/mc mb teable-storage/private;
exit 0;
"

View File

@ -0,0 +1,55 @@
log_format json_log escape=json '{'
'"timestamp":"$time_iso8601",'
'"remote_addr":"$remote_addr",'
'"remote_user":"$remote_user",'
'"request_method":"$request_method",'
'"request_uri":"$request_uri",'
'"protocol":"$server_protocol",'
'"status":$status,'
'"body_bytes_sent":$body_bytes_sent,'
'"request_time":$request_time,'
'"http_referrer":"$http_referer",'
'"http_user_agent":"$http_user_agent",'
'"http_x_forwarded_for":"$http_x_forwarded_for",'
'"upstream_addr":"$upstream_addr",'
'"upstream_status":"$upstream_status",'
'"upstream_response_time":"$upstream_response_time",'
'"server_name":"$server_name",'
'"http_host":"$host"'
'}';
access_log /dev/stdout json_log;
server_tokens off;
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream teable {
server teable:3000;
}
server {
server_name localhost;
listen 80;
listen [::]:80;
location / {
proxy_pass http://teable;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
location /healthcheck {
default_type application/json;
access_log off;
return 200 '{"status":"ok"}';
}
}

View File

@ -0,0 +1,59 @@
upstream storage_s3 {
server teable-storage:9000;
}
upstream storage_console {
server teable-storage:9001;
}
server {
server_name localhost;
listen 9000;
listen [::]:9000;
location / {
proxy_pass http://storage_s3;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 300;
# Default is HTTP/1, keepalive is only enabled in HTTP/1.1
proxy_http_version 1.1;
proxy_set_header Connection "";
chunked_transfer_encoding off;
}
}
server {
server_name localhost;
listen 9001;
listen [::]:9001;
location / {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-NginX-Proxy true;
# This is necessary to pass the correct IP to be hashed
real_ip_header X-Real-IP;
proxy_connect_timeout 300;
# To support websockets in MinIO versions released after January 2023
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Some environments may encounter CORS errors (Kubernetes + Nginx Ingress)
# Uncomment the following line to set the Origin request to an empty string
# proxy_set_header Origin '';
chunked_transfer_encoding off;
proxy_pass http://storage_console; # This uses the upstream directive definition to load balance
}
}

View File

@ -1,6 +1,6 @@
# Example with teable standalone
Look into the `.env` file and update the vaiables before executing `docker-compose up -d`.
Look into the `.env` file and update the vaiables before executing `docker compose up -d`.
## Teable

40
dockers/storage-minio.yml Normal file
View File

@ -0,0 +1,40 @@
version: '3.9'
services:
teable-storage:
image: minio/minio:RELEASE.2024-02-17T01-15-57Z
container_name: teable-storage
hostname: teable-storage
restart: always
ports:
- '9000:9000'
- '9001:9001'
networks:
- teable-net
environment:
- MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY}
- MINIO_SECRET_KEY=${MINIO_SECRET_KEY}
volumes:
- storage_data:/data:rw
# you may use a bind-mounted host directory instead,
# so that it is harder to accidentally remove the volume and lose all your data!
# - ./docker/storage/data:/data:rw
command: server /data --console-address ":9001"
createbuckets:
image: minio/mc:RELEASE.2024-02-16T11-05-48Z
networks:
- teable-net
depends_on:
- teable-storage
entrypoint: >
/bin/sh -c "
/usr/bin/mc alias set teable-storage http://teable-storage:9000 ${MINIO_ACCESS_KEY} ${MINIO_SECRET_KEY};
/usr/bin/mc mb teable-storage/public;
/usr/bin/mc anonymous set public teable-storage/public;
/usr/bin/mc mb teable-storage/private;
exit 0;
"
volumes:
storage_data:

View File

@ -80,7 +80,7 @@ ENV TZ=UTC
ENV PORT=${NEXTJS_APP_PORT:-3000}
RUN npm install zx -g && \
apt-get update && apt-get install -y openssl && \
apt-get update && apt-get install -y curl openssl && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app

View File

@ -1,5 +1,5 @@
ARG NODE_VERSION=20.9.0
ARG PRISMA_VERSION=5.9.1
ARG PRISMA_VERSION=5.10.2
FROM node:${NODE_VERSION}-bookworm AS prisma

View File

@ -54,7 +54,7 @@
"@commitlint/config-conventional": "18.6.2",
"@teable/eslint-config-bases": "workspace:^",
"@types/shell-quote": "1.7.5",
"eslint": "8.56.0",
"eslint": "8.57.0",
"husky": "9.0.11",
"lint-staged": "15.2.2",
"npm-run-all2": "6.1.2",
@ -70,5 +70,5 @@
"pnpm": ">=8.15.0",
"npm": "please-use-pnpm"
},
"packageManager": "pnpm@8.15.3"
"packageManager": "pnpm@8.15.4"
}

View File

@ -35,7 +35,7 @@
"@teable/eslint-config-bases": "workspace:^",
"@types/node": "20.9.0",
"cross-env": "7.0.3",
"eslint": "8.56.0",
"eslint": "8.57.0",
"prettier": "3.2.5",
"rimraf": "5.0.5",
"typescript": "5.3.3"

View File

@ -52,13 +52,13 @@
"antlr4ts-cli": "0.5.0-alpha.4",
"cross-env": "7.0.3",
"es-check": "7.1.1",
"eslint": "8.56.0",
"eslint": "8.57.0",
"get-tsconfig": "4.7.2",
"prettier": "3.2.5",
"rimraf": "5.0.5",
"size-limit": "11.0.2",
"typescript": "5.3.3",
"vite-tsconfig-paths": "4.3.1",
"vitest": "1.3.0"
"vitest": "1.3.1"
}
}

View File

@ -27,6 +27,7 @@ export type IUserFieldOptions = z.infer<typeof userFieldOptionsSchema>;
export const userCellValueSchema = z.object({
id: z.string().startsWith(IdPrefix.User),
title: z.string().optional(),
avatarUrl: z.string().optional().nullable(),
});
export type IUserCellValue = z.infer<typeof userCellValueSchema>;

View File

@ -39,8 +39,8 @@
"nestjs-cls": "^4.0.0"
},
"dependencies": {
"@prisma/client": "5.9.1",
"prisma": "5.9.1",
"@prisma/client": "5.10.2",
"prisma": "5.10.2",
"nanoid": "3.3.7"
},
"devDependencies": {
@ -52,7 +52,7 @@
"camelcase": "8.0.0",
"cross-env": "7.0.3",
"dotenv-flow-cli": "1.1.1",
"eslint": "8.56.0",
"eslint": "8.57.0",
"handlebars": "4.7.8",
"is-port-reachable": "3.1.0",
"mustache": "4.2.0",

View File

@ -74,15 +74,15 @@
"dependencies": {
"@rushstack/eslint-patch": "1.6.1",
"@tanstack/eslint-plugin-query": "4.36.1",
"@typescript-eslint/eslint-plugin": "7.0.1",
"@typescript-eslint/parser": "7.0.1",
"@typescript-eslint/eslint-plugin": "7.0.2",
"@typescript-eslint/parser": "7.0.2",
"eslint-config-prettier": "9.1.0",
"eslint-import-resolver-typescript": "3.6.1",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-jest": "27.9.0",
"eslint-plugin-jest-formatting": "3.1.0",
"eslint-plugin-jsx-a11y": "6.8.0",
"eslint-plugin-playwright": "1.0.0",
"eslint-plugin-playwright": "1.3.0",
"eslint-plugin-prettier": "5.1.3",
"eslint-plugin-react": "7.33.2",
"eslint-plugin-react-hooks": "4.6.0 || 5.0.0-canary-7118f5dd7-20230705",
@ -127,12 +127,12 @@
"@testing-library/jest-dom": "6.4.2",
"@testing-library/react": "14.2.1",
"@types/node": "20.9.0",
"@types/react": "18.2.56",
"@types/react": "18.2.58",
"@types/react-dom": "18.2.19",
"es-check": "7.1.1",
"eslint": "8.56.0",
"eslint": "8.57.0",
"eslint-plugin-mdx": "3.1.5",
"eslint-plugin-tailwindcss": "3.14.1",
"eslint-plugin-tailwindcss": "3.14.3",
"react": "18.2.0",
"react-dom": "18.2.0",
"rimraf": "5.0.5",

View File

@ -34,11 +34,11 @@
"@svgr/plugin-svgo": "8.1.0",
"@types/fs-extra": "11.0.4",
"@types/node": "20.9.0",
"@types/react": "18.2.56",
"@types/react": "18.2.58",
"axios": "1.6.7",
"chalk": "5.3.0",
"dotenv": "16.4.4",
"eslint": "8.56.0",
"dotenv": "16.4.5",
"eslint": "8.57.0",
"figma-js": "1.16.0",
"fs-extra": "11.2.0",
"lodash": "4.17.21",

View File

@ -23,12 +23,12 @@
"@asteasolutions/zod-to-openapi": "6.3.1",
"@teable/core": "workspace:^",
"axios": "1.6.7",
"openapi3-ts": "4.2.1",
"openapi3-ts": "4.2.2",
"zod": "3.22.4"
},
"devDependencies": {
"@teable/eslint-config-bases": "workspace:^",
"eslint": "8.56.0",
"eslint": "8.57.0",
"rimraf": "5.0.5",
"typescript": "5.3.3"
}

View File

@ -36,9 +36,9 @@
"@belgattitude/http-exception": "1.5.0",
"@codemirror/autocomplete": "6.12.0",
"@codemirror/commands": "6.3.3",
"@codemirror/language": "6.10.0",
"@codemirror/state": "6.4.0",
"@codemirror/view": "6.23.0",
"@codemirror/language": "6.10.1",
"@codemirror/state": "6.4.1",
"@codemirror/view": "6.24.1",
"@dnd-kit/core": "6.1.0",
"@dnd-kit/sortable": "8.0.0",
"@dnd-kit/utilities": "3.2.2",
@ -62,7 +62,7 @@
"knex": "3.1.0",
"lodash": "4.17.21",
"lru-cache": "10.2.0",
"lucide-react": "0.331.0",
"lucide-react": "0.341.0",
"mousetrap": "1.6.5",
"react-day-picker": "8.10.0",
"react-hammerjs": "1.0.1",
@ -72,7 +72,7 @@
"sharedb": "4.1.2",
"ts-key-enum": "2.0.12",
"ts-keycode-enum": "1.0.6",
"ts-mixer": "6.0.3",
"ts-mixer": "6.0.4",
"zustand": "4.5.1",
"react-textarea-autosize": "8.5.3"
},
@ -86,14 +86,14 @@
"@testing-library/react": "14.2.1",
"@types/lodash": "4.14.202",
"@types/node": "20.9.0",
"@types/react": "18.2.56",
"@types/react": "18.2.58",
"@types/react-dom": "18.2.19",
"@types/react-hammerjs": "1.0.7",
"@types/scroller": "0.1.5",
"@types/sharedb": "3.3.10",
"@vitejs/plugin-react-swc": "3.6.0",
"cross-env": "7.0.3",
"eslint": "8.56.0",
"eslint": "8.57.0",
"get-tsconfig": "4.7.2",
"microbundle": "0.15.1",
"npm-run-all2": "6.1.2",
@ -105,6 +105,6 @@
"typescript": "5.3.3",
"vite-plugin-svgr": "4.2.0",
"vite-tsconfig-paths": "4.3.1",
"vitest": "1.3.0"
"vitest": "1.3.1"
}
}

View File

@ -76,7 +76,7 @@ export function UserEditorMain(props: IUserEditorMainProps) {
<CommandItem
key={userId}
value={userName}
onSelect={() => onSelect({ id: userId, title: userName })}
onSelect={() => onSelect({ id: userId, title: userName, avatarUrl: avatar })}
className="flex justify-between"
>
<div className="flex items-center space-x-4">

View File

@ -1,4 +1,4 @@
import type { INumberShowAs, ISingleLineTextShowAs, IAttachmentCellValue } from '@teable/core';
import type { IAttachmentCellValue, INumberShowAs, ISingleLineTextShowAs } from '@teable/core';
import { CellValueType, ColorUtils, FieldType } from '@teable/core';
import { keyBy } from 'lodash';
import { LRUCache } from 'lru-cache';
@ -7,7 +7,7 @@ import colors from 'tailwindcss/colors';
import type { ChartType, ICell, IGridColumn, INumberShowAs as IGridNumberShowAs } from '../..';
import { CellType, getFileCover, hexToRGBA, onMixedTextClick } from '../..';
import { ThemeKey } from '../../../context';
import { useTablePermission, useFields, useView, useTheme } from '../../../hooks';
import { useFields, useTablePermission, useTheme, useView } from '../../../hooks';
import type { IFieldInstance, NumberField, Record } from '../../../model';
import type { GridView } from '../../../model/view';
import { getFilterFieldIds } from '../../filter/utils';
@ -17,8 +17,8 @@ import {
GridAttachmentEditor,
GridDateEditor,
GridLinkEditor,
GridSelectEditor,
GridNumberEditor,
GridSelectEditor,
} from '../editor';
import { GridUserEditor } from '../editor/GridUserEditor';
@ -403,7 +403,7 @@ export const createCellValue2GridDisplay =
}
case FieldType.User: {
const cv = cellValue ? (Array.isArray(cellValue) ? cellValue : [cellValue]) : [];
const data = cv.map(({ id, title }) => ({ id, name: title }));
const data = cv.map(({ title, ...data }) => ({ ...data, name: title }));
return {
...baseCellProps,

View File

@ -133,6 +133,7 @@ export interface IImageCell extends IEditableCell {
export interface IUserData {
id: string;
name: string;
avatarUrl?: string;
}
export interface IUserCell extends IEditableCell {

View File

@ -101,8 +101,8 @@ export const userCellRenderer: IInternalCellRenderer<IUserCell> = {
fontFamily,
user: {
...user,
avatar: user.avatarUrl,
email: '',
avatar: '',
},
});

View File

@ -51,25 +51,25 @@
},
"devDependencies": {
"@mdx-js/react": "3.0.1",
"@storybook/addon-actions": "7.6.16",
"@storybook/addon-docs": "7.6.16",
"@storybook/addon-essentials": "7.6.16",
"@storybook/addon-links": "7.6.16",
"@storybook/addon-actions": "7.6.17",
"@storybook/addon-docs": "7.6.17",
"@storybook/addon-essentials": "7.6.17",
"@storybook/addon-links": "7.6.17",
"@storybook/addon-postcss": "2.0.0",
"@storybook/addon-storysource": "7.6.16",
"@storybook/builder-webpack5": "7.6.16",
"@storybook/addon-storysource": "7.6.17",
"@storybook/builder-webpack5": "7.6.17",
"@storybook/manager-webpack5": "6.5.16",
"@storybook/react": "7.6.16",
"@storybook/react": "7.6.17",
"@tailwindcss/aspect-ratio": "0.4.2",
"@teable/eslint-config-bases": "workspace:^",
"@testing-library/react": "14.2.1",
"@types/node": "20.9.0",
"@types/react": "18.2.56",
"@types/react": "18.2.58",
"@types/react-dom": "18.2.19",
"autoprefixer": "10.4.17",
"core-js": "3.36.0",
"cross-env": "7.0.3",
"eslint": "8.56.0",
"eslint": "8.57.0",
"microbundle": "0.15.1",
"npm-run-all2": "6.1.2",
"postcss": "8.4.35",
@ -85,7 +85,7 @@
"tailwindcss": "3.4.1",
"tsconfig-paths-webpack-plugin": "4.1.0",
"typescript": "5.3.3",
"webpack": "5.90.2"
"webpack": "5.90.3"
},
"dependencies": {
"@hookform/resolvers": "3.3.4",
@ -119,8 +119,8 @@
"next-themes": "0.2.1",
"react-day-picker": "8.10.0",
"react-hook-form": "7.50.1",
"react-resizable-panels": "2.0.9",
"sonner": "1.4.0",
"react-resizable-panels": "2.0.11",
"sonner": "1.4.1",
"tailwind-merge": "2.2.1",
"tailwindcss-animate": "1.0.7",
"zod": "3.22.4"

File diff suppressed because it is too large Load Diff

View File

@ -1,19 +0,0 @@
#!/usr/bin/env bash
files=("apps/nextjs-app/.env" "apps/nextjs-app/.env.development" "apps/nextjs-app/.env.development.local")
for file in "${files[@]}"; do
if [[ -f $file ]]; then
while IFS='=' read -r key value; do
# Skip lines starting with #
if [[ $key == \#* ]]; then
continue
fi
# Check if key is empty to avoid exporting invalid environment variables
if [[ -n $key ]]; then
# Use eval to handle values that may contain spaces while removing any quotes
eval export $key=\"$(echo $value | sed -e 's/^"//' -e 's/"$//')\"
fi
done <$file
fi
done