feat: moved login entry point to the side-bar

Closes #1877

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
Akos Kitta 2023-02-10 18:17:41 +01:00 committed by Akos Kitta
parent d68bc4abdb
commit 4deaf4fb76
13 changed files with 278 additions and 114 deletions

View File

@ -10,10 +10,7 @@ import {
MenuContribution,
MenuModelRegistry,
} from '@theia/core';
import {
FrontendApplication,
FrontendApplicationContribution,
} from '@theia/core/lib/browser';
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
@ -77,7 +74,7 @@ export class ArduinoFrontendContribution
}
}
onStart(app: FrontendApplication): void {
onStart(): void {
this.electronWindowPreferences.onPreferenceChanged((event) => {
if (event.newValue !== event.oldValue) {
switch (event.preferenceName) {
@ -98,8 +95,6 @@ export class ArduinoFrontendContribution
webContents.setZoomLevel(zoomLevel);
})
);
// Removes the _Settings_ (cog) icon from the left sidebar
app.shell.leftPanelHandler.removeBottomMenu('settings-menu');
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {

View File

@ -347,6 +347,9 @@ import { ConfigServiceClient } from './config/config-service-client';
import { ValidateSketch } from './contributions/validate-sketch';
import { RenameCloudSketch } from './contributions/rename-cloud-sketch';
import { CreateFeatures } from './create/create-features';
import { Account } from './contributions/account';
import { SidebarBottomMenuWidget } from './theia/core/sidebar-bottom-menu-widget';
import { SidebarBottomMenuWidget as TheiaSidebarBottomMenuWidget } from '@theia/core/lib/browser/shell/sidebar-bottom-menu-widget';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
// Commands and toolbar items
@ -734,6 +737,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
Contribution.configure(bind, NewCloudSketch);
Contribution.configure(bind, ValidateSketch);
Contribution.configure(bind, RenameCloudSketch);
Contribution.configure(bind, Account);
bindContributionProvider(bind, StartupTaskProvider);
bind(StartupTaskProvider).toService(BoardsServiceProvider); // to reuse the boards config in another window
@ -1014,4 +1018,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
},
}))
.inSingletonScope();
bind(SidebarBottomMenuWidget).toSelf();
rebind(TheiaSidebarBottomMenuWidget).toService(SidebarBottomMenuWidget);
});

View File

@ -83,9 +83,13 @@ export class AuthenticationClientService
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(CloudUserCommands.LOGIN, {
execute: () => this.service.login(),
isEnabled: () => !this._session,
isVisible: () => !this._session,
});
registry.registerCommand(CloudUserCommands.LOGOUT, {
execute: () => this.service.logout(),
isEnabled: () => !!this._session,
isVisible: () => !!this._session,
});
}

View File

