├── src
├── vite-env.d.ts
├── pages
│ ├── PlaygroundPage.tsx
│ ├── WidgetsLibrary
│ │ ├── index.ts
│ │ ├── WidgetsSidebar.tsx
│ │ └── WidgetPreview.tsx
│ ├── index.ts
│ ├── WidgetsLibraryPage.tsx
│ ├── WelcomePage.tsx
│ ├── DatabasePage.tsx
│ └── DashboardPage.tsx
├── lib
│ ├── utils.ts
│ ├── dialogs.ts
│ ├── componentAnalyzer.ts
│ ├── compiler
│ │ ├── ESBuildCompiler.ts
│ │ ├── TailwindCompiler.ts
│ │ ├── ESBuildVFS.ts
│ │ └── ComponentCompiler.ts
│ └── runtime
│ │ ├── WidgetRuntime.ts
│ │ └── ComponentRuntime.ts
├── pipeline
│ ├── ValidationStage.ts
│ └── modules-mocks
│ │ ├── mockShadcnUiModules.tsx
│ │ ├── mockGlobalModules.tsx
│ │ └── mockUtilsModule.ts
├── virtual-fs
│ ├── shadcn-ui.ts
│ ├── types.ts
│ └── default-fs.ts
├── platform
│ ├── tauri
│ │ ├── index.ts
│ │ ├── platform.ts
│ │ ├── window.ts
│ │ ├── storage.ts
│ │ ├── dialogs.ts
│ │ └── database.ts
│ ├── browser
│ │ ├── index.ts
│ │ ├── platform.ts
│ │ ├── window.ts
│ │ └── database.ts
│ ├── abstract
│ │ ├── index.ts
│ │ ├── platform.ts
│ │ ├── window.ts
│ │ ├── storage.ts
│ │ ├── dialogs.ts
│ │ └── database.ts
│ └── index.ts
├── components
│ ├── PageHeader.tsx
│ ├── ErrorBoundary.tsx
│ ├── ui
│ │ ├── label.tsx
│ │ ├── textarea.tsx
│ │ ├── input.tsx
│ │ ├── badge.tsx
│ │ ├── tabs.tsx
│ │ ├── resizable.tsx
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ └── dialog.tsx
│ ├── LoadingState.tsx
│ ├── EmptyState.tsx
│ ├── dashboard
│ │ ├── GridOverlay.tsx
│ │ ├── AddWidgetModal.tsx
│ │ └── DashboardWidgetComponent.tsx
│ ├── DashboardsList.tsx
│ ├── database
│ │ └── DatabaseCard.tsx
│ ├── DashboardCard.tsx
│ └── AppHeader.tsx
├── types
│ ├── database.ts
│ ├── widget.ts
│ ├── dashboard.ts
│ └── storage.ts
├── main.tsx
├── hooks
│ └── useCellSize.ts
├── App.tsx
├── App.css
├── stores
│ ├── WidgetsRepository.ts
│ └── DatabaseStore.ts
└── assets
│ └── react.svg
├── src-tauri
├── build.rs
├── icons
│ ├── icon.ico
│ ├── icon.png
│ ├── 32x32.png
│ ├── icon.icns
│ ├── 128x128.png
│ ├── StoreLogo.png
│ ├── 128x128@2x.png
│ ├── Square30x30Logo.png
│ ├── Square44x44Logo.png
│ ├── Square71x71Logo.png
│ ├── Square89x89Logo.png
│ ├── Square107x107Logo.png
│ ├── Square142x142Logo.png
│ ├── Square150x150Logo.png
│ ├── Square284x284Logo.png
│ └── Square310x310Logo.png
├── .gitignore
├── src
│ ├── main.rs
│ └── lib.rs
├── tauri.conf.json
├── capabilities
│ └── default.json
└── Cargo.toml
├── public
├── icon.png
├── favicon.ico
├── apple-icon.png
├── vite.svg
└── tauri.svg
├── pnpm-workspace.yaml
├── assets
└── gh-screen.png
├── .vscode
└── extensions.json
├── .supercode
└── def.json
├── src-back
├── nodemon.json
├── .prettierrc
├── src
│ ├── middlewares
│ │ ├── errorHandler.ts
│ │ └── rateLimiter.ts
│ ├── types
│ │ └── index.ts
│ ├── index.ts
│ └── routes
│ │ └── sql.ts
├── tsconfig.json
├── package.json
├── .gitignore
└── README.md
├── __legacy
├── src-node
│ ├── src
│ │ ├── ipc
│ │ │ ├── index.ts
│ │ │ ├── types.ts
│ │ │ ├── protocol.ts
│ │ │ └── command-handler.ts
│ │ ├── types
│ │ │ └── index.ts
│ │ ├── utils
│ │ │ └── index.ts
│ │ ├── request-handler
│ │ │ └── index.ts
│ │ ├── compiler
│ │ │ ├── esbuild-compiler.ts
│ │ │ ├── index.ts
│ │ │ └── esbuild-vfs.ts
│ │ └── index.ts
│ ├── tsconfig.json
│ ├── scripts
│ │ └── build.js
│ └── package.json
├── SidecarEncoder.ts
├── sidecar.ts
├── SidecarExecutor.ts
└── compiler
│ ├── index.ts
│ ├── postcss-compiler.ts
│ └── tailwind-compiler.ts
├── tsconfig.node.json
├── .prettierrc
├── .gitignore
├── components.json
├── tsconfig.json
├── index.html
├── vite.config.ts
├── README.md
└── package.json
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src-tauri/build.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | tauri_build::build()
3 | }
4 |
--------------------------------------------------------------------------------
/public/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ElKornacio/qyp-mini/HEAD/public/icon.png
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | onlyBuiltDependencies:
2 | - esbuild
3 | - sharp
4 | - workerd
5 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ElKornacio/qyp-mini/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/assets/gh-screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ElKornacio/qyp-mini/HEAD/assets/gh-screen.png
--------------------------------------------------------------------------------
/public/apple-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ElKornacio/qyp-mini/HEAD/public/apple-icon.png
--------------------------------------------------------------------------------
/src-tauri/icons/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ElKornacio/qyp-mini/HEAD/src-tauri/icons/icon.ico
--------------------------------------------------------------------------------
/src-tauri/icons/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ElKornacio/qyp-mini/HEAD/src-tauri/icons/icon.png
--------------------------------------------------------------------------------
/src-tauri/icons/32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ElKornacio/qyp-mini/HEAD/src-tauri/icons/32x32.png
--------------------------------------------------------------------------------
/src-tauri/icons/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ElKornacio/qyp-mini/HEAD/src-tauri/icons/icon.icns
--------------------------------------------------------------------------------
/src-tauri/icons/128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ElKornacio/qyp-mini/HEAD/src-tauri/icons/128x128.png
--------------------------------------------------------------------------------
/src-tauri/icons/StoreLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ElKornacio/qyp-mini/HEAD/src-tauri/icons/StoreLogo.png
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
3 | }
4 |
--------------------------------------------------------------------------------
/src-tauri/icons/128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ElKornacio/qyp-mini/HEAD/src-tauri/icons/128x128@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square30x30Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ElKornacio/qyp-mini/HEAD/src-tauri/icons/Square30x30Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square44x44Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ElKornacio/qyp-mini/HEAD/src-tauri/icons/Square44x44Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square71x71Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ElKornacio/qyp-mini/HEAD/src-tauri/icons/Square71x71Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square89x89Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ElKornacio/qyp-mini/HEAD/src-tauri/icons/Square89x89Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square107x107Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ElKornacio/qyp-mini/HEAD/src-tauri/icons/Square107x107Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square142x142Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ElKornacio/qyp-mini/HEAD/src-tauri/icons/Square142x142Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square150x150Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ElKornacio/qyp-mini/HEAD/src-tauri/icons/Square150x150Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square284x284Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ElKornacio/qyp-mini/HEAD/src-tauri/icons/Square284x284Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square310x310Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ElKornacio/qyp-mini/HEAD/src-tauri/icons/Square310x310Logo.png
--------------------------------------------------------------------------------
/.supercode/def.json:
--------------------------------------------------------------------------------
1 | {
2 | "tags": ["react", "typescript", "nodejs", "rust", "tauri", "vite", "tailwindcss", "pnpm", "shadcn/ui"]
3 | }
4 |
--------------------------------------------------------------------------------
/src-back/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["src"],
3 | "ext": "ts,js,json",
4 | "ignore": ["src/**/*.spec.ts"],
5 | "exec": "ts-node src/index.ts"
6 | }
7 |
--------------------------------------------------------------------------------
/src/pages/PlaygroundPage.tsx:
--------------------------------------------------------------------------------
1 | export function PlaygroundPage() {
2 | //
3 |
4 | return
none
;
5 | }
6 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/src-tauri/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated by Cargo
2 | # will have compiled files and executables
3 | /target/
4 |
5 | # Generated by Tauri
6 | # will have schema files for capabilities auto-completion
7 | /gen/schemas
8 |
--------------------------------------------------------------------------------
/src-tauri/src/main.rs:
--------------------------------------------------------------------------------
1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!!
2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
3 |
4 | fn main() {
5 | qyp_mini_lib::run()
6 | }
7 |
--------------------------------------------------------------------------------
/src/pipeline/ValidationStage.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Здесь мы будем гонять линтеры, typechecking, etc.
3 | * Чтобы на раннем этапе выявлять ошибки и сообщать о них агенту.
4 | */
5 | export class ValidationStage {
6 | constructor() {}
7 | }
8 |
--------------------------------------------------------------------------------
/src/pages/WidgetsLibrary/index.ts:
--------------------------------------------------------------------------------
1 | export { WidgetsSidebar } from './WidgetsSidebar';
2 | export { WidgetPreview } from './WidgetPreview';
3 | export { CodeEditor } from './CodeEditor';
4 | export { WidgetEditor } from './WidgetEditor';
5 |
--------------------------------------------------------------------------------
/src/lib/dialogs.ts:
--------------------------------------------------------------------------------
1 | import { platform } from '@/platform';
2 |
3 | export const confirmDialog = async (message: string) => {
4 | return await platform.dialogs.confirm(message, {
5 | title: 'Подтверждение',
6 | kind: 'warning',
7 | });
8 | };
9 |
--------------------------------------------------------------------------------
/__legacy/src-node/src/ipc/index.ts:
--------------------------------------------------------------------------------
1 | export { IPCProtocol } from './protocol.js';
2 | export { CommandHandler, type CommandProcessor } from './command-handler.js';
3 | export { BaseRequest, BaseSuccessResponse, BaseErrorResponse, BaseResponse } from './types.js';
4 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true
8 | },
9 | "include": ["vite.config.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/src/pages/index.ts:
--------------------------------------------------------------------------------
1 | export { WelcomePage } from './WelcomePage';
2 | export { PlaygroundPage } from './PlaygroundPage';
3 | export { WidgetsLibraryPage } from './WidgetsLibraryPage';
4 | export { DatabasePage } from './DatabasePage';
5 | export { DashboardPage } from './DashboardPage';
6 |
--------------------------------------------------------------------------------
/src/virtual-fs/shadcn-ui.ts:
--------------------------------------------------------------------------------
1 | import { VirtualFS } from '@/virtual-fs/VirtualFS';
2 |
3 | export const buildShadcnUiFS = async (vfs: VirtualFS): Promise => {
4 | vfs.writeFile('/src/components/ui/button.tsx', `// nothing here for now`, {
5 | externalized: true,
6 | });
7 | };
8 |
--------------------------------------------------------------------------------
/src/platform/tauri/index.ts:
--------------------------------------------------------------------------------
1 | export { TauriWindowManager } from './window';
2 | export { TauriStorageManager } from './storage';
3 | export { TauriDialogManager } from './dialogs';
4 | export { TauriDatabaseConnection, TauriDatabaseManager } from './database';
5 | export { TauriPlatform } from './platform';
6 |
--------------------------------------------------------------------------------
/src/pipeline/modules-mocks/mockShadcnUiModules.tsx:
--------------------------------------------------------------------------------
1 | import * as ButtonModule from '@/components/ui/button';
2 |
3 | export const tryToMockShadcnUiModules = (_context: any, path: string) => {
4 | if (path === '@/components/ui/button') {
5 | return ButtonModule;
6 | }
7 |
8 | return null;
9 | };
10 |
--------------------------------------------------------------------------------
/src/platform/browser/index.ts:
--------------------------------------------------------------------------------
1 | export { BrowserWindowManager } from './window';
2 | export { BrowserStorageManager } from './storage';
3 | export { BrowserDialogManager } from './dialogs';
4 | export { BrowserDatabaseConnection, BrowserDatabaseManager } from './database';
5 | export { BrowserPlatform } from './platform';
6 |
--------------------------------------------------------------------------------
/src/pipeline/modules-mocks/mockGlobalModules.tsx:
--------------------------------------------------------------------------------
1 | import * as ReactRuntime from 'react';
2 | import * as ReactJSXRuntime from 'react/jsx-runtime';
3 |
4 | export const tryToMockGlobalModule = (_context: any, path: string) => {
5 | if (path === 'react') {
6 | return ReactRuntime;
7 | } else if (path === 'react/jsx-runtime') {
8 | return ReactJSXRuntime;
9 | }
10 |
11 | return null;
12 | };
13 |
--------------------------------------------------------------------------------
/src/platform/abstract/index.ts:
--------------------------------------------------------------------------------
1 | export type { WindowManager, WindowSize } from './window';
2 | export type { StorageManager, DirectoryEntry } from './storage';
3 | export type { DialogManager, ConfirmOptions, AlertOptions, MessageBoxOptions } from './dialogs';
4 | export { DatabaseConnection, type DatabaseManager, type ConnectionStatus } from './database';
5 | export type { PlatformManager } from './platform';
6 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 120,
3 | "tabWidth": 4,
4 | "useTabs": true,
5 | "semi": true,
6 | "singleQuote": true,
7 | "trailingComma": "all",
8 | "bracketSpacing": true,
9 | "jsxBracketSameLine": false,
10 | "arrowParens": "avoid",
11 | "quoteProps": "consistent",
12 | "proseWrap": "never",
13 | "jsxSingleQuote": false,
14 | "htmlWhitespaceSensitivity": "strict",
15 | "endOfLine": "lf"
16 | }
17 |
--------------------------------------------------------------------------------
/src-back/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 120,
3 | "tabWidth": 4,
4 | "useTabs": true,
5 | "semi": true,
6 | "singleQuote": true,
7 | "trailingComma": "all",
8 | "bracketSpacing": true,
9 | "jsxBracketSameLine": false,
10 | "arrowParens": "avoid",
11 | "quoteProps": "consistent",
12 | "proseWrap": "never",
13 | "jsxSingleQuote": false,
14 | "htmlWhitespaceSensitivity": "strict",
15 | "endOfLine": "lf"
16 | }
17 |
--------------------------------------------------------------------------------
/src/pages/WidgetsLibraryPage.tsx:
--------------------------------------------------------------------------------
1 | import { observer } from 'mobx-react';
2 | import { WidgetsSidebar, WidgetEditor } from './WidgetsLibrary';
3 |
4 | export const WidgetsLibraryPage = observer(() => {
5 | return (
6 |
7 | {/* Левый сайдбар со списком виджетов */}
8 |
9 |
10 | {/* Основная область редактирования */}
11 |
12 |
13 | );
14 | });
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
26 | /__temp
27 |
28 | src-tauri/binaries/*
29 |
30 | .env.wrangler
31 | .wrangler
--------------------------------------------------------------------------------
/__legacy/src-node/src/ipc/types.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Базовый интерфейс для запроса
3 | */
4 | export interface BaseRequest {
5 | command: string;
6 | }
7 |
8 | export interface BaseSuccessResponse {
9 | status: 'success';
10 | result: T;
11 | }
12 |
13 | export interface BaseErrorResponse {
14 | status: 'error';
15 | error: string;
16 | stack?: string;
17 | }
18 |
19 | export type BaseResponse = BaseSuccessResponse | BaseErrorResponse;
20 |
--------------------------------------------------------------------------------
/__legacy/src-node/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "module": "commonjs",
5 | "declaration": true,
6 | "outDir": "./dist",
7 | "sourceMap": true,
8 | "esModuleInterop": true,
9 | "experimentalDecorators": true,
10 | "emitDecoratorMetadata": true,
11 | "strict": true,
12 | "strictNullChecks": true,
13 | "skipLibCheck": true
14 | },
15 | "include": ["./src", "../src/virtual-fs/VirtualFS.ts", "../src/virtual-fs/types.ts"]
16 | }
17 |
--------------------------------------------------------------------------------
/__legacy/src-node/scripts/build.js:
--------------------------------------------------------------------------------
1 | const { execSync } = require('child_process');
2 | const fs = require('fs');
3 |
4 | const ext = process.platform === 'win32' ? '.exe' : '';
5 |
6 | const appName = process.argv[2];
7 |
8 | const rustInfo = execSync('rustc -vV');
9 | const targetTriple = /host: (\S+)/g.exec(rustInfo)[1];
10 | if (!targetTriple) {
11 | console.error('Failed to determine platform target triple');
12 | }
13 | fs.renameSync(`${appName}${ext}`, `../src-tauri/binaries/${appName}-${targetTriple}${ext}`);
14 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": false,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "",
8 | "css": "src/index.css",
9 | "baseColor": "zinc",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
--------------------------------------------------------------------------------
/src/pipeline/modules-mocks/mockUtilsModule.ts:
--------------------------------------------------------------------------------
1 | import { WidgetRuntimeContext } from '@/types/dashboard';
2 |
3 | export const tryToMockUtilsModule = (context: any, path: string) => {
4 | if (path === '@/lib/utils') {
5 | const runtimeContext = context as WidgetRuntimeContext;
6 |
7 | return {
8 | runSql: async (query: string): Promise => {
9 | const result = await runtimeContext.database.connection.select(query);
10 | return result as T;
11 | },
12 | };
13 | }
14 |
15 | return null;
16 | };
17 |
--------------------------------------------------------------------------------
/src/components/PageHeader.tsx:
--------------------------------------------------------------------------------
1 | export const PageHeader = ({
2 | title,
3 | subtitle,
4 | children,
5 | }: {
6 | title: string;
7 | subtitle: string;
8 | children: React.ReactNode;
9 | }) => {
10 | return (
11 |
12 |
13 |
{title}
14 |
{subtitle}
15 |
16 | {children}
17 |
18 | );
19 | };
20 |
--------------------------------------------------------------------------------
/src-back/src/middlewares/errorHandler.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response, NextFunction } from 'express';
2 | import { ErrorResponse } from '../types';
3 |
4 | export const errorHandler = (err: Error, req: Request, res: Response, next: NextFunction): void => {
5 | console.error('Error:', err);
6 |
7 | const errorResponse: ErrorResponse = {
8 | success: false,
9 | error: process.env.NODE_ENV === 'production' ? 'Internal server error' : err.message,
10 | timestamp: new Date().toISOString(),
11 | };
12 |
13 | res.status(500).json(errorResponse);
14 | };
15 |
--------------------------------------------------------------------------------
/__legacy/src-node/src/types/index.ts:
--------------------------------------------------------------------------------
1 | import { BaseRequest } from '../ipc/index.js';
2 | import { SerializedVirtualNode } from '../../../src/virtual-fs/types.js';
3 |
4 | export interface PingRequest extends BaseRequest {
5 | message: string;
6 | }
7 |
8 | export interface PingResponse {
9 | message: string;
10 | }
11 |
12 | export interface CompileComponentRequest extends BaseRequest {
13 | serializedVFS: SerializedVirtualNode[];
14 | entryPoint: string;
15 | }
16 |
17 | export interface CompileComponentResponse {
18 | jsBundle: string;
19 | cssBundle: string;
20 | }
21 |
--------------------------------------------------------------------------------
/__legacy/src-node/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Безопасно парсит JSON с обработкой ошибок
3 | */
4 | export function safeJsonParse(json: string): T | null {
5 | try {
6 | return JSON.parse(json) as T;
7 | } catch {
8 | return null;
9 | }
10 | }
11 |
12 | /**
13 | * Создает ошибку с дополнительной информацией
14 | */
15 | export function createError(message: string, originalError?: unknown): Error {
16 | const error = new Error(message);
17 |
18 | if (originalError instanceof Error) {
19 | error.stack = `${error.stack}\nCaused by: ${originalError.stack}`;
20 | }
21 |
22 | return error;
23 | }
24 |
--------------------------------------------------------------------------------
/src-back/src/types/index.ts:
--------------------------------------------------------------------------------
1 | export interface DatabaseCredentials {
2 | host: string;
3 | port: number;
4 | username: string;
5 | password: string;
6 | database: string;
7 | type: 'postgres' | 'mysql' | 'sqlite';
8 | ssl?: boolean;
9 | }
10 |
11 | export interface SqlRequest {
12 | credentials: DatabaseCredentials;
13 | query: string;
14 | parameters?: any[];
15 | }
16 |
17 | export interface SqlResponse {
18 | success: boolean;
19 | data?: any[];
20 | error?: string;
21 | executionTime?: number;
22 | rowCount?: number;
23 | }
24 |
25 | export interface ErrorResponse {
26 | success: false;
27 | error: string;
28 | timestamp: string;
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/ErrorBoundary.tsx:
--------------------------------------------------------------------------------
1 | import { makeObservable, observable } from 'mobx';
2 | import * as React from 'react';
3 |
4 | export class ErrorBoundary extends React.Component<{
5 | fallback: (error: any) => React.ReactNode;
6 | children: React.ReactNode;
7 | }> {
8 | @observable error = null;
9 |
10 | constructor(props: any) {
11 | super(props);
12 |
13 | makeObservable(this);
14 | }
15 |
16 | componentDidCatch(error: any, _info: any) {
17 | this.error = error;
18 | }
19 |
20 | render() {
21 | if (this.error) {
22 | // You can render any custom fallback UI
23 | return this.props.fallback(this.error);
24 | }
25 |
26 | return this.props.children;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/platform/tauri/platform.ts:
--------------------------------------------------------------------------------
1 | import { PlatformManager } from '../abstract/platform';
2 | import { TauriWindowManager } from './window';
3 | import { TauriStorageManager } from './storage';
4 | import { TauriDialogManager } from './dialogs';
5 | import { TauriDatabaseManager } from './database';
6 |
7 | /**
8 | * Tauri-реализация платформы
9 | */
10 | export class TauriPlatform implements PlatformManager {
11 | readonly type = 'tauri' as const;
12 |
13 | public readonly window = new TauriWindowManager();
14 | public readonly storage = new TauriStorageManager();
15 | public readonly dialogs = new TauriDialogManager();
16 | public readonly database = new TauriDatabaseManager();
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as LabelPrimitive from "@radix-ui/react-label"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | function Label({
7 | className,
8 | ...props
9 | }: React.ComponentProps) {
10 | return (
11 |
19 | )
20 | }
21 |
22 | export { Label }
23 |
--------------------------------------------------------------------------------
/src-tauri/tauri.conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://schema.tauri.app/config/2",
3 | "productName": "qyp-mini",
4 | "version": "0.1.0",
5 | "identifier": "ai.qyp.mini.app",
6 | "build": {
7 | "beforeDevCommand": "pnpm dev",
8 | "devUrl": "http://localhost:1420",
9 | "beforeBuildCommand": "pnpm build",
10 | "frontendDist": "../dist"
11 | },
12 | "app": {
13 | "windows": [
14 | {
15 | "title": "qyp-mini",
16 | "width": 800,
17 | "height": 600
18 | }
19 | ],
20 | "security": {
21 | "csp": null
22 | }
23 | },
24 | "bundle": {
25 | "active": true,
26 | "targets": "all",
27 | "icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"]
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/platform/abstract/platform.ts:
--------------------------------------------------------------------------------
1 | import { WindowManager } from './window';
2 | import { StorageManager } from './storage';
3 | import { DialogManager } from './dialogs';
4 | import { DatabaseManager } from './database';
5 |
6 | /**
7 | * Основной интерфейс платформы, объединяющий все менеджеры
8 | */
9 | export interface PlatformManager {
10 | /**
11 | * Менеджер управления окном
12 | */
13 | window: WindowManager;
14 |
15 | /**
16 | * Менеджер файловой системы
17 | */
18 | storage: StorageManager;
19 |
20 | /**
21 | * Менеджер диалогов
22 | */
23 | dialogs: DialogManager;
24 |
25 | /**
26 | * Менеджер подключений к БД
27 | */
28 | database: DatabaseManager;
29 |
30 | /**
31 | * Тип платформы
32 | */
33 | readonly type: 'tauri' | 'browser';
34 | }
35 |
--------------------------------------------------------------------------------
/src-back/src/middlewares/rateLimiter.ts:
--------------------------------------------------------------------------------
1 | import rateLimit from 'express-rate-limit';
2 |
3 | export const sqlRateLimit = rateLimit({
4 | windowMs: 1 * 60 * 1000, // 1 minute
5 | max: 10, // Maximum 10 requests per minute per IP
6 | message: {
7 | success: false,
8 | error: 'Too many SQL requests from this IP, please try again later.',
9 | timestamp: new Date().toISOString(),
10 | },
11 | standardHeaders: true,
12 | legacyHeaders: false,
13 | });
14 |
15 | export const generalRateLimit = rateLimit({
16 | windowMs: 1 * 60 * 1000, // 1 minute
17 | max: 100, // Maximum 100 requests per minute per IP
18 | message: {
19 | success: false,
20 | error: 'Too many requests from this IP, please try again later.',
21 | timestamp: new Date().toISOString(),
22 | },
23 | standardHeaders: true,
24 | legacyHeaders: false,
25 | });
26 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "experimentalDecorators": true,
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "noEmit": true,
16 | "jsx": "react-jsx",
17 |
18 | "baseUrl": ".",
19 |
20 | "paths": {
21 | "@/*": ["./src/*"]
22 | },
23 |
24 | /* Linting */
25 | "strict": true,
26 | "noUnusedLocals": true,
27 | "noUnusedParameters": true,
28 | "noFallthroughCasesInSwitch": true
29 | },
30 | "include": ["src", "src/virtual-fs/VirtualFS.ts"],
31 | "references": [{ "path": "./tsconfig.node.json" }]
32 | }
33 |
--------------------------------------------------------------------------------
/src-tauri/src/lib.rs:
--------------------------------------------------------------------------------
1 | // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
2 | #[tauri::command]
3 | fn greet(name: &str) -> String {
4 | format!("Hello, {}! You've been greeted from Rust!", name)
5 | }
6 |
7 | #[cfg_attr(mobile, tauri::mobile_entry_point)]
8 | pub fn run() {
9 | tauri::Builder::default()
10 | .plugin(tauri_plugin_stronghold::Builder::new(|pass| todo!()).build())
11 | .plugin(tauri_plugin_fs::init())
12 | .plugin(tauri_plugin_dialog::init())
13 | .plugin(tauri_plugin_sql::Builder::new().build())
14 | .plugin(tauri_plugin_shell::init())
15 | .plugin(tauri_plugin_opener::init())
16 | .invoke_handler(tauri::generate_handler![greet])
17 | .run(tauri::generate_context!())
18 | .expect("error while running tauri application");
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
6 | return (
7 |
15 | )
16 | }
17 |
18 | export { Textarea }
19 |
--------------------------------------------------------------------------------
/src-back/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "module": "commonjs",
5 | "lib": ["ES2020"],
6 | "outDir": "./dist",
7 | "rootDir": "./src",
8 | "strict": true,
9 | "esModuleInterop": true,
10 | "skipLibCheck": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "experimentalDecorators": true,
13 | "emitDecoratorMetadata": true,
14 | "resolveJsonModule": true,
15 | "moduleResolution": "node",
16 | "sourceMap": true,
17 | "declaration": true,
18 | "removeComments": true,
19 | "noImplicitAny": true,
20 | "strictNullChecks": true,
21 | "strictFunctionTypes": true,
22 | "noImplicitThis": true,
23 | "noImplicitReturns": true,
24 | "noFallthroughCasesInSwitch": true,
25 | "noUncheckedIndexedAccess": true
26 | },
27 | "include": ["src/**/*"],
28 | "exclude": ["node_modules", "dist"]
29 | }
30 |
--------------------------------------------------------------------------------
/src-tauri/capabilities/default.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../gen/schemas/desktop-schema.json",
3 | "identifier": "default",
4 | "description": "Capability for the main window",
5 | "windows": ["main"],
6 | "permissions": [
7 | "core:default",
8 | "core:window:default",
9 | "core:window:allow-set-size",
10 | "core:window:allow-center",
11 | "opener:default",
12 | "shell:allow-stdin-write",
13 | "shell:default",
14 | "sql:default",
15 | "sql:allow-execute",
16 | "sql:allow-close",
17 | "sql:allow-load",
18 | "dialog:default",
19 | "dialog:allow-confirm",
20 | "fs:default",
21 | "stronghold:default",
22 | {
23 | "identifier": "fs:scope",
24 | "allow": [{ "path": "$APPDATA" }, { "path": "$APPDATA/**/*" }]
25 | },
26 | {
27 | "identifier": "fs:allow-app-write-recursive",
28 | "allow": [{ "path": "$APPDATA" }, { "path": "$APPDATA/**/*" }]
29 | }
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------
/src/platform/abstract/window.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Абстрактный интерфейс для управления окном приложения
3 | */
4 | export interface WindowSize {
5 | width: number;
6 | height: number;
7 | }
8 |
9 | export interface WindowManager {
10 | /**
11 | * Устанавливает размер окна
12 | */
13 | setSize(width: number, height: number): Promise;
14 |
15 | /**
16 | * Получает текущий размер окна
17 | */
18 | getSize(): Promise;
19 |
20 | /**
21 | * Минимизирует окно
22 | */
23 | minimize(): Promise;
24 |
25 | /**
26 | * Максимизирует окно
27 | */
28 | maximize(): Promise;
29 |
30 | /**
31 | * Закрывает окно
32 | */
33 | close(): Promise;
34 |
35 | /**
36 | * Проверяет, максимизировано ли окно
37 | */
38 | isMaximized(): Promise;
39 |
40 | /**
41 | * Устанавливает заголовок окна
42 | */
43 | setTitle(title: string): Promise;
44 | }
45 |
--------------------------------------------------------------------------------
/src/types/database.ts:
--------------------------------------------------------------------------------
1 | import { DatabaseConnection } from '@/platform/abstract/database';
2 |
3 | /**
4 | * Интерфейс для подключения к базе данных
5 | */
6 | export interface DatabaseCredentials {
7 | host: string;
8 | port: number;
9 | database: string;
10 | username: string;
11 | password: string;
12 | type: 'postgres' | 'mysql';
13 | ssl?: boolean;
14 | }
15 |
16 | /**
17 | * Сущность базы данных
18 | */
19 | export interface Database {
20 | id: string;
21 | name: string;
22 | credentials: DatabaseCredentials;
23 | connection: DatabaseConnection;
24 | }
25 |
26 | /**
27 | * Опции для создания нового подключения к БД
28 | */
29 | export interface CreateDatabaseOptions {
30 | name: string;
31 | credentials: DatabaseCredentials;
32 | }
33 |
34 | /**
35 | * Опции для обновления БД
36 | */
37 | export interface UpdateDatabaseOptions {
38 | name?: string;
39 | credentials?: Partial;
40 | }
41 |
--------------------------------------------------------------------------------
/src/virtual-fs/types.ts:
--------------------------------------------------------------------------------
1 | export interface VirtualFileMetadata {
2 | readonly?: boolean; // индикатор что ИИ можно только читать
3 | externalized?: boolean; // индикатор, что этот файл не нужен для компиляции - он будет подключен в рантайме
4 | }
5 |
6 | export interface VirtualDirectoryMetadata {
7 | readonly?: boolean; // индикатор что ИИ можно только читать файлы из этой директории
8 | }
9 |
10 | export interface VirtualFile {
11 | type: 'file';
12 | /** Имя файла */
13 | name: string;
14 | /** Содержимое файла */
15 | content: string;
16 | /** Метаданные файла */
17 | metadata: VirtualFileMetadata;
18 | }
19 |
20 | export interface VirtualDirectory {
21 | type: 'directory';
22 | /** Имя директории */
23 | name: string;
24 | /** Метаданные директории */
25 | metadata: VirtualDirectoryMetadata;
26 | }
27 |
28 | export type VirtualNode = VirtualDirectory | VirtualFile;
29 |
30 | export type SerializedVirtualNode = {
31 | path: string;
32 | } & VirtualNode;
33 |
--------------------------------------------------------------------------------
/__legacy/src-node/src/request-handler/index.ts:
--------------------------------------------------------------------------------
1 | import { CompileComponentRequest, CompileComponentResponse } from '../types/index.js';
2 | import { createError } from '../utils/index.js';
3 | import { Compiler } from '../compiler/index.js';
4 |
5 | /**
6 | * Обработчик команды компиляции
7 | */
8 | export class CompileCommandHandler {
9 | private compiler: Compiler;
10 |
11 | constructor() {
12 | this.compiler = new Compiler();
13 | }
14 |
15 | /**
16 | * Обрабатывает запрос на компиляцию
17 | */
18 | async handle(request: CompileComponentRequest): Promise {
19 | try {
20 | if (request.serializedVFS.length === 0) {
21 | throw createError('Массив файлов пуст');
22 | }
23 |
24 | // Компиляция
25 | const result = await this.compiler.compile(request.serializedVFS, request.entryPoint);
26 |
27 | return result;
28 | } catch (error) {
29 | throw createError('Ошибка обработки запроса компиляции', error);
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src-tauri/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "qyp-mini"
3 | version = "0.1.0"
4 | description = "A Tauri App"
5 | authors = ["you"]
6 | edition = "2021"
7 |
8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
9 |
10 | [lib]
11 | # The `_lib` suffix may seem redundant but it is necessary
12 | # to make the lib name unique and wouldn't conflict with the bin name.
13 | # This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
14 | name = "qyp_mini_lib"
15 | crate-type = ["staticlib", "cdylib", "rlib"]
16 |
17 | [build-dependencies]
18 | tauri-build = { version = "2", features = [] }
19 |
20 | [dependencies]
21 | tauri = { version = "2", features = [] }
22 | tauri-plugin-opener = "2"
23 | serde = { version = "1", features = ["derive"] }
24 | serde_json = "1"
25 | tauri-plugin-shell = "2"
26 | tauri-plugin-sql = { version = "2", features = ["postgres"] }
27 | tauri-plugin-dialog = "2"
28 | tauri-plugin-fs = "2"
29 | tauri-plugin-stronghold = "2"
30 |
31 |
--------------------------------------------------------------------------------
/src-back/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "src-back",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "dist/index.js",
6 | "scripts": {
7 | "build": "tsc",
8 | "start": "node dist/index.js",
9 | "dev": "nodemon",
10 | "test": "echo \"Error: no test specified\" && exit 1",
11 | "format": "prettier --write \"src/**/*.ts\"",
12 | "format:check": "prettier --check \"src/**/*.ts\""
13 | },
14 | "keywords": [],
15 | "author": "",
16 | "license": "ISC",
17 | "type": "commonjs",
18 | "dependencies": {
19 | "cors": "^2.8.5",
20 | "dotenv": "^17.2.1",
21 | "express": "^5.1.0",
22 | "express-rate-limit": "^8.0.1",
23 | "mysql2": "^3.14.2",
24 | "pg": "^8.16.3",
25 | "reflect-metadata": "^0.2.2",
26 | "sqlite3": "^5.1.7",
27 | "typeorm": "^0.3.25"
28 | },
29 | "devDependencies": {
30 | "@types/cors": "^2.8.19",
31 | "@types/express": "^5.0.3",
32 | "@types/node": "^24.1.0",
33 | "nodemon": "^3.1.10",
34 | "prettier": "^3.6.2",
35 | "ts-node": "^10.9.2",
36 | "typescript": "^5.8.3"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) {
6 | return (
7 |
18 | )
19 | }
20 |
21 | export { Input }
22 |
--------------------------------------------------------------------------------
/src/platform/index.ts:
--------------------------------------------------------------------------------
1 | // Экспорт типов
2 | export * from './abstract';
3 |
4 | // Экспорт конкретных реализаций
5 | export * from './tauri';
6 | export * from './browser';
7 |
8 | // Импорт платформ для автоматической детекции
9 | import { TauriPlatform } from './tauri';
10 | import { BrowserPlatform } from './browser';
11 | import { PlatformManager } from './abstract';
12 |
13 | /**
14 | * Автоматическое определение и создание подходящей платформы
15 | */
16 | function createPlatform(): PlatformManager {
17 | // Проверяем наличие Tauri API
18 | if (typeof window !== 'undefined' && (window as any).__TAURI_INTERNALS__) {
19 | const tauriPlatform = new TauriPlatform();
20 | console.log('🖥️ Используется Tauri платформа');
21 | return tauriPlatform;
22 | } else {
23 | const browserPlatform = new BrowserPlatform();
24 | console.log('🌐 Используется Browser платформа');
25 | return browserPlatform;
26 | }
27 | }
28 |
29 | /**
30 | * Глобальный экземпляр платформы
31 | * Автоматически определяется при импорте
32 | */
33 | export const platform = createPlatform();
34 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import ReactDOM from 'react-dom/client';
2 |
3 | import './index.css';
4 |
5 | import App from './App';
6 | import { platform } from './platform';
7 | import { configure } from 'mobx';
8 | import { widgetsRepositoryStore } from './stores/WidgetsRepository';
9 | import { dashboardsRepository } from './stores/DashboardsRepository';
10 | import { databaseStore } from './stores/DatabaseStore';
11 |
12 | // Инициализируем платформу и устанавливаем размер окна
13 | async function initApp() {
14 | // Устанавливаем размер окна (работает только в Tauri)
15 | try {
16 | await platform.window.setSize(1400, 800);
17 | } catch (error) {
18 | // В браузере это может не работать, это нормально
19 | console.log('Размер окна не может быть установлен на данной платформе');
20 | }
21 |
22 | await databaseStore.loadDatabases();
23 | await widgetsRepositoryStore.loadWidgets();
24 | await dashboardsRepository.loadDashboards();
25 | }
26 |
27 | configure({
28 | enforceActions: 'never',
29 | });
30 |
31 | // Запускаем приложение
32 | initApp();
33 |
34 | ReactDOM.createRoot(document.getElementById('root')!).render();
35 |
--------------------------------------------------------------------------------
/src/platform/tauri/window.ts:
--------------------------------------------------------------------------------
1 | import { WindowManager, WindowSize } from '../abstract/window';
2 | import { getCurrentWindow, LogicalSize } from '@tauri-apps/api/window';
3 |
4 | /**
5 | * Tauri-реализация менеджера окна
6 | */
7 | export class TauriWindowManager implements WindowManager {
8 | private window = getCurrentWindow();
9 |
10 | async setSize(width: number, height: number): Promise {
11 | await this.window.setSize(new LogicalSize(width, height));
12 | }
13 |
14 | async getSize(): Promise {
15 | const size = await this.window.innerSize();
16 | return {
17 | width: size.width,
18 | height: size.height,
19 | };
20 | }
21 |
22 | async minimize(): Promise {
23 | await this.window.minimize();
24 | }
25 |
26 | async maximize(): Promise {
27 | await this.window.maximize();
28 | }
29 |
30 | async close(): Promise {
31 | await this.window.close();
32 | }
33 |
34 | async isMaximized(): Promise {
35 | return await this.window.isMaximized();
36 | }
37 |
38 | async setTitle(title: string): Promise {
39 | await this.window.setTitle(title);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/LoadingState.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { RefreshCw } from 'lucide-react';
3 |
4 | interface LoadingStateProps {
5 | message?: string;
6 | size?: 'sm' | 'md' | 'lg';
7 | fullScreen?: boolean;
8 | }
9 |
10 | /**
11 | * Компонент для отображения состояния загрузки
12 | */
13 | export const LoadingState: React.FC = ({
14 | message = 'Загрузка...',
15 | size = 'md',
16 | fullScreen = false,
17 | }) => {
18 | const sizeClasses = {
19 | sm: 'h-4 w-4',
20 | md: 'h-8 w-8',
21 | lg: 'h-12 w-12',
22 | };
23 |
24 | const content = (
25 |
26 |
27 | {message && (
28 |
29 | {message}
30 |
31 | )}
32 |
33 | );
34 |
35 | if (fullScreen) {
36 | return (
37 |
38 | {content}
39 |
40 | );
41 | }
42 |
43 | return content;
44 | };
45 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | qYp mini
8 |
9 |
10 |
11 |
12 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/platform/browser/platform.ts:
--------------------------------------------------------------------------------
1 | import { PlatformManager } from '../abstract/platform';
2 | import { BrowserWindowManager } from './window';
3 | import { BrowserStorageManager } from './storage';
4 | import { BrowserDialogManager } from './dialogs';
5 | import { BrowserDatabaseManager } from './database';
6 |
7 | /**
8 | * Опции для конфигурации Browser платформы
9 | */
10 | export interface BrowserPlatformOptions {
11 | proxyUrl?: string;
12 | }
13 |
14 | /**
15 | * Browser-реализация платформы
16 | */
17 | export class BrowserPlatform implements PlatformManager {
18 | readonly type = 'browser' as const;
19 |
20 | public readonly window = new BrowserWindowManager();
21 | public readonly storage = new BrowserStorageManager();
22 | public readonly dialogs = new BrowserDialogManager();
23 | public readonly database: BrowserDatabaseManager;
24 |
25 | constructor(options: BrowserPlatformOptions = {}) {
26 | // Используем переданный URL или переменную окружения, или дефолтный URL
27 | const proxyUrl = options.proxyUrl || import.meta.env.VITE_DATABASE_PROXY_URL || 'https://proxy.qyp.ai';
28 |
29 | this.database = new BrowserDatabaseManager(proxyUrl);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/__legacy/SidecarEncoder.ts:
--------------------------------------------------------------------------------
1 | import { SmartBuffer } from '@tiny-utils/bytes';
2 | import { BaseRequest, BaseResponse } from '../../src-node/src/ipc/types';
3 |
4 | /**
5 | * Утилиты для кодирования/декодирования запросов и ответов sidecar
6 | */
7 | export class SidecarEncoder {
8 | /**
9 | * Кодирует запрос в формат base64+\n для отправки в sidecar
10 | * @param request - JSON объект запроса
11 | * @returns Закодированная строка
12 | */
13 | static encodeRequest(request: BaseRequest): string {
14 | const jsonString = JSON.stringify(request);
15 | const base64String = SmartBuffer.ofUTF8String(jsonString).toBase64String();
16 | return base64String + '\n';
17 | }
18 |
19 | /**
20 | * Декодирует ответ от sidecar из формата base64
21 | * @param base64Response - base64 строка ответа
22 | * @returns Декодированный JSON объект
23 | */
24 | static decodeResponse(base64Response: string): BaseResponse {
25 | try {
26 | const jsonString = SmartBuffer.ofBase64String(base64Response.trim()).toUTF8String();
27 | return JSON.parse(jsonString);
28 | } catch (error) {
29 | throw new Error(`Ошибка декодирования ответа от sidecar: ${error}`);
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/components/EmptyState.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button } from '@/components/ui/button';
3 | import { LucideIcon } from 'lucide-react';
4 |
5 | interface EmptyStateProps {
6 | icon: LucideIcon;
7 | title: string;
8 | description?: string;
9 | action?: {
10 | label: string;
11 | onClick: () => void;
12 | variant?: 'default' | 'outline' | 'secondary';
13 | };
14 | className?: string;
15 | }
16 |
17 | /**
18 | * Компонент для отображения пустого состояния
19 | * Используется когда нет данных для отображения
20 | */
21 | export const EmptyState: React.FC = ({ icon: Icon, title, description, action, className = '' }) => {
22 | return (
23 |
24 |
25 |
{title}
26 | {description &&
{description}
}
27 | {action && (
28 |
31 | )}
32 |
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import react from '@vitejs/plugin-react-swc';
3 | import tsconfigPaths from 'vite-tsconfig-paths';
4 | import svgr from 'vite-plugin-svgr';
5 | import tailwindcss from '@tailwindcss/vite';
6 | import { nodePolyfills } from 'vite-plugin-node-polyfills';
7 |
8 | const host = process.env.TAURI_DEV_HOST;
9 |
10 | // https://vitejs.dev/config/
11 | export default defineConfig(async () => ({
12 | plugins: [
13 | react({
14 | tsDecorators: true,
15 | }),
16 | tsconfigPaths(),
17 | svgr(),
18 | tailwindcss(),
19 | nodePolyfills(),
20 | ],
21 |
22 | // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
23 | //
24 | // 1. prevent vite from obscuring rust errors
25 | clearScreen: false,
26 | // 2. tauri expects a fixed port, fail if that port is not available
27 | server: {
28 | port: 1420,
29 | strictPort: true,
30 | host: host || false,
31 | hmr: host
32 | ? {
33 | protocol: 'ws',
34 | host,
35 | port: 1421,
36 | }
37 | : undefined,
38 | watch: {
39 | // 3. tell vite to ignore watching `src-tauri`
40 | ignored: ['**/src-tauri/**'],
41 | },
42 | },
43 | }));
44 |
--------------------------------------------------------------------------------
/__legacy/src-node/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "qyp-mini-node-backend",
3 | "version": "0.1.0",
4 | "description": "Node.js sidecar для qYp-mini",
5 | "type": "commonjs",
6 | "main": "dist/index.js",
7 | "scripts": {
8 | "build-code": "rm -rf ./dist && tsc",
9 | "build-binary": "pnpm run build-code && pkg dist/index.js --output qyp-mini-node-backend",
10 | "build-test": "pnpm run build-code && pkg dist/test.js --output qyp-mini-node-test-backend",
11 | "package-binary": "node scripts/build.js qyp-mini-node-backend",
12 | "build": "pnpm run build-binary && pnpm run package-binary",
13 | "test": "tsx src/test.ts",
14 | "pkg-test": "pnpm run build-test && ./qyp-mini-node-test-backend"
15 | },
16 | "devDependencies": {
17 | "@yao-pkg/pkg": "^5.15.0",
18 | "tsx": "^4.20.3"
19 | },
20 | "bin": {
21 | "qyp-mini-node-backend": "dist/index.js"
22 | },
23 | "pkg": {
24 | "targets": [
25 | "latest-macos"
26 | ],
27 | "scripts": [
28 | "dist/index.js"
29 | ]
30 | },
31 | "dependencies": {
32 | "@tailwindcss/node": "^4.1.11",
33 | "@tailwindcss/oxide": "^4.1.11",
34 | "@tailwindcss/postcss": "^4.1.11",
35 | "@tiny-utils/bytes": "^1.1.0",
36 | "esbuild": "^0.25.8",
37 | "postcss": "^8.5.6",
38 | "tailwindcss": "^4.1.11"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # qYp mini app
4 |
5 | [](https://t.me/elkornacio) [](https://x.com/elkornacio) [](https://github.com/elkornacio/qyp-mini/issues) [](https://github.com/elkornacio/qyp-mini/stargazers)
6 |
7 | 🤖 Open-source AI-agent for SQL databases with generative UI. A little brother for [qyp.ai](https://qyp.ai).
8 |
9 |

10 |
11 |
12 |
13 | # Tech stack:
14 |
15 | - [Tauri](https://tauri.app/) + [Vite](https://vitejs.dev/) + [React](https://react.dev/) + [Typescript](https://www.typescriptlang.org/)
16 | - [Tailwind](https://tailwindcss.com/) + [shadcn/ui](https://ui.shadcn.com/)
17 | - [Cursor](https://www.cursor.com/) + [Supercode.sh](https://supercode.sh/)
18 |
--------------------------------------------------------------------------------
/src/hooks/useCellSize.ts:
--------------------------------------------------------------------------------
1 | import { useRef, useLayoutEffect, useState } from 'react';
2 |
3 | export const useCellSize = (cols: number) => {
4 | const containerRef = useRef(null);
5 | const [containerWidth, setContainerWidth] = useState(0);
6 |
7 | // Измеряем ширину контейнера
8 | useLayoutEffect(() => {
9 | const container = containerRef.current;
10 | if (!container) return;
11 |
12 | const updateWidth = () => {
13 | // Получаем computed width, который учитывает все стили
14 | const rect = container.getBoundingClientRect();
15 | setContainerWidth(rect.width);
16 | };
17 |
18 | // Используем ResizeObserver для точного отслеживания изменений размера
19 | const resizeObserver = new ResizeObserver(() => {
20 | updateWidth();
21 | });
22 |
23 | resizeObserver.observe(container);
24 |
25 | // Первоначальное измерение
26 | updateWidth();
27 |
28 | return () => {
29 | resizeObserver.disconnect();
30 | };
31 | }, []);
32 |
33 | // Рассчитываем размер ячейки динамически
34 | // Доступная ширина = общая ширина - промежутки между ячейками
35 | const availableWidth = containerWidth;
36 | const cellSize = availableWidth > 0 ? availableWidth / cols : 0;
37 |
38 | return {
39 | containerRef,
40 | containerWidth,
41 | cellSize,
42 | };
43 | };
44 |
--------------------------------------------------------------------------------
/src/lib/componentAnalyzer.ts:
--------------------------------------------------------------------------------
1 | import { ComponentPropInfo } from '@/types/widget';
2 |
3 | /**
4 | * Анализирует исходный код для извлечения информации о пропах компонента
5 | * Базовая реализация - в будущем можно улучшить с помощью AST парсинга
6 | * @param sourceCode - исходный код компонента
7 | * @returns массив информации о пропах
8 | */
9 | export function analyzeComponentProps(sourceCode: string): ComponentPropInfo[] {
10 | const props: ComponentPropInfo[] = [];
11 |
12 | // Простой regex для поиска интерфейса Props
13 | const propsInterfaceMatch = sourceCode.match(/interface\s+\w*Props\s*{([^}]*)}/);
14 | if (propsInterfaceMatch) {
15 | const propsBody = propsInterfaceMatch[1];
16 |
17 | // Ищем поля в интерфейсе
18 | const propMatches = propsBody.matchAll(/(\w+)(\?)?\s*:\s*([^;,\n]+)/g);
19 |
20 | for (const match of propMatches) {
21 | const [, name, optional, type] = match;
22 | props.push({
23 | name,
24 | type: type.trim(),
25 | required: !optional,
26 | description: `Проп ${name} типа ${type.trim()}`,
27 | });
28 | }
29 | }
30 |
31 | // TODO: Более продвинутый анализ с помощью TypeScript AST
32 | // - Анализ JSDoc комментариев
33 | // - Определение defaultValues из defaultProps
34 | // - Поддержка union types и generic types
35 |
36 | return props;
37 | }
38 |
39 |
--------------------------------------------------------------------------------
/src/types/widget.ts:
--------------------------------------------------------------------------------
1 | import { Database } from './database';
2 |
3 | export interface WidgetSourceCode {
4 | widgetTsx: string;
5 | }
6 |
7 | export interface WidgetCompiledBundle extends WidgetSourceCode {
8 | props: ComponentPropInfo[];
9 | jsBundle: string;
10 | cssBundle: string;
11 | }
12 |
13 | export interface WidgetBundleWithDatabase extends WidgetCompiledBundle {
14 | database: Database;
15 | }
16 |
17 | export interface WidgetRuntimeComponent extends WidgetBundleWithDatabase {
18 | component: React.FunctionComponent;
19 | }
20 |
21 | /**
22 | * Информация о пропах компонента
23 | */
24 | export interface ComponentPropInfo {
25 | name: string;
26 | type: string;
27 | required: boolean;
28 | defaultValue?: any;
29 | description?: string;
30 | }
31 |
32 | /**
33 | * Размер виджета по умолчанию для дашборда
34 | */
35 | export interface WidgetDefaultSize {
36 | width: number; // количество колонок
37 | height: number; // количество строк
38 | }
39 |
40 | /**
41 | * Метаданные виджета (только результаты компиляции)
42 | */
43 | export interface WidgetMetadata extends WidgetCompiledBundle {
44 | id: string;
45 | name: string;
46 | defaultSize: WidgetDefaultSize;
47 | }
48 |
49 | /**
50 | * Опции для рендеринга компонента
51 | */
52 | export interface RenderOptions {
53 | props?: Record;
54 | wrapperClassName?: string;
55 | onError?: (error: Error) => void;
56 | }
57 |
--------------------------------------------------------------------------------
/src/platform/abstract/storage.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Абстрактный интерфейс для работы с файловой системой
3 | */
4 |
5 | export interface DirectoryEntry {
6 | name: string;
7 | isFile: boolean;
8 | isDirectory: boolean;
9 | }
10 |
11 | export interface StorageManager {
12 | /**
13 | * Записывает текст в файл
14 | */
15 | writeTextFile(path: string, content: string): Promise;
16 |
17 | /**
18 | * Читает текст из файла
19 | */
20 | readTextFile(path: string): Promise;
21 |
22 | /**
23 | * Проверяет существование файла или директории
24 | */
25 | exists(path: string): Promise;
26 |
27 | /**
28 | * Создает директорию
29 | */
30 | mkdir(path: string, recursive?: boolean): Promise;
31 |
32 | /**
33 | * Читает содержимое директории
34 | */
35 | readDir(path: string): Promise;
36 |
37 | /**
38 | * Удаляет файл
39 | */
40 | removeFile(path: string): Promise;
41 |
42 | /**
43 | * Удаляет директорию
44 | */
45 | removeDir(path: string, recursive?: boolean): Promise;
46 |
47 | /**
48 | * Получает путь к директории пользовательских данных приложения
49 | */
50 | getAppDataPath(): Promise;
51 |
52 | /**
53 | * Получает путь к директории документов пользователя
54 | */
55 | getDocumentsPath(): Promise;
56 |
57 | /**
58 | * Получает путь к временной директории
59 | */
60 | getTempPath(): Promise;
61 | }
62 |
--------------------------------------------------------------------------------
/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/dashboard/GridOverlay.tsx:
--------------------------------------------------------------------------------
1 | interface GridOverlayProps {
2 | cellSize: number;
3 | gap: number;
4 | showEditMode?: boolean;
5 | }
6 |
7 | export const GridOverlay: React.FC = ({ cellSize, gap, showEditMode = false }) => {
8 | const cellWithGap = cellSize + gap;
9 |
10 | // Create CSS pattern for grid dots - more visible colors
11 | const dotColor = showEditMode ? '#3b82f6' : '#64748b';
12 | const dotSize = showEditMode ? '2px' : '2px';
13 |
14 | const gridDots = `radial-gradient(circle at center, ${dotColor} ${dotSize}, transparent ${dotSize})`;
15 |
16 | // Optional grid lines for edit mode
17 | const gridLines = showEditMode
18 | ? `linear-gradient(to right, rgba(59, 130, 246, 0.2) 1px, transparent 1px),
19 | linear-gradient(to bottom, rgba(59, 130, 246, 0.2) 1px, transparent 1px)`
20 | : '';
21 |
22 | const backgroundImage = gridLines ? `${gridDots}, ${gridLines}` : gridDots;
23 |
24 | // Debug log
25 | console.log('GridOverlay render:', {
26 | cellWithGap,
27 | showEditMode,
28 | dotColor,
29 | backgroundImage,
30 | });
31 |
32 | return (
33 |
42 | {showEditMode &&
}
43 |
44 | );
45 | };
46 |
--------------------------------------------------------------------------------
/src/platform/abstract/dialogs.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Абстрактный интерфейс для работы с диалогами
3 | */
4 |
5 | export interface ConfirmOptions {
6 | title?: string;
7 | kind?: 'info' | 'warning' | 'error';
8 | okLabel?: string;
9 | cancelLabel?: string;
10 | }
11 |
12 | export interface AlertOptions {
13 | title?: string;
14 | kind?: 'info' | 'warning' | 'error';
15 | okLabel?: string;
16 | }
17 |
18 | export interface MessageBoxOptions {
19 | title?: string;
20 | type?: 'info' | 'warning' | 'error' | 'question';
21 | buttons?: string[];
22 | defaultButton?: number;
23 | cancelButton?: number;
24 | }
25 |
26 | export interface DialogManager {
27 | /**
28 | * Показывает диалог подтверждения (да/нет)
29 | */
30 | confirm(message: string, options?: ConfirmOptions): Promise;
31 |
32 | /**
33 | * Показывает диалог с информацией
34 | */
35 | alert(message: string, options?: AlertOptions): Promise;
36 |
37 | /**
38 | * Показывает диалог с произвольными кнопками
39 | */
40 | messageBox(message: string, options?: MessageBoxOptions): Promise;
41 |
42 | /**
43 | * Показывает диалог выбора файла для открытия
44 | */
45 | openFile(options?: {
46 | title?: string;
47 | filters?: { name: string; extensions: string[] }[];
48 | multiple?: boolean;
49 | directory?: boolean;
50 | }): Promise;
51 |
52 | /**
53 | * Показывает диалог выбора файла для сохранения
54 | */
55 | saveFile(options?: {
56 | title?: string;
57 | defaultPath?: string;
58 | filters?: { name: string; extensions: string[] }[];
59 | }): Promise;
60 | }
61 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { observer } from 'mobx-react';
3 | import './App.css';
4 | import { AppHeader } from './components/AppHeader';
5 | import { DashboardsList } from './components/DashboardsList';
6 | import { EmptyTab } from './components/EmptyTab';
7 | import { WelcomePage, PlaygroundPage, WidgetsLibraryPage, DatabasePage, DashboardPage } from './pages';
8 | import { appTabsStore } from './stores/AppTabsStore';
9 | // import { dashboardStore } from './stores/DashboardStore';
10 |
11 | const App: React.FC = observer(() => {
12 | const { activeTab } = appTabsStore;
13 |
14 | // Функция для рендеринга контента вкладки
15 | const renderTabContent = () => {
16 | if (!activeTab) return ;
17 |
18 | switch (activeTab.type) {
19 | case 'empty-tab':
20 | return ;
21 |
22 | case 'dashboard-list':
23 | return ;
24 |
25 | case 'dashboard':
26 | return ;
27 |
28 | case 'database':
29 | return ;
30 |
31 | case 'widgets-library':
32 | return ;
33 |
34 | case 'playground':
35 | return ;
36 |
37 | case 'welcome':
38 | return ;
39 |
40 | default:
41 | return ;
42 | }
43 | };
44 |
45 | return (
46 |
47 | {/* Header с вкладками */}
48 |
49 |
50 | {/* Основной контент */}
51 |
{renderTabContent()}
52 |
53 | );
54 | });
55 |
56 | export default App;
57 |
--------------------------------------------------------------------------------
/src-back/.gitignore:
--------------------------------------------------------------------------------
1 | # Dependencies
2 | node_modules/
3 | npm-debug.log*
4 | yarn-debug.log*
5 | yarn-error.log*
6 |
7 | # Runtime data
8 | pids
9 | *.pid
10 | *.seed
11 | *.pid.lock
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage/
15 | *.lcov
16 |
17 | # nyc test coverage
18 | .nyc_output
19 |
20 | # Grunt intermediate storage
21 | .grunt
22 |
23 | # node-waf configuration
24 | .lock-wscript
25 |
26 | # Compiled binary addons
27 | build/Release
28 |
29 | # Dependency directories
30 | jspm_packages/
31 |
32 | # Optional npm cache directory
33 | .npm
34 |
35 | # Optional eslint cache
36 | .eslintcache
37 |
38 | # Output of 'npm pack'
39 | *.tgz
40 |
41 | # Yarn Integrity file
42 | .yarn-integrity
43 |
44 | # dotenv environment variables file
45 | .env
46 | .env.local
47 | .env.development.local
48 | .env.test.local
49 | .env.production.local
50 |
51 | # parcel-bundler cache
52 | .cache
53 | .parcel-cache
54 |
55 | # next.js build output
56 | .next
57 |
58 | # nuxt.js build output
59 | .nuxt
60 |
61 | # vuepress build output
62 | .vuepress/dist
63 |
64 | # Serverless directories
65 | .serverless
66 |
67 | # FuseBox cache
68 | .fusebox/
69 |
70 | # DynamoDB Local files
71 | .dynamodb/
72 |
73 | # TernJS port file
74 | .tern-port
75 |
76 | # Compiled TypeScript
77 | dist/
78 | *.js.map
79 | *.d.ts
80 |
81 | # IDE files
82 | .vscode/
83 | .idea/
84 | *.swp
85 | *.swo
86 | *~
87 |
88 | # OS generated files
89 | .DS_Store
90 | .DS_Store?
91 | ._*
92 | .Spotlight-V100
93 | .Trashes
94 | ehthumbs.db
95 | Thumbs.db
96 |
97 | # Logs
98 | logs
99 | *.log
100 |
101 | dist/
--------------------------------------------------------------------------------
/__legacy/sidecar.ts:
--------------------------------------------------------------------------------
1 | import { SidecarExecutor } from './SidecarExecutor';
2 | import { SerializedVirtualNode } from '@/virtual-fs/types';
3 | import { CompileComponentRequest, CompileComponentResponse, PingRequest, PingResponse } from 'src-node/src/types';
4 |
5 | /**
6 | * Класс для работы с Node.js sidecar
7 | */
8 | export class QypSidecar {
9 | static async ping(msg: string): Promise {
10 | const request: PingRequest = {
11 | command: 'ping',
12 | message: msg,
13 | };
14 |
15 | const response = await SidecarExecutor.execute({
16 | args: ['ping', msg],
17 | request,
18 | });
19 |
20 | if (response.status === 'error') {
21 | throw new Error(response.error || 'Ошибка пинга Node.js sidecar');
22 | }
23 |
24 | return response.result;
25 | }
26 |
27 | /**
28 | * Компилирует код компонента через Node.js sidecar
29 | * @param code - TSX/React код для компиляции
30 | * @returns Результат компиляции
31 | */
32 | static async compile(
33 | serializedVFS: SerializedVirtualNode[],
34 | entryPoint: string,
35 | ): Promise {
36 | // Подготавливаем запрос для компиляции
37 | const request: CompileComponentRequest = {
38 | command: 'compile',
39 | serializedVFS,
40 | entryPoint,
41 | };
42 |
43 | console.log('request', JSON.stringify(request));
44 |
45 | // Выполняем команду через sidecar
46 | const response = await SidecarExecutor.execute({
47 | args: ['compile'],
48 | request,
49 | });
50 |
51 | if (response.status === 'error') {
52 | throw new Error(response.error || 'Ошибка компиляции в sidecar');
53 | }
54 |
55 | return response.result;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/__legacy/src-node/src/compiler/esbuild-compiler.ts:
--------------------------------------------------------------------------------
1 | import * as esbuild from 'esbuild';
2 |
3 | import { VirtualFS } from '../../../src/virtual-fs/VirtualFS.js';
4 | import { createError } from '../utils/index.js';
5 | import { ESBuildVFS } from './esbuild-vfs.js';
6 |
7 | interface CompileOptions {
8 | minify?: boolean;
9 | }
10 |
11 | /**
12 | * Компилятор для JavaScript/TypeScript с использованием ESBuild
13 | */
14 | export class ESBuildCompiler {
15 | /**
16 | * Компилирует виртуальные файлы в JavaScript
17 | */
18 | async compile(vfs: VirtualFS, entryPoint: string, options: CompileOptions = {}): Promise<{ code: string }> {
19 | // Проверяем существование entry point
20 | if (!vfs.fileExists(entryPoint)) {
21 | throw createError(`Не найдена входная точка: ${entryPoint}`);
22 | }
23 |
24 | try {
25 | const vfsPlugin = new ESBuildVFS(vfs);
26 |
27 | const result = await esbuild.build({
28 | entryPoints: [entryPoint],
29 | bundle: true,
30 | write: false,
31 | format: 'cjs',
32 | target: 'es2020',
33 | jsx: 'automatic',
34 | minify: options.minify || false,
35 | sourcemap: false,
36 |
37 | // Плагин для работы с виртуальными файлами
38 | plugins: [vfsPlugin.get()],
39 | });
40 |
41 | if (result.errors.length > 0) {
42 | const errorMessages = result.errors.map(err => err.text).join('\n');
43 | throw createError(`Ошибки компиляции ESBuild:\n${errorMessages}`);
44 | }
45 |
46 | const outputFile = result.outputFiles?.[0];
47 | if (!outputFile) {
48 | throw createError('ESBuild не создал выходной файл');
49 | }
50 |
51 | return {
52 | code: outputFile.text,
53 | };
54 | } catch (error) {
55 | throw createError('Ошибка компиляции с ESBuild', error);
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/lib/compiler/ESBuildCompiler.ts:
--------------------------------------------------------------------------------
1 | import * as esbuild from 'esbuild-wasm';
2 | import esbuildWasmUrl from 'esbuild-wasm/esbuild.wasm?url';
3 |
4 | import { VirtualFS } from '../../virtual-fs/VirtualFS.js';
5 | import { ESBuildVFS } from './ESBuildVFS.js';
6 |
7 | interface CompileOptions {
8 | minify?: boolean;
9 | }
10 |
11 | const esbuildPromise = esbuild.initialize({
12 | wasmURL: esbuildWasmUrl,
13 | });
14 |
15 | /**
16 | * Компилятор для JavaScript/TypeScript с использованием ESBuild
17 | */
18 | export class ESBuildCompiler {
19 | /**
20 | * Компилирует виртуальные файлы в JavaScript
21 | */
22 | async compile(vfs: VirtualFS, entryPoint: string, options: CompileOptions = {}): Promise {
23 | // Проверяем существование entry point
24 | if (!vfs.fileExists(entryPoint)) {
25 | throw new Error(`Не найдена входная точка: ${entryPoint}`);
26 | }
27 |
28 | try {
29 | const vfsPlugin = new ESBuildVFS(vfs);
30 |
31 | await esbuildPromise;
32 |
33 | const result = await esbuild.build({
34 | entryPoints: [entryPoint],
35 | bundle: true,
36 | write: false,
37 | format: 'cjs',
38 | target: 'es2020',
39 | jsx: 'automatic',
40 | minify: options.minify || false,
41 | sourcemap: false,
42 |
43 | // Плагин для работы с виртуальными файлами
44 | plugins: [vfsPlugin.get()],
45 | });
46 |
47 | if (result.errors.length > 0) {
48 | const errorMessages = result.errors.map(err => err.text).join('\n');
49 | throw new Error(`Ошибки компиляции ESBuild:\n${errorMessages}`);
50 | }
51 |
52 | const outputFile = result.outputFiles?.[0];
53 | if (!outputFile) {
54 | throw new Error('ESBuild не создал выходной файл');
55 | }
56 |
57 | return outputFile.text;
58 | } catch (error) {
59 | throw error;
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const badgeVariants = cva(
8 | "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
14 | secondary:
15 | "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
16 | destructive:
17 | "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
18 | outline:
19 | "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
20 | },
21 | },
22 | defaultVariants: {
23 | variant: "default",
24 | },
25 | }
26 | )
27 |
28 | function Badge({
29 | className,
30 | variant,
31 | asChild = false,
32 | ...props
33 | }: React.ComponentProps<"span"> &
34 | VariantProps & { asChild?: boolean }) {
35 | const Comp = asChild ? Slot : "span"
36 |
37 | return (
38 |
43 | )
44 | }
45 |
46 | export { Badge, badgeVariants }
47 |
--------------------------------------------------------------------------------
/__legacy/SidecarExecutor.ts:
--------------------------------------------------------------------------------
1 | import { Command, TerminatedPayload } from '@tauri-apps/plugin-shell';
2 | import { SidecarEncoder } from './SidecarEncoder';
3 | import { BaseRequest, BaseResponse } from 'src-node/src/ipc/types';
4 |
5 | /**
6 | * Параметры для выполнения команды sidecar
7 | */
8 | export interface SidecarExecutionParams {
9 | args: string[];
10 | request: BaseRequest;
11 | }
12 |
13 | /**
14 | * Исполнитель команд sidecar - отвечает только за выполнение команд
15 | */
16 | export class SidecarExecutor {
17 | private static readonly SIDECAR_NAME = 'binaries/qyp-mini-node-backend';
18 |
19 | /**
20 | * Выполняет команду в sidecar с заданными параметрами
21 | * @param params - Параметры выполнения
22 | * @returns Результат выполнения команды
23 | */
24 | static async execute(params: SidecarExecutionParams): Promise> {
25 | const command = Command.sidecar(this.SIDECAR_NAME, params.args);
26 |
27 | let stdout = '';
28 | let stderr = '';
29 |
30 | command.stdout.on('data', data => {
31 | console.log('stdout: ', data);
32 | stdout += data;
33 | });
34 |
35 | command.stderr.on('data', data => {
36 | console.log('stderr: ', data);
37 | stderr += data;
38 | });
39 |
40 | const child = await command.spawn();
41 |
42 | const encodedRequest = SidecarEncoder.encodeRequest(params.request);
43 | console.log('Отправляем запрос:', encodedRequest);
44 |
45 | const output = await new Promise(resolve => {
46 | command.on('close', out => {
47 | console.log('close: ', out);
48 | resolve(out);
49 | });
50 |
51 | // Отправляем закодированный запрос
52 | child.write(encodedRequest);
53 | });
54 |
55 | if (output.code !== 0) {
56 | throw new Error(`Sidecar завершился с кодом: ${output.code}. Stderr: ${stderr}`);
57 | }
58 |
59 | return SidecarEncoder.decodeResponse(stdout);
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .logo.vite:hover {
2 | filter: drop-shadow(0 0 2em #747bff);
3 | }
4 |
5 | .logo.react:hover {
6 | filter: drop-shadow(0 0 2em #61dafb);
7 | }
8 | :root {
9 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
10 | font-size: 16px;
11 | line-height: 24px;
12 | font-weight: 400;
13 |
14 | color: #0f0f0f;
15 | background-color: #f6f6f6;
16 |
17 | font-synthesis: none;
18 | text-rendering: optimizeLegibility;
19 | -webkit-font-smoothing: antialiased;
20 | -moz-osx-font-smoothing: grayscale;
21 | -webkit-text-size-adjust: 100%;
22 | }
23 |
24 | .container {
25 | margin: 0;
26 | padding-top: 10vh;
27 | display: flex;
28 | flex-direction: column;
29 | justify-content: center;
30 | text-align: center;
31 | }
32 |
33 | .logo {
34 | height: 6em;
35 | padding: 1.5em;
36 | will-change: filter;
37 | transition: 0.75s;
38 | }
39 |
40 | .logo.tauri:hover {
41 | filter: drop-shadow(0 0 2em #24c8db);
42 | }
43 |
44 | .row {
45 | display: flex;
46 | justify-content: center;
47 | }
48 |
49 | a {
50 | font-weight: 500;
51 | color: #646cff;
52 | text-decoration: inherit;
53 | }
54 |
55 | a:hover {
56 | color: #535bf2;
57 | }
58 |
59 | h1 {
60 | text-align: center;
61 | }
62 |
63 | input {
64 | border-radius: 8px;
65 | border: 1px solid transparent;
66 | padding: 0.6em 1.2em;
67 | font-size: 1em;
68 | font-weight: 500;
69 | font-family: inherit;
70 | color: #0f0f0f;
71 | background-color: #ffffff;
72 | transition: border-color 0.25s;
73 | box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
74 | }
75 |
76 | input {
77 | outline: none;
78 | }
79 |
80 | #greet-input {
81 | margin-right: 5px;
82 | }
83 |
84 | @media (prefers-color-scheme: dark) {
85 | :root {
86 | color: #f6f6f6;
87 | background-color: #2f2f2f;
88 | }
89 |
90 | a:hover {
91 | color: #24c8db;
92 | }
93 |
94 | input {
95 | color: #ffffff;
96 | background-color: #0f0f0f98;
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/platform/browser/window.ts:
--------------------------------------------------------------------------------
1 | import { WindowManager, WindowSize } from '../abstract/window';
2 |
3 | /**
4 | * Browser-реализация менеджера окна
5 | * Ограниченная функциональность из-за безопасности браузера
6 | */
7 | export class BrowserWindowManager implements WindowManager {
8 | async setSize(width: number, height: number): Promise {
9 | // В браузере нельзя изменить размер окна из соображений безопасности
10 | console.warn('setSize не поддерживается в браузере');
11 |
12 | // Можем попробовать изменить размер через window.resizeTo,
13 | // но это работает только для окон, открытых скриптом
14 | try {
15 | window.resizeTo(width, height);
16 | } catch (error) {
17 | // Игнорируем ошибку, так как это ожидаемо
18 | }
19 | }
20 |
21 | async getSize(): Promise {
22 | return {
23 | width: window.innerWidth,
24 | height: window.innerHeight,
25 | };
26 | }
27 |
28 | async minimize(): Promise {
29 | console.warn('minimize не поддерживается в браузере');
30 | // В браузере нет API для минимизации окна
31 | }
32 |
33 | async maximize(): Promise {
34 | console.warn('maximize не поддерживается в браузере');
35 | // Пытаемся эмулировать через fullscreen API
36 | if (document.documentElement.requestFullscreen) {
37 | try {
38 | await document.documentElement.requestFullscreen();
39 | } catch (error) {
40 | // Игнорируем ошибку
41 | }
42 | }
43 | }
44 |
45 | async close(): Promise {
46 | // В браузере можем только попробовать закрыть окно
47 | // Работает только для окон, открытых скриптом
48 | try {
49 | window.close();
50 | } catch (error) {
51 | console.warn('Невозможно закрыть окно браузера программно');
52 | }
53 | }
54 |
55 | async isMaximized(): Promise {
56 | // Проверяем, находится ли браузер в полноэкранном режиме
57 | return !!document.fullscreenElement;
58 | }
59 |
60 | async setTitle(title: string): Promise {
61 | document.title = title;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/platform/tauri/storage.ts:
--------------------------------------------------------------------------------
1 | import { StorageManager, DirectoryEntry } from '../abstract/storage';
2 | import { writeTextFile, readTextFile, exists, mkdir, readDir, remove, BaseDirectory } from '@tauri-apps/plugin-fs';
3 | import { appDataDir, documentDir, tempDir } from '@tauri-apps/api/path';
4 |
5 | /**
6 | * Tauri-реализация менеджера файловой системы
7 | */
8 | export class TauriStorageManager implements StorageManager {
9 | async writeTextFile(path: string, content: string): Promise {
10 | await writeTextFile(path, content, {
11 | baseDir: BaseDirectory.AppData,
12 | });
13 | }
14 |
15 | async readTextFile(path: string): Promise {
16 | return await readTextFile(path, {
17 | baseDir: BaseDirectory.AppData,
18 | });
19 | }
20 |
21 | async exists(path: string): Promise {
22 | return await exists(path, {
23 | baseDir: BaseDirectory.AppData,
24 | });
25 | }
26 |
27 | async mkdir(path: string, recursive = true): Promise {
28 | await mkdir(path, {
29 | baseDir: BaseDirectory.AppData,
30 | recursive,
31 | });
32 | }
33 |
34 | async readDir(path: string): Promise {
35 | const entries = await readDir(path, {
36 | baseDir: BaseDirectory.AppData,
37 | });
38 |
39 | return entries.map(entry => ({
40 | name: entry.name || '',
41 | isFile: !!entry.isFile,
42 | isDirectory: !!entry.isDirectory,
43 | }));
44 | }
45 |
46 | async removeFile(path: string): Promise {
47 | await remove(path, {
48 | baseDir: BaseDirectory.AppData,
49 | });
50 | }
51 |
52 | async removeDir(path: string, recursive = true): Promise {
53 | await remove(path, {
54 | baseDir: BaseDirectory.AppData,
55 | recursive,
56 | });
57 | }
58 |
59 | async getAppDataPath(): Promise {
60 | return await appDataDir();
61 | }
62 |
63 | async getDocumentsPath(): Promise {
64 | return await documentDir();
65 | }
66 |
67 | async getTempPath(): Promise {
68 | return await tempDir();
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src-back/src/index.ts:
--------------------------------------------------------------------------------
1 | import 'reflect-metadata';
2 |
3 | import dotenv from 'dotenv';
4 |
5 | dotenv.config();
6 |
7 | import express from 'express';
8 | import cors from 'cors';
9 | import { generalRateLimit } from './middlewares/rateLimiter';
10 | import { errorHandler } from './middlewares/errorHandler';
11 | import sqlRoutes from './routes/sql';
12 |
13 | const app = express();
14 | const PORT = process.env.PORT || 3000;
15 |
16 | // CORS configuration
17 | app.use(
18 | cors({
19 | origin: process.env.CORS_ORIGIN || true, // Allow all origins in development
20 | credentials: true,
21 | methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
22 | allowedHeaders: ['Content-Type', 'Authorization'],
23 | }),
24 | );
25 |
26 | app.set('trust proxy', '127.0.0.1');
27 |
28 | // General rate limiting
29 | app.use(generalRateLimit);
30 |
31 | // Body parsing
32 | app.use(express.json({ limit: '10mb' }));
33 | app.use(express.urlencoded({ extended: true, limit: '10mb' }));
34 |
35 | // Health check endpoint
36 | app.get('/health', (req, res) => {
37 | res.json({
38 | success: true,
39 | message: 'SQL Proxy Server is running',
40 | timestamp: new Date().toISOString(),
41 | });
42 | });
43 |
44 | // API routes
45 | app.use('/api', sqlRoutes);
46 |
47 | // Root endpoint
48 | app.get('/', (req, res) => {
49 | res.json({
50 | success: true,
51 | message: 'SQL Proxy Server',
52 | version: '1.0.0',
53 | endpoints: {
54 | health: 'GET /health',
55 | select: 'POST /api/select',
56 | },
57 | });
58 | });
59 |
60 | // Error handling middleware (should be last)
61 | app.use(errorHandler);
62 |
63 | // 404 handler
64 | app.use((req, res) => {
65 | res.status(404).json({
66 | success: false,
67 | error: 'Endpoint not found',
68 | timestamp: new Date().toISOString(),
69 | });
70 | });
71 |
72 | app.listen(PORT, () => {
73 | console.log(`🚀 SQL Proxy Server is running on port ${PORT}`);
74 | console.log(`📍 Health check: http://localhost:${PORT}/health`);
75 | console.log(`📡 API endpoint: http://localhost:${PORT}/api/select`);
76 | });
77 |
--------------------------------------------------------------------------------
/src/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as TabsPrimitive from "@radix-ui/react-tabs"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | function Tabs({
7 | className,
8 | ...props
9 | }: React.ComponentProps) {
10 | return (
11 |
16 | )
17 | }
18 |
19 | function TabsList({
20 | className,
21 | ...props
22 | }: React.ComponentProps) {
23 | return (
24 |
32 | )
33 | }
34 |
35 | function TabsTrigger({
36 | className,
37 | ...props
38 | }: React.ComponentProps) {
39 | return (
40 |
48 | )
49 | }
50 |
51 | function TabsContent({
52 | className,
53 | ...props
54 | }: React.ComponentProps) {
55 | return (
56 |
61 | )
62 | }
63 |
64 | export { Tabs, TabsList, TabsTrigger, TabsContent }
65 |
--------------------------------------------------------------------------------
/src/components/ui/resizable.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { GripVerticalIcon } from "lucide-react"
3 | import * as ResizablePrimitive from "react-resizable-panels"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | function ResizablePanelGroup({
8 | className,
9 | ...props
10 | }: React.ComponentProps) {
11 | return (
12 |
20 | )
21 | }
22 |
23 | function ResizablePanel({
24 | ...props
25 | }: React.ComponentProps) {
26 | return
27 | }
28 |
29 | function ResizableHandle({
30 | withHandle,
31 | className,
32 | ...props
33 | }: React.ComponentProps & {
34 | withHandle?: boolean
35 | }) {
36 | return (
37 | div]:rotate-90",
41 | className
42 | )}
43 | {...props}
44 | >
45 | {withHandle && (
46 |
47 |
48 |
49 | )}
50 |
51 | )
52 | }
53 |
54 | export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
55 |
--------------------------------------------------------------------------------
/__legacy/src-node/src/compiler/index.ts:
--------------------------------------------------------------------------------
1 | import { CompileComponentResponse } from '../types/index.js';
2 | import { SerializedVirtualNode } from '../../../src/virtual-fs/types.js';
3 | import { VirtualFS } from '../../../src/virtual-fs/VirtualFS.js';
4 | import { ESBuildCompiler } from './esbuild-compiler.js';
5 | // import { TailwindCompiler } from './tailwind-compiler.js';
6 |
7 | /**
8 | * Virtual directory structure:
9 | * src/ # Корневая директория
10 | * ├── components/
11 | * │ ├── ui/ # Базовые UI-компоненты, readonly (shadcn)
12 | * ├── widget/
13 | * │ ├── index.tsx # Главный файл, которые будет генерировать ИИ - React-компонент с виджетом
14 | * │ ├── query.sql.ts # Файл с sql-запросом в базу, экспортирует одну async-функцию, делающую запрос. Тоже генерирует ИИ
15 | * ├── lib/
16 | * │ ├── utils.ts # readonly, здесь будут утилитарные функции аля `cn`
17 | */
18 |
19 | export class Compiler {
20 | private esbuildCompiler: ESBuildCompiler;
21 | // private tailwindCompiler: TailwindCompiler;
22 |
23 | constructor() {
24 | this.esbuildCompiler = new ESBuildCompiler();
25 | // this.tailwindCompiler = new TailwindCompiler();
26 | }
27 |
28 | async compile(serializedVfs: SerializedVirtualNode[], entryPoint: string): Promise {
29 | const vfs = new VirtualFS();
30 | vfs.deserialize(serializedVfs);
31 |
32 | try {
33 | // Компилируем используя ESBuild
34 | const result = await this.esbuildCompiler.compile(vfs, entryPoint, {
35 | minify: false,
36 | });
37 |
38 | // const tailwindResult = await this.tailwindCompiler.compile(vfs, {
39 | // minify: false,
40 | // });
41 |
42 | return {
43 | jsBundle: result.code,
44 | cssBundle: '',
45 | // cssBundle: tailwindResult.css,
46 | };
47 | } catch (error) {
48 | // В случае ошибки возвращаем исходный код для отладки
49 | const entryPointFile = vfs.readFile(entryPoint);
50 | const entryPointSource = entryPointFile.content;
51 |
52 | return {
53 | jsBundle: `// Ошибка компиляции: ${error instanceof Error ? error.message : 'Неизвестная ошибка'}\n\n// Исходный код:\n\n${entryPointSource}`,
54 | cssBundle: '/* Ошибка компиляции CSS */',
55 | };
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "qyp-mini",
3 | "private": true,
4 | "version": "0.1.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc && vite build",
9 | "preview": "tsc && vite build && vite preview",
10 | "tauri": "tauri",
11 | "deploy-web": "pnpm run build && dotenv -e .env.wrangler -- wrangler pages deploy --project-name qyp-mini ./dist"
12 | },
13 | "dependencies": {
14 | "@monaco-editor/react": "^4.7.0",
15 | "@radix-ui/react-dialog": "^1.1.14",
16 | "@radix-ui/react-dropdown-menu": "^2.1.15",
17 | "@radix-ui/react-label": "^2.1.7",
18 | "@radix-ui/react-select": "^2.2.5",
19 | "@radix-ui/react-slot": "^1.2.3",
20 | "@radix-ui/react-tabs": "^1.1.12",
21 | "@tailwindcss/oxide": "^4.1.11",
22 | "@tailwindcss/vite": "^4.1.11",
23 | "@tauri-apps/api": "^2",
24 | "@tauri-apps/plugin-dialog": "~2.3.1",
25 | "@tauri-apps/plugin-fs": "~2.4.1",
26 | "@tauri-apps/plugin-opener": "^2",
27 | "@tauri-apps/plugin-shell": "~2",
28 | "@tauri-apps/plugin-sql": "~2.3.0",
29 | "@tauri-apps/plugin-stronghold": "~2.3.0",
30 | "@tiny-utils/bytes": "^1.1.0",
31 | "class-variance-authority": "^0.7.1",
32 | "clsx": "^2.1.1",
33 | "esbuild-wasm": "^0.25.8",
34 | "lightningcss-wasm": "^1.30.1",
35 | "lucide-react": "^0.525.0",
36 | "mobx": "^6.13.5",
37 | "mobx-react": "^9.1.1",
38 | "monaco-editor": "^0.52.2",
39 | "monaco-editor-textmate": "^4.0.0",
40 | "monaco-textmate": "^3.0.1",
41 | "monaco-themes": "^0.4.6",
42 | "onigasm": "^2.2.5",
43 | "react": "^18.3.1",
44 | "react-dom": "^18.3.1",
45 | "react-resizable-panels": "^3.0.3",
46 | "tailwind-merge": "^3.3.1",
47 | "tailwindcss": "^4.1.11",
48 | "tailwindcss-v3": "npm:tailwindcss@^3.4.17"
49 | },
50 | "devDependencies": {
51 | "@tauri-apps/cli": "^2",
52 | "@types/node": "^24.1.0",
53 | "@types/react": "^18.3.1",
54 | "@types/react-dom": "^18.3.1",
55 | "@vitejs/plugin-react": "^4.3.4",
56 | "@vitejs/plugin-react-swc": "^3.11.0",
57 | "csstype": "^3.1.3",
58 | "prettier": "^3.3.3",
59 | "tw-animate-css": "^1.3.6",
60 | "typescript": "~5.6.2",
61 | "vite": "^6.0.3",
62 | "vite-plugin-node-polyfills": "^0.24.0",
63 | "vite-plugin-svgr": "^4.3.0",
64 | "vite-tsconfig-paths": "^5.1.4",
65 | "wrangler": "^3.95.0"
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/types/dashboard.ts:
--------------------------------------------------------------------------------
1 | import { Database } from './database';
2 |
3 | /**
4 | * Позиция и размер виджета на дашборде
5 | */
6 | export interface WidgetLayout {
7 | x: number;
8 | y: number;
9 | width: number;
10 | height: number;
11 | }
12 |
13 | /**
14 | * Виджет размещенный на дашборде
15 | */
16 | export interface DashboardWidget {
17 | id: string;
18 | widgetId: string; // ссылка на виджет из библиотеки
19 | layout: WidgetLayout;
20 | props?: Record; // пропы для виджета
21 | title?: string;
22 | }
23 |
24 | /**
25 | * Рантайм контекст для выполнения виджетов на дашборде
26 | */
27 | export interface WidgetRuntimeContext {
28 | database: Database;
29 | }
30 |
31 | /**
32 | * Настройки сетки дашборда
33 | */
34 | export interface DashboardGrid {
35 | cols: number; // количество колонок (fixed at 12)
36 | rows: number; // количество строк (customizable height)
37 | }
38 |
39 | /**
40 | * Сущность дашборда
41 | */
42 | export interface Dashboard {
43 | id: string;
44 | name: string;
45 | icon?: string; // emoji иконка для дешборда
46 | databaseId: string; // привязка к базе данных
47 | widgets: DashboardWidget[];
48 | grid: DashboardGrid;
49 | }
50 |
51 | /**
52 | * Опции для создания нового дашборда
53 | */
54 | export interface CreateDashboardOptions {
55 | name: string;
56 | description?: string;
57 | icon?: string; // emoji иконка для дешборда
58 | databaseId: string;
59 | grid?: Partial;
60 | }
61 |
62 | /**
63 | * Опции для обновления дашборда
64 | */
65 | export interface UpdateDashboardOptions {
66 | name?: string;
67 | description?: string;
68 | icon?: string; // emoji иконка для дешборда
69 | databaseId?: string;
70 | grid?: Partial;
71 | }
72 |
73 | /**
74 | * Опции для добавления виджета на дашборд
75 | */
76 | export interface AddWidgetToDashboardOptions {
77 | widgetId: string;
78 | layout: WidgetLayout;
79 | props?: Record;
80 | title?: string;
81 | }
82 |
83 | /**
84 | * Опции для обновления виджета на дашборде
85 | */
86 | export interface UpdateDashboardWidgetOptions {
87 | layout?: Partial;
88 | props?: Record;
89 | title?: string;
90 | }
91 |
92 | /**
93 | * Дефолтные настройки сетки
94 | */
95 | export const DEFAULT_GRID: DashboardGrid = {
96 | cols: 12,
97 | rows: 20,
98 | };
99 |
--------------------------------------------------------------------------------
/src-back/README.md:
--------------------------------------------------------------------------------
1 | # SQL Proxy Server
2 |
3 | TypeScript Node.js сервер для выполнения SQL запросов через HTTP API. Поддерживает PostgreSQL, MySQL и SQLite базы данных.
4 |
5 | ## Особенности
6 |
7 | - 🔒 Выполняет только SELECT запросы для безопасности
8 | - 🚦 Rate limiting для защиты от злоупотреблений
9 | - 🌐 CORS поддержка
10 | - 📊 Поддержка PostgreSQL, MySQL, SQLite
11 | - ⚡ TypeScript с строгой типизацией
12 | - 🔄 Автоматическое управление соединениями с БД
13 |
14 | ## Установка
15 |
16 | ```bash
17 | npm install
18 | ```
19 |
20 | ## Запуск
21 |
22 | ### Разработка
23 |
24 | ```bash
25 | npm run dev
26 | ```
27 |
28 | ### Продакшн
29 |
30 | ```bash
31 | npm run build
32 | npm start
33 | ```
34 |
35 | ## API
36 |
37 | ### POST /api/select
38 |
39 | Выполняет SELECT запрос к базе данных.
40 |
41 | **Тело запроса:**
42 |
43 | ```json
44 | {
45 | "credentials": {
46 | "host": "localhost",
47 | "port": 5432,
48 | "username": "user",
49 | "password": "password",
50 | "database": "mydb",
51 | "type": "postgres",
52 | "ssl": false
53 | },
54 | "query": "SELECT * FROM users WHERE id = $1",
55 | "parameters": [1]
56 | }
57 | ```
58 |
59 | **Ответ:**
60 |
61 | ```json
62 | {
63 | "success": true,
64 | "data": [{ "id": 1, "name": "John", "email": "john@example.com" }],
65 | "executionTime": 45,
66 | "rowCount": 1
67 | }
68 | ```
69 |
70 | ### GET /health
71 |
72 | Проверка состояния сервера.
73 |
74 | ## Поддерживаемые БД
75 |
76 | - **PostgreSQL** (`type: "postgres"`)
77 | - **MySQL** (`type: "mysql"`)
78 | - **SQLite** (`type: "sqlite"`)
79 |
80 | ## Переменные окружения
81 |
82 | Скопируйте `.env.example` в `.env` и настройте:
83 |
84 | ```bash
85 | cp .env.example .env
86 | ```
87 |
88 | ## Rate Limiting
89 |
90 | - **SQL запросы**: 10 запросов в минуту на IP
91 | - **Общие запросы**: 100 запросов в минуту на IP
92 |
93 | ## Безопасность
94 |
95 | - Разрешены только SELECT запросы
96 | - Rate limiting по IP
97 | - Автоматическое закрытие соединений с БД
98 | - Валидация входных данных
99 |
100 | ## Разработка
101 |
102 | ```bash
103 | # Форматирование кода
104 | npm run format
105 |
106 | # Проверка форматирования
107 | npm run format:check
108 |
109 | # Сборка проекта
110 | npm run build
111 | ```
112 |
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
16 | outline:
17 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
20 | ghost:
21 | "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
22 | link: "text-primary underline-offset-4 hover:underline",
23 | },
24 | size: {
25 | default: "h-9 px-4 py-2 has-[>svg]:px-3",
26 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
27 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
28 | icon: "size-9",
29 | },
30 | },
31 | defaultVariants: {
32 | variant: "default",
33 | size: "default",
34 | },
35 | }
36 | )
37 |
38 | function Button({
39 | className,
40 | variant,
41 | size,
42 | asChild = false,
43 | ...props
44 | }: React.ComponentProps<"button"> &
45 | VariantProps & {
46 | asChild?: boolean
47 | }) {
48 | const Comp = asChild ? Slot : "button"
49 |
50 | return (
51 |
56 | )
57 | }
58 |
59 | export { Button, buttonVariants }
60 |
--------------------------------------------------------------------------------
/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | function Card({ className, ...props }: React.ComponentProps<"div">) {
6 | return (
7 |
15 | )
16 | }
17 |
18 | function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
19 | return (
20 |
28 | )
29 | }
30 |
31 | function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
32 | return (
33 |
38 | )
39 | }
40 |
41 | function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
42 | return (
43 |
48 | )
49 | }
50 |
51 | function CardAction({ className, ...props }: React.ComponentProps<"div">) {
52 | return (
53 |
61 | )
62 | }
63 |
64 | function CardContent({ className, ...props }: React.ComponentProps<"div">) {
65 | return (
66 |
71 | )
72 | }
73 |
74 | function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
75 | return (
76 |
81 | )
82 | }
83 |
84 | export {
85 | Card,
86 | CardHeader,
87 | CardFooter,
88 | CardTitle,
89 | CardAction,
90 | CardDescription,
91 | CardContent,
92 | }
93 |
--------------------------------------------------------------------------------
/__legacy/src-node/src/ipc/protocol.ts:
--------------------------------------------------------------------------------
1 | import { SmartBuffer } from '@tiny-utils/bytes';
2 | import { safeJsonParse, createError } from '../utils/index.js';
3 | import { BaseErrorResponse, BaseResponse, BaseSuccessResponse } from './types.js';
4 |
5 | /**
6 | * Универсальный IPC протокол для работы с stdin/stdout через base64 + JSON
7 | */
8 | export class IPCProtocol {
9 | /**
10 | * Декодирует запрос из base64 строки
11 | */
12 | static decodeRequest(base64String: string): T | null {
13 | try {
14 | const jsonString = SmartBuffer.ofBase64String(base64String.trim()).toUTF8String();
15 | const request = safeJsonParse(jsonString);
16 |
17 | if (!request) {
18 | throw createError('Не удалось парсить JSON запрос');
19 | }
20 |
21 | return request;
22 | } catch (error) {
23 | console.error('Ошибка декодирования запроса:', error);
24 | return null;
25 | }
26 | }
27 |
28 | /**
29 | * Кодирует ответ в base64 строку
30 | */
31 | static encodeResponse(response: T): string {
32 | try {
33 | const jsonString = JSON.stringify(response);
34 | return SmartBuffer.ofUTF8String(jsonString).toBase64String();
35 | } catch (error) {
36 | // Fallback для критических ошибок кодирования
37 | const fallbackResponse = {
38 | status: 'error',
39 | message: `Ошибка кодирования ответа: ${error instanceof Error ? error.message : String(error)}`,
40 | };
41 |
42 | const fallbackJson = JSON.stringify(fallbackResponse);
43 | return SmartBuffer.ofUTF8String(fallbackJson).toBase64String();
44 | }
45 | }
46 |
47 | /**
48 | * Отправляет ответ в stdout
49 | */
50 | static sendResponse(response: T): void {
51 | const encodedResponse = this.encodeResponse(response);
52 | process.stdout.write(encodedResponse + '\n');
53 | }
54 |
55 | /**
56 | * Создает стандартный ответ с ошибкой
57 | */
58 | static createErrorResponse(error: unknown): BaseErrorResponse {
59 | if (error instanceof Error) {
60 | return {
61 | status: 'error',
62 | error: error.message,
63 | stack: error.stack,
64 | };
65 | }
66 |
67 | return {
68 | status: 'error',
69 | error: String(error),
70 | };
71 | }
72 |
73 | /**
74 | * Создает стандартный успешный ответ
75 | */
76 | static createSuccessResponse(result: T): BaseSuccessResponse {
77 | return {
78 | status: 'success',
79 | result: result,
80 | };
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/types/storage.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Типы для системы постоянного хранилища данных
3 | */
4 |
5 | import type { Database } from './database';
6 | import type { Dashboard } from './dashboard';
7 | import type { WidgetMetadata } from './widget';
8 |
9 | /**
10 | * Структура настроек приложения
11 | */
12 | export interface AppSettings {
13 | theme: 'light' | 'dark' | 'system';
14 | language: 'en' | 'ru';
15 | autoSave: boolean;
16 | backupInterval: number; // в минутах
17 | maxBackups: number;
18 | telemetryEnabled: boolean;
19 | debugMode: boolean;
20 | windowSize?: {
21 | width: number;
22 | height: number;
23 | };
24 | windowPosition?: {
25 | x: number;
26 | y: number;
27 | };
28 | }
29 |
30 | /**
31 | * Структура сообщения в чате
32 | */
33 | export interface ChatMessage {
34 | id: string;
35 | role: 'user' | 'assistant' | 'system';
36 | content: string;
37 | timestamp: Date;
38 | metadata?: {
39 | tokens?: number;
40 | model?: string;
41 | executionTime?: number;
42 | };
43 | }
44 |
45 | /**
46 | * Структура чата
47 | */
48 | export interface Chat {
49 | id: string;
50 | title: string;
51 | messages: ChatMessage[];
52 | createdAt: Date;
53 | updatedAt: Date;
54 | metadata?: {
55 | databaseId?: string;
56 | dashboardId?: string;
57 | tags?: string[];
58 | };
59 | }
60 |
61 | /**
62 | * Структура истории чатов (краткая информация)
63 | */
64 | export interface ChatHistoryItem {
65 | id: string;
66 | title: string;
67 | lastMessage?: string;
68 | messageCount: number;
69 | createdAt: Date;
70 | updatedAt: Date;
71 | metadata?: {
72 | databaseId?: string;
73 | dashboardId?: string;
74 | tags?: string[];
75 | };
76 | }
77 |
78 | /**
79 | * Структура экспорта данных приложения
80 | */
81 | export interface AppDataExport {
82 | version: string;
83 | exportedAt: Date;
84 | data: {
85 | databases: Database[];
86 | dashboards: Dashboard[];
87 | widgets: WidgetMetadata[];
88 | chatHistory: ChatHistoryItem[];
89 | chats: Chat[];
90 | settings: AppSettings;
91 | };
92 | }
93 |
94 | /**
95 | * Структура импорта данных приложения
96 | */
97 | export interface AppDataImport {
98 | databases?: Database[];
99 | dashboards?: Dashboard[];
100 | widgets?: WidgetMetadata[];
101 | chatHistory?: ChatHistoryItem[];
102 | chats?: Chat[];
103 | settings?: Partial;
104 | }
105 |
--------------------------------------------------------------------------------
/src/platform/tauri/dialogs.ts:
--------------------------------------------------------------------------------
1 | import { DialogManager, ConfirmOptions, AlertOptions, MessageBoxOptions } from '../abstract/dialogs';
2 | import { confirm, message } from '@tauri-apps/plugin-dialog';
3 | import { open, save } from '@tauri-apps/plugin-dialog';
4 |
5 | /**
6 | * Tauri-реализация менеджера диалогов
7 | */
8 | export class TauriDialogManager implements DialogManager {
9 | async confirm(message: string, options?: ConfirmOptions): Promise {
10 | return await confirm(message, {
11 | title: options?.title || 'Подтверждение',
12 | kind: options?.kind || 'warning',
13 | okLabel: options?.okLabel,
14 | cancelLabel: options?.cancelLabel,
15 | });
16 | }
17 |
18 | async alert(msg: string, options?: AlertOptions): Promise {
19 | await message(msg, {
20 | title: options?.title || 'Информация',
21 | kind: options?.kind || 'info',
22 | okLabel: options?.okLabel,
23 | });
24 | }
25 |
26 | async messageBox(msg: string, options?: MessageBoxOptions): Promise {
27 | // Tauri не поддерживает произвольные кнопки в диалогах,
28 | // поэтому используем confirm для простых случаев
29 | if (!options?.buttons || options.buttons.length <= 2) {
30 | const result = await this.confirm(msg, {
31 | title: options?.title,
32 | kind: options?.type === 'error' ? 'error' : options?.type === 'warning' ? 'warning' : 'info',
33 | });
34 | return result ? 0 : 1;
35 | }
36 |
37 | // Для сложных диалогов используем alert и возвращаем 0
38 | await this.alert(msg, {
39 | title: options.title,
40 | kind: options.type === 'error' ? 'error' : options.type === 'warning' ? 'warning' : 'info',
41 | });
42 | return 0;
43 | }
44 |
45 | async openFile(options?: {
46 | title?: string;
47 | filters?: { name: string; extensions: string[] }[];
48 | multiple?: boolean;
49 | directory?: boolean;
50 | }): Promise {
51 | const result = await open({
52 | title: options?.title,
53 | filters: options?.filters,
54 | multiple: options?.multiple,
55 | directory: options?.directory,
56 | });
57 |
58 | return result;
59 | }
60 |
61 | async saveFile(options?: {
62 | title?: string;
63 | defaultPath?: string;
64 | filters?: { name: string; extensions: string[] }[];
65 | }): Promise {
66 | return await save({
67 | title: options?.title,
68 | defaultPath: options?.defaultPath,
69 | filters: options?.filters,
70 | });
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/public/tauri.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/pages/WidgetsLibrary/WidgetsSidebar.tsx:
--------------------------------------------------------------------------------
1 | import { observer } from 'mobx-react';
2 | import { Button } from '@/components/ui/button';
3 | import { widgetsLibraryStore } from '@/stores/WidgetsLibrary';
4 | import { Trash2, Plus } from 'lucide-react';
5 | import { widgetsRepositoryStore } from '@/stores/WidgetsRepository';
6 |
7 | export const WidgetsSidebar = observer(() => {
8 | return (
9 |
10 |
11 | {/* Header */}
12 |
13 |
Виджеты
14 |
18 |
19 |
20 | {/* Widgets List */}
21 |
22 | {widgetsRepositoryStore.isLoading ? (
23 |
24 |
Загрузка виджетов...
25 |
26 | ) : widgetsRepositoryStore.allWidgets.length === 0 ? (
27 |
28 |
Нет виджетов
29 |
Создайте первый виджет
30 |
31 | ) : (
32 |
33 | {widgetsRepositoryStore.allWidgets.map(widget => (
34 |
widgetsLibraryStore.selectWidget(widget.id)}
41 | >
42 |
43 |
44 |
{widget.name}
45 |
46 |
57 |
58 |
59 | ))}
60 |
61 | )}
62 |
63 |
64 |
65 | );
66 | });
67 |
--------------------------------------------------------------------------------
/src/components/DashboardsList.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { observer } from 'mobx-react';
3 | import { Layout, Plus } from 'lucide-react';
4 | import { Button } from './ui/button';
5 | import { DashboardFormModal } from './modals/DashboardFormModal';
6 | import { appTabsStore } from '@/stores/AppTabsStore';
7 | import { dashboardsRepository } from '@/stores/DashboardsRepository';
8 | import { DashboardCard } from './DashboardCard';
9 | import { EmptyState } from './EmptyState';
10 |
11 | const DashboardsList: React.FC = observer(() => {
12 | const { allDashboards } = dashboardsRepository;
13 | const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
14 |
15 | const handleDashboardClick = (dashboardId: string, dashboardTitle: string, dashboardIcon?: string) => {
16 | appTabsStore.openDashboard(dashboardId, dashboardTitle, dashboardIcon);
17 | };
18 |
19 | const handleCreateDashboard = () => {
20 | setIsCreateDialogOpen(true);
21 | };
22 |
23 | const handleCloseCreateDialog = () => {
24 | setIsCreateDialogOpen(false);
25 | };
26 |
27 | return (
28 |
29 |
30 | {/* Заголовок */}
31 |
32 |
33 |
Дешборды
34 |
Управляйте своими дешбордами и создавайте новые
35 |
36 |
40 |
41 |
42 | {/* Список дешбордов */}
43 | {allDashboards.length === 0 ? (
44 |
53 | ) : (
54 |
55 | {allDashboards.map(dashboard => (
56 |
61 | ))}
62 |
63 | )}
64 |
65 |
66 | {/* Модальное окно создания дешборда */}
67 |
68 |
69 | );
70 | });
71 |
72 | export { DashboardsList };
73 |
--------------------------------------------------------------------------------
/src/pages/WidgetsLibrary/WidgetPreview.tsx:
--------------------------------------------------------------------------------
1 | import { observer } from 'mobx-react';
2 | import { createElement } from 'react';
3 | import { widgetsLibraryStore } from '@/stores/WidgetsLibrary';
4 | import { ErrorBoundary } from '@/components/ErrorBoundary';
5 |
6 | export const WidgetPreview = observer(() => {
7 | if (!widgetsLibraryStore.runtime.compiledBundle) {
8 | return (
9 |
10 |
⚙️
11 |
12 | {(() => {
13 | if (widgetsLibraryStore.compilationError) {
14 | return `Error: ${widgetsLibraryStore.compilationError}`;
15 | }
16 | if (widgetsLibraryStore.isCompiling) {
17 | return 'Компиляция бандла...';
18 | }
19 | return 'Нажмите "Скомпилировать" для просмотра';
20 | })()}
21 |
22 |
23 | );
24 | }
25 |
26 | if (!widgetsLibraryStore.previewDatabaseId) {
27 | return (
28 |
29 |
⚙️
30 |
Выберите базу данных для просмотра
31 |
32 | );
33 | }
34 |
35 | if (!widgetsLibraryStore.runtime.runtimeComponent) {
36 | return (
37 |
38 |
⚙️
39 |
Компиляция компонента...
40 |
41 | );
42 | }
43 |
44 | const renderPreview = () => {
45 | return (
46 | (
48 |
49 |
50 |
💥
51 |
Ошибка рендеринга
52 |
53 | {error instanceof Error ? error.message : 'Неизвестная ошибка'}
54 |
55 |
56 |
57 | )}
58 | >
59 |
60 | {createElement(widgetsLibraryStore.runtime.runtimeComponent!.component, {})}
61 |
62 |
63 | );
64 | };
65 |
66 | return (
67 |
68 |
69 |
70 | {widgetsLibraryStore.compilationError ? (
71 |
72 |
{widgetsLibraryStore.compilationError}
73 |
74 | ) : (
75 | renderPreview()
76 | )}
77 |
78 |
79 |
80 | );
81 | });
82 |
--------------------------------------------------------------------------------
/__legacy/compiler/index.ts:
--------------------------------------------------------------------------------
1 | import { VirtualFile, CompileOptions, CompileResult } from '../types/index.js';
2 | import { createError } from '../utils/index.js';
3 | import { ESBuildCompiler } from '../../src-node/src/compiler/esbuild-compiler.js';
4 | import { PostCSSCompiler } from './postcss-compiler.js';
5 |
6 | /**
7 | * Основной компилятор, который координирует работу всех компиляторов
8 | */
9 | export class Compiler {
10 | private esbuildCompiler: ESBuildCompiler;
11 | private postcssCompiler: PostCSSCompiler;
12 |
13 | constructor() {
14 | this.esbuildCompiler = new ESBuildCompiler();
15 | this.postcssCompiler = new PostCSSCompiler();
16 | }
17 |
18 | /**
19 | * Компилирует виртуальные файлы в JavaScript и CSS
20 | */
21 | async compile(files: VirtualFile[], options: CompileOptions = {}): Promise {
22 | try {
23 | // Валидация входных данных
24 | this.validateFiles(files);
25 |
26 | // Параллельная компиляция JavaScript и CSS
27 | const [jsResult, css] = await Promise.all([
28 | this.esbuildCompiler.compile(files, options),
29 | this.postcssCompiler.compile(files, options),
30 | ]);
31 |
32 | return {
33 | javascript: jsResult.code,
34 | css: css,
35 | sourcemap: jsResult.sourcemap,
36 | };
37 | } catch (error) {
38 | throw createError('Ошибка компиляции', error);
39 | }
40 | }
41 |
42 | /**
43 | * Валидирует входные файлы
44 | */
45 | private validateFiles(files: VirtualFile[]): void {
46 | if (!files || files.length === 0) {
47 | throw createError('Не предоставлены файлы для компиляции');
48 | }
49 |
50 | // Проверяем, что есть хотя бы один .tsx файл
51 | const hasTsxFiles = files.some(file => file.path.endsWith('.tsx'));
52 | if (!hasTsxFiles) {
53 | throw createError('Отсутствуют .tsx файлы для компиляции');
54 | }
55 |
56 | // Валидируем каждый файл
57 | for (const file of files) {
58 | if (!file.path) {
59 | throw createError('Файл должен иметь путь');
60 | }
61 |
62 | if (typeof file.content !== 'string') {
63 | throw createError(`Содержимое файла ${file.path} должно быть строкой`);
64 | }
65 |
66 | // Проверяем допустимые расширения
67 | const allowedExtensions = ['.tsx', '.ts', '.jsx', '.js', '.css', '.json'];
68 | const hasAllowedExtension = allowedExtensions.some(ext => file.path.endsWith(ext));
69 |
70 | if (!hasAllowedExtension) {
71 | throw createError(
72 | `Файл ${file.path} имеет недопустимое расширение. ` + `Разрешены: ${allowedExtensions.join(', ')}`,
73 | );
74 | }
75 | }
76 | }
77 | }
78 |
79 | // Экспортируем также отдельные компиляторы для гибкости
80 | export { ESBuildCompiler } from '../../src-node/src/compiler/esbuild-compiler.js';
81 | export { PostCSSCompiler } from './postcss-compiler.js';
82 |
--------------------------------------------------------------------------------
/__legacy/src-node/src/index.ts:
--------------------------------------------------------------------------------
1 | import { BaseResponse, CommandHandler, IPCProtocol } from './ipc/index.js';
2 | import { CompileCommandHandler } from './request-handler/index.js';
3 | import { CompileComponentRequest, CompileComponentResponse } from './types/index.js';
4 |
5 | /**
6 | * Обработчик команды ping для проверки работоспособности
7 | */
8 | async function handlePingCommand(request: { command: 'ping'; message?: string }): Promise<{ message: string }> {
9 | const message = request.message || 'qYp-mini';
10 | return { message: `pong, ${message}` };
11 | }
12 |
13 | /**
14 | * Настройка и запуск приложения
15 | */
16 | async function main(): Promise {
17 | // Инициализируем обработчики команд
18 | const commandHandler = new CommandHandler();
19 | const compileHandler = new CompileCommandHandler();
20 |
21 | // Регистрируем команду compile
22 | commandHandler.registerCommand('compile', async request => {
23 | return await compileHandler.handle(request);
24 | });
25 |
26 | // Регистрируем команду ping (для тестирования через IPC)
27 | commandHandler.registerCommand<{ command: 'ping'; message?: string }, { message: string }>(
28 | 'ping',
29 | handlePingCommand,
30 | );
31 |
32 | // Запускаем обработку команд через stdin/stdout
33 | await commandHandler.start();
34 | }
35 |
36 | /**
37 | * Показывает справку по использованию
38 | */
39 | function showHelp(): void {
40 | console.log(`
41 | qYp-mini Node.js Compiler Service
42 |
43 | Использование:
44 | node index.js compile - Запуск в режиме компиляции (основной режим)
45 | node index.js ping - Проверка работоспособности (простой режим)
46 | node index.js help - Показать эту справку
47 |
48 | Режим компиляции:
49 | Читает base64-encoded JSON запросы из stdin и возвращает результаты в stdout.
50 |
51 | Формат запроса для compile:
52 | {
53 | "command": "compile",
54 | "files": [
55 | {
56 | "path": "index.tsx",
57 | "content": "import React from 'react'; ..."
58 | }
59 | ],
60 | "options": {
61 | "minify": false,
62 | "sourcemap": true
63 | }
64 | }
65 |
66 | Формат запроса для ping:
67 | {
68 | "command": "ping",
69 | "message": "test"
70 | }
71 | `);
72 | }
73 |
74 | // Обработчики для некорректного завершения процесса
75 | process.on('uncaughtException', error => {
76 | console.error('Необработанное исключение:', error);
77 | process.exit(1);
78 | });
79 |
80 | process.on('unhandledRejection', (reason, promise) => {
81 | console.error('Необработанное отклонение промиса:', reason);
82 | process.exit(1);
83 | });
84 |
85 | // Запуск приложения
86 | main().catch(error => {
87 | console.error('Ошибка запуска:', error);
88 | process.exit(1);
89 | });
90 |
--------------------------------------------------------------------------------
/src/components/database/DatabaseCard.tsx:
--------------------------------------------------------------------------------
1 | import { Card } from '@/components/ui/card';
2 | import { confirmDialog } from '@/lib/dialogs';
3 | import { observer } from 'mobx-react';
4 | import { Database } from '@/types/database';
5 | import { databaseStore } from '@/stores/DatabaseStore';
6 | import { Button } from '@/components/ui/button';
7 | import { Database as DatabaseIcon } from 'lucide-react';
8 |
9 | export const DatabaseCard = observer(
10 | ({ database, onEdit }: { database: Database; onEdit: (database: Database) => void }) => {
11 | // Обработчик удаления базы данных
12 | const handleDelete = async (database: Database) => {
13 | const isConfirmed = await confirmDialog(
14 | `Вы уверены, что хотите удалить базу данных "${database.name}"? Это действие нельзя отменить.`,
15 | );
16 |
17 | if (isConfirmed) {
18 | await databaseStore.removeDatabase(database.id);
19 | }
20 | };
21 |
22 | return (
23 |
24 |
25 |
26 |
27 |
28 |
{database.name}
29 |
30 |
31 |
32 |
33 |
34 |
35 | Хост:
36 | {database.credentials.host}
37 |
38 |
39 | Порт:
40 | {database.credentials.port || 5432}
41 |
42 |
43 | База:
44 | {database.credentials.database}
45 |
46 |
47 | Пользователь:
48 | {database.credentials.username}
49 |
50 | {database.connection.version && (
51 |
52 | Версия:
53 | {database.connection.version?.split(',')[0]}
54 |
55 | )}
56 |
57 |
58 |
59 |
60 |
63 |
66 |
67 |
68 |
69 | );
70 | },
71 | );
72 |
--------------------------------------------------------------------------------
/src/platform/abstract/database.ts:
--------------------------------------------------------------------------------
1 | import { observable, makeObservable, computed, action } from 'mobx';
2 | import { DatabaseCredentials } from '@/types/database';
3 |
4 | /**
5 | * Статус подключения к базе данных
6 | */
7 | export type ConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'error';
8 |
9 | /**
10 | * Абстрактный класс подключения к БД
11 | * Две реализации: для Tauri и для Browser
12 | */
13 | export abstract class DatabaseConnection {
14 | @observable protected _status: ConnectionStatus = 'disconnected';
15 | @observable protected _lastError?: string;
16 | @observable protected _version?: string;
17 |
18 | constructor(
19 | public readonly databaseId: string,
20 | protected readonly credentials: DatabaseCredentials,
21 | ) {
22 | makeObservable(this);
23 | }
24 |
25 | @computed
26 | get status(): ConnectionStatus {
27 | return this._status;
28 | }
29 |
30 | @computed
31 | get lastError(): string | undefined {
32 | return this._lastError;
33 | }
34 |
35 | @computed
36 | get version(): string | undefined {
37 | return this._version;
38 | }
39 |
40 | /**
41 | * Устанавливает подключение к БД
42 | */
43 | abstract connect(): Promise;
44 |
45 | /**
46 | * Закрывает подключение к БД
47 | */
48 | abstract disconnect(): Promise;
49 |
50 | /**
51 | * Выполняет SQL запрос
52 | */
53 | abstract select(sql: string, params?: any[]): Promise;
54 |
55 | /**
56 | * Проверяет активность подключения
57 | */
58 | @computed
59 | get isConnected(): boolean {
60 | return this._status === 'connected';
61 | }
62 |
63 | /**
64 | * Получает информацию о подключении
65 | */
66 | @computed
67 | get connectionInfo() {
68 | return {
69 | host: this.credentials.host,
70 | database: this.credentials.database,
71 | username: this.credentials.username,
72 | version: this._version,
73 | };
74 | }
75 |
76 | /**
77 | * Обновляет статус подключения
78 | */
79 | @action
80 | protected updateStatus(newStatus: ConnectionStatus, error?: string): void {
81 | this._status = newStatus;
82 | this._lastError = error;
83 |
84 | if (newStatus === 'disconnected') {
85 | this._version = undefined;
86 | }
87 | }
88 |
89 | /**
90 | * Устанавливает версию БД
91 | */
92 | @action
93 | protected setVersion(version: string): void {
94 | this._version = version;
95 | }
96 | }
97 |
98 | /**
99 | * Менеджер подключений к БД для конкретной платформы
100 | * Часть PlatformManager абстракции
101 | */
102 | export interface DatabaseManager {
103 | /**
104 | * Создает подключение к БД для данной платформы
105 | */
106 | createConnection(databaseId: string, credentials: DatabaseCredentials): DatabaseConnection;
107 | }
108 |
--------------------------------------------------------------------------------
/src/lib/compiler/TailwindCompiler.ts:
--------------------------------------------------------------------------------
1 | import { compile } from 'tailwindcss';
2 | // @ts-ignore
3 | import { defaultExtractor as createDefaultExtractor } from 'tailwindcss-v3/lib/lib/defaultExtractor';
4 |
5 | import tailwindcssFile from 'tailwindcss/index.css?raw';
6 | import { VirtualFS } from '@/virtual-fs/VirtualFS';
7 |
8 | import initLightningCssModule, * as lightningcss from 'lightningcss-wasm';
9 | import lightningcssWasmModule from 'lightningcss-wasm/lightningcss_node.wasm?url';
10 |
11 | const moduleLoaded = initLightningCssModule(lightningcssWasmModule);
12 |
13 | export class TailwindCompiler {
14 | private readonly defaultExtractor: (content: string) => string[];
15 |
16 | constructor() {
17 | this.defaultExtractor = createDefaultExtractor({
18 | tailwindConfig: { separator: ':' },
19 | });
20 | }
21 |
22 | getBaseCss() {
23 | return `@import 'tailwindcss';`;
24 | }
25 |
26 | /**
27 | * Проходит по всем файлам в виртуальной файловой системе,
28 | * извлекает CSS кандидатов из файлов, которые не отмечены как externalized
29 | * @returns массив уникальных CSS кандидатов
30 | */
31 | buildCandidates(vfs: VirtualFS): string[] {
32 | const candidatesSet = new Set();
33 |
34 | // Проходим по всем файлам в VFS
35 | for (const [_filePath, fileNode] of vfs.filesNodes) {
36 | // Пропускаем файлы, отмеченные как externalized
37 | if (fileNode.metadata.externalized === true) {
38 | continue;
39 | }
40 |
41 | // Извлекаем кандидатов из содержимого файла
42 | const fileCandidates = this.defaultExtractor(fileNode.content);
43 |
44 | // Добавляем всех кандидатов в глобальный Set
45 | fileCandidates.forEach(candidate => candidatesSet.add(candidate));
46 | }
47 |
48 | // Возвращаем массив уникальных кандидатов
49 | return Array.from(candidatesSet);
50 | }
51 |
52 | async compile(vfs: VirtualFS, baseCss: string = this.getBaseCss()) {
53 | const result = await compile(baseCss, {
54 | loadStylesheet: async url => {
55 | if (url === 'tailwindcss') {
56 | return {
57 | path: url,
58 | base: url,
59 | content: tailwindcssFile,
60 | };
61 | } else {
62 | throw new Error(`Unknown stylesheet: ${url}`);
63 | }
64 | },
65 | });
66 |
67 | const intermediateCss = await result.build(this.buildCandidates(vfs));
68 |
69 | await moduleLoaded;
70 |
71 | const resultCss = new TextDecoder().decode(
72 | lightningcss.transform({
73 | filename: 'input.css',
74 | code: new TextEncoder().encode(intermediateCss),
75 | drafts: {
76 | customMedia: true,
77 | },
78 | nonStandard: {
79 | deepSelectorCombinator: true,
80 | },
81 | include: lightningcss.Features.Nesting,
82 | exclude: lightningcss.Features.LogicalProperties,
83 | targets: {
84 | safari: (16 << 16) | (4 << 8),
85 | },
86 | errorRecovery: true,
87 | }).code,
88 | );
89 |
90 | return resultCss;
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/pages/WelcomePage.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { Search, Mic, Send, Key, Database, Shuffle, Settings } from 'lucide-react';
3 | import { Textarea } from '@/components/ui/textarea';
4 | import { Button } from '@/components/ui/button';
5 | import { Badge } from '@/components/ui/badge';
6 |
7 | export function WelcomePage() {
8 | const [query, setQuery] = useState('');
9 |
10 | const actionButtons = [
11 | { icon: Key, label: 'Manage DB credentials', variant: 'secondary' as const },
12 | { icon: Database, label: 'Explore DB', variant: 'secondary' as const },
13 | { icon: Shuffle, label: 'Switch AI-model', variant: 'secondary' as const },
14 | { icon: Settings, label: 'Edit settings', variant: 'secondary' as const },
15 | ];
16 |
17 | const handleSubmit = (e: React.FormEvent) => {
18 | e.preventDefault();
19 | // Обработка поискового запроса
20 | console.log('Search query:', query);
21 | setQuery('Response: ' + query);
22 | };
23 |
24 | return (
25 |
26 | {/* Заголовок */}
27 |
28 |
qYp.ai mini
29 |
30 |
31 | {/* Основная форма поиска */}
32 |
59 |
60 | {/* Кнопки действий */}
61 |
62 | {actionButtons.map(({ icon: Icon, label, variant }) => (
63 |
68 |
69 | {label}
70 |
71 | ))}
72 |
73 |
74 | );
75 | }
76 |
--------------------------------------------------------------------------------
/src/virtual-fs/default-fs.ts:
--------------------------------------------------------------------------------
1 | import { VirtualFS } from '@/virtual-fs/VirtualFS';
2 | import { buildShadcnUiFS } from './shadcn-ui';
3 |
4 | /**
5 | * Virtual directory structure:
6 | * src/ # Корневая директория
7 | * ├── components/
8 | * │ ├── ui/ # Базовые UI-компоненты, readonly (shadcn)
9 | * ├── widget/
10 | * │ ├── index.tsx # Главный файл, которые будет генерировать ИИ - React-компонент с виджетом
11 | * │ ├── query.sql.ts # Файл с sql-запросом в базу, экспортирует одну async-функцию, делающую запрос. Тоже генерирует ИИ
12 | * ├── lib/
13 | * │ ├── utils.ts # readonly, здесь будут утилитарные функции аля `cn`
14 | */
15 |
16 | export const getDefaultWidgetIndexTsxContent = () => `import { useState, useEffect, useCallback } from 'react';
17 | import { Button } from '@/components/ui/button';
18 | import fetchCustomersCount from '@/widget/query.sql';
19 |
20 | export default function MyComponent() {
21 | const [customersCount, setCustomersCount] = useState(0);
22 | const [loading, setLoading] = useState(false);
23 |
24 | const reloadCustomersCount = useCallback(async () => {
25 | setLoading(true);
26 | const rows = await fetchCustomersCount();
27 | setCustomersCount(rows.length > 0 ? rows[0].count : -1);
28 | setLoading(false);
29 | }, []);
30 |
31 | useEffect(() => {
32 | reloadCustomersCount();
33 | }, [reloadCustomersCount]);
34 |
35 | return (
36 |
37 |
Customers Count
38 |
39 | {loading ? 'Loading...' : customersCount}
40 |
41 |
42 |
43 | );
44 | }`;
45 |
46 | export const getDefaultWidgetQuerySqlTsContent = () => `import { runSql } from '@/lib/utils';
47 |
48 | /**
49 | * This function used to fetch customers cound data from the database.
50 | */
51 |
52 | export default async function fetchCustomersCount() {
53 | return await runSql<{ count: number }[]>(\`SELECT COUNT(*) as count FROM customer\`);
54 | }
55 | `;
56 |
57 | export const buildDefaultFS = async (
58 | indexTsxContent: string = getDefaultWidgetIndexTsxContent(),
59 | querySqlTsContent: string = getDefaultWidgetQuerySqlTsContent(),
60 | ): Promise => {
61 | const vfs = new VirtualFS();
62 |
63 | vfs.makeDirectory('/src');
64 |
65 | vfs.makeDirectory('/src/components', { readonly: true });
66 | vfs.makeDirectory('/src/components/ui');
67 |
68 | await buildShadcnUiFS(vfs);
69 |
70 | vfs.makeDirectory('/src/widget');
71 | vfs.writeFile('/src/widget/index.tsx', indexTsxContent);
72 | vfs.writeFile('/src/widget/query.sql.ts', querySqlTsContent);
73 |
74 | vfs.makeDirectory('/src/lib', { readonly: true });
75 | vfs.writeFile('/src/lib/utils.ts', `// nothing here for now`, {
76 | externalized: true,
77 | });
78 |
79 | return vfs;
80 | };
81 |
--------------------------------------------------------------------------------
/src/pages/DatabasePage.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { observer } from 'mobx-react';
3 | import { Button } from '@/components/ui/button';
4 | import { Database } from '@/types/database';
5 | import { DatabaseFormModal } from '@/components/modals/DatabaseFormModal';
6 | import { Database as DatabaseIcon } from 'lucide-react';
7 | import { databaseStore } from '@/stores/DatabaseStore';
8 | import { PageHeader } from '@/components/PageHeader';
9 | import { DatabaseCard } from '@/components/database/DatabaseCard';
10 |
11 | export const DatabasePage: React.FC = observer(() => {
12 | const [isFormOpen, setIsFormOpen] = useState(false);
13 | const [editingDatabase, setEditingDatabase] = useState(null);
14 |
15 | // Обработчики для модального окна
16 | const handleOpenForm = (database?: Database) => {
17 | setEditingDatabase(database || null);
18 | setIsFormOpen(true);
19 | };
20 |
21 | const handleCloseForm = () => {
22 | setIsFormOpen(false);
23 | setEditingDatabase(null);
24 | };
25 |
26 | // Обработчик создания подключения к тестовой БД
27 | const handleCreateTestDatabase = async () => {
28 | try {
29 | await databaseStore.createDatabase({
30 | name: 'Тестовая БД (Pagila)',
31 | credentials: {
32 | host: 'example-database-do-user-1261283-0.j.db.ondigitalocean.com',
33 | port: 25060,
34 | username: 'readonly',
35 | password: 'AVNS_PHiKtaU9YG1vRyKWjGV',
36 | database: 'pagila',
37 | type: 'postgres',
38 | ssl: true,
39 | },
40 | });
41 | } catch (error) {
42 | console.error('Ошибка создания тестовой БД:', error);
43 | }
44 | };
45 |
46 | return (
47 |
48 | {/* Заголовок */}
49 |
50 |
51 |
52 |
53 | {/* Список баз данных */}
54 |
55 | {databaseStore.allDatabases.length === 0 ? (
56 |
57 |
58 |
Нет подключений к базам данных
59 |
60 | Создайте первое подключение для начала работы
61 |
62 |
63 |
64 |
67 |
68 |
69 | ) : (
70 |
71 | {databaseStore.allDatabases.map(database => (
72 |
73 | ))}
74 |
75 | )}
76 |
77 |
78 | {/* Модальное окно формы */}
79 |
80 |
81 | );
82 | });
83 |
--------------------------------------------------------------------------------
/src/platform/tauri/database.ts:
--------------------------------------------------------------------------------
1 | import Database from '@tauri-apps/plugin-sql';
2 | import { DatabaseConnection, DatabaseManager } from '../abstract/database';
3 | import { DatabaseCredentials } from '@/types/database';
4 |
5 | /**
6 | * Реализация подключения к БД для Tauri через tauri-plugin-sql
7 | */
8 | export class TauriDatabaseConnection extends DatabaseConnection {
9 | private _database?: Database;
10 |
11 | constructor(databaseId: string, credentials: DatabaseCredentials) {
12 | super(databaseId, credentials);
13 | }
14 |
15 | async connect(): Promise {
16 | if (this._status === 'connected') {
17 | return;
18 | }
19 |
20 | try {
21 | this.updateStatus('connecting');
22 |
23 | // Формируем connection string для PostgreSQL
24 | const connectionString = `postgres://${this.credentials.username}:${this.credentials.password}@${this.credentials.host}:${this.credentials.port || 5432}/${this.credentials.database}`;
25 |
26 | console.log('connectionString', connectionString);
27 |
28 | // Подключаемся через Tauri SQL plugin
29 | this._database = await Database.load(connectionString);
30 |
31 | console.log('yappy yappy ya');
32 |
33 | this.updateStatus('connected');
34 |
35 | // Получаем версию PostgreSQL
36 | try {
37 | const versionResult = await this._database.select<{ version: string }[]>('SELECT version()');
38 | if (versionResult.length > 0) {
39 | this.setVersion(versionResult[0].version);
40 | }
41 | } catch (versionError) {
42 | console.warn('Не удалось получить версию PostgreSQL:', versionError);
43 | // Не критично - продолжаем работу
44 | }
45 | } catch (error) {
46 | const errorMessage = error instanceof Error ? error.message : 'Неизвестная ошибка подключения';
47 | this.updateStatus('error', errorMessage);
48 | throw error;
49 | }
50 | }
51 |
52 | async disconnect(): Promise {
53 | if (this._database) {
54 | try {
55 | await this._database.close();
56 | } catch (error) {
57 | console.error('Ошибка при закрытии подключения:', error);
58 | }
59 | }
60 |
61 | this._database = undefined;
62 | this.updateStatus('disconnected');
63 | }
64 |
65 | async select(sql: string, params?: any[]): Promise {
66 | if (!this.isConnected || !this._database) {
67 | await this.connect();
68 | }
69 |
70 | if (!this._database) {
71 | throw new Error('Connection is not established');
72 | }
73 |
74 | try {
75 | // Выполняем запрос через Tauri SQL plugin
76 | const result = await this._database.select(sql, params);
77 |
78 | // Формируем результат в стандартном формате
79 | return result;
80 | } catch (error) {
81 | console.error('Error executing SQL query:', error);
82 | throw new Error(`Error executing query: ${error instanceof Error ? error.message : 'Unknown error'}`);
83 | }
84 | }
85 | }
86 |
87 | /**
88 | * Менеджер подключений к БД для Tauri платформы
89 | */
90 | export class TauriDatabaseManager implements DatabaseManager {
91 | createConnection(databaseId: string, credentials: DatabaseCredentials): DatabaseConnection {
92 | return new TauriDatabaseConnection(databaseId, credentials);
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/stores/WidgetsRepository.ts:
--------------------------------------------------------------------------------
1 | import { observable, action, makeObservable, computed } from 'mobx';
2 | import { WidgetMetadata } from '@/types/widget';
3 | import { persistentStorage } from '@/lib/PersistentStorage';
4 |
5 | export class WidgetsRepositoryStore {
6 | // Хранилище виджетов (только метаданные с бандлами)
7 | @observable private widgets = new Map();
8 | @observable isLoading = false;
9 |
10 | constructor() {
11 | makeObservable(this);
12 | }
13 |
14 | // Вычисляемые свойства
15 | @computed
16 | get allWidgets(): WidgetMetadata[] {
17 | return Array.from(this.widgets.values());
18 | }
19 |
20 | getWidget(id: string): WidgetMetadata | undefined {
21 | return this.widgets.get(id);
22 | }
23 |
24 | /**
25 | * Добавляет новый виджет
26 | */
27 | @action
28 | async addWidget(data: Omit): Promise {
29 | const id = `widget_${Date.now()}`;
30 |
31 | const widgetMetadata: WidgetMetadata = {
32 | id,
33 | ...data,
34 | };
35 |
36 | this.widgets.set(id, widgetMetadata);
37 |
38 | // Сохраняем виджет в файловую систему
39 | await persistentStorage.saveWidgetMetadata(id, widgetMetadata);
40 |
41 | return widgetMetadata;
42 | }
43 |
44 | /**
45 | * Обновляет существующий виджет
46 | */
47 | @action
48 | async updateWidget(id: string, data: WidgetMetadata): Promise {
49 | const existingWidget = this.widgets.get(id);
50 | if (!existingWidget) {
51 | throw new Error(`Виджет с ID "${id}" не найден`);
52 | }
53 |
54 | const updatedMetadata: WidgetMetadata = {
55 | ...existingWidget,
56 | ...data,
57 | };
58 |
59 | this.widgets.set(id, updatedMetadata);
60 |
61 | // Сохраняем обновленный виджет в файловую систему
62 | await persistentStorage.saveWidgetMetadata(id, updatedMetadata);
63 |
64 | return updatedMetadata;
65 | }
66 |
67 | /**
68 | * Удаляет виджет из репозитория
69 | */
70 | @action
71 | async removeWidget(id: string): Promise {
72 | if (!this.widgets.has(id)) {
73 | return false;
74 | }
75 |
76 | this.widgets.delete(id);
77 |
78 | // Удаляем из файловой системы
79 | await persistentStorage.deleteWidgetMetadata(id);
80 |
81 | return true;
82 | }
83 |
84 | // === UI МЕТОДЫ ===
85 |
86 | /**
87 | * Загрузка всех виджетов из PersistentStorage
88 | */
89 | @action
90 | async loadWidgets() {
91 | this.isLoading = true;
92 | // Загружаем метаданные виджетов
93 | const widgetMetadataList: WidgetMetadata[] = await persistentStorage.loadAllWidgetMetadata();
94 |
95 | const loadedWidgets = new Map();
96 |
97 | for (const metadata of widgetMetadataList) {
98 | try {
99 | loadedWidgets.set(metadata.id, metadata);
100 | } catch (error) {
101 | console.error(`Ошибка загрузки виджета ${metadata.id}:`, error);
102 | // Пропускаем виджеты с ошибками
103 | }
104 | }
105 |
106 | this.widgets = loadedWidgets;
107 | this.isLoading = false;
108 | }
109 | }
110 |
111 | export const widgetsRepositoryStore = new WidgetsRepositoryStore();
112 |
113 | // @ts-ignore
114 | window.widgetsRepositoryStore = widgetsRepositoryStore;
115 |
--------------------------------------------------------------------------------
/__legacy/src-node/src/ipc/command-handler.ts:
--------------------------------------------------------------------------------
1 | import { createInterface } from 'readline';
2 | import { IPCProtocol } from './protocol.js';
3 | import { BaseRequest, BaseResponse } from './types.js';
4 |
5 | /**
6 | * Тип обработчика команды
7 | */
8 | export type CommandProcessor = (request: TRequest) => Promise;
9 |
10 | /**
11 | * Универсальный обработчик команд через stdin/stdout
12 | */
13 | export class CommandHandler {
14 | private processors = new Map();
15 |
16 | /**
17 | * Регистрирует обработчик команды
18 | */
19 | registerCommand(
20 | command: string,
21 | processor: CommandProcessor,
22 | ): void {
23 | this.processors.set(command, processor);
24 | }
25 |
26 | /**
27 | * Запускает обработку команд через stdin/stdout
28 | */
29 | async start(): Promise {
30 | const rl = createInterface({
31 | input: process.stdin,
32 | output: process.stdout,
33 | terminal: false,
34 | });
35 |
36 | rl.on('line', async (line: string) => {
37 | try {
38 | await this.processLine(line);
39 | } catch (error) {
40 | console.error('Критическая ошибка обработки строки:', error);
41 | this.sendErrorAndExit('Критическая ошибка обработки запроса', error);
42 | }
43 | });
44 |
45 | rl.on('close', () => {
46 | process.exit(0);
47 | });
48 |
49 | rl.on('error', error => {
50 | console.error('Ошибка чтения stdin:', error);
51 | this.sendErrorAndExit('Ошибка чтения stdin', error);
52 | });
53 | }
54 |
55 | /**
56 | * Обрабатывает одну строку входных данных
57 | */
58 | private async processLine(line: string): Promise {
59 | // Декодируем запрос
60 | const request = IPCProtocol.decodeRequest(line);
61 |
62 | if (!request) {
63 | this.sendErrorAndExit('Не удалось декодировать запрос');
64 | return;
65 | }
66 |
67 | // Находим обработчик команды
68 | const processor = this.processors.get(request.command);
69 |
70 | if (!processor) {
71 | this.sendErrorAndExit(`Неизвестная команда: ${request.command}`);
72 | return;
73 | }
74 |
75 | try {
76 | // Выполняем команду
77 | const result = await processor(request);
78 |
79 | // Отправляем результат
80 | const response = IPCProtocol.createSuccessResponse(result);
81 | IPCProtocol.sendResponse(response);
82 |
83 | // Завершаем процесс успешно
84 | process.exit(0);
85 | } catch (error) {
86 | console.error(`Ошибка выполнения команды ${request.command}:`, error);
87 | this.sendErrorAndExit(`Ошибка выполнения команды ${request.command}`, error);
88 | }
89 | }
90 |
91 | /**
92 | * Отправляет ошибку и завершает процесс
93 | */
94 | private sendErrorAndExit(message: string, originalError?: unknown): void {
95 | const errorResponse = IPCProtocol.createErrorResponse(
96 | originalError
97 | ? new Error(
98 | `${message}: ${originalError instanceof Error ? originalError.message : String(originalError)}`,
99 | )
100 | : new Error(message),
101 | );
102 |
103 | IPCProtocol.sendResponse(errorResponse);
104 | process.exit(1);
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/lib/compiler/ESBuildVFS.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import type { PluginBuild, Loader, OnLoadArgs, OnResolveArgs } from 'esbuild-wasm';
3 |
4 | import { VirtualFS } from '../../virtual-fs/VirtualFS.js';
5 |
6 | export class ESBuildVFS {
7 | name = 'virtual-files';
8 |
9 | constructor(private vfs: VirtualFS) {}
10 |
11 | get() {
12 | return {
13 | name: this.name,
14 | setup: this.setup,
15 | };
16 | }
17 |
18 | private setup = (build: PluginBuild) => {
19 | // Резолвим импорты виртуальных файлов
20 | build.onResolve({ filter: /.*/ }, this.handleResolve);
21 | // Загружаем содержимое виртуальных файлов
22 | build.onLoad({ filter: /.*/, namespace: 'virtual' }, this.handleLoad);
23 | };
24 |
25 | private handleResolve = (args: OnResolveArgs) => {
26 | // Пропускаем внешние модули (node_modules)
27 | if (!args.path.startsWith('.') && !args.path.startsWith('/') && !args.path.startsWith('@')) {
28 | return { external: true };
29 | }
30 |
31 | const resolvedPath = args.path.startsWith('@')
32 | ? args.path.replace('@/', '/src/')
33 | : this.resolveVirtualPath(args.path, args.importer);
34 |
35 | let foundPath: string | undefined = undefined;
36 |
37 | if (this.vfs.fileExists(resolvedPath)) {
38 | foundPath = resolvedPath;
39 | } else if (this.vfs.fileExists(resolvedPath + '.tsx')) {
40 | foundPath = resolvedPath + '.tsx';
41 | } else if (this.vfs.fileExists(resolvedPath + '.ts')) {
42 | foundPath = resolvedPath + '.ts';
43 | }
44 |
45 | if (foundPath) {
46 | const meta = this.vfs.readFileMetadata(foundPath);
47 |
48 | if (meta.externalized) {
49 | return { external: true };
50 | } else {
51 | return {
52 | path: foundPath,
53 | namespace: 'virtual',
54 | };
55 | }
56 | } else {
57 | return undefined;
58 | }
59 | };
60 |
61 | private handleLoad = (args: OnLoadArgs) => {
62 | try {
63 | const file = this.vfs.readFile(args.path);
64 | return {
65 | contents: file.content,
66 | loader: this.getLoader(args.path),
67 | };
68 | } catch (error) {
69 | throw new Error(`Ошибка загрузки виртуального файла ${args.path}`);
70 | }
71 | };
72 |
73 | /**
74 | * Резолвит путь в виртуальной файловой системе
75 | */
76 | private resolveVirtualPath(importPath: string, importer?: string): string {
77 | // Если путь абсолютный, возвращаем как есть
78 | if (path.isAbsolute(importPath)) {
79 | return path.resolve(importPath);
80 | }
81 |
82 | // Если есть импортер, резолвим относительно него
83 | if (importer) {
84 | const importerDir = path.dirname(importer);
85 | return path.resolve(importerDir, importPath);
86 | }
87 |
88 | // Иначе резолвим относительно корня
89 | return path.resolve('/', importPath);
90 | }
91 |
92 | /**
93 | * Определяет загрузчик для файла по расширению
94 | */
95 | private getLoader(filePath: string): Loader {
96 | if (filePath.endsWith('.tsx')) return 'tsx';
97 | if (filePath.endsWith('.ts')) return 'ts';
98 | if (filePath.endsWith('.jsx')) return 'jsx';
99 | if (filePath.endsWith('.js')) return 'js';
100 | if (filePath.endsWith('.css')) return 'css';
101 | if (filePath.endsWith('.json')) return 'json';
102 |
103 | return 'js'; // fallback
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src-back/src/routes/sql.ts:
--------------------------------------------------------------------------------
1 | import { Router, Request, Response } from 'express';
2 | import { DataSource } from 'typeorm';
3 | import { SqlRequest, SqlResponse, DatabaseCredentials } from '../types';
4 | import { sqlRateLimit } from '../middlewares/rateLimiter';
5 |
6 | const router = Router();
7 |
8 | const createDataSource = (credentials: DatabaseCredentials): DataSource => {
9 | const baseConfig = {
10 | host: credentials.host,
11 | port: credentials.port,
12 | username: credentials.username,
13 | password: credentials.password,
14 | database: credentials.database,
15 | synchronize: false,
16 | logging: false,
17 | ssl: credentials.ssl ? { rejectUnauthorized: false } : false,
18 | };
19 |
20 | switch (credentials.type) {
21 | case 'postgres':
22 | return new DataSource({
23 | type: 'postgres',
24 | ...baseConfig,
25 | });
26 | case 'mysql':
27 | return new DataSource({
28 | type: 'mysql',
29 | ...baseConfig,
30 | });
31 | case 'sqlite':
32 | return new DataSource({
33 | type: 'sqlite',
34 | database: credentials.database, // For SQLite, this is the file path
35 | });
36 | default:
37 | throw new Error(`Unsupported database type: ${credentials.type}`);
38 | }
39 | };
40 |
41 | router.post(
42 | '/select',
43 | sqlRateLimit,
44 | async (req: Request<{}, SqlResponse, SqlRequest>, res: Response): Promise => {
45 | const startTime = Date.now();
46 | let dataSource: DataSource | null = null;
47 |
48 | try {
49 | const { credentials, query, parameters = [] } = req.body;
50 |
51 | // Validate request
52 | if (!credentials || !query) {
53 | res.status(400).json({
54 | success: false,
55 | error: 'Missing credentials or query',
56 | });
57 | return;
58 | }
59 |
60 | // Validate SQL query (basic check for SELECT only)
61 | const trimmedQuery = query.trim().toLowerCase();
62 | if (!trimmedQuery.startsWith('select')) {
63 | res.status(400).json({
64 | success: false,
65 | error: 'Only SELECT queries are allowed',
66 | });
67 | return;
68 | }
69 |
70 | // Create and initialize data source
71 | dataSource = createDataSource(credentials);
72 | await dataSource.initialize();
73 |
74 | // Execute query
75 | const result = await dataSource.query(query, parameters);
76 | const executionTime = Date.now() - startTime;
77 |
78 | const response: SqlResponse = {
79 | success: true,
80 | data: result,
81 | executionTime,
82 | rowCount: Array.isArray(result) ? result.length : 0,
83 | };
84 |
85 | res.json(response);
86 | } catch (error) {
87 | const executionTime = Date.now() - startTime;
88 | console.error('SQL execution error:', error);
89 |
90 | const response: SqlResponse = {
91 | success: false,
92 | error: error instanceof Error ? error.message : 'Unknown error',
93 | executionTime,
94 | };
95 |
96 | res.status(500).json(response);
97 | } finally {
98 | // Clean up connection
99 | if (dataSource && dataSource.isInitialized) {
100 | try {
101 | await dataSource.destroy();
102 | } catch (error) {
103 | console.error('Error closing database connection:', error);
104 | }
105 | }
106 | }
107 | },
108 | );
109 |
110 | export default router;
111 |
--------------------------------------------------------------------------------
/__legacy/src-node/src/compiler/esbuild-vfs.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import { PluginBuild, Loader, OnLoadArgs, OnResolveArgs } from 'esbuild';
3 |
4 | import { createError } from '../utils/index.js';
5 | import { VirtualFS } from '../../../src/virtual-fs/VirtualFS.js';
6 |
7 | export class ESBuildVFS {
8 | name = 'virtual-files';
9 |
10 | constructor(private vfs: VirtualFS) {}
11 |
12 | get() {
13 | return {
14 | name: this.name,
15 | setup: this.setup,
16 | };
17 | }
18 |
19 | private setup = (build: PluginBuild) => {
20 | // Резолвим импорты виртуальных файлов
21 | build.onResolve({ filter: /.*/ }, this.handleResolve);
22 | // Загружаем содержимое виртуальных файлов
23 | build.onLoad({ filter: /.*/, namespace: 'virtual' }, this.handleLoad);
24 | };
25 |
26 | private handleResolve = (args: OnResolveArgs) => {
27 | // Пропускаем внешние модули (node_modules)
28 | if (!args.path.startsWith('.') && !args.path.startsWith('/') && !args.path.startsWith('@')) {
29 | return { external: true };
30 | }
31 |
32 | const resolvedPath = args.path.startsWith('@')
33 | ? args.path.replace('@/', '/src/')
34 | : this.resolveVirtualPath(args.path, args.importer);
35 |
36 | let foundPath: string | undefined = undefined;
37 |
38 | if (this.vfs.fileExists(resolvedPath)) {
39 | foundPath = resolvedPath;
40 | } else if (this.vfs.fileExists(resolvedPath + '.tsx')) {
41 | foundPath = resolvedPath + '.tsx';
42 | } else if (this.vfs.fileExists(resolvedPath + '.ts')) {
43 | foundPath = resolvedPath + '.ts';
44 | }
45 |
46 | if (foundPath) {
47 | const meta = this.vfs.readFileMetadata(foundPath);
48 |
49 | if (meta.externalized) {
50 | return { external: true };
51 | } else {
52 | return {
53 | path: foundPath,
54 | namespace: 'virtual',
55 | };
56 | }
57 | } else {
58 | return undefined;
59 | }
60 | };
61 |
62 | private handleLoad = (args: OnLoadArgs) => {
63 | try {
64 | const file = this.vfs.readFile(args.path);
65 | return {
66 | contents: file.content,
67 | loader: this.getLoader(args.path),
68 | };
69 | } catch (error) {
70 | throw createError(`Ошибка загрузки виртуального файла ${args.path}`, error);
71 | }
72 | };
73 |
74 | /**
75 | * Резолвит путь в виртуальной файловой системе
76 | */
77 | private resolveVirtualPath(importPath: string, importer?: string): string {
78 | // Если путь абсолютный, возвращаем как есть
79 | if (path.isAbsolute(importPath)) {
80 | return path.resolve(importPath);
81 | }
82 |
83 | // Если есть импортер, резолвим относительно него
84 | if (importer) {
85 | const importerDir = path.dirname(importer);
86 | return path.resolve(importerDir, importPath);
87 | }
88 |
89 | // Иначе резолвим относительно корня
90 | return path.resolve('/', importPath);
91 | }
92 |
93 | /**
94 | * Определяет загрузчик для файла по расширению
95 | */
96 | private getLoader(filePath: string): Loader {
97 | if (filePath.endsWith('.tsx')) return 'tsx';
98 | if (filePath.endsWith('.ts')) return 'ts';
99 | if (filePath.endsWith('.jsx')) return 'jsx';
100 | if (filePath.endsWith('.js')) return 'js';
101 | if (filePath.endsWith('.css')) return 'css';
102 | if (filePath.endsWith('.json')) return 'json';
103 |
104 | return 'js'; // fallback
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/components/DashboardCard.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
3 | import { Button } from '@/components/ui/button';
4 | import { Database, Trash2 } from 'lucide-react';
5 | import { Dashboard } from '@/types/dashboard';
6 | import { databaseStore } from '@/stores/DatabaseStore';
7 | import { dashboardsRepository } from '@/stores/DashboardsRepository';
8 | import { confirmDialog } from '@/lib/dialogs';
9 | import { observer } from 'mobx-react';
10 |
11 | interface DashboardCardProps {
12 | dashboard: Dashboard;
13 | onClick: (dashboardId: string, dashboardTitle: string, dashboardIcon?: string) => void;
14 | showDetails?: boolean;
15 | onDelete?: (dashboardId: string) => void;
16 | }
17 |
18 | /**
19 | * Компонент карточки дашборда
20 | * Используется для отображения информации о дашборде в списках
21 | */
22 | export const DashboardCard: React.FC = observer(
23 | ({ dashboard, onClick, showDetails = true, onDelete }) => {
24 | const databaseName = databaseStore.getDatabase(dashboard.databaseId)?.name || 'Неизвестная база данных';
25 |
26 | const handleDelete = async (e: React.MouseEvent) => {
27 | e.stopPropagation(); // Предотвращаем открытие дашборда при клике на кнопку
28 |
29 | const isConfirmed = await confirmDialog(
30 | `Вы уверены, что хотите удалить дашборд "${dashboard.name}"? Это действие нельзя отменить.`,
31 | );
32 |
33 | if (isConfirmed) {
34 | if (onDelete) {
35 | onDelete(dashboard.id);
36 | } else {
37 | await dashboardsRepository.removeDashboard(dashboard.id);
38 | }
39 | }
40 | };
41 |
42 | return (
43 | onClick(dashboard.id, dashboard.name, dashboard.icon)}
46 | >
47 |
48 |
49 |
50 | {/* Иконка дашборда */}
51 |
52 | {dashboard.icon || '📈'}
53 |
54 |
55 |
58 | {dashboard.name}
59 |
60 |
61 |
62 | {/* Кнопка удаления */}
63 |
71 |
72 |
73 |
74 |
75 |
76 |
79 | {databaseName}
80 |
81 |
{dashboard.widgets?.length || 0} виджетов
82 |
83 |
84 |
85 | );
86 | },
87 | );
88 |
--------------------------------------------------------------------------------
/src/lib/compiler/ComponentCompiler.ts:
--------------------------------------------------------------------------------
1 | import { buildDefaultFS, getDefaultWidgetQuerySqlTsContent } from '@/virtual-fs/default-fs';
2 | import { TailwindCompiler } from './TailwindCompiler';
3 | import { ESBuildCompiler } from './ESBuildCompiler';
4 | // import { ValidationStage } from '@/pipeline/ValidationStage';
5 |
6 | export interface CompilationResult {
7 | jsBundle: string;
8 | cssBundle: string;
9 | }
10 |
11 | export interface ValidationResult {
12 | isValid: boolean;
13 | errors: string[];
14 | warnings: string[];
15 | }
16 |
17 | /**
18 | * Класс для валидации и компиляции компонентов React в runtime
19 | */
20 | export class ComponentCompiler {
21 | private esbuildCompiler: ESBuildCompiler;
22 | private tailwindCompiler: TailwindCompiler;
23 | // private validator: ValidationStage;
24 |
25 | constructor() {
26 | this.esbuildCompiler = new ESBuildCompiler();
27 | this.tailwindCompiler = new TailwindCompiler();
28 | // this.validator = new ValidationStage();
29 | }
30 |
31 | /**
32 | * Валидация исходного кода компонента
33 | * @param sourceCode - исходный код TypeScript/React компонента
34 | * @returns результат валидации с ошибками и предупреждениями
35 | */
36 | async validate(sourceCode: string): Promise {
37 | const errors: string[] = [];
38 | const warnings: string[] = [];
39 |
40 | // Проверка на пустой код
41 | if (!sourceCode.trim()) {
42 | errors.push('Код не может быть пустым');
43 | return { isValid: false, errors, warnings };
44 | }
45 |
46 | // Базовая проверка синтаксиса React
47 | if (!sourceCode.includes('export default')) {
48 | errors.push('Компонент должен содержать export default');
49 | }
50 |
51 | // Проверка на наличие React импорта (если используется JSX)
52 | if (
53 | sourceCode.includes('<') &&
54 | sourceCode.includes('>') &&
55 | !sourceCode.includes('import') &&
56 | !sourceCode.includes('React')
57 | ) {
58 | warnings.push('JSX код может потребовать импорт React');
59 | }
60 |
61 | // TODO: Здесь можно добавить более продвинутую валидацию:
62 | // - ESLint проверки
63 | // - TypeScript type checking
64 | // - Проверка разрешенных импортов
65 | // - Анализ безопасности кода
66 |
67 | return {
68 | isValid: errors.length === 0,
69 | errors,
70 | warnings,
71 | };
72 | }
73 |
74 | /**
75 | * Компиляция исходного кода в bundle и React компонент
76 | * @param sourceCode - исходный код TypeScript/React компонента
77 | * @returns скомпилированный результат с JS/CSS bundle и React компонентом
78 | */
79 | async compile(sourceCode: string): Promise {
80 | // Валидируем код перед компиляцией
81 | const validationResult = await this.validate(sourceCode);
82 | if (!validationResult.isValid) {
83 | throw new Error(`Ошибки валидации: ${validationResult.errors.join(', ')}`);
84 | }
85 |
86 | try {
87 | // Создаем виртуальную файловую систему
88 | const vfs = await buildDefaultFS(sourceCode, getDefaultWidgetQuerySqlTsContent());
89 |
90 | // Компилируем JS и CSS параллельно
91 | const [jsBundle, cssBundle] = await Promise.all([
92 | this.esbuildCompiler.compile(vfs, '/src/widget/index.tsx'),
93 | this.tailwindCompiler.compile(vfs),
94 | ]);
95 |
96 | return {
97 | jsBundle,
98 | cssBundle,
99 | };
100 | } catch (error) {
101 | throw new Error(`Ошибка компиляции: ${error instanceof Error ? error.message : 'Неизвестная ошибка'}`);
102 | }
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/src/components/dashboard/AddWidgetModal.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { observer } from 'mobx-react';
3 | import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
4 | import { Button } from '@/components/ui/button';
5 | import { Input } from '@/components/ui/input';
6 | import { Label } from '@/components/ui/label';
7 | import { Badge } from '@/components/ui/badge';
8 | import { DashboardGrid } from '@/types/dashboard';
9 | import { widgetsRepositoryStore } from '@/stores/WidgetsRepository';
10 |
11 | interface AddWidgetModalProps {
12 | isOpen: boolean;
13 | onClose: () => void;
14 | onAddWidget: (widgetId: string, title: string, width: number, height: number) => void;
15 | gridSettings: DashboardGrid;
16 | }
17 |
18 | export const AddWidgetModal: React.FC = observer(({ isOpen, onClose, onAddWidget }) => {
19 | const [selectedWidgetId, setSelectedWidgetId] = useState(null);
20 | const [widgetTitle, setWidgetTitle] = useState('');
21 |
22 | const handleSubmit = () => {
23 | if (!selectedWidgetId || !selectedWidget) return;
24 |
25 | const { width, height } = selectedWidget.defaultSize || { width: 4, height: 3 };
26 | onAddWidget(selectedWidgetId, widgetTitle, width, height);
27 |
28 | // Reset form
29 | setSelectedWidgetId(null);
30 | setWidgetTitle('');
31 | onClose();
32 | };
33 |
34 | const selectedWidget = selectedWidgetId ? widgetsRepositoryStore.getWidget(selectedWidgetId) : null;
35 |
36 | return (
37 |
101 | );
102 | });
103 |
--------------------------------------------------------------------------------
/src/assets/react.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/lib/runtime/WidgetRuntime.ts:
--------------------------------------------------------------------------------
1 | import { Database } from '@/types/database';
2 | import { ComponentCompiler } from '../compiler/ComponentCompiler';
3 | import {
4 | WidgetBundleWithDatabase,
5 | WidgetCompiledBundle,
6 | WidgetRuntimeComponent,
7 | WidgetSourceCode,
8 | } from '@/types/widget';
9 | import { ComponentRuntime } from './ComponentRuntime';
10 | import { computed, makeObservable, observable } from 'mobx';
11 | import { analyzeComponentProps } from '../componentAnalyzer';
12 |
13 | export class WidgetRuntime {
14 | private compiler: ComponentCompiler;
15 |
16 | @observable private _sourceCode: WidgetSourceCode | null = null;
17 | @observable private _compiledBundle: WidgetCompiledBundle | null = null;
18 | @observable private _bundleWithDatabase: WidgetBundleWithDatabase | null = null;
19 | @observable private _runtimeComponent: WidgetRuntimeComponent | null = null;
20 |
21 | constructor() {
22 | this.compiler = new ComponentCompiler();
23 |
24 | makeObservable(this);
25 | }
26 |
27 | set sourceCode(sourceCode: WidgetSourceCode) {
28 | this._sourceCode = sourceCode;
29 | this._compiledBundle = null;
30 | this._bundleWithDatabase = null;
31 | this._runtimeComponent = null;
32 | }
33 |
34 | @computed
35 | get sourceCode(): WidgetSourceCode | null {
36 | return this._sourceCode;
37 | }
38 |
39 | set compiledBundle(compiledBundle: WidgetCompiledBundle) {
40 | this._sourceCode = compiledBundle;
41 | this._compiledBundle = compiledBundle;
42 | this._bundleWithDatabase = null;
43 | this._runtimeComponent = null;
44 | }
45 |
46 | @computed
47 | get compiledBundle(): WidgetCompiledBundle | null {
48 | return this._compiledBundle;
49 | }
50 |
51 | set bundleWithDatabase(bundleWithDatabase: WidgetBundleWithDatabase) {
52 | this._sourceCode = bundleWithDatabase;
53 | this._compiledBundle = bundleWithDatabase;
54 | this._bundleWithDatabase = bundleWithDatabase;
55 | this._runtimeComponent = null;
56 | }
57 |
58 | @computed
59 | get bundleWithDatabase(): WidgetBundleWithDatabase | null {
60 | return this._bundleWithDatabase;
61 | }
62 |
63 | set runtimeComponent(runtimeComponent: WidgetRuntimeComponent) {
64 | this._sourceCode = runtimeComponent;
65 | this._compiledBundle = runtimeComponent;
66 | this._bundleWithDatabase = runtimeComponent;
67 | this._runtimeComponent = runtimeComponent;
68 | }
69 |
70 | get runtimeComponent(): WidgetRuntimeComponent | null {
71 | return this._runtimeComponent;
72 | }
73 |
74 | async compileBundle() {
75 | if (!this._sourceCode) {
76 | throw new Error('Source code is not set');
77 | }
78 |
79 | const validationResult = await this.compiler.validate(this._sourceCode.widgetTsx);
80 | if (!validationResult.isValid) {
81 | throw new Error('Widget TSX source code is not valid');
82 | }
83 |
84 | const compilationResult = await this.compiler.compile(this._sourceCode.widgetTsx);
85 | const props = analyzeComponentProps(this._sourceCode.widgetTsx);
86 |
87 | this.compiledBundle = {
88 | ...this._sourceCode,
89 | props,
90 | ...compilationResult,
91 | };
92 | }
93 |
94 | attachDatabase(database: Database) {
95 | if (!this._compiledBundle) {
96 | throw new Error('Compiled bundle is not set');
97 | }
98 |
99 | const bundleWithDatabase: WidgetBundleWithDatabase = {
100 | ...this._compiledBundle,
101 | database,
102 | };
103 |
104 | this.bundleWithDatabase = bundleWithDatabase;
105 | }
106 |
107 | async buildModule() {
108 | if (!this._bundleWithDatabase) {
109 | throw new Error('Bundle with database is not set');
110 | }
111 |
112 | const runtime = new ComponentRuntime({ database: this._bundleWithDatabase.database });
113 |
114 | const runtimeComponent: WidgetRuntimeComponent = {
115 | ...this._bundleWithDatabase,
116 | component: runtime.compileComponentModule(this._bundleWithDatabase.jsBundle),
117 | };
118 |
119 | this.runtimeComponent = runtimeComponent;
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/src/pages/DashboardPage.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { observer } from 'mobx-react';
3 | import { Button } from '@/components/ui/button';
4 | import { dashboardsRepository } from '@/stores/DashboardsRepository';
5 | import { DashboardCanvas } from '@/components/dashboard/DashboardCanvas';
6 | import { AddWidgetModal } from '@/components/dashboard/AddWidgetModal';
7 | import { Plus } from 'lucide-react';
8 |
9 | interface DashboardPageProps {
10 | dashboardId: string;
11 | }
12 |
13 | export const DashboardPage: React.FC = observer(({ dashboardId }) => {
14 | const [isAddWidgetModalOpen, setIsAddWidgetModalOpen] = useState(false);
15 |
16 | // Получаем дашборд по ID
17 | const dashboard = dashboardsRepository.getDashboard(dashboardId);
18 |
19 | // Если дашборд не найден
20 | if (!dashboard) {
21 | return (
22 |
23 |
24 |
25 |
Дашборд не найден
26 |
27 | Дашборд с ID "{dashboardId}" не существует или был удален
28 |
29 |
32 |
33 |
34 |
35 | );
36 | }
37 |
38 | // Widget management functions
39 | const handleAddWidget = async (widgetId: string, title: string, width: number, height: number) => {
40 | if (!dashboard) return;
41 |
42 | // Find an available position
43 | const position = dashboard.findAvailablePosition(width, height);
44 |
45 | await dashboard.addWidgetToDashboard({
46 | widgetId,
47 | title,
48 | layout: {
49 | x: position.x,
50 | y: position.y,
51 | width,
52 | height,
53 | },
54 | });
55 | };
56 |
57 | const handleWidgetMove = async (widgetId: string, x: number, y: number) => {
58 | console.log('handleWidgetMove', widgetId, x, y);
59 | await dashboard.updateDashboardWidget(widgetId, {
60 | layout: { x, y },
61 | });
62 | };
63 |
64 | const handleWidgetRemove = async (widgetId: string) => {
65 | await dashboard.removeWidgetFromDashboard(widgetId);
66 | };
67 |
68 | return (
69 |
70 | {/* Заголовок дашборда */}
71 |
72 |
73 |
74 | {/* Иконка дашборда */}
75 |
{dashboard.data.icon || '📈'}
76 |
77 |
{dashboard.data.name}
78 |
79 |
80 |
81 |
82 | Зажмите Shift для перетаскивания виджетов
83 |
84 |
88 |
89 |
90 |
91 |
92 | {/* Холст для виджетов */}
93 |
94 |
101 |
102 |
103 | {/* Modal for adding widgets */}
104 |
setIsAddWidgetModalOpen(false)}
107 | onAddWidget={handleAddWidget}
108 | gridSettings={dashboard.data.grid}
109 | />
110 |
111 | );
112 | });
113 |
--------------------------------------------------------------------------------
/src/lib/runtime/ComponentRuntime.ts:
--------------------------------------------------------------------------------
1 | import { tryToMockGlobalModule } from '@/pipeline/modules-mocks/mockGlobalModules';
2 | import { tryToMockShadcnUiModules } from '@/pipeline/modules-mocks/mockShadcnUiModules';
3 | import { tryToMockUtilsModule } from '@/pipeline/modules-mocks/mockUtilsModule';
4 |
5 | export interface RuntimeContext {
6 | // Контекст для модулей - можно расширить в будущем
7 | [key: string]: any;
8 | }
9 |
10 | export interface ModuleResolver {
11 | (context: RuntimeContext, path: string): any;
12 | }
13 |
14 | /**
15 | * Класс для создания runtime-окружения и выполнения скомпилированных компонентов
16 | * Отвечает за предоставление внешних модулей и выполнение JavaScript кода
17 | */
18 | export class ComponentRuntime {
19 | private context: RuntimeContext;
20 | private moduleResolvers: ModuleResolver[];
21 |
22 | constructor(context: RuntimeContext = {}) {
23 | this.context = context;
24 | this.moduleResolvers = [
25 | // Порядок важен - резолверы вызываются по очереди
26 | tryToMockGlobalModule,
27 | tryToMockShadcnUiModules,
28 | tryToMockUtilsModule,
29 | ];
30 | }
31 |
32 | /**
33 | * Выполняет скомпилированный JavaScript bundle и возвращает React компонент
34 | * @param jsBundle - скомпилированный JavaScript код
35 | * @returns React функциональный компонент
36 | */
37 | compileComponentModule(jsBundle: string): React.FunctionComponent {
38 | // Оборачиваем код в IIFE для изоляции
39 | const wrappedIIFE = `(function(module, require) {
40 | ${jsBundle}
41 | })(__module__, __require__)`;
42 |
43 | // Создаем функцию для выполнения модуля
44 | const componentModule = new Function('__module__', '__require__', wrappedIIFE);
45 |
46 | // Создаем объект модуля
47 | const customModule: any = { exports: {} };
48 |
49 | const customRequire = this.createCustomRequire();
50 |
51 | try {
52 | // Выполняем модуль с нашим кастомным require
53 | componentModule(customModule, customRequire);
54 | } catch (error) {
55 | console.error('Ошибка при выполнении модуля:', error);
56 | throw error;
57 | }
58 |
59 | // Возвращаем default export как React компонент
60 | const component = customModule.exports.default;
61 |
62 | if (!component) {
63 | throw new Error('Модуль не экспортирует default export');
64 | }
65 |
66 | if (typeof component !== 'function') {
67 | throw new Error('Default export должен быть функцией React компонента');
68 | }
69 |
70 | return component;
71 | }
72 |
73 | /**
74 | * Добавляет новый резолвер модулей
75 | * @param resolver - функция для резолва модулей
76 | */
77 | addModuleResolver(resolver: ModuleResolver): void {
78 | this.moduleResolvers.push(resolver);
79 | }
80 |
81 | /**
82 | * Обновляет контекст runtime
83 | * @param newContext - новый контекст
84 | */
85 | updateContext(newContext: Partial): void {
86 | this.context = { ...this.context, ...newContext };
87 | }
88 |
89 | /**
90 | * Получает текущий контекст
91 | * @returns текущий контекст runtime
92 | */
93 | getContext(): RuntimeContext {
94 | return { ...this.context };
95 | }
96 |
97 | /**
98 | * Создает кастомную функцию require для резолва модулей
99 | * @returns функция require
100 | */
101 | private createCustomRequire(): (path: string) => any {
102 | return (path: string) => {
103 | // Пытаемся резолвить модуль через все доступные резолверы
104 | for (const resolver of this.moduleResolvers) {
105 | try {
106 | const resolvedModule = resolver(this.context, path);
107 | if (resolvedModule !== null && resolvedModule !== undefined) {
108 | return resolvedModule;
109 | }
110 | } catch (error) {
111 | // Игнорируем ошибки от отдельных резолверов и пробуем следующий
112 | continue;
113 | }
114 | }
115 |
116 | // Если ни один резолвер не смог найти модуль
117 | throw new Error(`Модуль "${path}" не найден. Доступные резолверы: ${this.moduleResolvers.length}`);
118 | };
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as DialogPrimitive from '@radix-ui/react-dialog';
3 | import { XIcon } from 'lucide-react';
4 |
5 | import { cn } from '@/lib/utils';
6 |
7 | function Dialog({ ...props }: React.ComponentProps) {
8 | return ;
9 | }
10 |
11 | function DialogTrigger({ ...props }: React.ComponentProps) {
12 | return ;
13 | }
14 |
15 | function DialogPortal({ ...props }: React.ComponentProps) {
16 | return ;
17 | }
18 |
19 | function DialogClose({ ...props }: React.ComponentProps) {
20 | return ;
21 | }
22 |
23 | function DialogOverlay({ className, ...props }: React.ComponentProps) {
24 | return (
25 |
33 | );
34 | }
35 |
36 | function DialogContent({
37 | className,
38 | children,
39 | showCloseButton = true,
40 | ...props
41 | }: React.ComponentProps & {
42 | showCloseButton?: boolean;
43 | }) {
44 | return (
45 |
46 |
47 |
55 | {children}
56 | {showCloseButton && (
57 |
61 |
62 | Close
63 |
64 | )}
65 |
66 |
67 | );
68 | }
69 |
70 | function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
71 | return (
72 |
77 | );
78 | }
79 |
80 | function DialogFooter({ className, ...props }: React.ComponentProps<'div'>) {
81 | return (
82 |
87 | );
88 | }
89 |
90 | function DialogTitle({ className, ...props }: React.ComponentProps) {
91 | return (
92 |
97 | );
98 | }
99 |
100 | function DialogDescription({ className, ...props }: React.ComponentProps) {
101 | return (
102 |
107 | );
108 | }
109 |
110 | export {
111 | Dialog,
112 | DialogClose,
113 | DialogContent,
114 | DialogDescription,
115 | DialogFooter,
116 | DialogHeader,
117 | DialogOverlay,
118 | DialogPortal,
119 | DialogTitle,
120 | DialogTrigger,
121 | };
122 |
--------------------------------------------------------------------------------
/__legacy/compiler/postcss-compiler.ts:
--------------------------------------------------------------------------------
1 | import { VirtualFile, CompileOptions } from '../types/index.js';
2 | import { getTsxFilePaths, createError } from '../utils/index.js';
3 |
4 | /**
5 | * Компилятор для CSS с использованием PostCSS и Tailwind CSS
6 | */
7 | export class PostCSSCompiler {
8 | /**
9 | * Компилирует CSS стили с Tailwind
10 | */
11 | async compile(files: VirtualFile[], options: CompileOptions = {}): Promise {
12 | try {
13 | const tsxPaths = getTsxFilePaths(files);
14 |
15 | // Создаем конфигурацию Tailwind
16 | const tailwindConfig = this.createTailwindConfig(tsxPaths, options.tailwindConfig);
17 |
18 | // Создаем PostCSS процессор
19 | // const processor = postcss([tailwindcss(tailwindConfig)]);
20 |
21 | // // Обрабатываем CSS
22 | // const result = await processor.process(this.baseCss, {
23 | // from: undefined, // Виртуальный файл
24 | // to: undefined,
25 | // });
26 |
27 | return '// test'; // result.css;
28 | } catch (error) {
29 | throw createError('Ошибка компиляции CSS с PostCSS', error);
30 | }
31 | }
32 |
33 | /**
34 | * Создает конфигурацию для Tailwind CSS
35 | */
36 | private createTailwindConfig(tsxPaths: string[], customConfig?: Record) {
37 | const defaultConfig = {
38 | darkMode: 'class',
39 | content: tsxPaths,
40 | prefix: '',
41 | theme: {
42 | container: {
43 | center: true,
44 | padding: '2rem',
45 | screens: {
46 | '2xl': '1400px',
47 | },
48 | },
49 | extend: {
50 | colors: {
51 | border: 'hsl(var(--border))',
52 | input: 'hsl(var(--input))',
53 | ring: 'hsl(var(--ring))',
54 | background: 'hsl(var(--background))',
55 | foreground: 'hsl(var(--foreground))',
56 | primary: {
57 | DEFAULT: 'hsl(var(--primary))',
58 | foreground: 'hsl(var(--primary-foreground))',
59 | },
60 | secondary: {
61 | DEFAULT: 'hsl(var(--secondary))',
62 | foreground: 'hsl(var(--secondary-foreground))',
63 | },
64 | destructive: {
65 | DEFAULT: 'hsl(var(--destructive))',
66 | foreground: 'hsl(var(--destructive-foreground))',
67 | },
68 | muted: {
69 | DEFAULT: 'hsl(var(--muted))',
70 | foreground: 'hsl(var(--muted-foreground))',
71 | },
72 | accent: {
73 | DEFAULT: 'hsl(var(--accent))',
74 | foreground: 'hsl(var(--accent-foreground))',
75 | },
76 | popover: {
77 | DEFAULT: 'hsl(var(--popover))',
78 | foreground: 'hsl(var(--popover-foreground))',
79 | },
80 | card: {
81 | DEFAULT: 'hsl(var(--card))',
82 | foreground: 'hsl(var(--card-foreground))',
83 | },
84 | },
85 | borderRadius: {
86 | lg: 'var(--radius)',
87 | md: 'calc(var(--radius) - 2px)',
88 | sm: 'calc(var(--radius) - 4px)',
89 | },
90 | keyframes: {
91 | 'accordion-down': {
92 | from: { height: '0' },
93 | to: { height: 'var(--radix-accordion-content-height)' },
94 | },
95 | 'accordion-up': {
96 | from: { height: 'var(--radix-accordion-content-height)' },
97 | to: { height: '0' },
98 | },
99 | },
100 | animation: {
101 | 'accordion-down': 'accordion-down 0.2s ease-out',
102 | 'accordion-up': 'accordion-up 0.2s ease-out',
103 | },
104 | },
105 | },
106 | };
107 |
108 | // Объединяем с пользовательской конфигурацией, если есть
109 | if (customConfig) {
110 | return this.mergeDeep(defaultConfig, customConfig);
111 | }
112 |
113 | return defaultConfig;
114 | }
115 |
116 | /**
117 | * Глубокое слияние объектов
118 | */
119 | private mergeDeep(target: any, source: any): any {
120 | const output = Object.assign({}, target);
121 |
122 | if (this.isObject(target) && this.isObject(source)) {
123 | Object.keys(source).forEach(key => {
124 | if (this.isObject(source[key])) {
125 | if (!(key in target)) {
126 | Object.assign(output, { [key]: source[key] });
127 | } else {
128 | output[key] = this.mergeDeep(target[key], source[key]);
129 | }
130 | } else {
131 | Object.assign(output, { [key]: source[key] });
132 | }
133 | });
134 | }
135 |
136 | return output;
137 | }
138 |
139 | /**
140 | * Проверяет, является ли значение объектом
141 | */
142 | private isObject(item: any): boolean {
143 | return item && typeof item === 'object' && !Array.isArray(item);
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/src/stores/DatabaseStore.ts:
--------------------------------------------------------------------------------
1 | import { observable, action, makeObservable, runInAction, computed } from 'mobx';
2 | import { Database, CreateDatabaseOptions, UpdateDatabaseOptions } from '@/types/database';
3 | import { DatabaseConnection } from '@/platform/abstract/database';
4 | import { persistentStorage } from '@/lib/PersistentStorage';
5 | import { platform } from '@/platform';
6 |
7 | export class DatabaseStore {
8 | // Метаданные БД (без подключений)
9 | @observable private databases = new Map();
10 |
11 | // UI состояние
12 | @observable selectedDatabaseId: string | null = null;
13 | @observable isLoading: boolean = false;
14 |
15 | constructor() {
16 | makeObservable(this);
17 | }
18 |
19 | // Вычисляемые свойства
20 | @computed
21 | get allDatabases(): Database[] {
22 | return Array.from(this.databases.values());
23 | }
24 |
25 | // === МЕТОДЫ УПРАВЛЕНИЯ БАЗАМИ ДАННЫХ ===
26 |
27 | /**
28 | * Создает новое подключение к базе данных
29 | */
30 | @action
31 | async createDatabase(options: CreateDatabaseOptions): Promise {
32 | const id = `db_${Date.now()}`;
33 | const database: Database = {
34 | id,
35 | name: options.name,
36 | credentials: options.credentials,
37 | connection: platform.database.createConnection(id, options.credentials),
38 | };
39 |
40 | this.databases.set(id, database);
41 |
42 | // Сохраняем конкретную базу данных в файловую систему
43 | await persistentStorage.saveDatabase(id, database);
44 |
45 | return database;
46 | }
47 |
48 | /**
49 | * Обновляет существующую базу данных
50 | */
51 | @action
52 | async updateDatabase(id: string, options: UpdateDatabaseOptions): Promise {
53 | const database = this.databases.get(id);
54 | if (!database) {
55 | throw new Error(`База данных с ID ${id} не найдена`);
56 | }
57 |
58 | let newConnection: DatabaseConnection;
59 | if (options.credentials) {
60 | await database.connection.disconnect();
61 | const newCredentials = {
62 | ...database.credentials,
63 | ...options.credentials,
64 | };
65 | newConnection = platform.database.createConnection(id, newCredentials);
66 | } else {
67 | newConnection = database.connection;
68 | }
69 |
70 | const updatedDatabase: Database = {
71 | ...database,
72 | ...options,
73 | credentials: options.credentials
74 | ? { ...database.credentials, ...options.credentials }
75 | : database.credentials,
76 | connection: newConnection,
77 | };
78 |
79 | this.databases.set(id, updatedDatabase);
80 |
81 | // Сохраняем обновленную базу данных в файловую систему
82 | await persistentStorage.saveDatabase(id, {
83 | ...updatedDatabase,
84 | connection: undefined,
85 | });
86 |
87 | return updatedDatabase;
88 | }
89 |
90 | /**
91 | * Удаляет базу данных
92 | */
93 | @action
94 | async removeDatabase(id: string): Promise {
95 | const database = this.databases.get(id);
96 | if (!database) {
97 | return false;
98 | }
99 |
100 | // Если подключена, сначала отключаем
101 | await database.connection.disconnect();
102 |
103 | runInAction(() => {
104 | this.databases.delete(id);
105 | // Сбрасываем выбор, если удаляем выбранную БД
106 | if (this.selectedDatabaseId === id) {
107 | this.selectedDatabaseId = null;
108 | }
109 | });
110 |
111 | // Удаляем из файловой системы
112 | await persistentStorage.deleteDatabase(id);
113 |
114 | return true;
115 | }
116 |
117 | // === МЕТОДЫ УПРАВЛЕНИЯ ПОДКЛЮЧЕНИЯМИ ===
118 |
119 | /**
120 | * Получает базу данных по ID
121 | */
122 | getDatabase(id: string): Database | undefined {
123 | return this.databases.get(id);
124 | }
125 |
126 | // === ПРИВАТНЫЕ МЕТОДЫ ===
127 |
128 | /**
129 | * Загружает базы данных из PersistentStorage
130 | */
131 | @action
132 | async loadDatabases() {
133 | try {
134 | this.isLoading = true;
135 |
136 | const databases: Omit[] =
137 | await persistentStorage.loadAllDatabases>();
138 |
139 | runInAction(() => {
140 | this.databases.clear();
141 | databases.forEach(db => {
142 | this.databases.set(db.id, {
143 | ...db,
144 | connection: platform.database.createConnection(db.id, db.credentials),
145 | });
146 | });
147 | });
148 | } catch (error) {
149 | console.error('Ошибка загрузки баз данных:', error);
150 | } finally {
151 | this.isLoading = false;
152 | }
153 | }
154 | }
155 |
156 | export const databaseStore = new DatabaseStore();
157 |
158 | // @ts-ignore
159 | window.databaseStore = databaseStore;
160 |
--------------------------------------------------------------------------------
/src/components/AppHeader.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { observer } from 'mobx-react';
3 | import { X, Plus, Settings, Database, Layout, Layers } from 'lucide-react';
4 | import { Button } from './ui/button';
5 | import {
6 | DropdownMenu,
7 | DropdownMenuContent,
8 | DropdownMenuItem,
9 | DropdownMenuLabel,
10 | DropdownMenuSeparator,
11 | DropdownMenuTrigger,
12 | } from './ui/dropdown-menu';
13 | import { appTabsStore } from '@/stores/AppTabsStore';
14 |
15 | const AppHeader: React.FC = observer(() => {
16 | const { allTabs } = appTabsStore;
17 |
18 | const handleTabClick = (tabId: string) => {
19 | appTabsStore.setActiveTab(tabId);
20 | };
21 |
22 | const handleCloseTab = (e: React.MouseEvent, tabId: string) => {
23 | e.stopPropagation();
24 | appTabsStore.closeTab(tabId);
25 | };
26 |
27 | const handleNewTab = () => {
28 | appTabsStore.createNewTab();
29 | };
30 |
31 | const handleMenuItemClick = (action: string) => {
32 | switch (action) {
33 | case 'databases':
34 | // Открываем базы данных
35 | appTabsStore.openDatabase('all', 'Базы данных');
36 | break;
37 | case 'dashboards':
38 | appTabsStore.openDashboardList();
39 | break;
40 | case 'widgets-library':
41 | appTabsStore.openWidgetsLibrary();
42 | break;
43 | case 'playground':
44 | appTabsStore.openPlayground();
45 | break;
46 | }
47 | };
48 |
49 | const getTrimmedTitle = (title: string, maxLength: number = 20) => {
50 | return title.length > maxLength ? `${title.substring(0, maxLength)}...` : title;
51 | };
52 |
53 | return (
54 |
55 | {/* Область вкладок */}
56 |
57 | {allTabs.map(tab => (
58 |
handleTabClick(tab.id)}
66 | style={{ maxWidth: '200px' }}
67 | >
68 | {/* Иконка вкладки */}
69 | {appTabsStore.getTabIcon(tab)}
70 |
71 | {/* Заголовок вкладки */}
72 |
73 | {getTrimmedTitle(tab.title)}
74 |
75 |
76 | {/* Кнопка закрытия вкладки */}
77 | {allTabs.length > 1 && (
78 |
86 | )}
87 |
88 | ))}
89 |
90 | {/* Кнопка добавления новой вкладки */}
91 |
99 |
100 |
101 | {/* Меню настроек */}
102 |
103 |
104 |
105 |
106 |
107 |
108 | Навигация
109 |
110 |
111 | handleMenuItemClick('databases')}>
112 |
113 | Базы данных
114 |
115 |
116 | handleMenuItemClick('dashboards')}>
117 |
118 | Дешборды
119 |
120 |
121 | handleMenuItemClick('widgets-library')}>
122 |
123 | Библиотека виджетов
124 |
125 |
126 |
127 |
128 | handleMenuItemClick('playground')}>
129 |
130 | Playground
131 |
132 |
133 |
134 |
135 |
136 | );
137 | });
138 |
139 | export { AppHeader };
140 |
--------------------------------------------------------------------------------
/src/platform/browser/database.ts:
--------------------------------------------------------------------------------
1 | import { DatabaseConnection, DatabaseManager } from '../abstract/database';
2 | import { DatabaseCredentials } from '@/types/database';
3 |
4 | /**
5 | * HTTP клиент для взаимодействия с прокси-бекендом
6 | */
7 | class DatabaseHttpClient {
8 | private baseUrl: string;
9 |
10 | constructor(baseUrl: string = 'http://localhost:3000') {
11 | this.baseUrl = baseUrl;
12 | }
13 |
14 | /**
15 | * Отправляет SELECT запрос через прокси-бекенд
16 | */
17 | async select(credentials: DatabaseCredentials, query: string, parameters?: any[]): Promise {
18 | const response = await fetch(`${this.baseUrl}/api/select`, {
19 | method: 'POST',
20 | headers: {
21 | 'Content-Type': 'application/json',
22 | },
23 | body: JSON.stringify({
24 | credentials,
25 | query,
26 | parameters: parameters || [],
27 | }),
28 | });
29 |
30 | if (!response.ok) {
31 | throw new Error(`HTTP error! status: ${response.status}`);
32 | }
33 |
34 | const result = await response.json();
35 |
36 | if (!result.success) {
37 | throw new Error(result.error || 'Unknown error from proxy backend');
38 | }
39 |
40 | return result.data || [];
41 | }
42 |
43 | /**
44 | * Проверяет доступность прокси-бекенда
45 | */
46 | async checkHealth(): Promise {
47 | try {
48 | const response = await fetch(`${this.baseUrl}/health`);
49 | return response.ok;
50 | } catch {
51 | return false;
52 | }
53 | }
54 | }
55 |
56 | /**
57 | * Реализация подключения к БД для Browser платформы
58 | * Использует прокси-бекенд для выполнения SQL запросов
59 | */
60 | export class BrowserDatabaseConnection extends DatabaseConnection {
61 | private httpClient: DatabaseHttpClient;
62 |
63 | constructor(databaseId: string, credentials: DatabaseCredentials, proxyUrl?: string) {
64 | super(databaseId, credentials);
65 | this.httpClient = new DatabaseHttpClient(proxyUrl);
66 | }
67 |
68 | async connect(): Promise {
69 | if (this._status === 'connected') {
70 | return;
71 | }
72 |
73 | try {
74 | this.updateStatus('connecting');
75 |
76 | // Проверяем доступность прокси-бекенда
77 | const isHealthy = await this.httpClient.checkHealth();
78 | if (!isHealthy) {
79 | throw new Error('Proxy backend is not available');
80 | }
81 |
82 | // Тестируем подключение к БД через простой запрос
83 | try {
84 | await this.httpClient.select(this.credentials, 'SELECT 1 as test');
85 | } catch (error) {
86 | const errorMessage = error instanceof Error ? error.message : 'Unknown connection error';
87 | throw new Error(`Database connection failed: ${errorMessage}`);
88 | }
89 |
90 | this.updateStatus('connected');
91 |
92 | // Получаем версию БД
93 | try {
94 | const versionQuery =
95 | this.credentials.type === 'postgres'
96 | ? 'SELECT version() as version'
97 | : this.credentials.type === 'mysql'
98 | ? 'SELECT VERSION() as version'
99 | : 'SELECT sqlite_version() as version';
100 |
101 | const versionResult = await this.httpClient.select(this.credentials, versionQuery);
102 | if (versionResult.length > 0 && versionResult[0].version) {
103 | this.setVersion(versionResult[0].version);
104 | }
105 | } catch (error) {
106 | // Если не удалось получить версию, это не критично
107 | console.warn('Failed to get database version:', error);
108 | }
109 | } catch (error) {
110 | const errorMessage = error instanceof Error ? error.message : 'Unknown connection error';
111 | this.updateStatus('error', errorMessage);
112 | throw error;
113 | }
114 | }
115 |
116 | async disconnect(): Promise {
117 | this.updateStatus('disconnected');
118 | }
119 |
120 | async select(sql: string, params?: any[]): Promise {
121 | if (!this.isConnected) {
122 | await this.connect();
123 | }
124 |
125 | try {
126 | return await this.httpClient.select(this.credentials, sql, params);
127 | } catch (error) {
128 | const errorMessage = error instanceof Error ? error.message : 'Unknown error';
129 | console.error('Error executing select query:', error);
130 | throw new Error(`Error executing select query: ${errorMessage}`);
131 | }
132 | }
133 | }
134 |
135 | /**
136 | * Менеджер подключений к БД для Browser платформы
137 | */
138 | export class BrowserDatabaseManager implements DatabaseManager {
139 | constructor(private proxyUrl?: string) {}
140 |
141 | createConnection(databaseId: string, credentials: DatabaseCredentials): DatabaseConnection {
142 | return new BrowserDatabaseConnection(databaseId, credentials, this.proxyUrl);
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/src/components/dashboard/DashboardWidgetComponent.tsx:
--------------------------------------------------------------------------------
1 | import { observer } from 'mobx-react';
2 | import { createElement } from 'react';
3 | import { Button } from '@/components/ui/button';
4 | import { Badge } from '@/components/ui/badge';
5 | import { Trash2, Move } from 'lucide-react';
6 | import { WidgetDescriptior } from '@/stores/DashboardStore';
7 |
8 | interface DashboardWidgetComponentProps {
9 | descriptor: WidgetDescriptior;
10 | position: { x: number; y: number };
11 | size: { width: number; height: number };
12 | isDragging: boolean;
13 | isEditMode: boolean;
14 | onMouseDown: (e: React.MouseEvent) => void;
15 | onRemove: () => void;
16 | }
17 |
18 | export const DashboardWidgetComponent: React.FC = observer(
19 | ({ descriptor, position, size, isDragging, isEditMode, onMouseDown, onRemove }) => {
20 | const widgetMetadata = descriptor.widget;
21 | const runtime = descriptor.runtime;
22 |
23 | const renderWidgetContent = () => {
24 | if (!widgetMetadata) {
25 | return (
26 |
27 |
28 |
⚠️
29 |
Виджет не найден
30 |
ID: {descriptor.data.id}
31 |
32 |
33 | );
34 | }
35 |
36 | if (!runtime.runtimeComponent) {
37 | return (
38 |
39 |
40 |
❓
41 |
Компонент не готов
42 |
43 |
44 | );
45 | }
46 |
47 | try {
48 | return createElement(runtime.runtimeComponent.component, descriptor.data.props || {});
49 | } catch (error) {
50 | return (
51 |
52 |
53 |
💥
54 |
Ошибка рендеринга
55 |
56 | {error instanceof Error ? error.message : 'Неизвестная ошибка'}
57 |
58 |
59 |
60 | );
61 | }
62 | };
63 |
64 | return (
65 |
81 | {/* Widget Header - only show in edit mode */}
82 | {isEditMode && (
83 |
84 |
85 |
86 | {descriptor.data.title || widgetMetadata?.name || `Widget ${descriptor.data.id.slice(-4)}`}
87 |
88 |
89 | {descriptor.data.layout.width}×{descriptor.data.layout.height}
90 |
91 |
92 |
110 |
111 |
112 | )}
113 |
114 | {/* Widget Content */}
115 |
{
118 | if (isEditMode) {
119 | e.stopPropagation();
120 | e.preventDefault();
121 | }
122 | }}
123 | >
124 | {renderWidgetContent()}
125 |
126 |
127 | {/* Edit Mode Overlay */}
128 | {isEditMode &&
}
129 |
130 | );
131 | },
132 | );
133 |
--------------------------------------------------------------------------------
/__legacy/compiler/tailwind-compiler.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | // @ts-ignore
3 | import { compile, Instrumentation, optimize, toSourceMap } from '@tailwindcss/node';
4 | // import { Scanner } from '@tailwindcss/oxide';
5 |
6 | import { createError } from '../utils/index.js';
7 | import { VirtualFile } from '../virtual-fs/types.js';
8 | import { VirtualFS } from '../virtual-fs/VirtualFS.js';
9 |
10 | const css = String.raw;
11 |
12 | const logToFile = (content: string) => {
13 | fs.appendFileSync('./tailwind.log', content);
14 | };
15 |
16 | /**
17 | * Компилятор для CSS с использованием Tailwind CSS v4
18 | */
19 | export class TailwindCompiler {
20 | private baseCss = css`
21 | @import 'tailwindcss';
22 | `;
23 |
24 | /**
25 | * Компилирует CSS стили с Tailwind
26 | */
27 | async compile(vfs: VirtualFS, options?: { minify?: boolean; optimize?: boolean }): Promise<{ css: string }> {
28 | try {
29 | using I = new Instrumentation();
30 | logToFile('[@tailwindcss/compiler] CSS compilation');
31 |
32 | // Получаем все TSX файлы из виртуальной файловой системы
33 | const tsxFiles = Array.from(vfs.filesNodes.entries()).filter(
34 | ([filePath, fileNode]) => fileNode.metadata.externalized && fileNode.name.endsWith('.tsx'),
35 | );
36 |
37 | logToFile('Setup compiler');
38 |
39 | // Создаем компилятор
40 | const compiler = await compile(this.baseCss, {
41 | base: process.cwd(),
42 | onDependency: () => {},
43 | });
44 |
45 | // Настраиваем источники для сканирования
46 | const sources = this.createSources(tsxFiles);
47 | // const scanner = new Scanner({ sources });
48 |
49 | logToFile('Setup compiler');
50 |
51 | logToFile('Scan for candidates');
52 | // Сканируем содержимое виртуальных файлов на предмет классов Tailwind
53 | // const candidates = this.scanVirtualFiles(scanner, tsxFiles);
54 | logToFile('Scan for candidates');
55 |
56 | logToFile('Build CSS');
57 | // Компилируем CSS
58 | let compiledCss = compiler.build([]);
59 | logToFile('Build CSS');
60 |
61 | let sourceMap: any | null = null;
62 |
63 | // Оптимизация CSS если требуется
64 | if (options?.optimize || options?.minify) {
65 | logToFile('Optimize CSS');
66 | const optimized = optimize(compiledCss, {
67 | file: 'virtual.css',
68 | minify: options?.minify ?? false,
69 | });
70 | compiledCss = optimized.code;
71 |
72 | if (optimized.map) {
73 | sourceMap = toSourceMap(optimized.map);
74 | }
75 | logToFile('Optimize CSS');
76 | }
77 |
78 | logToFile('[@tailwindcss/compiler] CSS compilation');
79 |
80 | return {
81 | css: compiledCss,
82 | };
83 | } catch (error) {
84 | throw createError('Ошибка компиляции CSS с Tailwind', error);
85 | }
86 | }
87 |
88 | /**
89 | * Создает источники содержимого для Scanner
90 | */
91 | private createSources(tsxFiles: [string, VirtualFile][]) {
92 | // Если есть виртуальные файлы, создаем виртуальные источники
93 | if (tsxFiles.length > 0) {
94 | return tsxFiles.map(([filePath]) => ({
95 | base: '.',
96 | pattern: filePath,
97 | negated: false,
98 | }));
99 | }
100 |
101 | // Иначе используем базовые паттерны
102 | return [{ base: '.', pattern: '**/*.{html,js,ts,jsx,tsx,vue,svelte}', negated: false }];
103 | }
104 |
105 | // /**
106 | // * Сканирует виртуальные файлы на предмет классов Tailwind
107 | // */
108 | // private scanVirtualFiles(scanner: Scanner, tsxFiles: [string, VirtualFile][]) {
109 | // const candidates: string[] = [];
110 |
111 | // for (const [filePath, fileNode] of tsxFiles) {
112 | // try {
113 | // // Получаем содержимое файла
114 | // const content = fileNode.content;
115 |
116 | // if (!fileNode.metadata.externalized) {
117 | // // candidates.push(...scanner.scanFiles([{ file: filePath, extension: 'tsx' }]));
118 | // // no-op for now
119 | // }
120 |
121 | // // candidates.push(...fileCandidates);
122 | // } catch (error) {
123 | // console.warn(`Предупреждение: не удалось отсканировать файл ${filePath}:`, error);
124 | // }
125 | // }
126 |
127 | // return candidates;
128 | // }
129 |
130 | // // Если нет кандидатов из виртуальных файлов, делаем полное сканирование
131 | // if (candidates.length === 0) {
132 | // return scanner.scan();
133 | // }
134 |
135 | // return candidates;
136 | // }
137 |
138 | // /**
139 | // * Компилирует CSS с базовыми настройками
140 | // */
141 | // async compileBasic(): Promise {
142 | // try {
143 | // const compiler = await compile(this.baseCss, {
144 | // base: process.cwd(),
145 | // });
146 |
147 | // const scanner = new Scanner({
148 | // sources: [{ base: '.', pattern: '**/*.{html,js,ts,jsx,tsx}', negated: false }],
149 | // });
150 |
151 | // const candidates = scanner.scan();
152 | // return compiler.build(candidates);
153 | // } catch (error) {
154 | // throw createError('Ошибка базовой компиляции CSS', error);
155 | // }
156 | // }
157 | }
158 |
--------------------------------------------------------------------------------