Skip to content

Commit cbe82b5

Browse files
authored
Merge pull request #1061 from newwork-software/feature/enablePrivateNpmRegistries
[WIP] Enable private npm registries
2 parents 6173081 + 6e5e68a commit cbe82b5

File tree

9 files changed

+831
-5
lines changed

9 files changed

+831
-5
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
import { useEffect, useState } from "react";
2+
import { HelpText } from "./HelpText";
3+
import { FormInputItem, FormSelectItem, TacoSwitch } from "lowcoder-design";
4+
import { Form } from "antd";
5+
import { trans } from "@lowcoder-ee/i18n";
6+
import { FormStyled } from "@lowcoder-ee/pages/setting/idSource/styledComponents";
7+
import { SaveButton } from "@lowcoder-ee/pages/setting/styled";
8+
import { NpmRegistryConfigEntry } from "@lowcoder-ee/redux/reducers/uiReducers/commonSettingsReducer";
9+
10+
type NpmRegistryConfigEntryInput = {
11+
url: string;
12+
scope: "global" | "organization" | "package";
13+
pattern: string;
14+
authType: "none" | "basic" | "bearer";
15+
credentials: string;
16+
};
17+
18+
const initialRegistryConfig: NpmRegistryConfigEntryInput = {
19+
scope: "global",
20+
pattern: "",
21+
url: "",
22+
authType: "none",
23+
credentials: "",
24+
};
25+
26+
interface NpmRegistryConfigProps {
27+
initialData?: NpmRegistryConfigEntry;
28+
onSave: (registryConfig: NpmRegistryConfigEntry|null) => void;
29+
}
30+
31+
export function NpmRegistryConfig(props: NpmRegistryConfigProps) {
32+
const [initialConfigSet, setItialConfigSet] = useState<boolean>(false);
33+
const [enableRegistry, setEnableRegistry] = useState<boolean>(!!props.initialData);
34+
const [registryConfig, setRegistryConfig] = useState<NpmRegistryConfigEntryInput>(initialRegistryConfig);
35+
36+
useEffect(() => {
37+
if (props.initialData && !initialConfigSet) {
38+
let initConfig: NpmRegistryConfigEntryInput = {...initialRegistryConfig};
39+
if (props.initialData) {
40+
const {scope} = props.initialData;
41+
const {type: scopeTye, pattern} = scope;
42+
const {url, auth} = props.initialData.registry;
43+
const {type: authType, credentials} = props.initialData.registry.auth;
44+
initConfig.scope = scopeTye;
45+
initConfig.pattern = pattern || "";
46+
initConfig.url = url;
47+
initConfig.authType = authType;
48+
initConfig.credentials = credentials || "";
49+
}
50+
51+
form.setFieldsValue(initConfig);
52+
setRegistryConfig(initConfig);
53+
setEnableRegistry(true);
54+
setItialConfigSet(true);
55+
}
56+
}, [props.initialData, initialConfigSet]);
57+
58+
useEffect(() => {
59+
if (!enableRegistry) {
60+
form.resetFields();
61+
setRegistryConfig(initialRegistryConfig);
62+
}
63+
}, [enableRegistry]);
64+
65+
const [form] = Form.useForm();
66+
67+
const handleRegistryConfigChange = async (key: string, value: string) => {
68+
let keyConfg = { [key]: value };
69+
form.validateFields([key]);
70+
71+
// Reset the pattern field if the scope is global
72+
if (key === "scope") {
73+
if (value !== "global") {
74+
registryConfig.scope !== "global" && form.validateFields(["pattern"]);
75+
} else {
76+
form.resetFields(["pattern"]);
77+
keyConfg = {
78+
...keyConfg,
79+
pattern: ""
80+
};
81+
}
82+
}
83+
84+
// Reset the credentials field if the auth type is none
85+
if (key === "authType") {
86+
if (value !== "none") {
87+
registryConfig.authType !== "none" && form.validateFields(["credentials"]);
88+
} else {
89+
form.resetFields(["credentials"]);
90+
keyConfg = {
91+
...keyConfg,
92+
credentials: ""
93+
};
94+
}
95+
}
96+
97+
// Update the registry config
98+
setRegistryConfig((prevConfig) => ({
99+
...prevConfig,
100+
...keyConfg,
101+
}));
102+
};
103+
104+
const scopeOptions = [
105+
{
106+
value: "global",
107+
label: "Global",
108+
},
109+
{
110+
value: "organization",
111+
label: "Organization",
112+
},
113+
{
114+
value: "package",
115+
label: "Package",
116+
},
117+
];
118+
119+
const authOptions = [
120+
{
121+
value: "none",
122+
label: "None",
123+
},
124+
{
125+
value: "basic",
126+
label: "Basic",
127+
},
128+
{
129+
value: "bearer",
130+
label: "Token",
131+
},
132+
];
133+
134+
const onFinsish = () => {
135+
const registryConfigEntry: NpmRegistryConfigEntry = {
136+
scope: {
137+
type: registryConfig.scope,
138+
pattern: registryConfig.pattern,
139+
},
140+
registry: {
141+
url: registryConfig.url,
142+
auth: {
143+
type: registryConfig.authType,
144+
credentials: registryConfig.credentials,
145+
},
146+
},
147+
};
148+
props.onSave(registryConfigEntry);
149+
}
150+
151+
return (
152+
<FormStyled
153+
form={form}
154+
name="basic"
155+
layout="vertical"
156+
style={{ maxWidth: 440 }}
157+
initialValues={initialRegistryConfig}
158+
autoComplete="off"
159+
onValuesChange={(changedValues, allValues) => {
160+
for (const key in changedValues) {
161+
handleRegistryConfigChange(key, changedValues[key]);
162+
}
163+
}}
164+
onFinish={onFinsish}
165+
>
166+
<div style={{ paddingBottom: "10px"}}>
167+
<TacoSwitch checked={enableRegistry} label={trans("npmRegistry.npmRegistryEnable")} onChange={function (checked: boolean): void {
168+
setEnableRegistry(checked);
169+
if (!checked) {
170+
form.resetFields();
171+
}
172+
} }></TacoSwitch>
173+
</div>
174+
<div hidden={!enableRegistry}>
175+
<div className="ant-form-item-label" style={{ paddingBottom: "10px" }}>
176+
<label>Registry</label>
177+
</div>
178+
<FormInputItem
179+
name={"url"}
180+
placeholder={trans("npmRegistry.npmRegistryUrl")}
181+
style={{ width: "544px", height: "32px", marginBottom: 12 }}
182+
value={registryConfig.url}
183+
rules={[{
184+
required: true,
185+
message: trans("npmRegistry.npmRegistryUrlRequired"),
186+
},
187+
{
188+
type: "url",
189+
message: trans("npmRegistry.npmRegistryUrlInvalid"),
190+
}
191+
]}
192+
/>
193+
<div className="ant-form-item-label" style={{ paddingBottom: "10px" }}>
194+
<label>Scope</label>
195+
</div>
196+
<div
197+
style={{ display: "flex", alignItems: "baseline", maxWidth: "560px" }}
198+
>
199+
<div style={{ flex: 1, paddingRight: "8px" }}>
200+
<FormSelectItem
201+
name={"scope"}
202+
placeholder={trans("npmRegistry.npmRegistryScope")}
203+
style={{ width: "264px", height: "32px", marginBottom: 12 }}
204+
initialValue={registryConfig.scope}
205+
options={scopeOptions}
206+
/>
207+
</div>
208+
<div style={{ flex: 1, paddingRight: "8px" }}>
209+
<FormInputItem
210+
name={"pattern"}
211+
placeholder={trans("npmRegistry.npmRegistryPattern")}
212+
style={{ width: "264px", height: "32px", marginBottom: 12 }}
213+
hidden={
214+
registryConfig.scope !== "organization" &&
215+
registryConfig.scope !== "package"
216+
}
217+
value={registryConfig.pattern}
218+
rules={[{
219+
required: registryConfig.scope === "organization" || registryConfig.scope === "package",
220+
message: "Please input the package scope pattern",
221+
},
222+
{
223+
message: trans("npmRegistry.npmRegistryPatternInvalid"),
224+
validator: async (_, value) => {
225+
if (registryConfig.scope === "global") {
226+
return;
227+
}
228+
229+
if (registryConfig.scope === "organization") {
230+
if(!/^\@[a-zA-Z0-9-_.]+$/.test(value)) {
231+
throw new Error("Input pattern not starting with @");
232+
}
233+
} else {
234+
if(!/^[a-zA-Z0-9-_.]+$/.test(value)) {
235+
throw new Error("Input pattern not valid");
236+
}
237+
}
238+
}
239+
}
240+
]}
241+
/>
242+
</div>
243+
</div>
244+
<div className="ant-form-item-label" style={{ padding: "10px 0" }}>
245+
<label>{trans("npmRegistry.npmRegistryAuth")}</label>
246+
</div>
247+
<HelpText style={{ marginBottom: 12 }} hidden={registryConfig.authType === "none"}>
248+
{trans("npmRegistry.npmRegistryAuthCredentialsHelp")}
249+
</HelpText>
250+
<div style={{ display: "flex", alignItems: "baseline", maxWidth: "560px" }}>
251+
<div style={{ flex: 1, paddingRight: "8px" }}>
252+
<FormSelectItem
253+
name={"authType"}
254+
placeholder={trans("npmRegistry.npmRegistryAuthType")}
255+
style={{ width: "264px", height: "32px", marginBottom: 12 }}
256+
initialValue={registryConfig.authType}
257+
options={authOptions}
258+
/>
259+
</div>
260+
<div style={{ flex: 1, paddingRight: "8px" }}>
261+
<Form.Item rules={[{required: true}]}>
262+
<FormInputItem
263+
name={"credentials"}
264+
placeholder={trans("npmRegistry.npmRegistryAuthCredentials")}
265+
style={{ width: "264px", height: "32px", marginBottom: 12 }}
266+
hidden={registryConfig.authType === "none"}
267+
value={registryConfig.credentials}
268+
rules={[{
269+
message: trans("npmRegistry.npmRegistryAuthCredentialsRequired"),
270+
validator: async (_, value) => {
271+
if (registryConfig.authType === "none") {
272+
return;
273+
}
274+
if (!value) {
275+
throw new Error("No credentials provided");
276+
}
277+
}
278+
}]}
279+
/>
280+
</Form.Item>
281+
</div>
282+
</div>
283+
</div>
284+
<Form.Item>
285+
<SaveButton
286+
buttonType="primary"
287+
htmlType="submit"
288+
onClick={() => {
289+
if (!enableRegistry) {
290+
return props.onSave(null);
291+
}
292+
}
293+
}>
294+
{trans("advanced.saveBtn")}
295+
</SaveButton>
296+
</Form.Item>
297+
</FormStyled>
298+
);
299+
}

