@forgeportal/plugin-argocd 1.3.0
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/.turbo/turbo-build.log +4 -0
- package/LICENSE +21 -0
- package/dist/ArgocdTab.d.ts +8 -0
- package/dist/ArgocdTab.d.ts.map +1 -0
- package/dist/ArgocdTab.js +69 -0
- package/dist/ArgocdTab.js.map +1 -0
- package/dist/__tests__/api-client.test.d.ts +2 -0
- package/dist/__tests__/api-client.test.d.ts.map +1 -0
- package/dist/__tests__/api-client.test.js +129 -0
- package/dist/__tests__/api-client.test.js.map +1 -0
- package/dist/actions.d.ts +23 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +106 -0
- package/dist/actions.js.map +1 -0
- package/dist/api-client.d.ts +35 -0
- package/dist/api-client.d.ts.map +1 -0
- package/dist/api-client.js +73 -0
- package/dist/api-client.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/routes.d.ts +13 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +92 -0
- package/dist/routes.js.map +1 -0
- package/dist/types.d.ts +62 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/ui.d.ts +11 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +17 -0
- package/dist/ui.js.map +1 -0
- package/forgeportal-plugin.json +32 -0
- package/package.json +51 -0
- package/src/ArgocdTab.tsx +305 -0
- package/src/__tests__/api-client.test.ts +156 -0
- package/src/actions.ts +118 -0
- package/src/api-client.ts +90 -0
- package/src/index.ts +46 -0
- package/src/routes.ts +127 -0
- package/src/types.ts +65 -0
- package/src/ui.ts +18 -0
- package/tsconfig.json +11 -0
package/dist/routes.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { ArgocdApiClient } from './api-client.js';
|
|
2
|
+
/**
|
|
3
|
+
* Creates Fastify route handlers for the ArgoCD plugin.
|
|
4
|
+
* All routes are mounted under /api/v1/plugins/argocd/ by the plugin loader.
|
|
5
|
+
*
|
|
6
|
+
* Routes:
|
|
7
|
+
* GET entities/:entityId/app — app summary (status, health, revision)
|
|
8
|
+
* GET entities/:entityId/history — last 10 sync operations
|
|
9
|
+
* POST entities/:entityId/sync — trigger sync
|
|
10
|
+
*/
|
|
11
|
+
export function createRoutes(config) {
|
|
12
|
+
const client = new ArgocdApiClient(config);
|
|
13
|
+
return async function handler(fastify) {
|
|
14
|
+
/**
|
|
15
|
+
* GET /entities/:entityId/app?appName=<override>
|
|
16
|
+
*
|
|
17
|
+
* Returns the ArgoCD application summary: sync status, health, revision, operation state.
|
|
18
|
+
* The app name is read from the query param or entity annotations.
|
|
19
|
+
*/
|
|
20
|
+
fastify.get('entities/:entityId/app', async (request, reply) => {
|
|
21
|
+
const appName = request.query.appName;
|
|
22
|
+
if (!appName) {
|
|
23
|
+
return reply.status(400).send({
|
|
24
|
+
error: 'Bad Request',
|
|
25
|
+
message: 'Query parameter "appName" is required, or set the forgeportal.dev/argocd-app-name annotation.',
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const app = await client.getApp(appName);
|
|
30
|
+
return reply.send({ data: app });
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
34
|
+
request.log.error({ err }, 'argocd plugin: getApp failed');
|
|
35
|
+
if (message.includes('404')) {
|
|
36
|
+
return reply.status(404).send({ error: 'Not Found', message: `ArgoCD app "${appName}" not found.` });
|
|
37
|
+
}
|
|
38
|
+
return reply.status(502).send({ error: 'Bad Gateway', message });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
/**
|
|
42
|
+
* GET /entities/:entityId/history?appName=<override>
|
|
43
|
+
*
|
|
44
|
+
* Returns the last 10 sync history entries for the ArgoCD application.
|
|
45
|
+
*/
|
|
46
|
+
fastify.get('entities/:entityId/history', async (request, reply) => {
|
|
47
|
+
const appName = request.query.appName;
|
|
48
|
+
if (!appName) {
|
|
49
|
+
return reply.status(400).send({
|
|
50
|
+
error: 'Bad Request',
|
|
51
|
+
message: 'Query parameter "appName" is required.',
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const history = await client.getHistory(appName);
|
|
56
|
+
return reply.send({ data: history });
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
60
|
+
request.log.error({ err }, 'argocd plugin: getHistory failed');
|
|
61
|
+
return reply.status(502).send({ error: 'Bad Gateway', message });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
/**
|
|
65
|
+
* POST /entities/:entityId/sync
|
|
66
|
+
*
|
|
67
|
+
* Body: { appName: string }
|
|
68
|
+
* Triggers a manual sync of the ArgoCD application.
|
|
69
|
+
*/
|
|
70
|
+
fastify.post('entities/:entityId/sync', async (request, reply) => {
|
|
71
|
+
const { appName } = request.body ?? {};
|
|
72
|
+
if (!appName) {
|
|
73
|
+
return reply.status(400).send({
|
|
74
|
+
error: 'Bad Request',
|
|
75
|
+
message: 'Body field "appName" is required.',
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
await client.syncApp(appName);
|
|
80
|
+
return reply.status(202).send({
|
|
81
|
+
data: { appName, syncTriggeredAt: new Date().toISOString() },
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
86
|
+
request.log.error({ err }, 'argocd plugin: syncApp failed');
|
|
87
|
+
return reply.status(502).send({ error: 'Bad Gateway', message });
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export interface ArgocdAppStatus {
|
|
2
|
+
sync: {
|
|
3
|
+
status: 'Synced' | 'OutOfSync' | 'Unknown';
|
|
4
|
+
revision: string;
|
|
5
|
+
};
|
|
6
|
+
health: {
|
|
7
|
+
status: 'Healthy' | 'Degraded' | 'Progressing' | 'Suspended' | 'Missing' | 'Unknown';
|
|
8
|
+
};
|
|
9
|
+
operationState?: {
|
|
10
|
+
phase: string;
|
|
11
|
+
message?: string;
|
|
12
|
+
startedAt: string;
|
|
13
|
+
finishedAt?: string;
|
|
14
|
+
};
|
|
15
|
+
reconciledAt?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface ArgocdApp {
|
|
18
|
+
metadata: {
|
|
19
|
+
name: string;
|
|
20
|
+
namespace: string;
|
|
21
|
+
};
|
|
22
|
+
spec: {
|
|
23
|
+
project: string;
|
|
24
|
+
source?: {
|
|
25
|
+
repoURL: string;
|
|
26
|
+
targetRevision: string;
|
|
27
|
+
path?: string;
|
|
28
|
+
};
|
|
29
|
+
destination: {
|
|
30
|
+
server: string;
|
|
31
|
+
namespace: string;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
status: ArgocdAppStatus;
|
|
35
|
+
}
|
|
36
|
+
export interface ArgocdHistoryItem {
|
|
37
|
+
id: number;
|
|
38
|
+
revision: string;
|
|
39
|
+
deployedAt: string;
|
|
40
|
+
initiatedBy?: {
|
|
41
|
+
username?: string;
|
|
42
|
+
automated?: boolean;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export interface ArgocdResourceNode {
|
|
46
|
+
kind: string;
|
|
47
|
+
name: string;
|
|
48
|
+
namespace?: string;
|
|
49
|
+
status?: string;
|
|
50
|
+
health?: {
|
|
51
|
+
status: string;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export interface ArgocdResourceTree {
|
|
55
|
+
nodes: ArgocdResourceNode[];
|
|
56
|
+
}
|
|
57
|
+
export interface ArgocdConfig {
|
|
58
|
+
url: string;
|
|
59
|
+
token: string;
|
|
60
|
+
insecure: boolean;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE;QACJ,MAAM,EAAI,QAAQ,GAAG,WAAW,GAAG,SAAS,CAAC;QAC7C,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,MAAM,EAAE;QACN,MAAM,EAAE,SAAS,GAAG,UAAU,GAAG,aAAa,GAAG,WAAW,GAAG,SAAS,GAAG,SAAS,CAAC;KACtF,CAAC;IACF,cAAc,CAAC,EAAE;QACf,KAAK,EAAM,MAAM,CAAC;QAClB,OAAO,CAAC,EAAG,MAAM,CAAC;QAClB,SAAS,EAAE,MAAM,CAAC;QAClB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC;IACF,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE;QACR,IAAI,EAAO,MAAM,CAAC;QAClB,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,IAAI,EAAE;QACJ,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,CAAC,EAAE;YACP,OAAO,EAAS,MAAM,CAAC;YACvB,cAAc,EAAE,MAAM,CAAC;YACvB,IAAI,CAAC,EAAW,MAAM,CAAC;SACxB,CAAC;QACF,WAAW,EAAE;YACX,MAAM,EAAK,MAAM,CAAC;YAClB,SAAS,EAAE,MAAM,CAAC;SACnB,CAAC;KACH,CAAC;IACF,MAAM,EAAE,eAAe,CAAC;CACzB;AAED,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAU,MAAM,CAAC;IACnB,QAAQ,EAAI,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CAC1D;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAQ,MAAM,CAAC;IACnB,IAAI,EAAQ,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAK,MAAM,CAAC;IACnB,MAAM,CAAC,EAAK;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CAChC;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,kBAAkB,EAAE,CAAC;CAC7B;AAID,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAO,MAAM,CAAC;IACjB,KAAK,EAAK,MAAM,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;CACnB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,gFAAgF"}
|
package/dist/ui.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ForgePluginSDK } from '@forgeportal/plugin-sdk';
|
|
2
|
+
/**
|
|
3
|
+
* UI entry point for the ArgoCD plugin.
|
|
4
|
+
* Called by the ForgePortal UI shell at startup.
|
|
5
|
+
*
|
|
6
|
+
* Registration in apps/ui/src/plugins/index.ts:
|
|
7
|
+
* import { registerPlugin as registerArgocd } from '@forgeportal/plugin-argocd/ui';
|
|
8
|
+
* registerPluginById('argocd', registerArgocd);
|
|
9
|
+
*/
|
|
10
|
+
export declare function registerPlugin(sdk: ForgePluginSDK): void;
|
|
11
|
+
//# sourceMappingURL=ui.d.ts.map
|
package/dist/ui.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ui.d.ts","sourceRoot":"","sources":["../src/ui.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAG9D;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,cAAc,GAAG,IAAI,CAMxD"}
|
package/dist/ui.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ArgocdTab } from './ArgocdTab.js';
|
|
2
|
+
/**
|
|
3
|
+
* UI entry point for the ArgoCD plugin.
|
|
4
|
+
* Called by the ForgePortal UI shell at startup.
|
|
5
|
+
*
|
|
6
|
+
* Registration in apps/ui/src/plugins/index.ts:
|
|
7
|
+
* import { registerPlugin as registerArgocd } from '@forgeportal/plugin-argocd/ui';
|
|
8
|
+
* registerPluginById('argocd', registerArgocd);
|
|
9
|
+
*/
|
|
10
|
+
export function registerPlugin(sdk) {
|
|
11
|
+
sdk.registerEntityTab({
|
|
12
|
+
id: 'argocd-tab',
|
|
13
|
+
title: 'ArgoCD',
|
|
14
|
+
component: ArgocdTab,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=ui.js.map
|
package/dist/ui.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ui.js","sourceRoot":"","sources":["../src/ui.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,GAAmB;IAChD,GAAG,CAAC,iBAAiB,CAAC;QACpB,EAAE,EAAS,YAAY;QACvB,KAAK,EAAM,QAAQ;QACnB,SAAS,EAAE,SAAS;KACrB,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@forgeportal/plugin-argocd",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"forgeportal": {
|
|
5
|
+
"engineVersion": "^1.0.0",
|
|
6
|
+
"type": "fullstack",
|
|
7
|
+
"capabilities": {
|
|
8
|
+
"ui": {
|
|
9
|
+
"entityTabs": ["argocd-tab"]
|
|
10
|
+
},
|
|
11
|
+
"backend": {
|
|
12
|
+
"routes": ["/api/v1/plugins/argocd"],
|
|
13
|
+
"actionProviders": [
|
|
14
|
+
"argocd.syncApp@v1",
|
|
15
|
+
"argocd.rollbackApp@v1"
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"config": {
|
|
20
|
+
"url": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"description": "ArgoCD server URL, e.g. https://argocd.internal",
|
|
23
|
+
"required": true
|
|
24
|
+
},
|
|
25
|
+
"insecure": {
|
|
26
|
+
"type": "boolean",
|
|
27
|
+
"description": "Skip TLS verification (not recommended in production).",
|
|
28
|
+
"required": false
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@forgeportal/plugin-argocd",
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"import": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"./ui": {
|
|
11
|
+
"types": "./dist/ui.d.ts",
|
|
12
|
+
"import": "./dist/ui.js"
|
|
13
|
+
},
|
|
14
|
+
"./package.json": "./package.json",
|
|
15
|
+
"./forgeportal-plugin.json": "./forgeportal-plugin.json"
|
|
16
|
+
},
|
|
17
|
+
"main": "dist/index.js",
|
|
18
|
+
"types": "dist/index.d.ts",
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"undici": "^7.22.0",
|
|
24
|
+
"@forgeportal/plugin-sdk": "1.3.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@tanstack/react-query": "*",
|
|
28
|
+
"@types/react": "*",
|
|
29
|
+
"fastify": "^5.3.3",
|
|
30
|
+
"react": "*",
|
|
31
|
+
"vitest": "*"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"@tanstack/react-query": ">=5",
|
|
35
|
+
"react": ">=19"
|
|
36
|
+
},
|
|
37
|
+
"peerDependenciesMeta": {
|
|
38
|
+
"react": {
|
|
39
|
+
"optional": true
|
|
40
|
+
},
|
|
41
|
+
"@tanstack/react-query": {
|
|
42
|
+
"optional": true
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsc",
|
|
47
|
+
"test": "vitest run",
|
|
48
|
+
"lint": "eslint src/",
|
|
49
|
+
"clean": "rm -rf dist"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import React, { useState, useCallback } from 'react';
|
|
2
|
+
import { useApi } from '@forgeportal/plugin-sdk/react';
|
|
3
|
+
import type { Entity } from '@forgeportal/plugin-sdk';
|
|
4
|
+
|
|
5
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
interface SyncStatus { status: 'Synced' | 'OutOfSync' | 'Unknown'; revision?: string }
|
|
8
|
+
interface HealthStatus { status: 'Healthy' | 'Degraded' | 'Progressing' | 'Suspended' | 'Missing' | 'Unknown' }
|
|
9
|
+
|
|
10
|
+
interface ArgocdAppResponse {
|
|
11
|
+
data: {
|
|
12
|
+
metadata: { name: string; namespace: string };
|
|
13
|
+
spec: {
|
|
14
|
+
project: string;
|
|
15
|
+
source?: { repoURL: string; targetRevision: string; path?: string };
|
|
16
|
+
destination: { server: string; namespace: string };
|
|
17
|
+
};
|
|
18
|
+
status: {
|
|
19
|
+
sync: SyncStatus;
|
|
20
|
+
health: HealthStatus;
|
|
21
|
+
operationState?: { phase: string; message?: string; startedAt: string; finishedAt?: string };
|
|
22
|
+
reconciledAt?: string;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface HistoryItem {
|
|
28
|
+
id: number;
|
|
29
|
+
revision: string;
|
|
30
|
+
deployedAt: string;
|
|
31
|
+
initiatedBy?: { username?: string; automated?: boolean };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface HistoryResponse { data: HistoryItem[] }
|
|
35
|
+
|
|
36
|
+
// ─── Badges ───────────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
function SyncBadge({ status }: { status: string }): React.ReactElement {
|
|
39
|
+
const colours: Record<string, string> = {
|
|
40
|
+
Synced: 'bg-green-100 text-green-800',
|
|
41
|
+
OutOfSync: 'bg-amber-100 text-amber-800',
|
|
42
|
+
Unknown: 'bg-gray-100 text-gray-600',
|
|
43
|
+
};
|
|
44
|
+
return (
|
|
45
|
+
<span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${colours[status] ?? colours['Unknown']}`}>
|
|
46
|
+
{status}
|
|
47
|
+
</span>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function HealthBadge({ status }: { status: string }): React.ReactElement {
|
|
52
|
+
const colours: Record<string, string> = {
|
|
53
|
+
Healthy: 'bg-green-100 text-green-800',
|
|
54
|
+
Degraded: 'bg-red-100 text-red-800',
|
|
55
|
+
Progressing: 'bg-blue-100 text-blue-800',
|
|
56
|
+
Suspended: 'bg-orange-100 text-orange-800',
|
|
57
|
+
Missing: 'bg-gray-100 text-gray-600',
|
|
58
|
+
Unknown: 'bg-gray-100 text-gray-600',
|
|
59
|
+
};
|
|
60
|
+
return (
|
|
61
|
+
<span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${colours[status] ?? colours['Unknown']}`}>
|
|
62
|
+
{status}
|
|
63
|
+
</span>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ─── Main Tab ─────────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
interface ArgocdTabProps { entity: Entity }
|
|
70
|
+
|
|
71
|
+
export function ArgocdTab({ entity }: ArgocdTabProps): React.ReactElement {
|
|
72
|
+
const [syncing, setSyncing] = useState(false);
|
|
73
|
+
const [syncMessage, setSyncMessage] = useState<string | null>(null);
|
|
74
|
+
|
|
75
|
+
const annotations = entity.annotations ?? {};
|
|
76
|
+
const appName = annotations['forgeportal.dev/argocd-app-name'];
|
|
77
|
+
|
|
78
|
+
// Not configured state
|
|
79
|
+
if (!appName) {
|
|
80
|
+
return (
|
|
81
|
+
<div className="rounded-lg border border-dashed border-gray-200 bg-gray-50 p-8 text-center">
|
|
82
|
+
<p className="text-sm font-medium text-gray-700 mb-1">ArgoCD not configured for this entity</p>
|
|
83
|
+
<p className="text-xs text-gray-500 mb-4">
|
|
84
|
+
Add the annotation{' '}
|
|
85
|
+
<code className="rounded bg-gray-100 px-1 py-0.5">forgeportal.dev/argocd-app-name</code>{' '}
|
|
86
|
+
to your <code className="rounded bg-gray-100 px-1 py-0.5">entity.yaml</code> to see the ArgoCD status.
|
|
87
|
+
</p>
|
|
88
|
+
<pre className="mx-auto max-w-md rounded bg-gray-800 p-3 text-left text-xs text-green-300">
|
|
89
|
+
{`metadata:\n annotations:\n forgeportal.dev/argocd-app-name: my-app-prod\n forgeportal.dev/argocd-project: platform # optional`}
|
|
90
|
+
</pre>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const {
|
|
96
|
+
data: appData,
|
|
97
|
+
isPending: appLoading,
|
|
98
|
+
error: appError,
|
|
99
|
+
refetch: refetchApp,
|
|
100
|
+
} = useApi<ArgocdAppResponse>(
|
|
101
|
+
`/api/v1/plugins/argocd/entities/${entity.id}/app?appName=${encodeURIComponent(appName)}`,
|
|
102
|
+
{ refetchInterval: 30_000 },
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const {
|
|
106
|
+
data: historyData,
|
|
107
|
+
isPending: historyLoading,
|
|
108
|
+
} = useApi<HistoryResponse>(
|
|
109
|
+
`/api/v1/plugins/argocd/entities/${entity.id}/history?appName=${encodeURIComponent(appName)}`,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const triggerSync = useCallback(async () => {
|
|
113
|
+
if (!appName || syncing) return;
|
|
114
|
+
setSyncing(true);
|
|
115
|
+
try {
|
|
116
|
+
const res = await fetch(`/api/v1/plugins/argocd/entities/${entity.id}/sync`, {
|
|
117
|
+
method: 'POST',
|
|
118
|
+
credentials: 'include',
|
|
119
|
+
headers: { 'Content-Type': 'application/json' },
|
|
120
|
+
body: JSON.stringify({ appName }),
|
|
121
|
+
});
|
|
122
|
+
if (res.ok) {
|
|
123
|
+
const json = await res.json() as { data: { syncTriggeredAt: string } };
|
|
124
|
+
setSyncMessage(`Sync triggered at ${new Date(json.data.syncTriggeredAt).toLocaleTimeString()}`);
|
|
125
|
+
void refetchApp();
|
|
126
|
+
setTimeout(() => setSyncMessage(null), 5000);
|
|
127
|
+
}
|
|
128
|
+
} finally {
|
|
129
|
+
setSyncing(false);
|
|
130
|
+
}
|
|
131
|
+
}, [appName, entity.id, syncing, refetchApp]);
|
|
132
|
+
|
|
133
|
+
const app = appData?.data;
|
|
134
|
+
const history = historyData?.data ?? [];
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<div className="space-y-6">
|
|
138
|
+
{/* Toolbar */}
|
|
139
|
+
<div className="flex items-center justify-between">
|
|
140
|
+
<div className="flex items-center gap-3">
|
|
141
|
+
{app && (
|
|
142
|
+
<span className="text-xs text-gray-500">
|
|
143
|
+
App: <strong>{app.metadata.name}</strong> · Project: <strong>{app.spec.project}</strong>
|
|
144
|
+
</span>
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
<div className="flex items-center gap-2">
|
|
148
|
+
{syncMessage && (
|
|
149
|
+
<span className="text-xs text-green-600 font-medium">{syncMessage}</span>
|
|
150
|
+
)}
|
|
151
|
+
<button
|
|
152
|
+
onClick={() => void triggerSync()}
|
|
153
|
+
disabled={syncing || appLoading}
|
|
154
|
+
className="inline-flex items-center gap-1.5 rounded-md bg-indigo-600 px-3 py-1.5 text-xs font-medium text-white shadow-sm hover:bg-indigo-700 disabled:opacity-60 transition-colors"
|
|
155
|
+
>
|
|
156
|
+
{syncing ? (
|
|
157
|
+
<>
|
|
158
|
+
<svg className="h-3 w-3 animate-spin" fill="none" viewBox="0 0 24 24">
|
|
159
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
160
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
|
161
|
+
</svg>
|
|
162
|
+
Syncing…
|
|
163
|
+
</>
|
|
164
|
+
) : (
|
|
165
|
+
'Sync Now'
|
|
166
|
+
)}
|
|
167
|
+
</button>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
{/* Loading */}
|
|
172
|
+
{appLoading && (
|
|
173
|
+
<div className="flex items-center gap-2 text-sm text-gray-500">
|
|
174
|
+
<svg className="h-4 w-4 animate-spin text-indigo-500" fill="none" viewBox="0 0 24 24">
|
|
175
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
176
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
|
177
|
+
</svg>
|
|
178
|
+
Loading ArgoCD status…
|
|
179
|
+
</div>
|
|
180
|
+
)}
|
|
181
|
+
|
|
182
|
+
{/* Error */}
|
|
183
|
+
{appError && !appLoading && (
|
|
184
|
+
<div className="rounded-md bg-red-50 border border-red-200 p-4 text-sm text-red-700">
|
|
185
|
+
Failed to load ArgoCD data: {appError instanceof Error ? appError.message : 'Unknown error'}
|
|
186
|
+
</div>
|
|
187
|
+
)}
|
|
188
|
+
|
|
189
|
+
{/* Status cards */}
|
|
190
|
+
{app && (
|
|
191
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
|
192
|
+
{/* Sync status */}
|
|
193
|
+
<div className="rounded-lg border border-gray-200 bg-white p-4">
|
|
194
|
+
<p className="text-xs font-medium text-gray-500 mb-2">Sync Status</p>
|
|
195
|
+
<SyncBadge status={app.status.sync.status} />
|
|
196
|
+
{app.status.sync.revision && (
|
|
197
|
+
<p className="mt-2 font-mono text-xs text-gray-500 truncate" title={app.status.sync.revision}>
|
|
198
|
+
{app.status.sync.revision.slice(0, 8)}
|
|
199
|
+
</p>
|
|
200
|
+
)}
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
{/* Health */}
|
|
204
|
+
<div className="rounded-lg border border-gray-200 bg-white p-4">
|
|
205
|
+
<p className="text-xs font-medium text-gray-500 mb-2">Health</p>
|
|
206
|
+
<HealthBadge status={app.status.health.status} />
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
{/* Last reconciled */}
|
|
210
|
+
<div className="rounded-lg border border-gray-200 bg-white p-4">
|
|
211
|
+
<p className="text-xs font-medium text-gray-500 mb-2">Last Reconciled</p>
|
|
212
|
+
<p className="text-xs text-gray-700">
|
|
213
|
+
{app.status.reconciledAt
|
|
214
|
+
? new Date(app.status.reconciledAt).toLocaleString()
|
|
215
|
+
: '—'}
|
|
216
|
+
</p>
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
)}
|
|
220
|
+
|
|
221
|
+
{/* Operation state */}
|
|
222
|
+
{app?.status.operationState && (
|
|
223
|
+
<div className={`rounded-md border p-3 text-xs ${
|
|
224
|
+
app.status.operationState.phase === 'Succeeded'
|
|
225
|
+
? 'border-green-200 bg-green-50 text-green-800'
|
|
226
|
+
: app.status.operationState.phase === 'Failed'
|
|
227
|
+
? 'border-red-200 bg-red-50 text-red-800'
|
|
228
|
+
: 'border-blue-200 bg-blue-50 text-blue-800'
|
|
229
|
+
}`}>
|
|
230
|
+
<span className="font-medium">Last operation:</span>{' '}
|
|
231
|
+
{app.status.operationState.phase}
|
|
232
|
+
{app.status.operationState.message && ` — ${app.status.operationState.message}`}
|
|
233
|
+
</div>
|
|
234
|
+
)}
|
|
235
|
+
|
|
236
|
+
{/* Source */}
|
|
237
|
+
{app?.spec.source && (
|
|
238
|
+
<div className="rounded-lg border border-gray-200 bg-white p-4">
|
|
239
|
+
<h3 className="text-sm font-semibold text-gray-700 mb-3">Source</h3>
|
|
240
|
+
<dl className="grid grid-cols-1 gap-y-2 text-xs sm:grid-cols-3">
|
|
241
|
+
<div>
|
|
242
|
+
<dt className="text-gray-500">Repository</dt>
|
|
243
|
+
<dd className="font-mono text-gray-800 truncate">{app.spec.source.repoURL}</dd>
|
|
244
|
+
</div>
|
|
245
|
+
<div>
|
|
246
|
+
<dt className="text-gray-500">Revision</dt>
|
|
247
|
+
<dd className="font-mono text-gray-800">{app.spec.source.targetRevision}</dd>
|
|
248
|
+
</div>
|
|
249
|
+
{app.spec.source.path && (
|
|
250
|
+
<div>
|
|
251
|
+
<dt className="text-gray-500">Path</dt>
|
|
252
|
+
<dd className="font-mono text-gray-800">{app.spec.source.path}</dd>
|
|
253
|
+
</div>
|
|
254
|
+
)}
|
|
255
|
+
</dl>
|
|
256
|
+
</div>
|
|
257
|
+
)}
|
|
258
|
+
|
|
259
|
+
{/* Sync history */}
|
|
260
|
+
<div>
|
|
261
|
+
<h3 className="text-sm font-semibold text-gray-700 mb-3">
|
|
262
|
+
Sync History{' '}
|
|
263
|
+
<span className="rounded-full bg-gray-100 px-2 py-0.5 text-xs font-normal text-gray-500">
|
|
264
|
+
{history.length}
|
|
265
|
+
</span>
|
|
266
|
+
</h3>
|
|
267
|
+
|
|
268
|
+
{historyLoading ? (
|
|
269
|
+
<p className="text-xs text-gray-400">Loading history…</p>
|
|
270
|
+
) : history.length === 0 ? (
|
|
271
|
+
<p className="text-xs text-gray-400">No sync history available.</p>
|
|
272
|
+
) : (
|
|
273
|
+
<div className="overflow-x-auto rounded-lg border border-gray-200">
|
|
274
|
+
<table className="min-w-full text-xs">
|
|
275
|
+
<thead>
|
|
276
|
+
<tr className="border-b border-gray-200 bg-gray-50">
|
|
277
|
+
<th className="px-4 py-2 text-left font-medium text-gray-600">Revision</th>
|
|
278
|
+
<th className="px-4 py-2 text-left font-medium text-gray-600">Deployed At</th>
|
|
279
|
+
<th className="px-4 py-2 text-left font-medium text-gray-600">By</th>
|
|
280
|
+
</tr>
|
|
281
|
+
</thead>
|
|
282
|
+
<tbody className="divide-y divide-gray-100 bg-white">
|
|
283
|
+
{history.slice(0, 5).map((item) => (
|
|
284
|
+
<tr key={item.id} className="hover:bg-gray-50">
|
|
285
|
+
<td className="px-4 py-2 font-mono text-gray-800">
|
|
286
|
+
{item.revision.slice(0, 8)}
|
|
287
|
+
</td>
|
|
288
|
+
<td className="px-4 py-2 text-gray-600">
|
|
289
|
+
{new Date(item.deployedAt).toLocaleString()}
|
|
290
|
+
</td>
|
|
291
|
+
<td className="px-4 py-2 text-gray-500">
|
|
292
|
+
{item.initiatedBy?.automated
|
|
293
|
+
? 'Auto-sync'
|
|
294
|
+
: (item.initiatedBy?.username ?? '—')}
|
|
295
|
+
</td>
|
|
296
|
+
</tr>
|
|
297
|
+
))}
|
|
298
|
+
</tbody>
|
|
299
|
+
</table>
|
|
300
|
+
</div>
|
|
301
|
+
)}
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
);
|
|
305
|
+
}
|