mirror of
https://github.com/balena-io/etcher.git
synced 2025-08-03 08:27:44 +00:00
wip
This commit is contained in:
parent
5b2769d0e9
commit
6dbd425e89
249
lib/gui/app/components/drive-selector2/drive-selector.tsx
Normal file
249
lib/gui/app/components/drive-selector2/drive-selector.tsx
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 resin.io
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Meter } from 'grommet';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { Badge, Heading, Modal, Table } from 'rendition';
|
||||||
|
|
||||||
|
import { getDrives } from '../../models/available-drives';
|
||||||
|
import { getDriveImageCompatibilityStatuses } from '../../modules/drive-constraints';
|
||||||
|
import {
|
||||||
|
deselectDrive,
|
||||||
|
getImage,
|
||||||
|
getSelectedDrives,
|
||||||
|
isDriveSelected,
|
||||||
|
selectDrive,
|
||||||
|
} from '../../models/selection-state';
|
||||||
|
import { subscribe } from '../../models/store';
|
||||||
|
import { ThemedProvider } from '../../styled-components';
|
||||||
|
import { bytesToClosestUnit } from '../../modules/units';
|
||||||
|
|
||||||
|
interface Drive {
|
||||||
|
description: string;
|
||||||
|
device: string;
|
||||||
|
isSystem: boolean;
|
||||||
|
isReadOnly: boolean;
|
||||||
|
progress?: number;
|
||||||
|
size?: number;
|
||||||
|
link?: string;
|
||||||
|
linkCTA?: string;
|
||||||
|
displayName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Image {
|
||||||
|
path: string;
|
||||||
|
size: number;
|
||||||
|
url: string;
|
||||||
|
name: string;
|
||||||
|
supportUrl: string;
|
||||||
|
recommendedDriveSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CompatibilityStatus {
|
||||||
|
type: number;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DriveSelectorProps {
|
||||||
|
unique: boolean; // TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DriveSelectorState {
|
||||||
|
open: boolean;
|
||||||
|
drives: Drive[];
|
||||||
|
image: Image;
|
||||||
|
selectedDrivesCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// TODO: no hardcoded size
|
||||||
|
const modalStyle = {
|
||||||
|
width: '800px',
|
||||||
|
height: '600px',
|
||||||
|
paddingTop: '20px',
|
||||||
|
paddingLeft: '30px',
|
||||||
|
paddingRight: '30px',
|
||||||
|
paddingBottom: '11px',
|
||||||
|
}
|
||||||
|
|
||||||
|
const titleStyle = {
|
||||||
|
color: '#2a506f',
|
||||||
|
}
|
||||||
|
|
||||||
|
const subtitleStyle = {
|
||||||
|
marginLeft: '10px',
|
||||||
|
fontSize: '11px',
|
||||||
|
color: '#5b82a7',
|
||||||
|
};
|
||||||
|
|
||||||
|
const wrapperStyle = {
|
||||||
|
height: '250px',
|
||||||
|
overflowX: 'hidden' as 'hidden',
|
||||||
|
overflowY: 'auto' as 'auto',
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DriveSelector2 extends React.Component<DriveSelectorProps, DriveSelectorState> {
|
||||||
|
private table: React.RefObject<Table<Drive>>;
|
||||||
|
private columns: any; // TODO
|
||||||
|
|
||||||
|
constructor(props: DriveSelectorProps) {
|
||||||
|
super(props);
|
||||||
|
this.columns = [
|
||||||
|
{
|
||||||
|
field: 'description',
|
||||||
|
label: 'Name',
|
||||||
|
} as const,
|
||||||
|
{
|
||||||
|
field: 'size',
|
||||||
|
label: 'Size',
|
||||||
|
render: this.renderSize.bind(this),
|
||||||
|
} as const,
|
||||||
|
{
|
||||||
|
field: 'displayName',
|
||||||
|
label: 'Location',
|
||||||
|
render: this.renderLocation.bind(this),
|
||||||
|
} as const,
|
||||||
|
{
|
||||||
|
field: 'isReadOnly', // We don't use this, but a valid field that is not used in another column is required
|
||||||
|
label: ' ',
|
||||||
|
render: this.renderBadges.bind(this),
|
||||||
|
} as const,
|
||||||
|
];
|
||||||
|
this.state = {
|
||||||
|
drives: getDrives(),
|
||||||
|
selectedDrivesCount: getSelectedDrives().length,
|
||||||
|
image: getImage(),
|
||||||
|
open: true,
|
||||||
|
};
|
||||||
|
this.table = React.createRef();
|
||||||
|
subscribe(() => {
|
||||||
|
const drives: Drive[] = getDrives();
|
||||||
|
for (let i = 0; i < drives.length; i++) {
|
||||||
|
drives[i] = {...drives[i]};
|
||||||
|
}
|
||||||
|
const selected = drives.filter(d => isDriveSelected(d.device));
|
||||||
|
this.setState({
|
||||||
|
drives,
|
||||||
|
selectedDrivesCount: selected.length,
|
||||||
|
image: getImage(),
|
||||||
|
});
|
||||||
|
if (this.table.current != null) {
|
||||||
|
this.table.current.setRowSelection(selected);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderSize(size: number) {
|
||||||
|
if (size) {
|
||||||
|
return bytesToClosestUnit(size);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderLocation(displayName: string, drive: Drive) {
|
||||||
|
const result: Array<string | JSX.Element> = [displayName];
|
||||||
|
if (drive.link && drive.linkCTA) {
|
||||||
|
result.push(<a href={drive.link}>{drive.linkCTA}</a>);
|
||||||
|
}
|
||||||
|
return <React.Fragment>{result}</React.Fragment>;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderBadges(_value: any, row: Drive) {
|
||||||
|
const result = [];
|
||||||
|
if (row.progress !== undefined) {
|
||||||
|
result.push(<Meter
|
||||||
|
size="small"
|
||||||
|
thickness="xxsmall"
|
||||||
|
values={
|
||||||
|
[
|
||||||
|
{
|
||||||
|
value: row.progress,
|
||||||
|
label: row.progress + '%',
|
||||||
|
color: '#2297de',
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
/>);
|
||||||
|
}
|
||||||
|
result.push(...getDriveImageCompatibilityStatuses(row, this.state.image).map((status: CompatibilityStatus) => { // TODO: badge color
|
||||||
|
return <Badge xsmall>{status.message}</Badge>
|
||||||
|
}))
|
||||||
|
// TODO: drive contains source mountpoint
|
||||||
|
// TODO: large drive
|
||||||
|
return <React.Fragment>{result}</React.Fragment>;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderTbodyPrefix() {
|
||||||
|
if (this.state.drives.length === 0) {
|
||||||
|
return <tr>
|
||||||
|
<td
|
||||||
|
colSpan={this.columns.length}
|
||||||
|
style={{ textAlign: 'center' }}
|
||||||
|
>
|
||||||
|
<b>Connect a drive</b>
|
||||||
|
<div>No removable drive detected.</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
console.log('render', this.state.drives.map(d => d.device));
|
||||||
|
if (this.state.open) {
|
||||||
|
return <ThemedProvider>
|
||||||
|
<Modal
|
||||||
|
titleElement={
|
||||||
|
<Heading.h3 style={titleStyle}>
|
||||||
|
Available targets
|
||||||
|
<span style={subtitleStyle}>
|
||||||
|
{this.state.drives.length} found
|
||||||
|
</span>
|
||||||
|
</Heading.h3>
|
||||||
|
}
|
||||||
|
action={`Select (${this.state.selectedDrivesCount})`}
|
||||||
|
style={modalStyle}
|
||||||
|
done={() => {this.setState({open: false})}}
|
||||||
|
>
|
||||||
|
<div style={wrapperStyle}>
|
||||||
|
<Table<Drive>
|
||||||
|
ref={this.table}
|
||||||
|
rowKey='device'
|
||||||
|
onCheck={this.onCheck}
|
||||||
|
columns={this.columns}
|
||||||
|
data={this.state.drives}
|
||||||
|
tbodyPrefix={this.renderTbodyPrefix()}
|
||||||
|
>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</ThemedProvider>
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onCheck(checkedDrives: Drive[]): void {
|
||||||
|
const checkedDevices = checkedDrives.map(d => d.device);
|
||||||
|
for (const drive of getDrives()) {
|
||||||
|
if (checkedDevices.indexOf(drive.device) !== -1) {
|
||||||
|
selectDrive(drive.device);
|
||||||
|
} else {
|
||||||
|
deselectDrive(drive.device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
lib/gui/app/components/drive-selector2/index.ts
Normal file
28
lib/gui/app/components/drive-selector2/index.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 resin.io
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as angular from 'angular';
|
||||||
|
import { react2angular } from 'react2angular';
|
||||||
|
|
||||||
|
import { DriveSelector2 } from './drive-selector.tsx';
|
||||||
|
|
||||||
|
const MODULE_NAME = 'Etcher.Components.DriveSelector2'
|
||||||
|
|
||||||
|
angular
|
||||||
|
.module(MODULE_NAME, [])
|
||||||
|
.component('driveSelector2', react2angular(DriveSelector2))
|
||||||
|
|
||||||
|
export = MODULE_NAME;
|
Loading…
x
Reference in New Issue
Block a user