client/packages/lowcoder/src/comps/utils/remote.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export function getRemoteCompType(
1212
}
1313

1414
export function parseCompType(compType: string) {
15-
const [type, source, packageNameAndVersion, compName] = compType.split("#");
15+
let [type, source, packageNameAndVersion, compName] = compType.split("#");
1616
const isRemote = type === "remote";
1717

1818
if (!isRemote) {
@@ -22,7 +22,13 @@ export function parseCompType(compType: string) {
2222
};
2323
}
2424

25-
const [packageName, packageVersion] = packageNameAndVersion.split("@");
25+
const packageRegex = /^(?<packageName>(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*)@(?<packageVersion>([0-9]+.[0-9]+.[0-9]+)(-[\w\d-]+)?)$/;
26+
const matches = packageNameAndVersion.match(packageRegex);
27+
if (!matches?.groups) {
28+
throw new Error(`Invalid package name and version: ${packageNameAndVersion}`);
29+
}
30+
31+
const {packageName, packageVersion} = matches.groups;
2632
return {
2733
compName,
2834
isRemote,
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
1-
export const NPM_REGISTRY_URL = "https://registry.npmjs.com";
2-
export const NPM_PLUGIN_ASSETS_BASE_URL = "https://unpkg.com";
1+
import { sdkConfig } from "./sdkConfig";
2+
3+
const baseUrl = sdkConfig.baseURL || LOWCODER_NODE_SERVICE_URL || "";
4+
export const NPM_REGISTRY_URL = `${baseUrl}/node-service/api/npm/registry`;
5+
export const NPM_PLUGIN_ASSETS_BASE_URL = `${baseUrl}/node-service/api/npm/package`;

client/packages/lowcoder/src/i18n/locales/en.ts

+16
Original file line numberDiff line numberDiff line change
@@ -2623,6 +2623,8 @@ export const en = {
26232623
"APIConsumptionDescription": "Here you can see the API Consumption for All Apps in the Current Workspace.",
26242624
"overallAPIConsumption": "Overall API Consumption in this Workspace till now",
26252625
"lastMonthAPIConsumption": "Last Month API Consumption, in this Workspace",
2626+
"npmRegistryTitle": "Custom NPM Registry",
2627+
"npmRegistryHelp": "Setup a custom NPM Registry to enable fetching of plugins from a private NPM registry.",
26262628
"showHeaderInPublicApps": "Show Header In Public View",
26272629
"showHeaderInPublicAppsHelp": "Set visibility of header in public view for all apps",
26282630
},
@@ -2988,6 +2990,20 @@ export const en = {
29882990
"createAppContent": "Welcome! Click 'App' and Start to Create Your First Application.",
29892991
"createAppTitle": "Create App"
29902992
},
2993+
"npmRegistry": {
2994+
"npmRegistryEnable": "Enable custom NPM Registry",
2995+
"npmRegistryUrl": "NPM Registry Url",
2996+
"npmRegistryUrlRequired": "Please input the registry URL",
2997+
"npmRegistryUrlInvalid": "Please input a valid URL",
2998+
"npmRegistryScope": "Package Scope",
2999+
"npmRegistryPattern": "Pattern",
3000+
"npmRegistryPatternInvalid": "Please input a valid pattern (starting with @ for oragnizations).",
3001+
"npmRegistryAuth": "Authentication",
3002+
"npmRegistryAuthType": "Authentication Type",
3003+
"npmRegistryAuthCredentials": "Authentication Credentials",
3004+
"npmRegistryAuthCredentialsRequired": "Please input the registry credentials",
3005+
"npmRegistryAuthCredentialsHelp": "For basic auth provide the base64 encoded username and password in the format 'base64(username:password)', for token auth provide the token.",
3006+
},
29913007

29923008

29933009
// nineteenth part

0 commit comments

Comments
 (0)