@ -1,5 +1,8 @@
import { Command } from '@theia/core/lib/common/command';
export const LEARN_MORE_URL =
'https://docs.arduino.cc/software/ide-v2/tutorials/ide-v2-cloud-sketch-sync';
export namespace CloudUserCommands {
export const LOGIN = Command.toLocalizedCommand(
{
@ -16,9 +19,4 @@ export namespace CloudUserCommands {
},
'arduino/cloud/signOut'
);
export const OPEN_PROFILE_CONTEXT_MENU: Command = {
id: 'arduino-cloud-sketchbook--open-profile-menu',
label: 'Contextual menu',
};
}

View File

@ -0,0 +1,145 @@
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { SidebarMenu } from '@theia/core/lib/browser/shell/sidebar-menu-widget';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { MenuPath } from '@theia/core/lib/common/menu';
import { nls } from '@theia/core/lib/common/nls';
import { inject, injectable } from '@theia/core/shared/inversify';
import { CloudUserCommands, LEARN_MORE_URL } from '../auth/cloud-user-commands';
import { CreateFeatures } from '../create/create-features';
import { ArduinoMenus } from '../menu/arduino-menus';
import {
Command,
CommandRegistry,
Contribution,
MenuModelRegistry,
} from './contribution';
export const accountMenu: SidebarMenu = {
id: 'arduino-accounts-menu',
iconClass: 'codicon codicon-account',
title: nls.localize('arduino/account/menuTitle', 'Arduino Cloud'),
menuPath: ArduinoMenus.ARDUINO_ACCOUNT__CONTEXT,
order: 0,
};
@injectable()
export class Account extends Contribution {
@inject(WindowService)
private readonly windowService: WindowService;
@inject(CreateFeatures)
private readonly createFeatures: CreateFeatures;
private readonly toDispose = new DisposableCollection();
private app: FrontendApplication;
override onStart(app: FrontendApplication): void {
this.app = app;
this.updateSidebarCommand();
this.toDispose.push(
this.createFeatures.onDidChangeEnabled((enabled) =>
this.updateSidebarCommand(enabled)
)
);
}
onStop(): void {
this.toDispose.dispose();
}
override registerCommands(registry: CommandRegistry): void {
const openExternal = (url: string) =>
this.windowService.openNewWindow(url, { external: true });
registry.registerCommand(Account.Commands.LEARN_MORE, {
execute: () => openExternal(LEARN_MORE_URL),
isEnabled: () => !Boolean(this.createFeatures.session),
});
registry.registerCommand(Account.Commands.GO_TO_PROFILE, {
execute: () => openExternal('https://id.arduino.cc/'),
isEnabled: () => Boolean(this.createFeatures.session),
});
registry.registerCommand(Account.Commands.GO_TO_CLOUD_EDITOR, {
execute: () => openExternal('https://create.arduino.cc/editor'),
isEnabled: () => Boolean(this.createFeatures.session),
});
registry.registerCommand(Account.Commands.GO_TO_IOT_CLOUD, {
execute: () => openExternal('https://create.arduino.cc/iot/'),
isEnabled: () => Boolean(this.createFeatures.session),
});
}
override registerMenus(registry: MenuModelRegistry): void {
const register = (
menuPath: MenuPath,
...commands: (Command | [command: Command, menuLabel: string])[]
) =>
commands.forEach((command, index) => {
const commandId = Array.isArray(command) ? command[0].id : command.id;
const label = Array.isArray(command) ? command[1] : command.label;
registry.registerMenuAction(menuPath, {
label,
commandId,
order: String(index),
});
});
register(ArduinoMenus.ARDUINO_ACCOUNT__CONTEXT__SIGN_IN_GROUP, [
CloudUserCommands.LOGIN,
nls.localize('arduino/cloud/signInToCloud', 'Sign in to Arduino Cloud'),
]);
register(ArduinoMenus.ARDUINO_ACCOUNT__CONTEXT__LEARN_MORE_GROUP, [
Account.Commands.LEARN_MORE,
nls.localize('arduino/cloud/learnMore', 'Learn more'),
]);
register(
ArduinoMenus.ARDUINO_ACCOUNT__CONTEXT__GO_TO_GROUP,
[
Account.Commands.GO_TO_PROFILE,
nls.localize('arduino/account/goToProfile', 'Go to Profile'),
],
[
Account.Commands.GO_TO_CLOUD_EDITOR,
nls.localize('arduino/account/goToCloudEditor', 'Go to Cloud Editor'),
],
[
Account.Commands.GO_TO_IOT_CLOUD,
nls.localize('arduino/account/goToIoTCloud', 'Go to IoT Cloud'),
]
);
register(
ArduinoMenus.ARDUINO_ACCOUNT__CONTEXT__SIGN_OUT_GROUP,
CloudUserCommands.LOGOUT
);
}
private updateSidebarCommand(
visible: boolean = this.preferences['arduino.cloud.enabled']
): void {
if (!this.app) {
return;
}
const handler = this.app.shell.leftPanelHandler;
if (visible) {
handler.addBottomMenu(accountMenu);
} else {
handler.removeBottomMenu(accountMenu.id);
}
}
}
export namespace Account {
export namespace Commands {
export const GO_TO_PROFILE: Command = {
id: 'arduino-go-to-profile',
};
export const GO_TO_CLOUD_EDITOR: Command = {
id: 'arduino-go-to-cloud-editor',
};
export const GO_TO_IOT_CLOUD: Command = {
id: 'arduino-go-to-iot-cloud',
};
export const LEARN_MORE: Command = {
id: 'arduino-learn-more',
};
}
}

View File

@ -154,6 +154,25 @@ export namespace ArduinoMenus {
'2_resources',
];
// -- Account
export const ARDUINO_ACCOUNT__CONTEXT = ['arduino-account--context'];
export const ARDUINO_ACCOUNT__CONTEXT__SIGN_IN_GROUP = [
...ARDUINO_ACCOUNT__CONTEXT,
'0_sign_in',
];
export const ARDUINO_ACCOUNT__CONTEXT__LEARN_MORE_GROUP = [
...ARDUINO_ACCOUNT__CONTEXT,
'1_learn_more',
];
export const ARDUINO_ACCOUNT__CONTEXT__GO_TO_GROUP = [
...ARDUINO_ACCOUNT__CONTEXT,
'2_go_to',
];
export const ARDUINO_ACCOUNT__CONTEXT__SIGN_OUT_GROUP = [
...ARDUINO_ACCOUNT__CONTEXT,
'3_sign_out',
];
// -- ROOT SSL CERTIFICATES
export const ROOT_CERTIFICATES__CONTEXT = [
'arduino-root-certificates--context',

View File

@ -119,8 +119,8 @@
.account-icon {
background: url("./account-icon.svg") center center no-repeat;
width: var(--theia-icon-size);
height: var(--theia-icon-size);
width: var(--theia-private-sidebar-icon-size);
height: var(--theia-private-sidebar-icon-size);
border-radius: 50%;
overflow: hidden;
}

View File

@ -0,0 +1,83 @@
import { SidebarBottomMenuWidget as TheiaSidebarBottomMenuWidget } from '@theia/core/lib/browser/shell/sidebar-bottom-menu-widget';
import type { SidebarMenu } from '@theia/core/lib/browser/shell/sidebar-menu-widget';
import type { MenuPath } from '@theia/core/lib/common/menu';
import { nls } from '@theia/core/lib/common/nls';
import {
inject,
injectable,
postConstruct,
} from '@theia/core/shared/inversify';
import * as React from '@theia/core/shared/react';
import { accountMenu } from '../../contributions/account';
import { CreateFeatures } from '../../create/create-features';
@injectable()
export class SidebarBottomMenuWidget extends TheiaSidebarBottomMenuWidget {
@inject(CreateFeatures)
private readonly createFeatures: CreateFeatures;
@postConstruct()
protected init(): void {
this.toDispose.push(
this.createFeatures.onDidChangeSession(() => this.update())
);
}
protected override onClick(
e: React.MouseEvent<HTMLElement, MouseEvent>,
menuPath: MenuPath
): void {
const button = e.currentTarget.getBoundingClientRect();
this.contextMenuRenderer.render({
menuPath,
includeAnchorArg: false,
anchor: {
x: button.left + button.width,
// Bogus y coordinate?
// https://github.com/eclipse-theia/theia/discussions/12170
y: button.top,
},
});
}
protected override render(): React.ReactNode {
return (
<React.Fragment>
{this.menus.map((menu) => this.renderMenu(menu))}
</React.Fragment>
);
}
private renderMenu(menu: SidebarMenu): React.ReactNode {
// Removes the _Settings_ (cog) icon from the left sidebar
if (menu.id === 'settings-menu') {
return undefined;
}
const arduinoAccount = menu.id === accountMenu.id;
const picture =
arduinoAccount && this.createFeatures.session?.account.picture;
const className = typeof picture === 'string' ? undefined : menu.iconClass;
return (
<i
key={menu.id}
className={className}
title={menu.title}
onClick={(e) => this.onClick(e, menu.menuPath)}
onMouseDown={this.onMouseDown}
onMouseOut={this.onMouseOut}
>
{picture && (
<div className="account-icon">
<img
src={picture}
alt={nls.localize(
'arduino/cloud/profilePicture',
'Profile picture'
)}
/>
</div>
)}
</i>
);
}
}

View File

@ -5,7 +5,7 @@ import {
injectable,
postConstruct,
} from '@theia/core/shared/inversify';
import { UserStatus } from './cloud-user-status';
import { CloudStatus } from './cloud-user-status';
import { nls } from '@theia/core/lib/common/nls';
import { CloudSketchbookTreeWidget } from './cloud-sketchbook-tree-widget';
import { AuthenticationClientService } from '../../auth/authentication-client-service';
@ -61,7 +61,7 @@ export class CloudSketchbookCompositeWidget extends BaseSketchbookCompositeWidge
onClick={this.onDidClickCreateNew}
/>
)}
<UserStatus
<CloudStatus
model={
this.cloudSketchbookTreeWidget.model as CloudSketchbookTreeModel
}

