plugin-git-manager 1.1.7 → 1.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/187.08dd0bf4d0f68036.js +10 -0
- package/dist/client/components/LLMModelSelect.d.ts +10 -0
- package/dist/client/components/RunReviewButton.d.ts +1 -1
- package/dist/client/context/GitManagerContext.d.ts +2 -0
- package/dist/client/index.js +1 -1
- package/dist/externalVersion.js +6 -4
- package/dist/locale/en-US.json +8 -1
- package/dist/server/actions/gitlab-api.js +14 -13
- package/dist/server/actions/review.js +15 -9
- package/dist/server/ai-tools.js +29 -3
- package/dist/server/collections/gitCodeReviews.d.ts +1 -1
- package/dist/server/collections/gitRepositories.d.ts +1 -1
- package/dist/server/collections/gitRepositories.js +12 -0
- package/dist/server/collections/gitReviewFlows.d.ts +1 -1
- package/dist/server/migrations/20260508000000-add-auto-review-flow-id.d.ts +6 -0
- package/dist/server/migrations/20260508000000-add-auto-review-flow-id.js +57 -0
- package/dist/server/plugin.d.ts +4 -0
- package/dist/server/plugin.js +38 -6
- package/dist/server/poller.js +3 -1
- package/package.json +1 -1
- package/src/client/components/CommitHistory.tsx +18 -3
- package/src/client/components/GitOperations.tsx +29 -16
- package/src/client/components/LLMModelSelect.tsx +63 -0
- package/src/client/components/PollingStatus.tsx +27 -1
- package/src/client/components/RepositoryConfig.tsx +76 -3
- package/src/client/components/ReviewFlows.tsx +17 -2
- package/src/client/components/RunReviewButton.tsx +375 -278
- package/src/client/context/GitManagerContext.tsx +2 -0
- package/src/client/index.tsx +31 -31
- package/src/locale/en-US.json +8 -1
- package/src/server/actions/gitlab-api.ts +8 -4
- package/src/server/actions/review.ts +9 -3
- package/src/server/ai-tools.ts +31 -3
- package/src/server/collections/gitRepositories.ts +12 -0
- package/src/server/migrations/20260508000000-add-auto-review-flow-id.ts +29 -0
- package/src/server/plugin.ts +216 -180
- package/src/server/poller.ts +11 -2
- package/dist/client/322.c934c7c0e25a75b0.js +0 -10
package/dist/server/ai-tools.js
CHANGED
|
@@ -49,8 +49,16 @@ function registerGitReviewAiTools(app) {
|
|
|
49
49
|
(_c = (_b = app.log) == null ? void 0 : _b.warn) == null ? void 0 : _c.call(_b, "plugin-git-manager: AIManager.toolsManager not available; skipping AI tool registration");
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
|
-
const enforceAcl = (ctx, resource, action) => {
|
|
52
|
+
const enforceAcl = (ctx, resource, action, params) => {
|
|
53
53
|
var _a2, _b2, _c2, _d2;
|
|
54
|
+
if (ctx.isBackgroundReview) {
|
|
55
|
+
if (params.repositoryId && params.repositoryId !== ctx.reviewTargetRepositoryId) {
|
|
56
|
+
const err = new Error("Permission denied: AI cannot access other repositories");
|
|
57
|
+
err.status = 403;
|
|
58
|
+
throw err;
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
54
62
|
const user = (_a2 = ctx == null ? void 0 : ctx.state) == null ? void 0 : _a2.currentUser;
|
|
55
63
|
if (!(user == null ? void 0 : user.id)) {
|
|
56
64
|
const err = new Error("AI tool requires an authenticated user context");
|
|
@@ -72,13 +80,31 @@ function registerGitReviewAiTools(app) {
|
|
|
72
80
|
throw err;
|
|
73
81
|
}
|
|
74
82
|
};
|
|
83
|
+
const sanitizeForDb = (obj) => {
|
|
84
|
+
if (typeof obj === "string") {
|
|
85
|
+
return obj.replace(/\u0000/g, "");
|
|
86
|
+
}
|
|
87
|
+
if (Array.isArray(obj)) {
|
|
88
|
+
return obj.map(sanitizeForDb);
|
|
89
|
+
}
|
|
90
|
+
if (obj && typeof obj === "object") {
|
|
91
|
+
const sanitized = {};
|
|
92
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
93
|
+
sanitized[key] = sanitizeForDb(value);
|
|
94
|
+
}
|
|
95
|
+
return sanitized;
|
|
96
|
+
}
|
|
97
|
+
return obj;
|
|
98
|
+
};
|
|
75
99
|
const runResourceAction = async (ctx, handler, params, gate) => {
|
|
76
|
-
|
|
100
|
+
var _a2;
|
|
101
|
+
enforceAcl(ctx, gate.resource, gate.action, params);
|
|
77
102
|
const synthCtx = {
|
|
78
103
|
...ctx,
|
|
79
104
|
app: ctx.app,
|
|
80
105
|
db: ctx.db,
|
|
81
106
|
action: { params },
|
|
107
|
+
request: { ...ctx.request || {}, body: ((_a2 = ctx.request) == null ? void 0 : _a2.body) || {} },
|
|
82
108
|
throw: (status, message) => {
|
|
83
109
|
const err = new Error(message);
|
|
84
110
|
err.status = status;
|
|
@@ -86,7 +112,7 @@ function registerGitReviewAiTools(app) {
|
|
|
86
112
|
}
|
|
87
113
|
};
|
|
88
114
|
await handler(synthCtx, async () => void 0);
|
|
89
|
-
return synthCtx.body;
|
|
115
|
+
return sanitizeForDb(synthCtx.body);
|
|
90
116
|
};
|
|
91
117
|
aiManager.toolsManager.registerTools([
|
|
92
118
|
{
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default:
|
|
1
|
+
declare const _default: any;
|
|
2
2
|
export default _default;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default:
|
|
1
|
+
declare const _default: any;
|
|
2
2
|
export default _default;
|
|
@@ -87,6 +87,18 @@ var gitRepositories_default = (0, import_database.defineCollection)({
|
|
|
87
87
|
interface: "checkbox",
|
|
88
88
|
uiSchema: { title: "Auto Review", type: "boolean", "x-component": "Checkbox" }
|
|
89
89
|
},
|
|
90
|
+
{
|
|
91
|
+
type: "belongsTo",
|
|
92
|
+
name: "autoReviewFlow",
|
|
93
|
+
target: "gitReviewFlows",
|
|
94
|
+
foreignKey: "autoReviewFlowId",
|
|
95
|
+
interface: "m2o",
|
|
96
|
+
uiSchema: {
|
|
97
|
+
title: "Primary Auto Review Flow",
|
|
98
|
+
"x-component": "AssociationField",
|
|
99
|
+
"x-component-props": { fieldNames: { label: "name", value: "id" } }
|
|
100
|
+
}
|
|
101
|
+
},
|
|
90
102
|
{
|
|
91
103
|
type: "date",
|
|
92
104
|
name: "lastPolledAt",
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default:
|
|
1
|
+
declare const _default: any;
|
|
2
2
|
export default _default;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
var __defProp = Object.defineProperty;
|
|
11
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
12
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
13
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
14
|
+
var __export = (target, all) => {
|
|
15
|
+
for (var name in all)
|
|
16
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
17
|
+
};
|
|
18
|
+
var __copyProps = (to, from, except, desc) => {
|
|
19
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
20
|
+
for (let key of __getOwnPropNames(from))
|
|
21
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
22
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
23
|
+
}
|
|
24
|
+
return to;
|
|
25
|
+
};
|
|
26
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
27
|
+
var add_auto_review_flow_id_exports = {};
|
|
28
|
+
__export(add_auto_review_flow_id_exports, {
|
|
29
|
+
default: () => AddAutoReviewFlowIdMigration
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(add_auto_review_flow_id_exports);
|
|
32
|
+
var import_server = require("@nocobase/server");
|
|
33
|
+
var import_sequelize = require("sequelize");
|
|
34
|
+
class AddAutoReviewFlowIdMigration extends import_server.Migration {
|
|
35
|
+
on = "afterLoad";
|
|
36
|
+
async up() {
|
|
37
|
+
var _a;
|
|
38
|
+
const queryInterface = this.db.sequelize.getQueryInterface();
|
|
39
|
+
const tablePrefix = ((_a = this.db.options) == null ? void 0 : _a.tablePrefix) || "";
|
|
40
|
+
const tableName = `${tablePrefix}gitRepositories`;
|
|
41
|
+
const tableInfo = await queryInterface.describeTable(tableName).catch(() => null);
|
|
42
|
+
if (!tableInfo || tableInfo.autoReviewFlowId) return;
|
|
43
|
+
await queryInterface.addColumn(tableName, "autoReviewFlowId", {
|
|
44
|
+
type: import_sequelize.DataTypes.INTEGER,
|
|
45
|
+
allowNull: true
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
async down() {
|
|
49
|
+
var _a;
|
|
50
|
+
const queryInterface = this.db.sequelize.getQueryInterface();
|
|
51
|
+
const tablePrefix = ((_a = this.db.options) == null ? void 0 : _a.tablePrefix) || "";
|
|
52
|
+
const tableName = `${tablePrefix}gitRepositories`;
|
|
53
|
+
const tableInfo = await queryInterface.describeTable(tableName).catch(() => null);
|
|
54
|
+
if (!(tableInfo == null ? void 0 : tableInfo.autoReviewFlowId)) return;
|
|
55
|
+
await queryInterface.removeColumn(tableName, "autoReviewFlowId");
|
|
56
|
+
}
|
|
57
|
+
}
|
package/dist/server/plugin.d.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { Plugin } from '@nocobase/server';
|
|
2
2
|
export declare class PluginGitManagerServer extends Plugin {
|
|
3
|
+
app: any;
|
|
4
|
+
db: any;
|
|
5
|
+
beforeLoad(): Promise<void>;
|
|
3
6
|
load(): Promise<void>;
|
|
4
7
|
install(): Promise<void>;
|
|
5
8
|
beforeDisable(): Promise<void>;
|
|
6
9
|
beforeUnload(): Promise<void>;
|
|
7
10
|
}
|
|
11
|
+
export declare function ensureAutoReviewFlowSchema(app: any): Promise<void>;
|
|
8
12
|
export default PluginGitManagerServer;
|
package/dist/server/plugin.js
CHANGED
|
@@ -37,11 +37,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
37
37
|
var plugin_exports = {};
|
|
38
38
|
__export(plugin_exports, {
|
|
39
39
|
PluginGitManagerServer: () => PluginGitManagerServer,
|
|
40
|
-
default: () => plugin_default
|
|
40
|
+
default: () => plugin_default,
|
|
41
|
+
ensureAutoReviewFlowSchema: () => ensureAutoReviewFlowSchema
|
|
41
42
|
});
|
|
42
43
|
module.exports = __toCommonJS(plugin_exports);
|
|
43
44
|
var import_server = require("@nocobase/server");
|
|
44
45
|
var import_path = require("path");
|
|
46
|
+
var import_sequelize = require("sequelize");
|
|
45
47
|
var gitActions = __toESM(require("./actions/git-actions"));
|
|
46
48
|
var gitlabApi = __toESM(require("./actions/gitlab-api"));
|
|
47
49
|
var reviewActions = __toESM(require("./actions/review"));
|
|
@@ -50,15 +52,22 @@ var import_review = require("./actions/review");
|
|
|
50
52
|
var import_ai_tools = require("./ai-tools");
|
|
51
53
|
var import_poller = require("./poller");
|
|
52
54
|
class PluginGitManagerServer extends import_server.Plugin {
|
|
55
|
+
async beforeLoad() {
|
|
56
|
+
await this.app.db.import({
|
|
57
|
+
directory: (0, import_path.resolve)(__dirname, "collections")
|
|
58
|
+
});
|
|
59
|
+
this.app.db.addMigrations({
|
|
60
|
+
namespace: this.name,
|
|
61
|
+
directory: (0, import_path.resolve)(__dirname, "migrations"),
|
|
62
|
+
context: { plugin: this }
|
|
63
|
+
});
|
|
64
|
+
}
|
|
53
65
|
async load() {
|
|
54
66
|
const dayjsLib = require("dayjs");
|
|
55
67
|
const utcPlugin = require("dayjs/plugin/utc");
|
|
56
68
|
const timezonePlugin = require("dayjs/plugin/timezone");
|
|
57
69
|
dayjsLib.extend(utcPlugin);
|
|
58
70
|
dayjsLib.extend(timezonePlugin);
|
|
59
|
-
await this.db.import({
|
|
60
|
-
directory: (0, import_path.resolve)(__dirname, "collections")
|
|
61
|
-
});
|
|
62
71
|
this.app.resourceManager.define({
|
|
63
72
|
name: "gitManager",
|
|
64
73
|
actions: {
|
|
@@ -97,7 +106,13 @@ class PluginGitManagerServer extends import_server.Plugin {
|
|
|
97
106
|
return next();
|
|
98
107
|
});
|
|
99
108
|
(0, import_ai_tools.registerGitReviewAiTools)(this.app);
|
|
100
|
-
this.app.on("afterStart", () => {
|
|
109
|
+
this.app.on("afterStart", async () => {
|
|
110
|
+
await ensureAutoReviewFlowSchema(this.app).catch(
|
|
111
|
+
(err) => {
|
|
112
|
+
var _a, _b;
|
|
113
|
+
return (_b = (_a = this.app.log) == null ? void 0 : _a.error) == null ? void 0 : _b.call(_a, "plugin-git-manager: ensure schema error", err);
|
|
114
|
+
}
|
|
115
|
+
);
|
|
101
116
|
(0, import_review.recoverStuckReviews)(this.app).catch(
|
|
102
117
|
(err) => {
|
|
103
118
|
var _a, _b;
|
|
@@ -188,6 +203,8 @@ class PluginGitManagerServer extends import_server.Plugin {
|
|
|
188
203
|
});
|
|
189
204
|
}
|
|
190
205
|
async install() {
|
|
206
|
+
var _a;
|
|
207
|
+
await ((_a = this.app.db.getCollection("gitRepositories")) == null ? void 0 : _a.sync());
|
|
191
208
|
}
|
|
192
209
|
async beforeDisable() {
|
|
193
210
|
(0, import_poller.stopPoller)();
|
|
@@ -196,8 +213,23 @@ class PluginGitManagerServer extends import_server.Plugin {
|
|
|
196
213
|
(0, import_poller.stopPoller)();
|
|
197
214
|
}
|
|
198
215
|
}
|
|
216
|
+
async function ensureAutoReviewFlowSchema(app) {
|
|
217
|
+
var _a, _b, _c;
|
|
218
|
+
const sequelize = (_a = app.db) == null ? void 0 : _a.sequelize;
|
|
219
|
+
const queryInterface = (_b = sequelize == null ? void 0 : sequelize.getQueryInterface) == null ? void 0 : _b.call(sequelize);
|
|
220
|
+
if (!queryInterface) return;
|
|
221
|
+
const tablePrefix = ((_c = app.db.options) == null ? void 0 : _c.tablePrefix) || "";
|
|
222
|
+
const tableName = `${tablePrefix}gitRepositories`;
|
|
223
|
+
const tableInfo = await queryInterface.describeTable(tableName).catch(() => null);
|
|
224
|
+
if (!tableInfo || tableInfo.autoReviewFlowId) return;
|
|
225
|
+
await queryInterface.addColumn(tableName, "autoReviewFlowId", {
|
|
226
|
+
type: import_sequelize.DataTypes.INTEGER,
|
|
227
|
+
allowNull: true
|
|
228
|
+
});
|
|
229
|
+
}
|
|
199
230
|
var plugin_default = PluginGitManagerServer;
|
|
200
231
|
// Annotate the CommonJS export names for ESM import in node:
|
|
201
232
|
0 && (module.exports = {
|
|
202
|
-
PluginGitManagerServer
|
|
233
|
+
PluginGitManagerServer,
|
|
234
|
+
ensureAutoReviewFlowSchema
|
|
203
235
|
});
|
package/dist/server/poller.js
CHANGED
|
@@ -202,13 +202,15 @@ async function pollOneRepo(app, repo) {
|
|
|
202
202
|
sort: ["-repositoryId"]
|
|
203
203
|
});
|
|
204
204
|
if (!(flows == null ? void 0 : flows.length)) return { scanned: 0, triggered: 0 };
|
|
205
|
+
const primaryFlowId = repo.get("autoReviewFlowId");
|
|
206
|
+
const primaryFlow = primaryFlowId ? flows.find((flow) => Number(flow.get("id")) === Number(primaryFlowId)) : null;
|
|
205
207
|
const pat = repo.get("pat");
|
|
206
208
|
if (!pat) return { scanned: 0, triggered: 0 };
|
|
207
209
|
const lastPolledAt = repo.get("lastPolledAt");
|
|
208
210
|
const mrs = await listMergeRequests(repo, lastPolledAt);
|
|
209
211
|
let triggered = 0;
|
|
210
212
|
for (const mr of mrs) {
|
|
211
|
-
const flow = flows.find((f) => (0, import_review.branchMatches)(f, mr.source_branch));
|
|
213
|
+
const flow = primaryFlow && (0, import_review.branchMatches)(primaryFlow, mr.source_branch) ? primaryFlow : flows.find((f) => (0, import_review.branchMatches)(f, mr.source_branch));
|
|
212
214
|
if (!flow) continue;
|
|
213
215
|
const existing = await reviewsRepo.findOne({
|
|
214
216
|
filter: {
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"displayName": "Git Manager",
|
|
4
4
|
"displayName.zh-CN": "Git 管理器",
|
|
5
5
|
"description": "Manage Git repositories with PAT authentication - pull, push, fetch, diff, file browsing",
|
|
6
|
-
"version": "1.1.
|
|
6
|
+
"version": "1.1.10",
|
|
7
7
|
"license": "Apache-2.0",
|
|
8
8
|
"main": "dist/server/index.js",
|
|
9
9
|
"files": [
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useEffect, useState, useCallback, memo } from 'react';
|
|
2
|
-
import { Table, Empty, Spin, Typography, Tag, Drawer, List, Button, Space, Select, theme } from 'antd';
|
|
2
|
+
import { Table, Empty, Spin, Typography, Tag, Drawer, List, Button, Space, Select, Input, theme } from 'antd';
|
|
3
3
|
import {
|
|
4
4
|
BranchesOutlined,
|
|
5
5
|
UserOutlined,
|
|
@@ -37,6 +37,7 @@ export const CommitHistory: React.FC = () => {
|
|
|
37
37
|
const [detailLoading, setDetailLoading] = useState(false);
|
|
38
38
|
const [diffContent, setDiffContent] = useState<string | null>(null);
|
|
39
39
|
const [diffFile, setDiffFile] = useState<string | null>(null);
|
|
40
|
+
const [searchKeyword, setSearchKeyword] = useState('');
|
|
40
41
|
|
|
41
42
|
const loadHistory = useCallback(async () => {
|
|
42
43
|
if (!selectedRepo) return;
|
|
@@ -61,6 +62,12 @@ export const CommitHistory: React.FC = () => {
|
|
|
61
62
|
}
|
|
62
63
|
}, [selectedRepo]);
|
|
63
64
|
|
|
65
|
+
const filteredCommits = commits.filter((commit) => {
|
|
66
|
+
const keyword = searchKeyword.trim().toLowerCase();
|
|
67
|
+
if (!keyword) return true;
|
|
68
|
+
return String(commit.message || commit.subject || commit.body || '').toLowerCase().includes(keyword);
|
|
69
|
+
});
|
|
70
|
+
|
|
64
71
|
const openCommitDetail = async (commit: any) => {
|
|
65
72
|
setSelectedCommit(commit);
|
|
66
73
|
setDetailLoading(true);
|
|
@@ -174,14 +181,22 @@ export const CommitHistory: React.FC = () => {
|
|
|
174
181
|
options={branchList.map((b) => ({ label: b, value: b }))}
|
|
175
182
|
disabled
|
|
176
183
|
/>
|
|
177
|
-
<
|
|
184
|
+
<Input.Search
|
|
185
|
+
allowClear
|
|
186
|
+
size="small"
|
|
187
|
+
style={{ width: 280 }}
|
|
188
|
+
placeholder={t('Search commit title or message')}
|
|
189
|
+
value={searchKeyword}
|
|
190
|
+
onChange={(event) => setSearchKeyword(event.target.value)}
|
|
191
|
+
/>
|
|
192
|
+
<Text type="secondary">{filteredCommits.length} commits</Text>
|
|
178
193
|
</div>
|
|
179
194
|
|
|
180
195
|
{loading ? (
|
|
181
196
|
<Spin />
|
|
182
197
|
) : (
|
|
183
198
|
<Table
|
|
184
|
-
dataSource={
|
|
199
|
+
dataSource={filteredCommits}
|
|
185
200
|
columns={columns}
|
|
186
201
|
rowKey="hash"
|
|
187
202
|
size="small"
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import React, { useState, useCallback } from 'react';
|
|
1
|
+
import React, { useState, useCallback, useEffect } from 'react';
|
|
2
2
|
import { Button, Space, Card, Typography, Tag, Empty, message, Spin, Descriptions, List, Badge, theme } from 'antd';
|
|
3
3
|
import {
|
|
4
4
|
CloudDownloadOutlined,
|
|
5
|
-
CloudUploadOutlined,
|
|
6
5
|
SyncOutlined,
|
|
7
6
|
BranchesOutlined,
|
|
8
7
|
CheckCircleOutlined,
|
|
@@ -37,6 +36,29 @@ export const GitOperations: React.FC = () => {
|
|
|
37
36
|
const [statusData, setStatusData] = useState<any>(null);
|
|
38
37
|
const [lastResult, setLastResult] = useState<{ action: string; success: boolean; message: string } | null>(null);
|
|
39
38
|
|
|
39
|
+
const loadStatus = useCallback(async () => {
|
|
40
|
+
if (!selectedRepo || selectedRepo.status !== 'connected') {
|
|
41
|
+
setStatusData(null);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
setActionLoading('status');
|
|
45
|
+
try {
|
|
46
|
+
const { data } = await api.request({
|
|
47
|
+
url: 'gitManager:status',
|
|
48
|
+
method: 'post',
|
|
49
|
+
params: { repositoryId: selectedRepo.id },
|
|
50
|
+
});
|
|
51
|
+
const responseData = data?.data?.data || data?.data;
|
|
52
|
+
setStatusData(responseData);
|
|
53
|
+
} finally {
|
|
54
|
+
setActionLoading(null);
|
|
55
|
+
}
|
|
56
|
+
}, [api, selectedRepo]);
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
loadStatus();
|
|
60
|
+
}, [loadStatus]);
|
|
61
|
+
|
|
40
62
|
const execAction = useCallback(
|
|
41
63
|
async (action: string) => {
|
|
42
64
|
if (!selectedRepo) return;
|
|
@@ -50,10 +72,9 @@ export const GitOperations: React.FC = () => {
|
|
|
50
72
|
});
|
|
51
73
|
setLastResult({ action, success: true, message: t('{{action}} completed successfully', { action }) });
|
|
52
74
|
message.success(t('{{action}} completed', { action }));
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
75
|
+
const responseData = data?.data?.data || data?.data;
|
|
76
|
+
if (action === 'status') setStatusData(responseData);
|
|
77
|
+
if (action === 'fetch' || action === 'pull') await loadStatus();
|
|
57
78
|
await refreshRepos();
|
|
58
79
|
} catch (err: any) {
|
|
59
80
|
const msg = err?.response?.data?.errors?.[0]?.message || t('{{action}} failed', { action });
|
|
@@ -63,7 +84,7 @@ export const GitOperations: React.FC = () => {
|
|
|
63
84
|
setActionLoading(null);
|
|
64
85
|
}
|
|
65
86
|
},
|
|
66
|
-
[api, selectedRepo, refreshRepos],
|
|
87
|
+
[api, selectedRepo, refreshRepos, loadStatus],
|
|
67
88
|
);
|
|
68
89
|
|
|
69
90
|
if (!selectedRepo) {
|
|
@@ -104,19 +125,11 @@ export const GitOperations: React.FC = () => {
|
|
|
104
125
|
>
|
|
105
126
|
{t('Pull')}
|
|
106
127
|
</Button>
|
|
107
|
-
<Button
|
|
108
|
-
size="large"
|
|
109
|
-
icon={<CloudUploadOutlined />}
|
|
110
|
-
loading={actionLoading === 'push'}
|
|
111
|
-
onClick={() => execAction('push')}
|
|
112
|
-
>
|
|
113
|
-
{t('Push')}
|
|
114
|
-
</Button>
|
|
115
128
|
<Button
|
|
116
129
|
size="large"
|
|
117
130
|
icon={<BranchesOutlined />}
|
|
118
131
|
loading={actionLoading === 'status'}
|
|
119
|
-
onClick={
|
|
132
|
+
onClick={loadStatus}
|
|
120
133
|
>
|
|
121
134
|
{t('Status')}
|
|
122
135
|
</Button>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { Select } from 'antd';
|
|
3
|
+
import { useAPIClient } from '@nocobase/client';
|
|
4
|
+
|
|
5
|
+
export const LLMModelSelect: React.FC<{
|
|
6
|
+
llmService?: string;
|
|
7
|
+
value?: string;
|
|
8
|
+
onChange?: (value: string) => void;
|
|
9
|
+
allowClear?: boolean;
|
|
10
|
+
size?: 'small' | 'middle' | 'large';
|
|
11
|
+
style?: React.CSSProperties;
|
|
12
|
+
placeholder?: string;
|
|
13
|
+
}> = ({ llmService, value, onChange, allowClear = true, size, style, placeholder }) => {
|
|
14
|
+
const api = useAPIClient();
|
|
15
|
+
const [models, setModels] = useState<{ id: string }[]>([]);
|
|
16
|
+
const [loading, setLoading] = useState(false);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
let cancelled = false;
|
|
20
|
+
if (!llmService) {
|
|
21
|
+
setModels([]);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
setLoading(true);
|
|
26
|
+
api
|
|
27
|
+
.request({ url: 'ai:listModels', params: { llmService } })
|
|
28
|
+
.then((res) => {
|
|
29
|
+
if (cancelled) return;
|
|
30
|
+
const list = res?.data?.data || [];
|
|
31
|
+
setModels(Array.isArray(list) ? list : []);
|
|
32
|
+
})
|
|
33
|
+
.catch(() => {
|
|
34
|
+
if (!cancelled) setModels([]);
|
|
35
|
+
})
|
|
36
|
+
.finally(() => {
|
|
37
|
+
if (!cancelled) setLoading(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return () => {
|
|
41
|
+
cancelled = true;
|
|
42
|
+
};
|
|
43
|
+
}, [api, llmService]);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<Select
|
|
47
|
+
value={value}
|
|
48
|
+
onChange={onChange}
|
|
49
|
+
allowClear={allowClear}
|
|
50
|
+
loading={loading}
|
|
51
|
+
size={size}
|
|
52
|
+
style={style}
|
|
53
|
+
placeholder={placeholder || 'Select LLM Model'}
|
|
54
|
+
options={models.map((m) => ({
|
|
55
|
+
value: m.id,
|
|
56
|
+
label: m.id,
|
|
57
|
+
}))}
|
|
58
|
+
showSearch
|
|
59
|
+
optionFilterProp="label"
|
|
60
|
+
disabled={!llmService}
|
|
61
|
+
/>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
@@ -24,12 +24,26 @@ export const PollingStatus: React.FC = () => {
|
|
|
24
24
|
const [status, setStatus] = useState<PollerStatus | null>(null);
|
|
25
25
|
const [loading, setLoading] = useState(false);
|
|
26
26
|
const [pollingId, setPollingId] = useState<number | 'all' | null>(null);
|
|
27
|
+
const [flows, setFlows] = useState<any[]>([]);
|
|
27
28
|
|
|
28
29
|
const reload = useCallback(async () => {
|
|
29
30
|
setLoading(true);
|
|
30
31
|
try {
|
|
31
|
-
const { data } = await
|
|
32
|
+
const [{ data }, flowsRes] = await Promise.all([
|
|
33
|
+
api.request({ url: 'gitManager:pollerStatus' }),
|
|
34
|
+
api.request({
|
|
35
|
+
url: 'gitReviewFlows:list',
|
|
36
|
+
params: {
|
|
37
|
+
pageSize: 100,
|
|
38
|
+
filter: {
|
|
39
|
+
enabled: true,
|
|
40
|
+
triggerMode: { $in: ['onMergeRequestCreated', 'both'] },
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
}),
|
|
44
|
+
]);
|
|
32
45
|
setStatus(data?.data || null);
|
|
46
|
+
setFlows(flowsRes?.data?.data || []);
|
|
33
47
|
await refreshRepos();
|
|
34
48
|
} finally {
|
|
35
49
|
setLoading(false);
|
|
@@ -162,6 +176,18 @@ export const PollingStatus: React.FC = () => {
|
|
|
162
176
|
pagination={false}
|
|
163
177
|
columns={[
|
|
164
178
|
{ title: t('Repository Name'), dataIndex: 'name' },
|
|
179
|
+
{
|
|
180
|
+
title: t('Primary Auto Flow'),
|
|
181
|
+
dataIndex: 'autoReviewFlowId',
|
|
182
|
+
width: 220,
|
|
183
|
+
render: (flowId: number | null, record: any) => {
|
|
184
|
+
const flow = flows.find((item) => Number(item.id) === Number(flowId));
|
|
185
|
+
const fallback = flows.find((item) => item.repositoryId === record.id || item.repositoryId == null);
|
|
186
|
+
if (flow) return <Tag color="blue">{flow.name}</Tag>;
|
|
187
|
+
if (fallback) return <Tag color="default">{t('Fallback')}: {fallback.name}</Tag>;
|
|
188
|
+
return <Tag color="red">{t('No matching flow available')}</Tag>;
|
|
189
|
+
},
|
|
190
|
+
},
|
|
165
191
|
{
|
|
166
192
|
title: t('Last Polled At'),
|
|
167
193
|
dataIndex: 'lastPolledAt',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
2
|
-
import { Table, Button, Modal, Form, Input, Space, Tag, Popconfirm, Switch, Tooltip, message } from 'antd';
|
|
1
|
+
import React, { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import { Table, Button, Modal, Form, Input, Space, Tag, Popconfirm, Switch, Tooltip, Select, message } from 'antd';
|
|
3
3
|
import { PlusOutlined, DeleteOutlined, LinkOutlined, RobotOutlined } from '@ant-design/icons';
|
|
4
4
|
import { useAPIClient } from '@nocobase/client';
|
|
5
5
|
import { useGitManager } from '../context/GitManagerContext';
|
|
@@ -13,6 +13,33 @@ export const RepositoryConfig: React.FC = () => {
|
|
|
13
13
|
const [editingRepo, setEditingRepo] = useState<any>(null);
|
|
14
14
|
const [form] = Form.useForm();
|
|
15
15
|
const [actionLoading, setActionLoading] = useState<string | null>(null);
|
|
16
|
+
const [reviewFlows, setReviewFlows] = useState<any[]>([]);
|
|
17
|
+
|
|
18
|
+
const loadReviewFlows = useCallback(async () => {
|
|
19
|
+
const { data } = await api.request({
|
|
20
|
+
url: 'gitReviewFlows:list',
|
|
21
|
+
params: {
|
|
22
|
+
pageSize: 100,
|
|
23
|
+
filter: {
|
|
24
|
+
enabled: true,
|
|
25
|
+
triggerMode: { $in: ['onMergeRequestCreated', 'both'] },
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
setReviewFlows(data?.data || []);
|
|
30
|
+
}, [api]);
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
loadReviewFlows().catch(() => undefined);
|
|
34
|
+
}, [loadReviewFlows]);
|
|
35
|
+
|
|
36
|
+
const getAutoFlowOptions = (repoId: number) =>
|
|
37
|
+
reviewFlows
|
|
38
|
+
.filter((flow) => flow.repositoryId === repoId || flow.repositoryId == null)
|
|
39
|
+
.map((flow) => ({
|
|
40
|
+
value: flow.id,
|
|
41
|
+
label: `${flow.name}${flow.repositoryId == null ? ` (${t('global')})` : ''}`,
|
|
42
|
+
}));
|
|
16
43
|
|
|
17
44
|
const handleSave = async () => {
|
|
18
45
|
const values = await form.validateFields();
|
|
@@ -100,11 +127,15 @@ export const RepositoryConfig: React.FC = () => {
|
|
|
100
127
|
checked={!!v}
|
|
101
128
|
onChange={async (checked) => {
|
|
102
129
|
try {
|
|
130
|
+
const options = getAutoFlowOptions(record.id);
|
|
103
131
|
await api.request({
|
|
104
132
|
url: 'gitRepositories:update',
|
|
105
133
|
method: 'post',
|
|
106
134
|
params: { filterByTk: record.id },
|
|
107
|
-
data: {
|
|
135
|
+
data: {
|
|
136
|
+
autoReview: checked,
|
|
137
|
+
autoReviewFlowId: checked && !record.autoReviewFlowId ? options[0]?.value ?? null : record.autoReviewFlowId,
|
|
138
|
+
},
|
|
108
139
|
});
|
|
109
140
|
await refreshRepos();
|
|
110
141
|
} catch (err: any) {
|
|
@@ -114,6 +145,40 @@ export const RepositoryConfig: React.FC = () => {
|
|
|
114
145
|
/>
|
|
115
146
|
),
|
|
116
147
|
},
|
|
148
|
+
{
|
|
149
|
+
title: t('Primary Auto Flow'),
|
|
150
|
+
dataIndex: 'autoReviewFlowId',
|
|
151
|
+
key: 'autoReviewFlowId',
|
|
152
|
+
width: 220,
|
|
153
|
+
render: (value: number | null, record: any) => {
|
|
154
|
+
const options = getAutoFlowOptions(record.id);
|
|
155
|
+
return (
|
|
156
|
+
<Select
|
|
157
|
+
allowClear
|
|
158
|
+
size="small"
|
|
159
|
+
style={{ width: '100%' }}
|
|
160
|
+
disabled={!record.autoReview}
|
|
161
|
+
placeholder={t('Select a flow')}
|
|
162
|
+
value={value || undefined}
|
|
163
|
+
options={options}
|
|
164
|
+
notFoundContent={t('No matching flow available')}
|
|
165
|
+
onChange={async (flowId) => {
|
|
166
|
+
try {
|
|
167
|
+
await api.request({
|
|
168
|
+
url: 'gitRepositories:update',
|
|
169
|
+
method: 'post',
|
|
170
|
+
params: { filterByTk: record.id },
|
|
171
|
+
data: { autoReviewFlowId: flowId || null },
|
|
172
|
+
});
|
|
173
|
+
await refreshRepos();
|
|
174
|
+
} catch (err: any) {
|
|
175
|
+
message.error(err?.message || t('Failed to save'));
|
|
176
|
+
}
|
|
177
|
+
}}
|
|
178
|
+
/>
|
|
179
|
+
);
|
|
180
|
+
},
|
|
181
|
+
},
|
|
117
182
|
{
|
|
118
183
|
title: t('Status'),
|
|
119
184
|
dataIndex: 'status',
|
|
@@ -216,6 +281,14 @@ export const RepositoryConfig: React.FC = () => {
|
|
|
216
281
|
<Form.Item name="defaultBranch" label={t('Default Branch')}>
|
|
217
282
|
<Input placeholder="main" />
|
|
218
283
|
</Form.Item>
|
|
284
|
+
<Form.Item name="autoReviewFlowId" label={t('Primary Auto Review Flow')} extra={t('Only flows with automatic trigger modes are shown')}>
|
|
285
|
+
<Select
|
|
286
|
+
allowClear
|
|
287
|
+
placeholder={t('Select a flow')}
|
|
288
|
+
options={getAutoFlowOptions(editingRepo?.id)}
|
|
289
|
+
notFoundContent={t('No matching flow available')}
|
|
290
|
+
/>
|
|
291
|
+
</Form.Item>
|
|
219
292
|
</Form>
|
|
220
293
|
</Modal>
|
|
221
294
|
</div>
|