chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:08:39 +08:00
commit a0df89c693
5252 changed files with 523444 additions and 0 deletions
@@ -0,0 +1,55 @@
import { List } from 'lucide-react';
import { AutomationTestingProps } from '@/types';
import { CodeEditor } from '@@/CodeEditor';
import { TextTip } from '@@/Tip/TextTip';
import { Button } from '@@/buttons';
import { convertToArrayOfStrings, parseDotEnvFile } from './utils';
import { type Values } from './types';
export function AdvancedMode({
value,
onChange,
onSimpleModeClick,
'data-cy': dataCy,
}: {
value: Values;
onChange: (value: Values) => void;
onSimpleModeClick: () => void;
} & AutomationTestingProps) {
const editorValue = convertToArrayOfStrings(value).join('\n');
return (
<>
<Button
size="small"
color="link"
icon={List}
className="!ml-0 p-0 hover:no-underline"
onClick={onSimpleModeClick}
data-cy="env-simple-mode-button"
>
Simple mode
</Button>
<TextTip color="blue" inline={false}>
Switch to simple mode to define variables line by line, or load from
.env file
</TextTip>
<CodeEditor
id="environment-variables-editor"
value={editorValue}
onChange={handleEditorChange}
textTip="e.g. key=value"
data-cy={dataCy}
/>
</>
);
function handleEditorChange(value: string) {
onChange(parseDotEnvFile(value));
}
}
@@ -0,0 +1,61 @@
import { FormError } from '../FormError';
import { InputLabeled } from '../Input/InputLabeled';
import { ItemProps } from '../InputList';
import { EnvVar } from './types';
export function EnvironmentVariableItem({
item,
onChange,
disabled,
error,
readOnly,
index,
}: ItemProps<EnvVar>) {
return (
<div className="relative flex w-full flex-col">
<div className="flex w-full items-start gap-2">
<div className="w-1/2">
<InputLabeled
className="w-full"
data-cy={`env-name_${index}`}
label="name"
required
value={item.name}
onChange={(e) => handleChange({ name: e.target.value })}
disabled={disabled}
needsDeletion={item.needsDeletion}
readOnly={readOnly}
placeholder="e.g. FOO"
size="small"
id={`env-name${index}`}
/>
{error && (
<div>
<FormError className="!mb-0 mt-1">
{Object.values(error)[0]}
</FormError>
</div>
)}
</div>
<InputLabeled
className="w-1/2"
data-cy={`env-value_${index}`}
label="value"
value={item.value}
onChange={(e) => handleChange({ value: e.target.value })}
disabled={disabled}
needsDeletion={item.needsDeletion}
readOnly={readOnly}
placeholder="e.g. bar"
size="small"
id={`env-value${index}`}
/>
</div>
</div>
);
function handleChange(partial: Partial<EnvVar>) {
onChange({ ...item, ...partial });
}
}
@@ -0,0 +1,61 @@
import { useState } from 'react';
import { array, boolean, object, SchemaOf, string } from 'yup';
import { ArrayError } from '../InputList/InputList';
import { buildUniquenessTest } from '../validate-unique';
import { AdvancedMode } from './AdvancedMode';
import { SimpleMode } from './SimpleMode';
import { Values } from './types';
export function EnvironmentVariablesFieldset({
onChange,
values,
errors,
canUndoDelete,
}: {
values: Values;
onChange(value: Values): void;
errors?: ArrayError<Values>;
canUndoDelete?: boolean;
}) {
const [simpleMode, setSimpleMode] = useState(true);
return (
<>
{simpleMode ? (
<SimpleMode
onAdvancedModeClick={() => setSimpleMode(false)}
onChange={onChange}
value={values}
errors={errors}
canUndoDelete={canUndoDelete}
/>
) : (
<AdvancedMode
onSimpleModeClick={() => setSimpleMode(true)}
data-cy="env-var-advanced-mode"
onChange={onChange}
value={values}
/>
)}
</>
);
}
export function envVarValidation(): SchemaOf<Values> {
return array(
object({
name: string().required('Environment variable name is required'),
value: string().default(''),
needsDeletion: boolean().default(false),
})
).test(
'unique',
'This environment variable is already defined',
buildUniquenessTest(
() => 'This environment variable is already defined',
'name'
)
);
}
@@ -0,0 +1,59 @@
import React, { ComponentProps } from 'react';
import { FormSection } from '@@/form-components/FormSection';
import { TextTip } from '@@/Tip/TextTip';
import { EnvironmentVariablesFieldset } from './EnvironmentVariablesFieldset';
type FieldsetProps = ComponentProps<typeof EnvironmentVariablesFieldset>;
export function EnvironmentVariablesPanel({
explanation,
onChange,
values,
showHelpMessage,
errors,
isFoldable = false,
alertMessage,
}: {
explanation?: React.ReactNode;
showHelpMessage?: boolean;
isFoldable?: boolean;
alertMessage?: React.ReactNode;
} & FieldsetProps) {
return (
<FormSection
title="Environment variables"
isFoldable={isFoldable}
defaultFolded={isFoldable}
className="flex w-full flex-col"
>
<div className="form-group">
{!!explanation && (
<div className="col-sm-12 environment-variables-panel--explanation">
{explanation}
</div>
)}
{alertMessage}
<div className="col-sm-12">
<EnvironmentVariablesFieldset
values={values}
onChange={onChange}
errors={errors}
/>
</div>
{showHelpMessage && (
<div className="col-sm-12">
<TextTip color="blue" inline={false}>
Environment changes will not take effect until redeployment occurs
manually or via webhook.
</TextTip>
</div>
)}
</div>
</FormSection>
);
}
@@ -0,0 +1,115 @@
import { Edit, Plus } from 'lucide-react';
import { useState } from 'react';
import { readFileAsText } from '@/portainer/services/fileUploadReact';
import { Button } from '@@/buttons';
import { TextTip } from '@@/Tip/TextTip';
import { FileUploadField } from '@@/form-components/FileUpload';
import { InputList } from '@@/form-components/InputList';
import { ArrayError } from '@@/form-components/InputList/InputList';
import type { Values } from './types';
import { parseDotEnvFile } from './utils';
import { EnvironmentVariableItem } from './EnvironmentVariableItem';
export function SimpleMode({
value,
onChange,
onAdvancedModeClick,
errors,
canUndoDelete,
}: {
value: Values;
onChange: (value: Values) => void;
onAdvancedModeClick: () => void;
errors?: ArrayError<Values>;
canUndoDelete?: boolean;
}) {
return (
<>
<Button
size="small"
color="link"
icon={Edit}
className="!ml-0 p-0 hover:no-underline"
onClick={onAdvancedModeClick}
data-cy="environment-variables-advanced-mode-button"
>
Advanced mode
</Button>
<TextTip color="blue" inline={false}>
Switch to advanced mode to copy & paste multiple variables
</TextTip>
<InputList
aria-label="environment variables list"
onChange={onChange}
value={value}
isAddButtonHidden
item={EnvironmentVariableItem}
errors={errors}
canUndoDelete={canUndoDelete}
data-cy="simple-environment-variable-fieldset"
/>
<div className="flex gap-2">
<Button
onClick={() =>
onChange([...value, { name: '', value: '', needsDeletion: false }])
}
className="!ml-0"
color="default"
icon={Plus}
data-cy="add-environment-variable-button"
>
Add an environment variable
</Button>
<FileEnv onChooseFile={(add) => onChange([...value, ...add])} />
</div>
</>
);
}
function FileEnv({ onChooseFile }: { onChooseFile: (file: Values) => void }) {
const [file, setFile] = useState<File | null>(null);
const fileTooBig = file && file.size > 1024 * 1024;
return (
<>
<FileUploadField
inputId="env-file-upload"
onChange={handleChange}
title="Load variables from .env file"
accept=".env"
value={file}
color="default"
data-cy="load-environment-variables-from-file-button"
/>
{fileTooBig && (
<TextTip color="orange" inline>
File too large! Try uploading a file smaller than 1MB
</TextTip>
)}
</>
);
async function handleChange(file: File) {
setFile(file);
if (!file) {
return;
}
const text = await readFileAsText(file);
if (!text) {
return;
}
const parsed = parseDotEnvFile(text);
onChooseFile(parsed);
}
}
@@ -0,0 +1,70 @@
import { ComponentProps } from 'react';
import { Alert } from '@@/Alert';
import { useDocsUrl } from '@@/PageHeader/ContextHelp';
import { EnvironmentVariablesFieldset } from './EnvironmentVariablesFieldset';
import { EnvironmentVariablesPanel } from './EnvironmentVariablesPanel';
type FieldsetProps = ComponentProps<typeof EnvironmentVariablesFieldset>;
export function StackEnvironmentVariablesPanel({
onChange,
values,
errors,
isFoldable = false,
showHelpMessage,
}: {
isFoldable?: boolean;
showHelpMessage?: boolean;
} & FieldsetProps) {
return (
<EnvironmentVariablesPanel
explanation={
<div>
You may use{' '}
<a
href={`${useDocsUrl(
'/user/docker/stacks/add#environment-variables'
)}`}
target="_blank"
data-cy="stack-env-vars-help-link"
rel="noreferrer noopener"
>
environment variables in your compose file
</a>
. The environment variable values set below will be used as
substitutions in the compose file. Note that you may also reference a
stack.env file in your compose file. A stack.env file contains the
environment variables and their values (e.g. TAG=v1.5).
</div>
}
onChange={onChange}
values={values}
errors={errors}
isFoldable={isFoldable}
showHelpMessage={showHelpMessage}
alertMessage={
<div className="flex p-4">
<Alert color="info" className="col-sm-12">
<div>
<p>
<strong>stack.env file operation</strong>
</p>
<div>
When deploying via <strong>Repository</strong>, the stack.env
file must already reside in the Git repo.
</div>
<div>
When deploying via <strong>Web editor</strong>,{' '}
<strong>Upload</strong> or{' '}
<strong>Custom template deployment</strong>, the stack.env file
is auto created from what you set below.
</div>
</div>
</Alert>
</div>
}
/>
);
}
@@ -0,0 +1,9 @@
export {
EnvironmentVariablesFieldset,
envVarValidation,
} from './EnvironmentVariablesFieldset';
export { EnvironmentVariablesPanel } from './EnvironmentVariablesPanel';
export { StackEnvironmentVariablesPanel } from './StackEnvironmentVariablesPanel';
export { type Values as EnvVarValues } from './types';
@@ -0,0 +1,7 @@
export interface EnvVar {
name: string;
value?: string;
needsDeletion?: boolean;
}
export type Values = Array<EnvVar>;
@@ -0,0 +1,54 @@
import _ from 'lodash';
import { EnvVar } from './types';
export const KEY_REGEX = /(.+?)/.source;
export const VALUE_REGEX = /(.*)?/.source;
const KEY_VALUE_REGEX = new RegExp(`^(${KEY_REGEX})\\s*=(${VALUE_REGEX})$`);
const NEWLINES_REGEX = /\n|\r|\r\n/;
export function parseDotEnvFile(src: string) {
return parseArrayOfStrings(
_.compact(src.split(NEWLINES_REGEX))
.map((v) => v.trim())
.filter((v) => !v.startsWith('#') && v !== '')
);
}
export function parseArrayOfStrings(array: Array<string> = []): Array<EnvVar> {
if (!array) {
return [];
}
return _.compact(
array.map((variableString) => {
if (!variableString.includes('=')) {
return { name: variableString };
}
const parsedKeyValArr = variableString.trim().match(KEY_VALUE_REGEX);
if (parsedKeyValArr == null || parsedKeyValArr.length < 4) {
return null;
}
return {
name: parsedKeyValArr[1].trim(),
value: parsedKeyValArr[3].trim() || '',
needsDeletion: false,
};
})
);
}
export function convertToArrayOfStrings(array: Array<EnvVar>) {
if (!array) {
return [];
}
return array
.filter((variable) => variable.name)
.map(({ name, value }) =>
value || value === '' ? `${name}=${value}` : name
);
}