View File

@ -14,7 +14,6 @@ import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import { CloudSketchbookTree } from './cloud-sketchbook-tree';
import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model';
import { CloudUserCommands } from '../../auth/cloud-user-commands';
import { ShareSketchDialog } from '../../dialogs/cloud-share-sketch-dialog';
import { CreateApi } from '../../create/create-api';
import {
@ -40,16 +39,6 @@ export const SKETCHBOOKSYNC__CONTEXT__MAIN_GROUP = [
'0_main',
];
export const CLOUD_USER__CONTEXT = ['arduino-cloud-user--context'];
export const CLOUD_USER__CONTEXT__USERNAME = [
...CLOUD_USER__CONTEXT,
'0_username',
];
export const CLOUD_USER__CONTEXT__MAIN_GROUP = [
...CLOUD_USER__CONTEXT,
'1_main',
];
export namespace CloudSketchbookCommands {
export interface Arg {
model: CloudSketchbookTreeModel;
@ -328,52 +317,6 @@ export class CloudSketchbookContribution extends Contribution {
}
);
registry.registerCommand(CloudUserCommands.OPEN_PROFILE_CONTEXT_MENU, {
execute: async (arg) => {
this.toDisposeBeforeNewContextMenu.dispose();
const container = arg.event.target;
if (!container) {
return;
}
this.menuRegistry.registerMenuAction(CLOUD_USER__CONTEXT__MAIN_GROUP, {
commandId: CloudUserCommands.LOGOUT.id,
label: CloudUserCommands.LOGOUT.label,
});
this.toDisposeBeforeNewContextMenu.push(
Disposable.create(() =>
this.menuRegistry.unregisterMenuAction(CloudUserCommands.LOGOUT)
)
);
const placeholder = new PlaceholderMenuNode(
CLOUD_USER__CONTEXT__USERNAME,
arg.username
);
this.menuRegistry.registerMenuNode(
CLOUD_USER__CONTEXT__USERNAME,
placeholder
);
this.toDisposeBeforeNewContextMenu.push(
Disposable.create(() =>
this.menuRegistry.unregisterMenuNode(placeholder.id)
)
);
const options: RenderContextMenuOptions = {
menuPath: CLOUD_USER__CONTEXT,
anchor: {
x: container.getBoundingClientRect().left,
y:
container.getBoundingClientRect().top -
3.5 * container.offsetHeight,
},
args: [arg],
};
this.contextMenuRenderer.render(options);
},
});
this.registerMenus(this.menuRegistry);
}
}

