Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit cbe82b5

Browse files
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
Lines changed: 299 additions & 0 deletions
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

Lines changed: 8 additions & 2 deletions
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,
Lines changed: 5 additions & 2 deletions
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

Lines changed: 16 additions & 0 deletions
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)

AltStyle によって変換されたページ (->オリジナル) /