chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
+61
@@ -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 });
|
||||
}
|
||||
}
|
||||
+61
@@ -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'
|
||||
)
|
||||
);
|
||||
}
|
||||
+59
@@ -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);
|
||||
}
|
||||
}
|
||||
+70
@@ -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
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user