mirror of
https://github.com/teableio/teable
synced 2024-11-23 07:51:00 +00:00
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:
parent
3a7e95cf00
commit
e8c5fe8720
@ -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",
|
||||
|
15
apps/nestjs-backend/src/cache/cache.provider.ts
vendored
15
apps/nestjs-backend/src/cache/cache.provider.ts
vendored
@ -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);
|
||||
},
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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}`;
|
||||
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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) => ({
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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]}.
|
||||
*/
|
||||
|
@ -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: {
|
||||
|
@ -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:'],
|
||||
|
@ -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"
|
||||
|
@ -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
|
@ -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:
|
||||
|
@ -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
|
||||
|
||||
|
@ -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: {}
|
||||
|
55
dockers/examples/cluster/gateway/conf.d/default.conf
Normal file
55
dockers/examples/cluster/gateway/conf.d/default.conf
Normal 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"}';
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
10
dockers/examples/docker-swarm/.env
Normal file
10
dockers/examples/docker-swarm/.env
Normal 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
|
22
dockers/examples/docker-swarm/README.md
Normal file
22
dockers/examples/docker-swarm/README.md
Normal 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
|
||||
```
|
65
dockers/examples/docker-swarm/deploy.sh
Executable file
65
dockers/examples/docker-swarm/deploy.sh
Executable 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
|
38
dockers/examples/docker-swarm/docker-compose.app.yml
Normal file
38
dockers/examples/docker-swarm/docker-compose.app.yml
Normal 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
|
10
dockers/examples/docker-swarm/docker-compose.default.yml
Normal file
10
dockers/examples/docker-swarm/docker-compose.default.yml
Normal file
@ -0,0 +1,10 @@
|
||||
version: '3.9'
|
||||
|
||||
networks:
|
||||
teable-swarm:
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
teable-db:
|
||||
teable-cache:
|
||||
teable-storage:
|
19
dockers/examples/docker-swarm/docker-compose.gateway.yml
Normal file
19
dockers/examples/docker-swarm/docker-compose.gateway.yml
Normal 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
|
77
dockers/examples/docker-swarm/docker-compose.kit.yml
Normal file
77
dockers/examples/docker-swarm/docker-compose.kit.yml
Normal 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;
|
||||
"
|
55
dockers/examples/docker-swarm/gateway/conf.d/default.conf
Normal file
55
dockers/examples/docker-swarm/gateway/conf.d/default.conf
Normal 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"}';
|
||||
}
|
||||
}
|
59
dockers/examples/docker-swarm/gateway/conf.d/minio.conf
Normal file
59
dockers/examples/docker-swarm/gateway/conf.d/minio.conf
Normal 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
|
||||
}
|
||||
}
|
@ -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
40
dockers/storage-minio.yml
Normal 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:
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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>;
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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">
|
||||
|
@ -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,
|
||||
|
@ -133,6 +133,7 @@ export interface IImageCell extends IEditableCell {
|
||||
export interface IUserData {
|
||||
id: string;
|
||||
name: string;
|
||||
avatarUrl?: string;
|
||||
}
|
||||
|
||||
export interface IUserCell extends IEditableCell {
|
||||
|
@ -101,8 +101,8 @@ export const userCellRenderer: IInternalCellRenderer<IUserCell> = {
|
||||
fontFamily,
|
||||
user: {
|
||||
...user,
|
||||
avatar: user.avatarUrl,
|
||||
email: '',
|
||||
avatar: '',
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -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"
|
||||
|
2957
pnpm-lock.yaml
2957
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -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
|
Loading…
Reference in New Issue
Block a user