Add dialog to insert user fields for board that require them to upload (#550)

* Rebuild gRPC protocol interfaces

* Implement methods to get user fields for board/port combination

* Implement dialog to input board user fields

* Add configure and upload step when uploading to board requiring user fields

* Disable Sketch > Configure and Upload menu if board doesn't support user fields

* Fix serial upload not working with all boards

* Update i18n source file

* fix user fields UI

* regenerate cli protocol

* fix localisation

* check if user fields are empty

Co-authored-by: Alberto Iannaccone <a.iannaccone@arduino.cc>
This commit is contained in:
Silvano Cerza
2021-11-25 18:22:51 +01:00
committed by GitHub
parent 74bfdc4c56
commit a090dfe99c
19 changed files with 568 additions and 679 deletions

View File

@@ -445,7 +445,7 @@ export class ArduinoFrontendContribution
'arduino/debug/optimizeForDebugging',
'Optimize for Debugging'
),
order: '4',
order: '5',
});
}

View File

@@ -254,6 +254,11 @@ import {
UploadCertificateDialogWidget,
} from './dialogs/certificate-uploader/certificate-uploader-dialog';
import { PlotterFrontendContribution } from './serial/plotter/plotter-frontend-contribution';
import {
UserFieldsDialog,
UserFieldsDialogProps,
UserFieldsDialogWidget,
} from './dialogs/user-fields/user-fields-dialog';
import { nls } from '@theia/core/lib/browser/nls';
const ElementQueries = require('css-element-queries/src/ElementQueries');
@@ -739,4 +744,10 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(UploadCertificateDialogProps).toConstantValue({
title: 'UploadCertificate',
});
bind(UserFieldsDialogWidget).toSelf().inSingletonScope();
bind(UserFieldsDialog).toSelf().inSingletonScope();
bind(UserFieldsDialogProps).toConstantValue({
title: 'UserFields',
});
});

View File

