Skip to content

Commit aec795b

Browse files
author
hulutter
committed
feat(privateNpmRegistry): add workspace config
1 parent 3ad71b4 commit aec795b

File tree

4 files changed

+348
-3
lines changed

4 files changed

+348
-3
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/i18n/locales/en.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2600,8 +2600,9 @@ export const en = {
26002600
"APIConsumption": "API Consumption",
26012601
"APIConsumptionDescription": "Here you can see the API Consumption for All Apps in the Current Workspace.",
26022602
"overallAPIConsumption": "Overall API Consumption in this Workspace till now",
2603-
"lastMonthAPIConsumption": "Last Month API Consumption, in this Workspace"
2604-
2603+
"lastMonthAPIConsumption": "Last Month API Consumption, in this Workspace",
2604+
"npmRegistryTitle": "Custom NPM Registry",
2605+
"npmRegistryHelp": "Setup a custom NPM Registry to enable fetching of plugins from a private NPM registry.",
26052606
},
26062607

26072608

@@ -2949,6 +2950,20 @@ export const en = {
29492950
"createAppContent": "Welcome! Click 'App' and Start to Create Your First Application.",
29502951
"createAppTitle": "Create App"
29512952
},
2953+
"npmRegistry": {
2954+
"npmRegistryEnable": "Enable custom NPM Registry",
2955+
"npmRegistryUrl": "NPM Registry Url",
2956+
"npmRegistryUrlRequired": "Please input the registry URL",
2957+
"npmRegistryUrlInvalid": "Please input a valid URL",
2958+
"npmRegistryScope": "Package Scope",
2959+
"npmRegistryPattern": "Pattern",
2960+
"npmRegistryPatternInvalid": "Please input a valid pattern (starting with @ for oragnizations).",
2961+
"npmRegistryAuth": "Authentication",
2962+
"npmRegistryAuthType": "Authentication Type",
2963+
"npmRegistryAuthCredentials": "Authentication Credentials",
2964+
"npmRegistryAuthCredentialsRequired": "Please input the registry credentials",
2965+
"npmRegistryAuthCredentialsHelp": "For basic auth provide the base64 encoded username and password in the format 'base64(username:password)', for token auth provide the token.",
2966+
},
29522967

29532968

29542969
// nineteenth part

client/packages/lowcoder/src/pages/setting/advanced/AdvancedSetting.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { EmptyContent } from "components/EmptyContent";
22
import { HelpText } from "components/HelpText";
33
import { GreyTextColor } from "constants/style";
44
import { CustomModal, CustomSelect, TacoButton } from "lowcoder-design";
5-
import React, { lazy, useEffect, useState } from "react";
5+
import { lazy, useEffect, useState } from "react";
66
import { useDispatch, useSelector } from "react-redux";
77
import { fetchCommonSettings, setCommonSettings } from "redux/reduxActions/commonSettingsActions";
88
import { getCommonSettings } from "redux/selectors/commonSettingSelectors";
@@ -25,6 +25,8 @@ import { getGlobalSettings } from "comps/utils/globalSettings";
2525
import { fetchJSLibrary } from "util/jsLibraryUtils";
2626
import { evalFunc } from "lowcoder-core";
2727
import { messageInstance } from "lowcoder-design/src/components/GlobalInstances";
28+
import { NpmRegistryConfig } from "@lowcoder-ee/components/NpmRegistryConfig";
29+
import { NpmRegistryConfigEntry } from "@lowcoder-ee/redux/reducers/uiReducers/commonSettingsReducer";
2830

2931
const CodeEditor = lazy(
3032
() => import("base/codeEditor/codeEditor")
@@ -277,6 +279,20 @@ export function AdvancedSetting() {
277279
/>
278280
)}
279281
</div>
282+
<div className="section-title">{trans("advanced.npmRegistryTitle")}</div>
283+
<HelpText style={{ marginBottom: 12 }}>{trans("advanced.npmRegistryHelp")}</HelpText>
284+
<div className="section-content">
285+
<div>
286+
<NpmRegistryConfig initialData={settings.npmRegistries?.at(0)} onSave={(config: NpmRegistryConfigEntry|null) => {
287+
// Wrap in array to enable future option for multiple registries
288+
if (config === null) {
289+
handleSave("npmRegistries")([]);
290+
} else {
291+
handleSave("npmRegistries")([config]);
292+
}
293+
}} />
294+
</div>
295+
</div>
280296
{extraAdvanceSettings}
281297
<div className="section-title">{trans("advanced.APIConsumption")}</div>
282298
<HelpText style={{ marginBottom: 12 }}>{trans("advanced.APIConsumptionDescription")}</HelpText>

client/packages/lowcoder/src/redux/reducers/uiReducers/commonSettingsReducer.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,24 @@ import { ReduxAction, ReduxActionTypes } from "constants/reduxActionConstants";
33
import { CommonSettingResponseData, ThemeType } from "api/commonSettingApi";
44
import { GenericApiResponse } from "api/apiResponses";
55

6+
export interface NpmRegistryConfigEntry {
7+
scope: {
8+
type: "organization" | "package" | "global";
9+
pattern?: string;
10+
};
11+
registry: {
12+
url: string;
13+
auth: {
14+
type: "none" | "basic" | "bearer";
15+
credentials?: string;
16+
};
17+
};
18+
}
19+
620
export interface CommonSettingsState {
721
settings: {
822
npmPlugins?: string[] | null;
23+
npmRegistries?: NpmRegistryConfigEntry[] | null
924
themeList?: ThemeType[] | null;
1025
defaultTheme?: string | null;
1126
defaultHomePage?: string | null;

0 commit comments

Comments
 (0)