View File

@ -5,7 +5,10 @@ import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model';
import { AuthenticationClientService } from '../../auth/authentication-client-service';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { CloudSketchbookTree } from './cloud-sketchbook-tree';
import { CloudUserCommands } from '../../auth/cloud-user-commands';
import {
CloudUserCommands,
LEARN_MORE_URL,
} from '../../auth/cloud-user-commands';
import { NodeProps } from '@theia/core/lib/browser/tree/tree-widget';
import { TreeNode } from '@theia/core/lib/browser/tree';
import { CompositeTreeNode } from '@theia/core/lib/browser';
@ -13,9 +16,6 @@ import { shell } from '@theia/core/electron-shared/@electron/remote';
import { SketchbookTreeWidget } from '../sketchbook/sketchbook-tree-widget';
import { nls } from '@theia/core/lib/common';
const LEARN_MORE_URL =
'https://docs.arduino.cc/software/ide-v2/tutorials/ide-v2-cloud-sketch-sync';
@injectable()
export class CloudSketchbookTreeWidget extends SketchbookTreeWidget {
@inject(AuthenticationClientService)

View File

@ -5,11 +5,9 @@ import {
} from '@theia/core/lib/common/disposable';
import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model';
import { AuthenticationClientService } from '../../auth/authentication-client-service';
import { CloudUserCommands } from '../../auth/cloud-user-commands';
import { AuthenticationSessionAccountInformation } from '../../../common/protocol/authentication-service';
import { nls } from '@theia/core/lib/common';
export class UserStatus extends React.Component<
export class CloudStatus extends React.Component<
UserStatus.Props,
UserStatus.State
> {
@ -19,7 +17,6 @@ export class UserStatus extends React.Component<
super(props);
this.state = {
status: this.status,
accountInfo: props.authenticationService.session?.account,
refreshing: false,
};
}
@ -29,9 +26,6 @@ export class UserStatus extends React.Component<
window.addEventListener('online', statusListener);
window.addEventListener('offline', statusListener);
this.toDispose.pushAll([
this.props.authenticationService.onSessionDidChange((session) =>
this.setState({ accountInfo: session?.account })
),
Disposable.create(() =>
window.removeEventListener('online', statusListener)
),
@ -73,34 +67,6 @@ export class UserStatus extends React.Component<
onClick={this.onDidClickRefresh}
/>
</div>
<div className="account item flex-line">
<div
title={nls.localize('arduino/cloud/account', 'Account')}
className="account-icon"
style={{ cursor: 'pointer' }}
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
this.props.model.commandRegistry.executeCommand(
CloudUserCommands.OPEN_PROFILE_CONTEXT_MENU.id,
{
event: event.nativeEvent,
username: this.state.accountInfo?.label,
}
);
}}
>
{this.state.accountInfo?.picture && (
<img
src={this.state.accountInfo?.picture}
alt={nls.localize(
'arduino/cloud/profilePicture',
'Profile picture'
)}
/>
)}
</div>
</div>
</div>
);
}
@ -128,7 +94,6 @@ export namespace UserStatus {
}
export interface State {
status: 'connected' | 'offline';
accountInfo?: AuthenticationSessionAccountInformation;
refreshing?: boolean;
}
}

View File

@ -4,6 +4,12 @@
"detail": "Version: {0}\nDate: {1}{2}\nCLI Version: {3}\n\n{4}",
"label": "About {0}"
},
"account": {
"goToCloudEditor": "Go to Cloud Editor",
"goToIoTCloud": "Go to IoT Cloud",
"goToProfile": "Go to Profile",
"menuTitle": "Arduino Cloud"
},
"board": {
"board": "Board{0}",
"boardConfigDialogTitle": "Select Other Board and Port",
@ -83,7 +89,6 @@
"mouseError": "'Mouse' not found. Does your sketch include the line '#include <Mouse.h>'?"
},
"cloud": {
"account": "Account",
"chooseSketchVisibility": "Choose visibility of your Sketch:",
"cloudSketchbook": "Cloud Sketchbook",
"connected": "Connected",