@@ -12,6 +12,7 @@ import {
BoardsPackage,
AttachedBoardsChangeEvent,
BoardWithPackage,
BoardUserField,
} from '../../common/protocol';
import { BoardsConfig } from './boards-config';
import { naturalCompare } from '../../common/utils';
@@ -68,7 +69,8 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
* This event is also emitted when the board package for the currently selected board was uninstalled.
*/
readonly onBoardsConfigChanged = this.onBoardsConfigChangedEmitter.event;
readonly onAvailableBoardsChanged = this.onAvailableBoardsChangedEmitter.event;
readonly onAvailableBoardsChanged =
this.onAvailableBoardsChangedEmitter.event;
readonly onAvailablePortsChanged = this.onAvailablePortsChangedEmitter.event;
onStart(): void {
@@ -183,8 +185,8 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
const selectedAvailableBoard = AvailableBoard.is(selectedBoard)
? selectedBoard
: this._availableBoards.find((availableBoard) =>
Board.sameAs(availableBoard, selectedBoard)
);
Board.sameAs(availableBoard, selectedBoard)
);
if (
selectedAvailableBoard &&
selectedAvailableBoard.selected &&
@@ -274,6 +276,18 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
return boards;
}
async selectedBoardUserFields(): Promise<BoardUserField[]> {
if (!this._boardsConfig.selectedBoard || !this._boardsConfig.selectedPort) {
return [];
}
const fqbn = this._boardsConfig.selectedBoard.fqbn;
if (!fqbn) {
return [];
}
const protocol = this._boardsConfig.selectedPort.protocol;
return await this.boardsService.getBoardUserFields({ fqbn, protocol });
}
/**
* `true` if the `config.selectedBoard` is defined; hence can compile against the board. Otherwise, `false`.
*/
@@ -361,14 +375,14 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
const timeoutTask =
!!timeout && timeout > 0
? new Promise<void>((_, reject) =>
setTimeout(
() => reject(new Error(`Timeout after ${timeout} ms.`)),
timeout
setTimeout(
() => reject(new Error(`Timeout after ${timeout} ms.`)),
timeout
)
)
)
: new Promise<void>(() => {
/* never */
});
/* never */
});
const waitUntilTask = new Promise<void>((resolve) => {
let candidate = find(what, this.availableBoards);
if (candidate) {
@@ -406,7 +420,7 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
const availableBoards: AvailableBoard[] = [];
const attachedBoards = this._attachedBoards.filter(({ port }) => !!port);
const availableBoardPorts = availablePorts.filter((port) => {
if (port.protocol === "serial") {
if (port.protocol === 'serial') {
// We always show all serial ports, even if there
// is no recognized board connected to it
return true;
@@ -424,8 +438,12 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
});
for (const boardPort of availableBoardPorts) {
let board = attachedBoards.find(({ port }) => Port.sameAs(boardPort, port));
const lastSelectedBoard = await this.getLastSelectedBoardOnPort(boardPort);
const board = attachedBoards.find(({ port }) =>
Port.sameAs(boardPort, port)
);
const lastSelectedBoard = await this.getLastSelectedBoardOnPort(
boardPort
);
let availableBoard = {} as AvailableBoard;
if (board) {
@@ -454,11 +472,16 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
availableBoards.push(availableBoard);
}
if (boardsConfig.selectedBoard && !availableBoards.some(({ selected }) => selected)) {
if (
boardsConfig.selectedBoard &&
!availableBoards.some(({ selected }) => selected)
) {
// If the selected board has the same port of an unknown board
// that is already in availableBoards we might get a duplicate port.
// So we remove the one already in the array and add the selected one.
const found = availableBoards.findIndex(board => board.port?.address === boardsConfig.selectedPort?.address);
const found = availableBoards.findIndex(
(board) => board.port?.address === boardsConfig.selectedPort?.address
);
if (found >= 0) {
availableBoards.splice(found, 1);
}
@@ -475,7 +498,9 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
let hasChanged = availableBoards.length !== currentAvailableBoards.length;
for (let i = 0; !hasChanged && i < availableBoards.length; i++) {
const [left, right] = [availableBoards[i], currentAvailableBoards[i]];
hasChanged = !!AvailableBoard.compare(left, right) || left.selected !== right.selected;
hasChanged =
!!AvailableBoard.compare(left, right) ||
left.selected !== right.selected;
}
if (hasChanged) {
this._availableBoards = availableBoards;
@@ -483,7 +508,9 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
}
}
protected async getLastSelectedBoardOnPort(port: Port): Promise<Board | undefined> {
protected async getLastSelectedBoardOnPort(
port: Port
): Promise<Board | undefined> {
const key = this.getLastSelectedBoardOnPortKey(port);
return this.getData<Board>(key);
}
@@ -504,8 +531,11 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
]);
}
protected getLastSelectedBoardOnPortKey(port: Port): string {
return `last-selected-board-on-port:${Port.toString(port)}`;
protected getLastSelectedBoardOnPortKey(port: Port | string): string {
// TODO: we lose the port's `protocol` info (`serial`, `network`, etc.) here if the `port` is a `string`.
return `last-selected-board-on-port:${
typeof port === 'string' ? port : Port.toString(port)
}`;
}
protected async loadState(): Promise<void> {
@@ -596,13 +626,22 @@ export namespace AvailableBoard {
// 4. Network with recognized boards
// 5. Other protocols with recognized boards
export const compare = (left: AvailableBoard, right: AvailableBoard) => {
if (left.port?.protocol === "serial" && right.port?.protocol !== "serial") {
if (left.port?.protocol === 'serial' && right.port?.protocol !== 'serial') {
return -1;
} else if (left.port?.protocol !== "serial" && right.port?.protocol === "serial") {
} else if (
left.port?.protocol !== 'serial' &&
right.port?.protocol === 'serial'
) {
return 1;
} else if (left.port?.protocol === "network" && right.port?.protocol !== "network") {
} else if (
left.port?.protocol === 'network' &&
right.port?.protocol !== 'network'
) {
return -1;
} else if (left.port?.protocol !== "network" && right.port?.protocol === "network") {
} else if (
left.port?.protocol !== 'network' &&
right.port?.protocol === 'network'
) {
return 1;
} else if (left.port?.protocol === right.port?.protocol) {
// We show all ports, including those that have guessed
@@ -614,5 +653,5 @@ export namespace AvailableBoard {
}
}
return naturalCompare(left.port?.address!, right.port?.address!);
}
};
}

View File

@@ -1,7 +1,7 @@
import { inject, injectable } from 'inversify';
import { inject, injectable, postConstruct } from 'inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { CoreService } from '../../common/protocol';
import { ArduinoMenus } from '../menu/arduino-menus';
import { BoardUserField, CoreService } from '../../common/protocol';
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { BoardsDataStore } from '../boards/boards-data-store';
import { SerialConnectionManager } from '../serial/serial-connection-manager';
@@ -14,7 +14,9 @@ import {
KeybindingRegistry,
TabBarToolbarRegistry,
} from './contribution';
import { UserFieldsDialog } from '../dialogs/user-fields/user-fields-dialog';
import { nls } from '@theia/core/lib/browser/nls';
import { DisposableCollection } from '@theia/core';
@injectable()
export class UploadSketch extends SketchContribution {
@@ -24,22 +26,96 @@ export class UploadSketch extends SketchContribution {
@inject(SerialConnectionManager)
protected readonly serialConnection: SerialConnectionManager;
@inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry;
@inject(BoardsDataStore)
protected readonly boardsDataStore: BoardsDataStore;
@inject(BoardsServiceProvider)
protected readonly boardsServiceClientImpl: BoardsServiceProvider;
@inject(UserFieldsDialog)
protected readonly userFieldsDialog: UserFieldsDialog;
protected cachedUserFields: Map<string, BoardUserField[]> = new Map();
protected readonly onDidChangeEmitter = new Emitter<Readonly<void>>();
readonly onDidChange = this.onDidChangeEmitter.event;
protected uploadInProgress = false;
protected boardRequiresUserFields = false;
protected readonly menuActionsDisposables = new DisposableCollection();
@postConstruct()
protected init(): void {
this.boardsServiceClientImpl.onBoardsConfigChanged(async () => {
const userFields =
await this.boardsServiceClientImpl.selectedBoardUserFields();
this.boardRequiresUserFields = userFields.length > 0;
this.registerMenus(this.menuRegistry);
});
}
private selectedFqbnAddress(): string {
const { boardsConfig } = this.boardsServiceClientImpl;
const fqbn = boardsConfig.selectedBoard?.fqbn;
if (!fqbn) {
return '';
}
const address = boardsConfig.selectedBoard?.port?.address;
if (!address) {
return '';
}
return fqbn + '|' + address;
}
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(UploadSketch.Commands.UPLOAD_SKETCH, {
execute: () => this.uploadSketch(),
execute: async () => {
const key = this.selectedFqbnAddress();
if (!key) {
return;
}
if (this.boardRequiresUserFields && !this.cachedUserFields.has(key)) {
// Deep clone the array of board fields to avoid editing the cached ones
this.userFieldsDialog.value = (
await this.boardsServiceClientImpl.selectedBoardUserFields()
).map((f) => ({ ...f }));
const result = await this.userFieldsDialog.open();
if (!result) {
return;
}
this.cachedUserFields.set(key, result);
}
this.uploadSketch();
},
isEnabled: () => !this.uploadInProgress,
});
registry.registerCommand(UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION, {
execute: async () => {
const key = this.selectedFqbnAddress();
if (!key) {
return;
}
const cached = this.cachedUserFields.get(key);
// Deep clone the array of board fields to avoid editing the cached ones
this.userFieldsDialog.value = (
cached ??
(await this.boardsServiceClientImpl.selectedBoardUserFields())
).map((f) => ({ ...f }));
const result = await this.userFieldsDialog.open();
if (!result) {
return;
}
this.cachedUserFields.set(key, result);
this.uploadSketch();
},
isEnabled: () => !this.uploadInProgress && this.boardRequiresUserFields,
});
registry.registerCommand(
UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER,
{
@@ -58,19 +134,46 @@ export class UploadSketch extends SketchContribution {
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: UploadSketch.Commands.UPLOAD_SKETCH.id,
label: nls.localize('arduino/sketch/upload', 'Upload'),
order: '1',
});
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER.id,
label: nls.localize(
'arduino/sketch/uploadUsingProgrammer',
'Upload Using Programmer'
),
order: '2',
});
this.menuActionsDisposables.dispose();
this.menuActionsDisposables.push(
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: UploadSketch.Commands.UPLOAD_SKETCH.id,
label: nls.localize('arduino/sketch/upload', 'Upload'),
order: '1',
})
);
if (this.boardRequiresUserFields) {
this.menuActionsDisposables.push(
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id,
label: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label,
order: '2',
})
);
} else {
this.menuActionsDisposables.push(
registry.registerMenuNode(
ArduinoMenus.SKETCH__MAIN_GROUP,
new PlaceholderMenuNode(
ArduinoMenus.SKETCH__MAIN_GROUP,
// commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id,
UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label!,
{ order: '2' }
)
)
);
}
this.menuActionsDisposables.push(
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER.id,
label: nls.localize(
'arduino/sketch/uploadUsingProgrammer',
'Upload Using Programmer'
),
order: '3',
})
);
}
registerKeybindings(registry: KeybindingRegistry): void {
@@ -127,6 +230,17 @@ export class UploadSketch extends SketchContribution {
const optimizeForDebug = this.editorMode.compileForDebug;
const { selectedPort } = boardsConfig;
const port = selectedPort;
const userFields =
this.cachedUserFields.get(this.selectedFqbnAddress()) ?? [];
if (userFields.length === 0 && this.boardRequiresUserFields) {
this.messageService.error(
nls.localize(
'arduino/sketch/userFieldsNotFoundError',
"Can't find user fields for connected board"
)
);
return;
}
if (usingProgrammer) {
const programmer = selectedProgrammer;
@@ -139,6 +253,7 @@ export class UploadSketch extends SketchContribution {
verbose,
verify,
sourceOverride,
userFields,
};
} else {
options = {
@@ -149,6 +264,7 @@ export class UploadSketch extends SketchContribution {
verbose,
verify,
sourceOverride,
userFields,
};
}
this.outputChannelManager.getChannel('Arduino').clear();
@@ -197,6 +313,14 @@ export namespace UploadSketch {
export const UPLOAD_SKETCH: Command = {
id: 'arduino-upload-sketch',
};
export const UPLOAD_WITH_CONFIGURATION: Command = {
id: 'arduino-upload-with-configuration-sketch',
label: nls.localize(
'arduino/sketch/configureAndUpload',
'Configure And Upload'
),
category: 'Arduino',
};
export const UPLOAD_SKETCH_USING_PROGRAMMER: Command = {
id: 'arduino-upload-sketch-using-programmer',
};

View File

@@ -62,7 +62,7 @@ export class VerifySketch extends SketchContribution {
'arduino/sketch/exportBinary',
'Export Compiled Binary'
),
order: '3',
order: '4',
});
}

View File

@@ -0,0 +1,98 @@
import * as React from 'react';
import { BoardUserField } from '../../../common/protocol';
import { nls } from '@theia/core/lib/browser/nls';
export const UserFieldsComponent = ({
initialBoardUserFields,
updateUserFields,
cancel,
accept,
}: {
initialBoardUserFields: BoardUserField[];
updateUserFields: (userFields: BoardUserField[]) => void;
cancel: () => void;
accept: () => Promise<void>;
}): React.ReactElement => {
const [boardUserFields, setBoardUserFields] = React.useState<
BoardUserField[]
>(initialBoardUserFields);
const [uploadButtonDisabled, setUploadButtonDisabled] =
React.useState<boolean>(true);
React.useEffect(() => {
setBoardUserFields(initialBoardUserFields);
}, [initialBoardUserFields]);
const updateUserField =
(index: number) => (e: React.ChangeEvent<HTMLInputElement>) => {
const newBoardUserFields = [...boardUserFields];
newBoardUserFields[index].value = e.target.value;
setBoardUserFields(newBoardUserFields);
};
const allFieldsHaveValues = (userFields: BoardUserField[]): boolean => {
return (
userFields &&
userFields.length > 0 &&
userFields
.map<boolean>((field: BoardUserField): boolean => {
return field.value.length > 0;
})
.reduce((previous: boolean, current: boolean): boolean => {
return previous && current;
})
);
};
React.useEffect(() => {
updateUserFields(boardUserFields);
setUploadButtonDisabled(!allFieldsHaveValues(boardUserFields));
}, [boardUserFields]);
return (
<div>
<div className="user-fields-container">
<div className="user-fields-list">
{boardUserFields.map((field, index) => {
return (
<div className="dialogSection" key={index}>
<div className="dialogRow">
<label className="field-label">{field.label}</label>
</div>
<div className="dialogRow">
<input
type={field.secret ? 'password' : 'text'}
value={field.value}
className="theia-input"
placeholder={'Enter ' + field.label}
onChange={updateUserField(index)}
/>
</div>
</div>
);
})}
</div>
</div>
<div className="dialogSection">
<div className="dialogRow button-container">
<button
type="button"
className="theia-button secondary install-cert-btn"
onClick={cancel}
>
{nls.localize('arduino/userFields/cancel', 'Cancel')}
</button>
<button
type="button"
className="theia-button primary install-cert-btn"
disabled={uploadButtonDisabled}
onClick={accept}
>
{nls.localize('arduino/userFields/upload', 'Upload')}
</button>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,121 @@
import * as React from 'react';
import { inject, injectable } from 'inversify';
import {
AbstractDialog,
DialogProps,
ReactWidget,
} from '@theia/core/lib/browser';
import { Widget } from '@phosphor/widgets';
import { Message } from '@phosphor/messaging';
import { UploadSketch } from '../../contributions/upload-sketch';
import { UserFieldsComponent } from './user-fields-component';
import { BoardUserField } from '../../../common/protocol';
@injectable()
export class UserFieldsDialogWidget extends ReactWidget {
protected _currentUserFields: BoardUserField[] = [];
constructor(private cancel: () => void, private accept: () => Promise<void>) {
super();
}
set currentUserFields(userFields: BoardUserField[]) {
this.setUserFields(userFields);
}
get currentUserFields(): BoardUserField[] {
return this._currentUserFields;
}
resetUserFieldsValue(): void {
this._currentUserFields = this._currentUserFields.map((field) => {
field.value = '';
return field;
});
}
protected setUserFields(userFields: BoardUserField[]): void {
this._currentUserFields = userFields;
}
protected render(): React.ReactNode {
return (
<form>
<UserFieldsComponent
initialBoardUserFields={this._currentUserFields}
updateUserFields={this.setUserFields.bind(this)}
cancel={this.cancel}
accept={this.accept}
/>
</form>
);
}
}
@injectable()
export class UserFieldsDialogProps extends DialogProps {}
@injectable()
export class UserFieldsDialog extends AbstractDialog<BoardUserField[]> {
protected readonly widget: UserFieldsDialogWidget;
constructor(
@inject(UserFieldsDialogProps)
protected readonly props: UserFieldsDialogProps
) {
super({
title: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label || '',
});
this.titleNode.classList.add('user-fields-dialog-title');
this.contentNode.classList.add('user-fields-dialog-content');
this.acceptButton = undefined;
this.widget = new UserFieldsDialogWidget(
this.close.bind(this),
this.accept.bind(this)
);
}
set value(userFields: BoardUserField[]) {
this.widget.currentUserFields = userFields;
}
get value(): BoardUserField[] {
return this.widget.currentUserFields;
}
protected onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
Widget.attach(this.widget, this.contentNode);
super.onAfterAttach(msg);
this.update();
}
protected onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.widget.update();
}
protected onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.widget.activate();
}
protected async accept(): Promise<void> {
// If the user presses enter and at least
// a field is empty don't accept the input
for (const field of this.value) {
if (field.value.length === 0) {
return;
}
}
return super.accept();
}
close(): void {
this.widget.resetUserFieldsValue();
this.widget.close();
super.close();
}
}

View File

@@ -10,6 +10,7 @@
@import './settings-dialog.css';
@import './firmware-uploader-dialog.css';
@import './certificate-uploader-dialog.css';
@import './user-fields-dialog.css';
@import './debug.css';
@import './sketchbook.css';
@import './cloud-sketchbook.css';
@@ -17,87 +18,91 @@
@import './custom-codicon.css';
.theia-input.warning:focus {
outline-width: 1px;
outline-style: solid;
outline-offset: -1px;
opacity: 1 !important;
color: var(--theia-warningForeground);
background-color: var(--theia-warningBackground);
outline-width: 1px;
outline-style: solid;
outline-offset: -1px;
opacity: 1 !important;
color: var(--theia-warningForeground);
background-color: var(--theia-warningBackground);
}
.theia-input.warning {
background-color: var(--theia-warningBackground);
background-color: var(--theia-warningBackground);
}
.theia-input.warning::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
color: var(--theia-warningForeground);
background-color: var(--theia-warningBackground);
opacity: 1; /* Firefox */
.theia-input.warning::placeholder {
/* Chrome, Firefox, Opera, Safari 10.1+ */
color: var(--theia-warningForeground);
background-color: var(--theia-warningBackground);
opacity: 1; /* Firefox */
}
.theia-input.warning:-ms-input-placeholder { /* Internet Explorer 10-11 */
color: var(--theia-warningForeground);
background-color: var(--theia-warningBackground);
.theia-input.warning:-ms-input-placeholder {
/* Internet Explorer 10-11 */
color: var(--theia-warningForeground);
background-color: var(--theia-warningBackground);
}
.theia-input.warning::-ms-input-placeholder { /* Microsoft Edge */
color: var(--theia-warningForeground);
background-color: var(--theia-warningBackground);
.theia-input.warning::-ms-input-placeholder {
/* Microsoft Edge */
color: var(--theia-warningForeground);
background-color: var(--theia-warningBackground);
}
/* Makes the sidepanel a bit wider when opening the widget */
/* Makes the sidepanel a bit wider when opening the widget */
.p-DockPanel-widget {
min-width: 200px;
min-height: 200px;
min-width: 200px;
min-height: 200px;
}
/* Overrule the default Theia CSS button styles. */
button.theia-button,
.theia-button {
border: 1px solid var(--theia-dropdown-border);
border: 1px solid var(--theia-dropdown-border);
}
button.theia-button:hover,
.theia-button:hover {
border: 1px solid var(--theia-focusBorder);
border: 1px solid var(--theia-focusBorder);
}
button.theia-button {
height: 31px;
height: 31px;
}
button.theia-button.secondary {
background-color: var(--theia-secondaryButton-background);
color: var(--theia-secondaryButton-foreground);
background-color: var(--theia-secondaryButton-background);
color: var(--theia-secondaryButton-foreground);
}
button.theia-button.main {
color: var(--theia-button-foreground);
color: var(--theia-button-foreground);
}
/* To make the progress-bar slightly thicker, and use the color from the status bar */
.theia-progress-bar-container {
width: 100%;
height: 4px;
width: 100%;
height: 4px;
}
.theia-progress-bar {
height: 4px;
width: 3%;
animation: progress-animation 1.3s 0s infinite cubic-bezier(0.645, 0.045, 0.355, 1);
height: 4px;
width: 3%;
animation: progress-animation 1.3s 0s infinite
cubic-bezier(0.645, 0.045, 0.355, 1);
}
.theia-notification-item-progressbar {
height: 4px;
width: 66%;
height: 4px;
width: 66%;
}
.flex-line {
display: flex;
align-items: center;
white-space: nowrap;
display: flex;
align-items: center;
white-space: nowrap;
}
.fa-reload {
font-size: 14px;
}
font-size: 14px;
}

View File

@@ -0,0 +1,32 @@
.user-fields-container {
max-height: 332px;
overflow: auto;
padding: 2px;
}
.user-fields-list {
margin: 16px 0;
}
.user-fields-dialog-content {
width: 408px;
max-height: 491px;
}
.user-fields-dialog-content .field-label {
color: #2c353a;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 21px;
letter-spacing: 0.01em;
text-align: left;
}
.user-fields-dialog-content .theia-input {
flex-grow: 1;
}
.user-fields-dialog-content .button-container {
justify-content: flex-end;
}