Migrate frontend to shadcn/ui + add Gitea branch panel in endpoint editor

shadcn/ui migration:
- Installed shadcn components: Button, Input, Label, Card, Badge, Checkbox,
  Switch, Tabs, Dialog, Select, Tooltip, Toaster (sonner)
- Migrated all pages: Login, Dashboard, Settings, Endpoints, EndpointEditor,
  Folders, ApiKeys, Logs
- Replaced react-hot-toast with sonner
- Added CSS variables for theming
- Updated tailwind.config.js for shadcn

Gitea branch panel in EndpointEditor:
- GiteaBranchPanel component: branch selector, save to branch, compare
  with main, create PR, merge, commit history
- New backend endpoints: getEndpointFileContent, commitEndpoint,
  getEndpointCommits, getEndpointFromBranch, commitEndpointToBranch
- Frontend giteaApi: extended with endpoint-specific methods

Fixes:
- Fixed sync-all PostgreSQL-compatible UPDATE LIMIT
- .gitignore: added tmp/

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-24 00:16:15 +03:00
parent 59ea0eb9c2
commit 44e4b45f50
45 changed files with 4442 additions and 1752 deletions

View File

@@ -133,6 +133,42 @@ export const getEndpointInfo = async (req: AuthRequest, res: Response) => {
}
};
export const getEndpointFileContent = async (req: AuthRequest, res: Response) => {
try {
const { id } = req.params;
const { branch } = req.query as { branch: string };
if (!branch) return res.status(400).json({ error: 'branch query param required' });
const content = await giteaService.getEndpointFromBranch(id, branch);
if (!content) return res.status(404).json({ error: 'Not found' });
res.json(content);
} catch (error: any) {
res.status(500).json({ error: error.message });
}
};
export const commitEndpoint = async (req: AuthRequest, res: Response) => {
try {
const { id } = req.params;
const { branch, message, changes } = req.body;
if (!branch || !message) return res.status(400).json({ error: 'branch and message required' });
const result = await giteaService.commitEndpointToBranch(id, branch, message, changes || {});
res.json(result);
} catch (error: any) {
res.status(500).json({ error: error.message });
}
};
export const getEndpointCommits = async (req: AuthRequest, res: Response) => {
try {
const { id } = req.params;
const { branch } = req.query as { branch?: string };
const commits = await giteaService.getEndpointCommitHistory(id, branch);
res.json(commits);
} catch (error: any) {
res.status(500).json({ error: error.message });
}
};
export const createPR = async (req: AuthRequest, res: Response) => {
try {
const { title, head, base, body } = req.body;

View File

@@ -12,6 +12,9 @@ import {
compareBranches,
getCommitHistory,
getEndpointInfo,
getEndpointFileContent,
commitEndpoint,
getEndpointCommits,
createPR,
mergePR,
} from '../controllers/giteaController';
@@ -40,6 +43,9 @@ router.get('/commits', getCommitHistory);
// Endpoint-specific
router.get('/endpoints/:id/info', getEndpointInfo);
router.get('/endpoints/:id/file-content', getEndpointFileContent);
router.post('/endpoints/:id/commit', commitEndpoint);
router.get('/endpoints/:id/commits', getEndpointCommits);
// Pull Requests
router.post('/pulls', createPR);

View File

@@ -419,6 +419,100 @@ class GiteaService {
return { synced, errors };
}
// --- Endpoint branch operations ---
private async getEndpointRepoPath(endpointId: string): Promise<{ basePath: string; endpoint: any } | null> {
const result = await mainPool.query(
`SELECT e.*, f.name as folder_name, d.name as database_name
FROM endpoints e
LEFT JOIN folders f ON e.folder_id = f.id
LEFT JOIN databases d ON e.database_id = d.id
WHERE e.id = $1`,
[endpointId]
);
if (result.rows.length === 0) return null;
const ep = result.rows[0];
const folder = ep.folder_name ? this.sanitize(ep.folder_name) : '_root';
const basePath = `endpoints/${folder}/${this.sanitize(ep.name)}`;
return { basePath, endpoint: ep };
}
async getEndpointFromBranch(endpointId: string, branch: string): Promise<any> {
if (!await this.isEnabled()) return null;
const cfg = await this.getConfig();
if (!cfg) return null;
const info = await this.getEndpointRepoPath(endpointId);
if (!info) return null;
const { basePath } = info;
const metaData = await this.api('GET', `/repos/${cfg.owner}/${cfg.repo}/contents/${basePath}/endpoint.json?ref=${encodeURIComponent(branch)}`);
let metadata = null;
if (metaData?.content) {
metadata = JSON.parse(Buffer.from(metaData.content, 'base64').toString('utf-8'));
}
let query = null;
const sqlData = await this.api('GET', `/repos/${cfg.owner}/${cfg.repo}/contents/${basePath}/query.sql?ref=${encodeURIComponent(branch)}`);
if (sqlData?.content) {
query = Buffer.from(sqlData.content, 'base64').toString('utf-8');
}
let script = null;
for (const ext of ['js', 'py']) {
const scriptData = await this.api('GET', `/repos/${cfg.owner}/${cfg.repo}/contents/${basePath}/main.${ext}?ref=${encodeURIComponent(branch)}`);
if (scriptData?.content) {
script = Buffer.from(scriptData.content, 'base64').toString('utf-8');
break;
}
}
return { metadata, query, script, branch, repo_path: basePath };
}
async commitEndpointToBranch(
endpointId: string,
branch: string,
message: string,
changes: { query?: string; script?: string; metadata?: any }
): Promise<{ sha: string | null }> {
if (!await this.isEnabled()) throw new Error('Gitea not enabled');
const info = await this.getEndpointRepoPath(endpointId);
if (!info) throw new Error('Endpoint not found');
const { basePath } = info;
let lastSha: string | null = null;
if (changes.metadata) {
lastSha = await this.commitFile(
`${basePath}/endpoint.json`,
JSON.stringify(changes.metadata, null, 2),
message,
branch
);
}
if (changes.query !== undefined) {
lastSha = await this.commitFile(`${basePath}/query.sql`, changes.query, message, branch);
}
if (changes.script !== undefined) {
const ext = changes.metadata?.script_language === 'python' ? 'py' : 'js';
lastSha = await this.commitFile(`${basePath}/main.${ext}`, changes.script, message, branch);
}
return { sha: lastSha };
}
async getEndpointCommitHistory(endpointId: string, branch?: string): Promise<GiteaCommit[]> {
const info = await this.getEndpointRepoPath(endpointId);
if (!info) return [];
return this.getCommitHistory(info.basePath, branch);
}
// --- Helpers ---
async getEndpointGiteaInfo(endpointId: string): Promise<any> {