nucleus-core-ts 0.9.850 → 0.9.852
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/fe/components/IntegrationsPage/components/ConnectionExport.d.ts +6 -0
- package/dist/fe/components/IntegrationsPage/components/ConnectionExport.js +104 -0
- package/dist/fe/components/IntegrationsPage/components/ConnectionSettingsTab.js +5 -0
- package/dist/fe/components/IntegrationsPage/components/ConnectionTest.js +13 -2
- package/dist/fe/components/IntegrationsPage/components/ConnectionTransfer.js +13 -52
- package/dist/fe/components/IntegrationsPage/components/EndpointSample.js +11 -3
- package/dist/fe/components/IntegrationsPage/components/MappingsTab.js +4 -1
- package/dist/fe/components/IntegrationsPage/store/index.js +4 -1
- package/dist/fe/components/IntegrationsPage/store/state.d.ts +14 -1
- package/dist/fe/components/IntegrationsPage/store/state.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { IntegrationSource, IntegrationsActions } from '../types';
|
|
2
|
+
export type ConnectionExportProps = {
|
|
3
|
+
actions: IntegrationsActions;
|
|
4
|
+
source: IntegrationSource | null;
|
|
5
|
+
};
|
|
6
|
+
export declare function ConnectionExport({ actions, source }: ConnectionExportProps): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { useIntegrationsStore } from '../store';
|
|
5
|
+
import { integrationsPageTheme as theme } from '../theme';
|
|
6
|
+
import { Notice } from './Primitives';
|
|
7
|
+
/**
|
|
8
|
+
* Taking one whole integration somewhere else.
|
|
9
|
+
*
|
|
10
|
+
* A connection is the address, the calls it makes, the parameters those calls
|
|
11
|
+
* take from each other, and the mappings that turn the answers into rows.
|
|
12
|
+
* Rebuilding that by hand in a second environment is hours of work and a fresh
|
|
13
|
+
* chance to get one field wrong in a way nobody notices until an import writes
|
|
14
|
+
* it. So it travels as one file.
|
|
15
|
+
*
|
|
16
|
+
* ## Why this is not next to Import
|
|
17
|
+
*
|
|
18
|
+
* It was, and it could not be pressed. Both buttons lived in the connection
|
|
19
|
+
* rail, and Export is only meaningful once a connection is CHOSEN — but
|
|
20
|
+
* choosing one replaces the rail with the connection's own screens, taking the
|
|
21
|
+
* button with it. So the button existed, was correct, and was reachable only in
|
|
22
|
+
* the state where it was disabled.
|
|
23
|
+
*
|
|
24
|
+
* Import belongs where there is nothing chosen yet, because that is when you
|
|
25
|
+
* bring one in. Export belongs here, on the connection being exported.
|
|
26
|
+
*
|
|
27
|
+
* The file carries NO credentials — deliberately, and it says so on both sides,
|
|
28
|
+
* because a connection that arrives looking configured and cannot authenticate
|
|
29
|
+
* is a confusing half hour otherwise.
|
|
30
|
+
*/ /** A filename somebody can still recognise a week later. */ const fileNameFor = (slug)=>`integration-${slug || 'connection'}.json`;
|
|
31
|
+
export function ConnectionExport({ actions, source }) {
|
|
32
|
+
const store = useIntegrationsStore();
|
|
33
|
+
const [busy, setBusy] = useState(false);
|
|
34
|
+
const [note, setNote] = useState(null);
|
|
35
|
+
if (!source) return null;
|
|
36
|
+
const exportOne = ()=>{
|
|
37
|
+
setBusy(true);
|
|
38
|
+
setNote(null);
|
|
39
|
+
store.setError(null);
|
|
40
|
+
actions.exportConnection(source.id).then((document)=>{
|
|
41
|
+
setBusy(false);
|
|
42
|
+
if (!document) {
|
|
43
|
+
store.setError('The connection could not be exported.');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
// Written from the browser rather than served as a download, so the file
|
|
47
|
+
// never travels back through a URL somebody could share by accident.
|
|
48
|
+
const blob = new Blob([
|
|
49
|
+
JSON.stringify(document, null, 2)
|
|
50
|
+
], {
|
|
51
|
+
type: 'application/json'
|
|
52
|
+
});
|
|
53
|
+
const url = URL.createObjectURL(blob);
|
|
54
|
+
const link = window.document.createElement('a');
|
|
55
|
+
link.href = url;
|
|
56
|
+
link.download = fileNameFor(source.slug);
|
|
57
|
+
link.click();
|
|
58
|
+
URL.revokeObjectURL(url);
|
|
59
|
+
setNote('Exported. Credentials are not in the file — enter them wherever it lands.');
|
|
60
|
+
}).catch((error)=>{
|
|
61
|
+
setBusy(false);
|
|
62
|
+
store.setError(error instanceof Error ? error.message : 'The request failed.');
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
return /*#__PURE__*/ _jsxs("section", {
|
|
66
|
+
className: theme.card.wrapper,
|
|
67
|
+
children: [
|
|
68
|
+
/*#__PURE__*/ _jsxs("header", {
|
|
69
|
+
className: theme.card.header,
|
|
70
|
+
children: [
|
|
71
|
+
/*#__PURE__*/ _jsx("h3", {
|
|
72
|
+
className: theme.card.title,
|
|
73
|
+
children: "Take this connection elsewhere"
|
|
74
|
+
}),
|
|
75
|
+
/*#__PURE__*/ _jsx("button", {
|
|
76
|
+
className: theme.button.secondary,
|
|
77
|
+
disabled: busy,
|
|
78
|
+
onClick: exportOne,
|
|
79
|
+
type: "button",
|
|
80
|
+
children: busy ? 'Exporting…' : 'Export'
|
|
81
|
+
})
|
|
82
|
+
]
|
|
83
|
+
}),
|
|
84
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
85
|
+
className: theme.card.body,
|
|
86
|
+
children: [
|
|
87
|
+
/*#__PURE__*/ _jsx("p", {
|
|
88
|
+
className: theme.field.hint,
|
|
89
|
+
children: "One file holding this connection, its calls and its mappings — enough to rebuild the whole integration in another installation. Bring it in there with Import, on the connections screen."
|
|
90
|
+
}),
|
|
91
|
+
/*#__PURE__*/ _jsx(Notice, {
|
|
92
|
+
title: "No secrets travel in the file",
|
|
93
|
+
tone: "info",
|
|
94
|
+
children: "Tokens, passwords and keys are left out on purpose. Whatever this connection authenticates with has to be entered again wherever the file lands."
|
|
95
|
+
}),
|
|
96
|
+
note ? /*#__PURE__*/ _jsx("p", {
|
|
97
|
+
className: theme.field.hint,
|
|
98
|
+
children: note
|
|
99
|
+
}) : null
|
|
100
|
+
]
|
|
101
|
+
})
|
|
102
|
+
]
|
|
103
|
+
});
|
|
104
|
+
}
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useIntegrationsStore } from '../store';
|
|
4
4
|
import { integrationsPageTheme as theme } from '../theme';
|
|
5
5
|
import { ConnectionDelete } from './ConnectionDelete';
|
|
6
|
+
import { ConnectionExport } from './ConnectionExport';
|
|
6
7
|
import { ConnectionForm } from './ConnectionForm';
|
|
7
8
|
import { ConnectionTest } from './ConnectionTest';
|
|
8
9
|
import { CredentialFields } from './CredentialFields';
|
|
@@ -53,6 +54,10 @@ import { CredentialFields } from './CredentialFields';
|
|
|
53
54
|
loading: waitingForRecord,
|
|
54
55
|
source: source
|
|
55
56
|
}),
|
|
57
|
+
/*#__PURE__*/ _jsx(ConnectionExport, {
|
|
58
|
+
actions: actions,
|
|
59
|
+
source: source
|
|
60
|
+
}),
|
|
56
61
|
/*#__PURE__*/ _jsx(ConnectionDelete, {
|
|
57
62
|
actions: actions,
|
|
58
63
|
onDeleted: onDeleted,
|
|
@@ -25,10 +25,21 @@ export function ConnectionTest({ actions, source, loading = false }) {
|
|
|
25
25
|
setResult(null);
|
|
26
26
|
setTesting(false);
|
|
27
27
|
});
|
|
28
|
-
|
|
28
|
+
/**
|
|
29
|
+
* A verdict describes the settings it was given, not the connection's name.
|
|
30
|
+
*
|
|
31
|
+
* Keyed on the id alone, "Reachable" survived editing the base URL and saving:
|
|
32
|
+
* the record object is replaced but its id is not, so this never re-ran and a
|
|
33
|
+
* green badge sat under an address that had never been called. Everything the
|
|
34
|
+
* test actually depends on is in the key.
|
|
35
|
+
*/ useEffect(()=>{
|
|
29
36
|
clear();
|
|
30
37
|
}, [
|
|
31
|
-
source?.id
|
|
38
|
+
source?.id,
|
|
39
|
+
source?.baseUrl,
|
|
40
|
+
source?.authType,
|
|
41
|
+
source?.tokenUrl,
|
|
42
|
+
source?.tlsVerify
|
|
32
43
|
]);
|
|
33
44
|
const handleTest = ()=>{
|
|
34
45
|
if (!source) return;
|
|
@@ -6,7 +6,7 @@ import { useIntegrationsStore } from '../store';
|
|
|
6
6
|
import { integrationsPageTheme } from '../theme';
|
|
7
7
|
import { Notice } from './Primitives';
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
9
|
+
* Bringing a whole integration in from another installation.
|
|
10
10
|
*
|
|
11
11
|
* A connection is the connection, the calls it makes, the parameters those calls
|
|
12
12
|
* take from each other, and the mappings that turn the answers into rows.
|
|
@@ -17,46 +17,20 @@ import { Notice } from './Primitives';
|
|
|
17
17
|
* The file carries NO credentials — deliberately, and it says so on both sides,
|
|
18
18
|
* because a connection that arrives looking configured and cannot authenticate
|
|
19
19
|
* is a confusing half hour otherwise.
|
|
20
|
+
*
|
|
21
|
+
* Export used to sit beside this button and could never be pressed: it needs a
|
|
22
|
+
* connection to be CHOSEN, and choosing one replaces this rail with that
|
|
23
|
+
* connection's own screens. It now lives there, on the thing being exported —
|
|
24
|
+
* see ConnectionExport. Importing is the one that belongs here, because
|
|
25
|
+
* importing is what you do when you have nothing chosen yet.
|
|
20
26
|
*/ const theme = integrationsPageTheme;
|
|
21
27
|
const messageOf = (error)=>error instanceof Error ? error.message : 'The request failed.';
|
|
22
|
-
/** A filename somebody can still recognise a week later. */ const fileNameFor = (slug)=>`integration-${slug || 'connection'}.json`;
|
|
23
28
|
export function ConnectionTransfer({ actions, onImported }) {
|
|
24
29
|
const store = useIntegrationsStore();
|
|
25
30
|
const fileRef = useRef(null);
|
|
26
31
|
const [busy, setBusy] = useState(false);
|
|
27
32
|
const [problems, setProblems] = useState([]);
|
|
28
33
|
const [note, setNote] = useState(null);
|
|
29
|
-
const selected = store.sources.find((source)=>source.id === store.selectedSourceId);
|
|
30
|
-
const exportOne = ()=>{
|
|
31
|
-
if (!selected) return;
|
|
32
|
-
setBusy(true);
|
|
33
|
-
setProblems([]);
|
|
34
|
-
setNote(null);
|
|
35
|
-
actions.exportConnection(selected.id).then((document)=>{
|
|
36
|
-
setBusy(false);
|
|
37
|
-
if (!document) {
|
|
38
|
-
store.setError('The connection could not be exported.');
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
// Written from the browser rather than served as a download, so the file
|
|
42
|
-
// never travels back through a URL somebody could share by accident.
|
|
43
|
-
const blob = new Blob([
|
|
44
|
-
JSON.stringify(document, null, 2)
|
|
45
|
-
], {
|
|
46
|
-
type: 'application/json'
|
|
47
|
-
});
|
|
48
|
-
const url = URL.createObjectURL(blob);
|
|
49
|
-
const link = window.document.createElement('a');
|
|
50
|
-
link.href = url;
|
|
51
|
-
link.download = fileNameFor(selected.slug);
|
|
52
|
-
link.click();
|
|
53
|
-
URL.revokeObjectURL(url);
|
|
54
|
-
setNote('Exported. Credentials are not in the file — enter them wherever it lands.');
|
|
55
|
-
}).catch((error)=>{
|
|
56
|
-
setBusy(false);
|
|
57
|
-
store.setError(messageOf(error));
|
|
58
|
-
});
|
|
59
|
-
};
|
|
60
34
|
const importFile = (file)=>{
|
|
61
35
|
setBusy(true);
|
|
62
36
|
setProblems([]);
|
|
@@ -97,25 +71,12 @@ export function ConnectionTransfer({ actions, onImported }) {
|
|
|
97
71
|
return /*#__PURE__*/ _jsxs("section", {
|
|
98
72
|
className: "flex flex-col gap-2",
|
|
99
73
|
children: [
|
|
100
|
-
/*#__PURE__*/
|
|
101
|
-
className:
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
onClick: exportOne,
|
|
107
|
-
title: selected ? `Export ${selected.name} — its calls and mappings, without any credential` : 'Choose a connection to export',
|
|
108
|
-
type: "button",
|
|
109
|
-
children: busy ? 'Working…' : 'Export'
|
|
110
|
-
}),
|
|
111
|
-
/*#__PURE__*/ _jsx("button", {
|
|
112
|
-
className: cn(theme.button.secondary, 'w-full justify-center whitespace-nowrap'),
|
|
113
|
-
disabled: busy,
|
|
114
|
-
onClick: ()=>fileRef.current?.click(),
|
|
115
|
-
type: "button",
|
|
116
|
-
children: "Import"
|
|
117
|
-
})
|
|
118
|
-
]
|
|
74
|
+
/*#__PURE__*/ _jsx("button", {
|
|
75
|
+
className: cn(theme.button.secondary, 'w-full justify-center whitespace-nowrap'),
|
|
76
|
+
disabled: busy,
|
|
77
|
+
onClick: ()=>fileRef.current?.click(),
|
|
78
|
+
type: "button",
|
|
79
|
+
children: busy ? 'Working…' : 'Import a connection file'
|
|
119
80
|
}),
|
|
120
81
|
/*#__PURE__*/ _jsx("input", {
|
|
121
82
|
accept: "application/json,.json",
|
|
@@ -26,7 +26,15 @@ export function EndpointSample({ endpoint, actions }) {
|
|
|
26
26
|
const selectedRef = useRef(store.selectedEndpointId);
|
|
27
27
|
selectedRef.current = store.selectedEndpointId;
|
|
28
28
|
const params = pathParamNames(endpoint.path);
|
|
29
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Only a sample taken of THIS call.
|
|
31
|
+
*
|
|
32
|
+
* The card is keyed per selection, so switching calls removes it and a request
|
|
33
|
+
* still in flight settles with nowhere to report to — except the shared slot,
|
|
34
|
+
* which it writes to regardless. Before the tag, that answer appeared under
|
|
35
|
+
* whichever call was open when it landed, and the mapping built next took its
|
|
36
|
+
* field list from a call that had never returned those fields.
|
|
37
|
+
*/ const sample = store.sampleEndpointId === endpoint.id ? store.sample : null;
|
|
30
38
|
let unfilled = 0;
|
|
31
39
|
for (const name of params){
|
|
32
40
|
if (!(values[name] ?? '').trim()) unfilled += 1;
|
|
@@ -60,11 +68,11 @@ export function EndpointSample({ endpoint, actions }) {
|
|
|
60
68
|
store.setError(null);
|
|
61
69
|
actions.sampleEndpoint(requestedFor, params.length > 0 ? values : undefined).then((result)=>{
|
|
62
70
|
if (settleElsewhere(mine, requestedFor)) return;
|
|
63
|
-
store.setSample(result);
|
|
71
|
+
store.setSample(result, requestedFor);
|
|
64
72
|
if (!result.ok && result.error) store.setError(result.error);
|
|
65
73
|
}).catch((error)=>{
|
|
66
74
|
if (settleElsewhere(mine, requestedFor)) return;
|
|
67
|
-
store.setSample(null);
|
|
75
|
+
store.setSample(null, requestedFor);
|
|
68
76
|
store.setError(error instanceof Error ? error.message : 'Sampling the endpoint failed.');
|
|
69
77
|
});
|
|
70
78
|
};
|
|
@@ -196,7 +196,10 @@ export function MappingsTab({ actions }) {
|
|
|
196
196
|
// to and the editor matches it against the mapping's own choice. The editor
|
|
197
197
|
// falls back to the endpoint's STORED sample — which is why the sample is
|
|
198
198
|
// persisted at all, and what a connection revisited tomorrow has.
|
|
199
|
-
const freshSample =
|
|
199
|
+
const freshSample = // The tag must match: a field list offered here becomes the mapping, and a
|
|
200
|
+
// sample left over from another call would name fields the chosen one does
|
|
201
|
+
// not return.
|
|
202
|
+
store.selectedEndpointId && store.sample && store.sampleEndpointId === store.selectedEndpointId ? {
|
|
200
203
|
endpointId: store.selectedEndpointId,
|
|
201
204
|
fields: store.sample.fields ?? []
|
|
202
205
|
} : null;
|
|
@@ -52,6 +52,7 @@ export const { useStore: useIntegrationsStore } = createStore(initial, {
|
|
|
52
52
|
store.mappingsLoading = false;
|
|
53
53
|
store.runsLoading = false;
|
|
54
54
|
store.sample = null;
|
|
55
|
+
store.sampleEndpointId = null;
|
|
55
56
|
store.sampling = false;
|
|
56
57
|
store.plan = null;
|
|
57
58
|
store.planning = false;
|
|
@@ -100,6 +101,7 @@ export const { useStore: useIntegrationsStore } = createStore(initial, {
|
|
|
100
101
|
// The sample describes ONE endpoint; carrying it over would offer the
|
|
101
102
|
// wrong field list to the next mapping.
|
|
102
103
|
store.sample = null;
|
|
104
|
+
store.sampleEndpointId = null;
|
|
103
105
|
// And the WAIT for a sample belongs to the endpoint that was left, not to
|
|
104
106
|
// the one arrived at. Clearing the record without clearing the flag left
|
|
105
107
|
// the next endpoint showing a skeleton for a request that was never going
|
|
@@ -157,8 +159,9 @@ export const { useStore: useIntegrationsStore } = createStore(initial, {
|
|
|
157
159
|
setRunsLoading: (store)=>(loading)=>{
|
|
158
160
|
store.runsLoading = loading;
|
|
159
161
|
},
|
|
160
|
-
setSample: (store)=>(sample)=>batch(()=>{
|
|
162
|
+
setSample: (store)=>(sample, endpointId)=>batch(()=>{
|
|
161
163
|
store.sample = sample;
|
|
164
|
+
store.sampleEndpointId = sample ? endpointId : null;
|
|
162
165
|
store.sampling = false;
|
|
163
166
|
}),
|
|
164
167
|
setSampling: (store)=>(sampling)=>{
|
|
@@ -45,6 +45,19 @@ export type State = {
|
|
|
45
45
|
runsLoading: boolean;
|
|
46
46
|
/** The real shape of the selected endpoint's response, once sampled. */
|
|
47
47
|
sample: EndpointSampleValue | null;
|
|
48
|
+
/**
|
|
49
|
+
* The call the sample in hand describes.
|
|
50
|
+
*
|
|
51
|
+
* A sample is a field list, and a field list is what the next mapping is built
|
|
52
|
+
* from — so one written against the wrong call produces a mapping of fields
|
|
53
|
+
* that call never returns. The card is keyed per selection and therefore
|
|
54
|
+
* REMOVED when the operator clicks another call, which means a request still
|
|
55
|
+
* in flight settles into a component that no longer exists and writes to this
|
|
56
|
+
* shared slot anyway. Tagging is what makes that write harmless: it lands
|
|
57
|
+
* under the call it was asked for, and whoever is showing a different one
|
|
58
|
+
* simply does not read it.
|
|
59
|
+
*/
|
|
60
|
+
sampleEndpointId: string | null;
|
|
48
61
|
sampling: boolean;
|
|
49
62
|
/** What a run would do. Cleared whenever the mapping changes underneath it. */
|
|
50
63
|
plan: RunPlanValue | null;
|
|
@@ -114,7 +127,7 @@ export type Actions = {
|
|
|
114
127
|
selectMapping: (id: string | null) => void;
|
|
115
128
|
setRuns: (items: IntegrationRun[], total: number) => void;
|
|
116
129
|
setRunsLoading: (loading: boolean) => void;
|
|
117
|
-
setSample: (sample: EndpointSampleValue | null) => void;
|
|
130
|
+
setSample: (sample: EndpointSampleValue | null, endpointId: string | null) => void;
|
|
118
131
|
setSampling: (sampling: boolean) => void;
|
|
119
132
|
setPlan: (plan: RunPlanValue | null) => void;
|
|
120
133
|
setPlanning: (planning: boolean) => void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nucleus-core-ts",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.852",
|
|
4
4
|
"description": "Production-ready, enterprise-grade TypeScript framework for building multi-tenant APIs",
|
|
5
5
|
"author": "Hidayet Can Özcan <hidayetcan@gmail.com>",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|