Skip to content

Commit 6740c80

Browse files
authored
feat(configuration): improved UX experience with configuration (#44)
* Made mandatory config name * Added instruction on how to start Pro * Added suggestions and persistence of runningConfig * Added description to env var at creation time * Added final touches to make everything work * updated readme * Removed counting logs * Fixed container sometimes now showing running
1 parent 900c427 commit 6740c80

File tree

11 files changed

+203
-63
lines changed

11 files changed

+203
-63
lines changed

Makefile

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
IMAGE?=localstack/localstack-docker-desktop
2-
TAG?=0.4.2
2+
TAG?=0.4.4
33

44
BUILDER=buildx-multi-arch
55

@@ -15,6 +15,16 @@ install-extension: build-extension ## Install the extension
1515
update-extension: build-extension ## Update the extension
1616
docker extension update $(IMAGE):$(TAG)
1717

18+
debug: ## Start the extension in debug mode
19+
docker extension dev debug $(IMAGE)
20+
21+
hot-reload: ## Enable hot reloading
22+
docker extension dev ui-source $(IMAGE) http://localhost:3000
23+
cd ui/ && npm start
24+
25+
stop-hot-realoading: ## Disable hot reloading
26+
docker extension dev reset $(IMAGE)
27+
1828
prepare-buildx: ## Create buildx builder for multi-arch build, if not exists
1929
docker buildx inspect $(BUILDER) || docker buildx create --name=$(BUILDER) --driver=docker-container --driver-opt=network=host
2030

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,18 @@ To contribute, check out our [issue tracker](https://github.com/localstack/local
4444

4545
2. Open the Developer Tools or create new features:
4646
```bash
47-
$ docker extension dev debug localstack/localstack-docker-desktop
47+
$ make debug
4848
```
4949

5050
3. Start the Extension on Docker Desktop and enable hot-reload using the following command:
5151
```bash
52-
$ npm start
53-
$ docker extension dev ui-source localstack/localstack-docker-desktop http://localhost:3000
52+
$ make hot-reloading
5453
```
5554

55+
4. Disable hot reloading:
56+
```bash
57+
$ make stop-hot-reloading
58+
```
5659
## Releases
5760

5861
Please refer to [`CHANGELOG`](CHANGELOG.md) to see the complete list of changes for each release.

ui/src/App.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ export const App = (): ReactElement => {
1818

1919
return (
2020
<>
21-
{showForm && <SettingsForm initialState={showSetupWarning ? 0 : 1} />}
21+
{showForm &&
22+
<SettingsForm initialState={showSetupWarning ? 0 : 2} />
23+
}
2224
<div className={classes.sticky}>
2325
<Header />
2426
</div>

ui/src/components/Header/Controller.tsx

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ import { ProgressButton } from '../Feedback';
2525
const EXCLUDED_ERROR_TOAST = ['INFO', 'WARN', 'DEBUG'];
2626

2727
export const Controller = (): ReactElement => {
28-
const { runConfigs, isLoading, createConfig } = useRunConfigs();
28+
const { configData, isLoading, setRunningConfig: setBackendRunningConfig, createConfig } = useRunConfigs();
2929
const { data, mutate } = useLocalStack();
3030
const { user, os, hasSkippedConfiguration } = useMountPoint();
31-
const [runningConfig, setRunningConfig] = useState<string>('Default');
31+
const [runningConfig, setRunningConfig] = useState<string>(configData.runningConfig ?? DEFAULT_CONFIGURATION_ID);
3232
const [downloadProps, setDownloadProps] = useState({ open: false, image: IMAGE });
3333
const [isStarting, setIsStarting] = useState<boolean>(false);
3434
const [isStopping, setIsStopping] = useState<boolean>(false);
@@ -38,14 +38,18 @@ export const Controller = (): ReactElement => {
3838
const tooltipLabel = isUnhealthy ? 'Unhealthy' : 'Healthy';
3939

4040
useEffect(() => {
41-
if (!isLoading && (!runConfigs || !runConfigs.find(item => item.name === 'Default'))) {
41+
if (!isLoading &&
42+
(!configData?.configs || !configData.configs?.find(item => item.id === DEFAULT_CONFIGURATION_ID))) {
4243
createConfig({
4344
name: 'Default', id: DEFAULT_CONFIGURATION_ID, vars: [],
44-
},
45-
);
45+
});
46+
}
47+
if (!isLoading) {
48+
setRunningConfig(configData.runningConfig ?? DEFAULT_CONFIGURATION_ID);
4649
}
4750
}, [isLoading]);
4851

52+
4953
const buildMountArg = () => {
5054
let location = 'LOCALSTACK_VOLUME_DIR=/tmp/localstack/volume';
5155

@@ -69,7 +73,7 @@ export const Controller = (): ReactElement => {
6973

7074
const corsArg = ['-e', `EXTRA_CORS_ALLOWED_ORIGINS=${CORS_ALLOW_DEFAULT}`];
7175

72-
const addedArgs = runConfigs.find(config => config.name === runningConfig)
76+
const addedArgs = configData.configs.find(config => config.id === runningConfig)
7377
.vars.map(item => {
7478
if (item.variable === 'EXTRA_CORS_ALLOWED_ORIGINS') { // prevent overriding variable
7579
corsArg.slice(0, 0);
@@ -88,8 +92,8 @@ export const Controller = (): ReactElement => {
8892
const start = async () => {
8993
const images = await ddClient.docker.listImages() as [DockerImage];
9094

91-
const isPro = runConfigs.find(config => config.name === runningConfig)
92-
.vars.some(item => item.variable === 'LOCALSTACK_API_KEY');
95+
const isPro = configData.configs.find(config => config.id === runningConfig)
96+
.vars.some(item => item.variable === 'LOCALSTACK_API_KEY' && item.value);
9397

9498
const haveCommunity = images.some(image => image.RepoTags?.at(0) === IMAGE);
9599
if (!haveCommunity) {
@@ -180,11 +184,11 @@ export const Controller = (): ReactElement => {
180184
<FormControl sx={{ m: 1, minWidth: 120, border: 'none' }} size='small'>
181185
<Select
182186
value={runningConfig}
183-
onChange={({ target }) => setRunningConfig(target.value)}
187+
onChange={({ target }) => setBackendRunningConfig(target.value)}
184188
>
185189
{
186-
runConfigs?.map(config => (
187-
<MenuItem key={config.id} value={config.name}>{config.name}</MenuItem>
190+
configData?.configs?.map(config => (
191+
<MenuItem key={config.id} value={config.id}>{config.name}</MenuItem>
188192
))
189193
}
190194
</Select>

ui/src/components/Views/Configs/ConfigPage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const useStyles = makeStyles((theme: Theme) => createStyles({
1818

1919
export const ConfigPage = (): ReactElement => {
2020

21-
const { runConfigs, deleteConfig } = useRunConfigs();
21+
const { configData, deleteConfig } = useRunConfigs();
2222
const mountPoint = useMountPoint();
2323
const [openModal, setOpenModal] = useState<boolean>(false);
2424
const [targetConfig, setTargetConfig] = useState<RunConfig | null>(null);
@@ -89,7 +89,7 @@ export const ConfigPage = (): ReactElement => {
8989
<Box sx={{ marginTop: 3 }}>
9090
<DataGrid
9191
autoHeight
92-
rows={runConfigs.filter(config => config.id !== DEFAULT_CONFIGURATION_ID)}
92+
rows={configData?.configs?.filter(config => config.id !== DEFAULT_CONFIGURATION_ID) || []}
9393
columns={columns}
9494
getRowId={(row) => (row).id as string || uuid()}
9595
disableSelectionOnClick

ui/src/components/Views/Configs/SettingsForm.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export const SettingsForm = ({ initialState }: MountPointFormProps): ReactElemen
4141
const { setMountPointData, user, os } = useMountPoint();
4242
const ddClient = useDDClient();
4343

44-
const steps = ['Enable Docker Desktop option', 'Set mount point'];
44+
const steps = ['Enable Docker Desktop option', 'Launching pro container', 'Set mount point'];
4545

4646
const handleNext = () => {
4747
if (activeStep !== steps.length - 1) {
@@ -158,7 +158,7 @@ export const SettingsForm = ({ initialState }: MountPointFormProps): ReactElemen
158158
)}
159159
</Stepper>
160160
<Box sx={{ margin: 5 }}>
161-
{activeStep === 0 ?
161+
{activeStep === 0 &&
162162
<>
163163
<Typography>
164164
Make sure to have the option &quot;Show Docker Extensions system containers&quot; enabled.
@@ -171,7 +171,15 @@ export const SettingsForm = ({ initialState }: MountPointFormProps): ReactElemen
171171
<li>In the bottom-right corner, select Apply & Restart</li>
172172
</ul>
173173
</>
174-
:
174+
}
175+
{
176+
activeStep === 1 &&
177+
<Typography>
178+
In order to start the Pro container, add a configuration with the variable LOCALSTACK_API_KEY
179+
set to your API key and select that configuration in the top right corner
180+
</Typography>
181+
}
182+
{activeStep === 2 &&
175183
<>
176184
<Typography variant='h3'>
177185
Default mount point settings

ui/src/components/Views/Configs/UpsertConfig.tsx

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ import { RunConfig } from '../../../types';
2020

2121
const DEFAULT_COLUMN_WIDTH = 2000;
2222

23+
const COMMON_CONFIGURATIONS = [
24+
['DEBUG', '0', 'Flag to increase log level and print more verbose logs'],
25+
['PERSISTENCE', '0', 'Enable persistence'],
26+
['LOCALSTACK_API_KEY', '', 'API key to activate LocalStack Pro.'],
27+
];
28+
2329
type Props = {
2430
config?: RunConfig,
2531
open: boolean,
@@ -42,7 +48,14 @@ export const UpsertConfig = ({ config, open, onClose }: Props): ReactElement =>
4248
const [newVar, setNewVar] = useState<string>('');
4349
const [newValue, setNewValue] = useState<string>('');
4450
const [configName, setConfigName] = useState<string>(config?.name || '');
45-
const [newConfig, setNewConfig] = useState<RunConfig>(config || { name: '', id: uuid(), vars: [] } as RunConfig);
51+
const [newConfig, setNewConfig] = useState<RunConfig>(config ||
52+
{
53+
name: '',
54+
id: uuid(),
55+
vars: COMMON_CONFIGURATIONS.map(
56+
([variable, value, description]) => ({ variable, value, id: uuid(), description }),
57+
),
58+
} as RunConfig);
4659
const classes = useStyles();
4760

4861
const handleAddButtonPress = () => {
@@ -105,34 +118,41 @@ export const UpsertConfig = ({ config, open, onClose }: Props): ReactElement =>
105118
variant='outlined'
106119
label='Configuration Name'
107120
value={configName}
121+
required
108122
onChange={(e) => setConfigName(e.target.value)}
109123
/>
110124
</Box>
111125
<Box className={classes.emptyBox} />
112126
<List
113127
subheader={
114-
<Typography>Environment Variables </Typography>
128+
<Typography>Environment Variables</Typography>
115129
}
116130
>
117131
{newConfig?.vars.map(item => (
118132
<ListItem key={item.id} disableGutters>
119-
<Box display='flex' width={DEFAULT_COLUMN_WIDTH} key={item.id}>
120-
<TextField
121-
fullWidth
122-
variant='outlined'
123-
className={classes.textField}
124-
onChange={(e) => updateConfigKey(item.id, e.target.value.toLocaleUpperCase())}
125-
value={item.variable}
126-
/>
127-
<TextField
128-
fullWidth
129-
variant='outlined'
130-
className={classes.textField}
131-
onChange={(e) => updateConfigValue(item.id, e.target.value)}
132-
value={item.value} />
133-
<IconButton onClick={() => handleRemoveButtonPress(item.id)} >
134-
<Remove />
135-
</IconButton>
133+
<Box width={DEFAULT_COLUMN_WIDTH}>
134+
<Box display='flex' key={item.id}>
135+
<TextField
136+
fullWidth
137+
variant='outlined'
138+
className={classes.textField}
139+
onChange={(e) => updateConfigKey(item.id, e.target.value.toLocaleUpperCase())}
140+
value={item.variable}
141+
/>
142+
<TextField
143+
fullWidth
144+
variant='outlined'
145+
label='Value'
146+
className={classes.textField}
147+
onChange={(e) => updateConfigValue(item.id, e.target.value)}
148+
value={item.value} />
149+
<IconButton onClick={() => handleRemoveButtonPress(item.id)} >
150+
<Remove />
151+
</IconButton>
152+
</Box>
153+
{item.description &&
154+
<Typography variant='caption'>{item.description}</Typography>
155+
}
136156
</Box>
137157
</ListItem>
138158
))}
@@ -173,6 +193,7 @@ export const UpsertConfig = ({ config, open, onClose }: Props): ReactElement =>
173193
<Button
174194
variant='contained'
175195
onClick={handleSaveButtonPress}
196+
disabled={!configName}
176197
>
177198
Save & Exit
178199
</Button>

ui/src/services/hooks/api.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import useSWR from 'swr';
22
import { STORAGE_KEY_ENVVARS, STORAGE_KEY_LOCALSTACK, STORAGE_KEY_MOUNT } from '../../constants';
3-
import { DockerContainer, mountPointData, RunConfig } from '../../types';
3+
import { ConfigData, DockerContainer, mountPointData, RunConfig } from '../../types';
44
import { isALocalStackContainer, isJson } from '../util';
55
import { useDDClient } from './utils';
66

77
interface useRunConfigsReturn {
8-
runConfigs: RunConfig[],
8+
configData: ConfigData,
99
isLoading: boolean,
10+
setRunningConfig: (data: string) => unknown;
1011
createConfig: (data: RunConfig) => unknown;
1112
updateConfig: (data: RunConfig) => unknown;
1213
deleteConfig: (data: string) => unknown;
@@ -16,6 +17,17 @@ interface HTTPMessageBody {
1617
Message: string,
1718
}
1819

20+
const adaptVersionData = (data: HTTPMessageBody, error: Error) => {
21+
const newData = (!data || !data?.Message || error) ?
22+
{ configs: [], runningConfig: null }
23+
:
24+
JSON.parse(data?.Message);
25+
if (Array.isArray(newData)) {
26+
return { configs: newData, runningConfig: newData.at(0).id ?? null };
27+
}
28+
return newData;
29+
};
30+
1931
export const useRunConfigs = (): useRunConfigsReturn => {
2032
const cacheKey = STORAGE_KEY_ENVVARS;
2133
const ddClient = useDDClient();
@@ -29,6 +41,11 @@ export const useRunConfigs = (): useRunConfigsReturn => {
2941
mutate();
3042
};
3143

44+
const setRunningConfig = async (configId: string) => {
45+
await ddClient.extension.vm.service.put('/configs/running', { Data: JSON.stringify(configId) });
46+
mutate();
47+
};
48+
3249
const createConfig = async (newData: RunConfig) => {
3350
await ddClient.extension.vm.service.post('/configs', { Data: JSON.stringify(newData) });
3451
mutate();
@@ -39,9 +56,11 @@ export const useRunConfigs = (): useRunConfigsReturn => {
3956
mutate();
4057
};
4158

59+
4260
return {
43-
runConfigs: (!data || !data?.Message || error) ? [] : JSON.parse(data?.Message),
61+
configData: adaptVersionData(data, error),
4462
isLoading: isValidating || (!error && !data),
63+
setRunningConfig,
4564
createConfig,
4665
updateConfig,
4766
deleteConfig,
@@ -78,7 +97,7 @@ export const useMountPoint = (): useMountPointReturn => {
7897
return {
7998
user: mountPointData?.user,
8099
os: mountPointData?.os,
81-
showForm: mountPointData?.showForm == null? true : mountPointData?.showForm,
100+
showForm: mountPointData?.showForm == null ? true : mountPointData?.showForm,
82101
showSetupWarning: mountPointData?.showSetupWarning == null ? true : mountPointData?.showSetupWarning,
83102
hasSkippedConfiguration: mountPointData?.hasSkippedConfiguration || false,
84103
isLoading: isValidating || (!error && !data),
@@ -105,7 +124,7 @@ export const useLocalStack = (): useLocalStackReturn => {
105124
/*
106125
* compares whether the old (b) status aligns with that of new (a) status
107126
*/
108-
(a, b) => a?.Id === b?.Id && a?.Status.includes('unhealthy') === b?.Status.includes('unhealthy'),
127+
(a, b) => a?.Id === b?.Id && a?.Status.includes('unhealthy') === b?.Status.includes('unhealthy'),
109128
},
110129
);
111130

ui/src/services/util/containers.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { PRO_IMAGE } from '../../constants';
12
import { DockerContainer, DockerImage } from '../../types';
23

34
/**
@@ -10,9 +11,23 @@ export function removeRepoFromImage(repoTag: string) {
1011
return repoTag.split('/').at(-1);
1112
}
1213

14+
const removeDockerTag = (imageString:string ) => {
15+
// Split the image string by ":" to check if it has a tag
16+
const parts = imageString.split(':');
17+
18+
// If there is no tag, return the original string as is
19+
if (parts.length === 1) {
20+
return imageString;
21+
}
22+
23+
// If there is a tag, return the part before the last ":" (image without tag)
24+
return parts.slice(0, -1).join(':');
25+
};
1326
export function removeTagFromImage(image: DockerImage){
14-
return image.RepoTags?.at(0)?.split(':').slice(0, -1).join(':');
27+
return removeDockerTag(image.RepoTags?.at(0));
1528
}
1629

17-
export const isALocalStackContainer = (container: DockerContainer) =>
18-
(container.Image === 'localstack/localstack' || container.Image === 'localstack/localstack-pro');
30+
export const isALocalStackContainer = (container: DockerContainer) => {
31+
const image = removeDockerTag(container.Image);
32+
return image === 'localstack/localstack' || image === PRO_IMAGE;
33+
};

0 commit comments

Comments
 (0)