Designing Cabana v1.1.2 (#83)

* developing v1.1 design with scrollable sticky columns

* add font-awesome fonts, import styles

* remove and ignore compiled src css

* styling legend entries, packet list, light meta styling

* refactor and style signal packet list, add signal id to field labels, re-add remove signal, re-add part selector
main
Andrew Valish 2017-07-19 22:40:20 -07:00 committed by GitHub
parent 349f03fecf
commit 03daabad0b
26 changed files with 3607 additions and 870 deletions

2
.gitignore vendored
View File

@ -4,6 +4,8 @@
/node_modules
# stylesheets
/src/*.css
/src/*.css.map
/src/**/**/*.css
/src/**/**/*.css.map

View File

@ -36,6 +36,7 @@ export default class CanExplorer extends Component {
super(props);
this.state = {
messages: {},
selectedMessages: [],
route: {},
canFrameOffset: -1,
firstCanTime: 0,
@ -75,6 +76,7 @@ export default class CanExplorer extends Component {
this.onMessageSelected = this.onMessageSelected.bind(this);
this.onMessageUnselected = this.onMessageUnselected.bind(this);
this.initCanData = this.initCanData.bind(this);
this.updateSelectedMessages = this.updateSelectedMessages.bind(this);
}
componentWillMount() {
@ -342,80 +344,102 @@ export default class CanExplorer extends Component {
this.setState({seekTime, seekIndex, selectedMessage: msgKey});
}
updateSelectedMessages(selectedMessages) {
this.setState({selectedMessages});
}
onMessageUnselected(msgKey) {
this.setState({selectedMessage: null});
}
loginWithGithub() {
return <a href={GithubAuth.authorizeUrl(this.state.route.fullname || '')}>Log in with Github</a>
return (
<a href={GithubAuth.authorizeUrl(this.state.route.fullname || '')}>
<i className='fa fa-github'></i>
<span> Log in with Github</span>
</a>
)
}
render() {
return (<div>
{this.state.isLoading ?
<LoadingBar
isLoading={this.state.isLoading}
/> : null}
<div className='cabana'>
<Meta url={this.state.route.url}
messages={this.state.messages}
currentParts={this.state.currentParts}
partsCount={this.state.route.proclog || 0}
onMessageSelected={this.onMessageSelected}
onMessageUnselected={this.onMessageUnselected}
showLoadDbc={this.showLoadDbc}
showSaveDbc={this.showSaveDbc}
dbcFilename={this.state.dbcFilename}
dbcLastSaved={this.state.dbcLastSaved}
onPartChange={this.onPartChange}
showEditMessageModal={this.showEditMessageModal}
dongleId={this.props.dongleId}
name={this.props.name}
route={this.state.route}
seekTime={this.state.seekTime}
maxByteStateChangeCount={this.state.maxByteStateChangeCount}
githubAuthToken={this.props.githubAuthToken}
loginWithGithub={this.loginWithGithub()}
isDemo={this.props.isDemo}
/>
{this.state.route.url ?
<Explorer
url={this.state.route.url}
messages={this.state.messages}
selectedMessage={this.state.selectedMessage}
onConfirmedSignalChange={this.onConfirmedSignalChange}
onSeek={this.onSeek}
onUserSeek={this.onUserSeek}
canFrameOffset={this.state.canFrameOffset}
firstCanTime={this.state.firstCanTime}
seekTime={this.state.seekTime}
seekIndex={this.state.seekIndex}
currentParts={this.state.currentParts}
partsLoaded={this.state.partsLoaded}
autoplay={this.props.autoplay}
/>
: null}
return (
<div id="cabana">
{this.state.isLoading ?
<LoadingBar
isLoading={this.state.isLoading}
/> : null}
<div className='cabana-header'>
<a className='cabana-header-logo' href=''>Comma Cabana</a>
<div className='cabana-header-account'>
{this.props.githubAuthToken ?
<p className={css(Styles.githubAuth)}>GitHub Authenticated</p>
: this.loginWithGithub()
}
</div>
</div>
<div className='cabana-window'>
<Meta url={this.state.route.url}
messages={this.state.messages}
selectedMessages={this.state.selectedMessages}
updateSelectedMessages={this.updateSelectedMessages}
showEditMessageModal={this.showEditMessageModal}
currentParts={this.state.currentParts}
onMessageSelected={this.onMessageSelected}
onMessageUnselected={this.onMessageUnselected}
showLoadDbc={this.showLoadDbc}
showSaveDbc={this.showSaveDbc}
dbcFilename={this.state.dbcFilename}
dbcLastSaved={this.state.dbcLastSaved}
dongleId={this.props.dongleId}
name={this.props.name}
route={this.state.route}
seekTime={this.state.seekTime}
maxByteStateChangeCount={this.state.maxByteStateChangeCount}
isDemo={this.props.isDemo}
/>
{this.state.route.url ?
<Explorer
url={this.state.route.url}
messages={this.state.messages}
selectedMessage={this.state.selectedMessage}
onConfirmedSignalChange={this.onConfirmedSignalChange}
onSeek={this.onSeek}
onUserSeek={this.onUserSeek}
canFrameOffset={this.state.canFrameOffset}
firstCanTime={this.state.firstCanTime}
seekTime={this.state.seekTime}
seekIndex={this.state.seekIndex}
currentParts={this.state.currentParts}
partsLoaded={this.state.partsLoaded}
autoplay={this.props.autoplay}
showEditMessageModal={this.showEditMessageModal}
onPartChange={this.onPartChange}
route={this.state.route}
partsCount={this.state.route.proclog || 0}
/>
: null}
</div>
{this.state.showLoadDbc ? <LoadDbcModal
onDbcSelected={this.onDbcSelected}
onCancel={this.hideLoadDbc}
openDbcClient={this.openDbcClient}
loginWithGithub={this.loginWithGithub()}
/> : null}
{this.state.showSaveDbc ? <SaveDbcModal
dbc={this.state.dbc}
sourceDbcFilename={this.state.dbcFilename}
onDbcSaved={this.onDbcSaved}
onCancel={this.hideSaveDbc}
openDbcClient={this.openDbcClient}
hasGithubAuth={this.props.githubAuthToken !== null}
loginWithGithub={this.loginWithGithub()} /> : null}
{this.state.showEditMessageModal ?
<EditMessageModal
onCancel={this.hideEditMessageModal}
onMessageFrameEdited={this.onMessageFrameEdited}
message={this.state.messages[this.state.editMessageModalMessage]} /> : null}
</div>);
{this.state.showLoadDbc ? <LoadDbcModal
onDbcSelected={this.onDbcSelected}
onCancel={this.hideLoadDbc}
openDbcClient={this.openDbcClient}
loginWithGithub={this.loginWithGithub()}
/> : null}
{this.state.showSaveDbc ? <SaveDbcModal
dbc={this.state.dbc}
sourceDbcFilename={this.state.dbcFilename}
onDbcSaved={this.onDbcSaved}
onCancel={this.hideSaveDbc}
openDbcClient={this.openDbcClient}
hasGithubAuth={this.props.githubAuthToken !== null}
loginWithGithub={this.loginWithGithub()} /> : null}
{this.state.showEditMessageModal ?
<EditMessageModal
onCancel={this.hideEditMessageModal}
onMessageFrameEdited={this.onMessageFrameEdited}
message={this.state.messages[this.state.editMessageModalMessage]} /> : null}
</div>
);
}
}

View File

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import ReactList from 'react-list';
import { StyleSheet, css } from 'aphrodite/no-important';
import cx from 'classnames';
import { formatMsgDec, formatMsgHex } from '../models/can-msg-fmt';
import { elementWiseEquals } from '../utils/array';
import Images from '../styles/images';
@ -29,14 +30,16 @@ export default class CanLog extends Component {
length: 0,
expandedMessages: [],
messageHeights: [],
expandAllChecked: false
allPacketsExpanded: false
}
this.messageRow = this.messageRow.bind(this);
this.renderLogListItemMessage = this.renderLogListItemMessage.bind(this);
this.addDisplayedMessages = this.addDisplayedMessages.bind(this);
this.renderMessage = this.renderMessage.bind(this);
this.renderTable = this.renderTable.bind(this);
this.renderLogListItem = this.renderLogListItem.bind(this);
this.renderLogList = this.renderLogList.bind(this);
this.onExpandAllChanged = this.onExpandAllChanged.bind(this);
this.toggleExpandAllPackets = this.toggleExpandAllPackets.bind(this);
this.toggleSignalPlot = this.toggleSignalPlot.bind(this);
}
componentWillReceiveProps(nextProps) {
@ -97,110 +100,113 @@ export default class CanLog extends Component {
} else return value;
}
expandedMessage(msg) {
isMessageExpanded(msg) {
return this.state.expandedMessages.indexOf(msg.time) !== -1;
}
toggleSignalPlot(msg, name, plotted) {
if (!plotted) {
this.props.onSignalPlotPressed(msg, name);
} else {
this.props.onSignalUnplotPressed(msg, name);
}
}
toggleExpandPacketSignals(msg) {
const msgIsExpanded = this.state.allPacketsExpanded || this.isMessageExpanded(msg);
const msgHasSignals = Object.keys(msg.signals).length > 0;
if (msgIsExpanded && msgHasSignals) {
this.setState({expandedMessages: this.state.expandedMessages
.filter((expMsgTime) => expMsgTime !== msg.time)})
} else if (msgHasSignals) {
this.setState({expandedMessages: this.state.expandedMessages.concat([msg.time])})
this.props.onMessageExpanded();
} else { return; }
}
renderLogListItemSignals(msg) {
const { message } = this.props;
return (
<div className='signal-log' key={msg.time + '-expanded'}>
<div className={css(Styles.col)}>
<div className={css(Styles.signalCol)}>
<table className={css(Styles.signalTable)}>
<tbody>
{Object.entries(msg.signals).map(([name, value]) => {
return [name, value, this.isSignalPlotted(this.props.message.id, name)]
}).map(([name, value, isPlotted]) => {
const signal = msg.signals[name];
const {unit} = signal;
return (<tr key={name}>
<td>{name}</td>
<td><p className={css(Styles.signalValue)}>{this.signalValuePretty(signal, value)} {unit}</p></td>
{isPlotted ?
<td className={css(Styles.pointerUnderlineHover)}
onClick={() => {this.props.onSignalUnplotPressed(this.props.message.id, name)}}>[unplot]</td>
:
<td className={css(Styles.pointerUnderlineHover)}
onClick={() => {this.props.onSignalPlotPressed(this.props.message.id, name)}}>[plot]</td>
}
</tr>);
})}
</tbody>
</table>
</div>
<div className='signals-log-list-signals'>
{ Object.entries(msg.signals).map(([name, value]) => {
return [name, value, this.isSignalPlotted(message.id, name)]
}).map(([name, value, isPlotted]) => {
const signal = msg.signals[name];
const plottedButtonClass = isPlotted ? null : 'button--alpha';
const plottedButtonText = isPlotted ? 'Hide Plot' : 'Show Plot';
const { unit } = signal;
return (
<div key={ name } className='signals-log-list-signal'>
<div className='signals-log-list-signal-message'>
<span>{ name }</span>
</div>
<div className='signals-log-list-signal-value'>
<span>{ this.signalValuePretty(signal, value) } { unit }</span>
</div>
<div className='signals-log-list-signal-action'
onClick={ () => { this.toggleSignalPlot(this.props.message.id, name, isPlotted) } }>
<button className={ cx('button--tiny', plottedButtonClass) }>
<span>{ plottedButtonText }</span>
</button>
</div>
</div>
);
})}
</div>
</div>
)
}
isMessageExpanded(msg) {
return this.state.expandedMessages.indexOf(msg.time) !== -1;
renderLogListItemMessage(msg, key) {
const msgIsExpanded = this.state.allPacketsExpanded || this.isMessageExpanded(msg);
const msgHasSignals = Object.keys(msg.signals).length > 0;
const hasSignalsClass = msgHasSignals ? 'has-signals' : null;
const expandedClass = msgIsExpanded ? 'is-expanded' : null;
const row = (
<div key={key} className={cx('signals-log-list-item', hasSignalsClass, expandedClass)}>
<div className='signals-log-list-item-header'
onClick={ () => { this.toggleExpandPacketSignals(msg) } }>
<div className='signals-log-list-time'>
<span>{msg.relTime.toFixed(3)}</span>
</div>
<div className='signals-log-list-message'>
<span>{(this.props.message.frame ? this.props.message.frame.name : null) || this.props.message.id}</span>
</div>
<div className='signals-log-list-bytes'>
<span>{msg.hexData}</span>
</div>
</div>
<div className='signals-log-list-item-body'>
{ msgIsExpanded ? this.renderLogListItemSignals(msg) : null}
</div>
</div>
);
return row;
}
messageRow(msg, key) {
const msgIsExpanded = this.state.expandAllChecked || this.isMessageExpanded(msg);
const hasSignals = Object.keys(msg.signals).length > 0;
const rowStyle = (hasSignals ? Styles.pointer : null);
const row = [<div key={key}
className={css(Styles.row, Styles.messageRow, rowStyle)}
onClick={() => {
if(!hasSignals) return;
if(msgIsExpanded) {
this.collapseMessage(msg);
} else {
this.expandMessage(msg);
}
}}>
{hasSignals ?
(msgIsExpanded ? <div className={css(Styles.col, Styles.arrowCell)}>{<Images.downArrow styles={[Styles.arrow]} />}</div>
:
<div className={css(Styles.col, Styles.arrowCell)}>{<Images.rightArrow styles={[Styles.arrow]} />}</div>
)
: <div className={css(Styles.col)}></div>
}
<div className={css(Styles.col, Styles.timefieldCol)}>
{msg.relTime.toFixed(3)}
</div>
<div className={css(Styles.col,
Styles.messageCol)}>
{(this.props.message.frame ? this.props.message.frame.name : null) || this.props.message.id}
</div>
<div className={css(Styles.col,
Styles.messageCol,
Styles.hex)}>
{msg.hexData}
</div>
</div>];
renderLogListItem(index, key) {
let offset = this.props.messageIndex;
if(offset === 0 && this.props.segmentIndices.length === 2) {
offset = this.props.segmentIndices[0];
}
if(msgIsExpanded) {
row.push(this.expandedMessage(msg));
}
return row;
return this.renderLogListItemMessage(this.props.message.entries[offset + index], key);
}
renderMessage(index, key) {
let offset = this.props.messageIndex;
if(offset === 0 && this.props.segmentIndices.length === 2) {
offset = this.props.segmentIndices[0];
}
return this.messageRow(this.props.message.entries[offset + index], key);
}
renderTable(items, ref) {
return (<div className={css(Styles.root)}>
<div className={css(Styles.row)}>
<div className={css(Styles.col, Styles.dropdownCol)}>&nbsp;</div>
<div className={css(Styles.col, Styles.timefieldCol)}>Time (s)</div>
<div className={css(Styles.col)}>
Message
</div>
<div className={css(Styles.col)}>
Bytes
</div>
renderLogList(items, ref) {
return (
<div className='signals-log-list'>
<div className='signals-log-list-header'>
<div className='signals-log-list-time'>Time</div>
<div className='signals-log-list-message'>Message</div>
<div className='signals-log-list-bytes'>Bytes</div>
</div>
<div className={css(Styles.tableRowGroup)}
<div className='signals-log-list-items'
ref={ref}>
{items}
{items}
</div>
</div>)
</div>
)
}
listLength() {
@ -218,25 +224,36 @@ export default class CanLog extends Component {
}
onExpandAllChanged(e) {
this.setState({expandAllChecked: e.target.checked});
this.setState({allPacketsExpanded: e.target.checked});
}
toggleExpandAllPackets() {
this.setState({allPacketsExpanded: !this.state.allPacketsExpanded});
}
render() {
return <div className='cabana-explorer-signals-log'>
<p>Expand all messages:
<input type="checkbox"
checked={this.state.expandAllChecked}
onChange={this.onExpandAllChanged} />
</p>
<ReactList
itemRenderer={this.renderMessage}
itemsRenderer={this.renderTable}
length={this.listLength()}
pageSize={50}
updateWhenThisValueChanges={this.props.messageIndex}
type='variable' />
</div>;
let expandAllText = this.state.allPacketsExpanded ? 'Collapse All' : 'Expand All';
let expandAllClass = this.state.allPacketsExpanded ? null : 'button--alpha';
return (
<div className='cabana-explorer-signals-log'>
<div className='cabana-explorer-signals-log-header'>
<strong>Message Packets</strong>
<button className={cx('button--tiny', expandAllClass)}
onClick={this.toggleExpandAllPackets}>
{expandAllText}
</button>
</div>
<div className='cabana-explorer-signals-log-body'>
<ReactList
itemRenderer={this.renderLogListItem}
itemsRenderer={this.renderLogList}
length={this.listLength()}
pageSize={50}
updateWhenThisValueChanges={this.props.messageIndex}
type='variable' />
</div>
</div>
);
}
}

View File

@ -5,23 +5,91 @@
*/
@import '../../styles/_global/colors';
@import '../../styles/_global/all';
.cabana {
#cabana {
display: flex;
height: 100%;
flex-direction: column;
flex-grow: 1;
}
.cabana-header {
background: $color-grey-90;
color: #fff;
height: 68px;
padding: 22px;
&-logo {
color: #fff;
font-size: 20px;
font-weight: 600;
text-decoration: none;
pointer-events: none; // disable actual click
}
&-account {
float: right;
a {
color: #fff;
font-size: 14px;
font-weight: 600;
text-decoration: none;
}
}
}
.cabana-window {
display: flex;
flex-grow: 1;
overflow: hidden;
}
.cabana-explorer {
display: flex;
flex: 1;
&-header {
}
&-signals {
flex: 1;
height: 100vh;
overflow-x: hidden;
overflow-y: scroll;
padding: 3%;
&-header {}
&-controller {}
max-height: 100vh;
&-wrapper {
display: flex;
height: 100%;
flex-direction: column;
}
&-header {
border-bottom: 1px solid rgba(0,0,0,.2);
display: flex;
flex-direction: row;
min-height: 89px;
padding: 5%;
h6,
h3 {
margin: 0;
}
h6 {
font-size: 13px;
font-weight: normal;
padding-bottom: 5px;
text-transform: uppercase;
}
&-context {
flex: 4;
}
&-action {
flex: 1;
button {
width: 100%;
}
}
}
&-window {
overflow-x: hidden;
overflow-y: scroll;
height: 100%;
}
&-controller {
padding: 5%;
}
&-matrix {
table {
border: 1px solid rgba(0,0,0,.2);
@ -32,19 +100,41 @@
}
}
&-log {
background: $color-grey-10;
border: 1px solid rgba(0,0,0,.2);
border-radius: 5px;
padding: 3%;
border-top: 1px solid $color-grey-30;
padding: 0 5%;
&-header {
padding: 5% 0;
strong {
color: $color-grey-70;
font-size: 14px;
}
button {
float: right;
text-transform: uppercase;
}
}
&-body {
background: $color-grey-10;
border: 1px solid rgba(0,0,0,.2);
border-radius: 5px;
}
&-list {
padding: 3%;
}
}
}
&-visuals {
background: $color-grey-10;
flex: 1;
height: 100vh;
max-height: 100vh;
overflow-x: hidden;
overflow-y: scroll;
padding: 1.5%;
padding-left: 0;
padding: 1.5% 3%;
&-header {
border-bottom: 1px solid rgba(0,0,0,.1);
margin-bottom: 2%;
padding: 2% 0;
}
&-camera {
border: 5px solid #000;
border-radius: 5px;
@ -66,11 +156,13 @@
}
}
&-plots {
border: 1px solid $color-grey-30;
border: 1px solid $color-grey-40;
border-radius: 5px;
margin: 3% 0;
}
&-plot {
background: #fff;
border-radius: 5px;
padding: 1.5% 3%;
&:first-child,
&:last-child {

View File

@ -5,15 +5,99 @@
*/
@import '../../styles/_global/colors';
@import '../../styles/_global/all';
.cabana-meta {
background: $color-grey-20;
display: flex;
flex: .5;
flex-direction: column;
height: 100vh;
max-height: 100vh;
overflow-y: scroll;
&-header {
border-bottom: 1px solid rgba(0,0,0,.1);
flex-direction: row;
padding: 22px;
@extend %clearfix;
&-label {
display: block;
font-size: 13px;
padding-bottom: 1%;
text-transform: uppercase;
}
&-filename {
display: block;
}
&-last-saved {
color: $color-grey-60;
font-size: 13px;
margin-bottom: -5px;
padding-top: 3px;
}
&-actions {
padding: 18px 0;
}
&-action {
float: left;
padding: 0 1%;
width: (100%/3);
&:first-child {
padding-left: 0;
}
&:last-child {
padding-right: 0;
}
button {
width: 100%;
}
}
}
&-messages {
display: flex;
flex-direction: column;
height: 100%;
padding-left: 11px;
&-header {
height: 34px;
padding: 22px 11px;
}
&-window {
background: $color-grey-30;
border-radius: 5px;
display: flex;
flex-direction: column;
height: 100%;
}
&-filter {
border-bottom: 1px solid rgba(0,0,0,.05);
height: 64px;
padding: 11px;
}
&-list {
height: 100%;
overflow-x: hidden;
overflow-y: scroll;
&-item {
cursor: pointer;
position: relative;
transition-duration: .1s;
&:not(.is-selected):hover {
background: rgba(0,0,0,.05);
}
&.is-selected {
background: $color-grey-40;
font-weight: 600;
}
}
}
}
table {
border-spacing: 0;
font-size: 12px;
width: 100%;
thead tr td:first-child,
tbody tr td:first-child {
padding-left: 11px;
}
}
}

View File

@ -131,7 +131,7 @@ const Styles = StyleSheet.create({
position: 'relative'
},
selectedPart: {
backgroundColor: 'black',
backgroundColor: '#6f6f6f',
height: '100%',
position: 'absolute',
},

View File

@ -5,38 +5,112 @@
*/
@import '../../styles/_global/index';
@import '../../styles/_global/all';
.cabana-explorer-signals-legend {
background: $color-grey-10;
border: 1px solid rgba(0,0,0,.2);
border-radius: 5px;
margin: 5% 0;
padding: 2%;
margin-top: 5%;
}
.signals-legend-entry {
$entry-header-height: 42px;
$entry-header-icon-size: 28px;
overflow: auto;
transition-duration: .1s;
padding: 0 2%;
&:not(:last-child) {
border-bottom: 1px solid rgba(0,0,0,.05);
}
&-header {
cursor: pointer;
line-height: 42px;
&-name {
float: left;
width: 70%;
&:hover {
background: rgba(0,0,0,.02);
}
&.is-expanded {
.signals-legend-entry-header {
border-bottom-color: rgba(0,0,0,.05);
height: $entry-header-height + 3px;
&:before {
left: 0;
padding-top: 5px;
top: -3px;
transform: rotate(90deg);
}
}
&-plotted {
.signals-legend-entry-body {
display: block;
}
}
&-header {
border-bottom: 1px solid transparent;
cursor: pointer;
height: $entry-header-height;
line-height: $entry-header-height;
position: relative;
transition-duration: .2s;
&:before {
color: $color-grey-80;
content: $fa-var-chevron-right;
display: block;
font-size: 10px;
font-family: 'FontAwesome';
height: $entry-header-height;
left: -2px;
top: 0;
pointer-events: none;
position: absolute;
transition-duration: .1s;
text-align: center;
width: $entry-header-icon-size;
}
&-name {
color: $color-grey-80;
font-size: 14px;
float: left;
padding-left: $entry-header-icon-size;
width: 75%;
}
&-action {
float: left;
text-align: right;
width: 30%;
padding-right: 2%;
width: 25%;
button {
display: inline-block;
width: 100%;
}
}
}
&-body {
display: none;
padding: 2%;
}
&.is-expanded {
&-form {
&-field {
min-height: $input-height;
padding: 1px 0;
@extend %clearfix;
&:not(:last-child) {
border-bottom: 1px solid rgba(0,0,0,.05);
margin-bottom: 3px;
}
label {
color: $color-grey-80;
cursor: pointer;
float: left;
font-size: 12px;
font-weight: 600;
line-height: $input-height--small;
width: 40%;
}
input,
select {
float: left;
width: 60%;
}
}
&-remove {
padding: 2% 0;
}
}
}

View File

@ -3,6 +3,7 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import { StyleSheet, css } from 'aphrodite/no-important';
import cx from 'classnames';
import Signal from '../models/can/signal';
import DbcUtils from '../utils/dbc';
@ -150,6 +151,7 @@ export default class SignalLegendEntry extends Component {
this.toggleEditing = this.toggleEditing.bind(this);
this.updateField = this.updateField.bind(this);
this.onNameChange = this.onNameChange.bind(this);
this.toggleSignalPlot = this.toggleSignalPlot.bind(this);
}
componentWillReceiveProps(nextProps) {
@ -159,12 +161,16 @@ export default class SignalLegendEntry extends Component {
}
}
field(field, title, valueCol) {
field(field, title, valueCol, signal) {
const value = this.props.signal[field];
let titleCol = <td>{title}</td>;
return <tr key={field}>{titleCol}<td>{valueCol}</td></tr>;
return (
<div key={field} className='form-field form-field--small'>
<label htmlFor={`${signal}_${field}`}>{title}</label>
{valueCol}
</div>
)
}
updateField(fieldSpec, value) {
@ -187,7 +193,7 @@ export default class SignalLegendEntry extends Component {
this.props.onSignalChange(signalCopy, signal);
}
numberField(fieldSpec) {
renderNumberField(fieldSpec, signal) {
const {field, title, options} = fieldSpec;
let valueCol;
@ -197,39 +203,40 @@ export default class SignalLegendEntry extends Component {
let num = Number(value);
value = (isNaN(num) ? '' : num);
}
valueCol = <input type="number"
value={value}
onChange={(e) => {
let {value} = e.target;
this.updateField(fieldSpec, value);
}}/>;
valueCol = (
<input id={`${signal}_${field}`}
type="number"
value={value}
onChange={(e) => {this.updateField(fieldSpec, e.target)}
}/>
);
} else {
let value = this.props.signal[field];
valueCol = <span>{value}</span>;
}
return this.field(field, title, valueCol);
return this.field(field, title, valueCol, signal);
}
stringField(fieldSpec) {
renderStringField(fieldSpec, signal) {
const {field, title} = fieldSpec;
let valueCol;
if(this.state.isEditing) {
valueCol = <input type="text"
value={this.state.signalEdited[field] || ''}
onChange={(e) => {
this.updateField(fieldSpec, e.target.value)
}}
/>;
valueCol = (
<input id={`${signal}_${field}`}
type="text"
value={this.state.signalEdited[field] || ''}
onChange={(e) => {
this.updateField(fieldSpec, e.target.value)
}}
/>);
} else {
valueCol = <span>{this.props.signal[field]}</span>;
}
return this.field(field, title, valueCol);
return this.field(field, title, valueCol, signal);
}
optionField(fieldSpec) {
renderOptionField(fieldSpec, signal) {
let valueCol;
const {field, title} = fieldSpec;
const {options, optionValues} = fieldSpec.options;
@ -243,24 +250,20 @@ export default class SignalLegendEntry extends Component {
value={optionValues[opt]}>{opt}</option>
}
);
valueCol = <select
valueCol = (
<select id={`${signal}_${field}`}
defaultValue={this.state.signalEdited[field]}
onChange={(e) => {
this.updateField(fieldSpec, e.target.value === "true")
}}>
{optionEles}
</select>;
onChange={
(e) => { this.updateField(fieldSpec, e.target.value === "true") }
}>
{optionEles}
</select>
);
} else {
valueCol = <span>{valueOptions[this.props.signal[field]]}</span>;
}
return this.field(field, title, valueCol);
}
removeSignal(signal) {
return (<tr>
<td onClick={() => {this.props.onSignalRemove(signal)}}>Remove Signal</td>
</tr>);
return this.field(field, title, valueCol, signal);
}
titleForField(field, signal) {
@ -271,14 +274,14 @@ export default class SignalLegendEntry extends Component {
}
}
fieldNode(field, signal) {
renderFieldNode(field, signal) {
field.title = this.titleForField(field, signal);
if(field.type === 'number') {
return this.numberField(field);
return this.renderNumberField(field, signal);
} else if(field.type === 'option') {
return this.optionField(field);
return this.renderOptionField(field, signal);
} else if(field.type === 'string') {
return this.stringField(field);
return this.renderStringField(field, signal);
}
}
@ -304,8 +307,6 @@ export default class SignalLegendEntry extends Component {
this.props.onSignalChange(signalCopy, signal);
} else {
signalEdited = signalCopy;
// Show plot when expanding
this.props.onSignalPlotChange(true, signal.name)
}
// Expand and enable signal editing
@ -320,57 +321,62 @@ export default class SignalLegendEntry extends Component {
}
onNameChange(e) {
// this.updateField('name', e.target.value);
this.setState({nameEdited: e.target.value})
}
expandedSignal(signal) {
renderSignalForm(signal) {
const startBitTitle = signal.isLittleEndian ? 'Least significant bit' : 'Most significant bit';
return (
<table>
<tr key={signal.uid + '-expanded'} className=''>
<td colSpan="3">
<table>
<tbody>
{SignalLegendEntry.fields.map((field) => this.fieldNode(field, signal))}
{this.removeSignal(signal)}
</tbody>
</table>
</td>
</tr>
</table>
<div className='signals-legend-entry-form'>
{SignalLegendEntry.fields.map((field) => {
return (
<div className='signals-legend-entry-form-field'>
{this.renderFieldNode(field, signal.name)}
</div>
)
})}
<div className='signals-legend-entry-form-remove'>
<button className='button--tiny button--alpha'
onClick={ () => this.props.onSignalRemove(signal) }>Remove Signal</button>
</div>
</div>
);
}
toggleSignalPlot(e) {
const {signal, isPlotted} = this.props;
e.preventDefault();
this.props.onSignalPlotChange(!isPlotted, signal.name);
}
render() {
const {isExpanded, isEditing, signalEdited, nameEdited} = this.state;
const {signal, highlightedStyle, plottedSignals, isPlotted} = this.props;
const expandedEntryClass = this.props.isExpanded ? 'is-expanded' : null;
const plottedButtonClass = this.props.isPlotted ? 'button' : 'button--alpha';
const plottedButtonText = this.props.isPlotted ? 'Hide Plot' : 'Show Plot';
return (
<div
className="signals-legend-entry"
onMouseEnter={() => this.props.onSignalHover(signal)}
onMouseLeave={() => this.props.onSignalHoverEnd(signal)}>
<div
className="signals-legend-entry-header"
onClick={this.toggleEditing}>
<div className="signals-legend-entry-header-name">
<span>{this.props.isExpanded ? '\u2193' : '\u2192'}</span>
<strong>{signal.name}</strong>
className={cx('signals-legend-entry', expandedEntryClass)}
onMouseEnter={() => this.props.onSignalHover(signal)}
onMouseLeave={() => this.props.onSignalHoverEnd(signal)}>
<div className="signals-legend-entry-header">
<div
className="signals-legend-entry-header-name"
onClick={this.toggleEditing}>
<strong>{signal.name}</strong>
</div>
<div
className="signals-legend-entry-header-action"
onClick={this.toggleSignalPlot}>
<button className={cx('button--tiny', plottedButtonClass)}>
{plottedButtonText}
</button>
</div>
</div>
<div className="signals-legend-entry-header-plotted">
<span>Plot: </span>
<input type="checkbox"
checked={isPlotted}
onClick={(e) => e.stopPropagation()}
onChange={(e) => {
this.props.onSignalPlotChange(e.target.checked, signal.name)
}}
/>
<div className="signals-legend-entry-body">
{this.props.isExpanded ? this.renderSignalForm(signal) : null}
</div>
</div>
<div className="signals-legend-entry-body">
{this.props.isExpanded ? this.expandedSignal(signal) : null}
</div>
</div>
);
}

View File

@ -6,4 +6,91 @@
*/
@import '../../styles/_global/index';
@import '../../styles/_global/all';
.signals-log-list {
color: $color-grey-80;
font-size: 14px;
&-time {
flex: 1;
}
&-message {
flex: 3;
}
&-bytes {
flex: 2;
}
&-header {
border-bottom: 1px solid rgba(0,0,0,.1);
display: flex;
font-size: 11px;
padding: 2% 3% 2% 7%;
text-transform: uppercase;
}
&-item {
border-color: transparent;
border-style: solid;
border-width: 1px 0;
padding-right: 3%;
position: relative;
transition-duration: .1s;
&:not(.is-expanded):hover {
background: rgba(0,0,0,.02);
}
&:not(.is-expanded):active {
padding-top: 0.5%;
padding-bottom: 0.5%;
}
&.has-signals {
&:before {
color: $color-grey-80;
content: $fa-var-chevron-right;
display: block;
font-family: 'FontAwesome';
font-size: 8px;
line-height: 26px;
text-align: center;
pointer-events: none;
position: absolute;
transition-duration: .1s;
width: 8%;
}
}
&.is-expanded {
background: rgba(0,0,0,.03);
border-color: rgba(0,0,0,.05);
padding-top: 1%;
&:before {
margin-top: -3px;
transform: rotate(90deg);
}
}
&-header {
cursor: pointer;
display: flex;
padding: 1% 0 1% 7%;
}
}
&-signals {}
&-signal {
align-items: center;
border-top: 1px solid rgba(0,0,0,.05);
display: flex;
font-size: 12px;
padding: 1% 0 1% 7%;
&-message {
flex: 3;
}
&-value {
flex: 1;
}
&-action {
flex: 1;
button {
font-size: 10px;
width: 100%
}
}
}
}

View File

@ -2,6 +2,7 @@ import React, {Component} from 'react';
import PropTypes from 'prop-types';
import { StyleSheet, css } from 'aphrodite/no-important';
import Moment from 'moment';
import AddSignals from './AddSignals';
import CanHistogram from './CanHistogram';
@ -13,6 +14,7 @@ import Entries from '../models/can/entries';
import debounce from '../utils/debounce';
import CommonStyles from '../styles/styles';
import Images from '../styles/images';
import PartSelector from './PartSelector';
export default class Explorer extends Component {
static propTypes = {
@ -24,6 +26,8 @@ export default class Explorer extends Component {
firstCanTime: PropTypes.number,
onSeek: PropTypes.func,
autoplay: PropTypes.bool,
onPartChanged: PropTypes.func,
partsCount: PropTypes.number,
};
constructor(props) {
@ -191,6 +195,19 @@ export default class Explorer extends Component {
}
}
timeWindow() {
const {route, currentParts} = this.props;
if(route) {
const partStartOffset = currentParts[0] * 60,
partEndOffset = (currentParts[1] + 1) * 60;
const windowStartTime = Moment(route.start_time).add(partStartOffset, 's').format('HH:mm:ss');
const windowEndTime = Moment(route.start_time).add(partEndOffset, 's').format('HH:mm:ss');
return `${windowStartTime} - ${windowEndTime}`;
} else return '';
}
calcGraphData(msg, signalName) {
if(!msg) return null;
@ -426,33 +443,46 @@ export default class Explorer extends Component {
}
renderExplorerSignals() {
const selectedMessageKey = this.props.selectedMessage;
const selectedMessage = this.props.messages[selectedMessageKey];
const selectedMessageName = selectedMessage.frame !== undefined ? selectedMessage.frame.name : 'undefined';
return (
<div className=''>
<div className='cabana-explorer-signals-wrapper'>
<div
className="cabana-explorer-signals-header"
onClick={this.toggleEditSignals}>
<span>Edit Signals</span>
className='cabana-explorer-signals-header'
onClick={this.toggleEditSignals}>
<div className='cabana-explorer-signals-header-context'>
<h6>Selected Message:</h6>
<h3>{selectedMessageName}</h3>
</div>
<div className='cabana-explorer-signals-header-action'>
<button
className='button--small'
onClick={() => this.props.showEditMessageModal(selectedMessageKey)}>Edit</button>
</div>
</div>
<div className='cabana-explorer-signals-window'>
{this.state.shouldShowAddSignal ?
<AddSignals
onConfirmedSignalChange={this.props.onConfirmedSignalChange}
message={this.props.messages[this.props.selectedMessage]}
onClose={() => {this.setState({shouldShowAddSignal: false})}}
messageIndex={this.props.seekIndex}
onSignalPlotChange={this.onSignalPlotChanged}
plottedSignals={this.state.plottedSignals.filter(
({messageId, signalName}) => messageId === this.props.selectedMessage
).map(({messageId, signalName}) => signalName)
}
/> : null}
<CanLog message={this.props.messages[this.props.selectedMessage]}
messageIndex={this.props.seekIndex}
segmentIndices={this.state.segmentIndices}
plottedSignals={this.state.plottedSignals}
onSignalPlotPressed={this.onSignalPlotPressed}
onSignalUnplotPressed={this.onSignalUnplotPressed}
showAddSignal={this.showAddSignal}
onMessageExpanded={this.onPause} />
</div>
{this.state.shouldShowAddSignal ?
<AddSignals
onConfirmedSignalChange={this.props.onConfirmedSignalChange}
message={this.props.messages[this.props.selectedMessage]}
onClose={() => {this.setState({shouldShowAddSignal: false})}}
messageIndex={this.props.seekIndex}
onSignalPlotChange={this.onSignalPlotChanged}
plottedSignals={this.state.plottedSignals.filter(
({messageId, signalName}) => messageId === this.props.selectedMessage
).map(({messageId, signalName}) => signalName)
}
/> : null}
<CanLog message={this.props.messages[this.props.selectedMessage]}
messageIndex={this.props.seekIndex}
segmentIndices={this.state.segmentIndices}
plottedSignals={this.state.plottedSignals}
onSignalPlotPressed={this.onSignalPlotPressed}
onSignalUnplotPressed={this.onSignalUnplotPressed}
showAddSignal={this.showAddSignal}
onMessageExpanded={this.onPause} />
</div>
)
}
@ -464,7 +494,7 @@ export default class Explorer extends Component {
<CanGraph key={messageId + '_' + signalName}
unplot={() => {this.onSignalUnplotPressed(messageId, signalName)}}
messageId={messageId}
messageName={msg.frame ? msg.frame.name : null}
message={msg.frame ? msg.frame.name : null}
signalSpec={Object.assign(Object.create(msg.signals[signalName]), msg.signals[signalName])}
onSegmentChanged={this.onSegmentChanged}
segment={this.state.segment}
@ -483,6 +513,13 @@ export default class Explorer extends Component {
: this.selectMessagePrompt()}
</div>
<div className='cabana-explorer-visuals'>
<div className='cabana-explorer-visuals-header'>
{this.timeWindow()}
<PartSelector
onPartChange={this.props.onPartChange}
partsCount={this.props.partsCount}
/>
</div>
<RouteVideoSync
message={this.props.messages[this.props.selectedMessage]}
secondsLoaded={this.secondsLoaded()}

View File

@ -1,11 +1,10 @@
import React, {Component} from 'react';
import { StyleSheet, css } from 'aphrodite/no-important';
import cx from 'classnames';
import PropTypes from 'prop-types';
import Moment from 'moment';
import Clipboard from 'clipboard';
import {modifyQueryParameters} from '../utils/url';
import PartSelector from './PartSelector';
import LoadDbcModal from './LoadDbcModal';
import * as GithubAuth from '../api/github-auth';
import Images from '../styles/images';
@ -19,8 +18,7 @@ export default class Meta extends Component {
dongleId: PropTypes.string,
name: PropTypes.string,
messages: PropTypes.objectOf(PropTypes.object),
onPartChanged: PropTypes.func,
partsCount: PropTypes.number,
selectedMessages: PropTypes.array,
showLoadDbc: PropTypes.func,
showSaveDbc: PropTypes.func,
dbcFilename: PropTypes.string,
@ -40,7 +38,6 @@ export default class Meta extends Component {
this.state = {
filterText: 'Filter',
lastSaved: dbcLastSaved !== null ? this.props.dbcLastSaved.fromNow() : null,
selectedMessages: [],
hoveredMessages: []
};
this.onFilterChanged = this.onFilterChanged.bind(this);
@ -68,9 +65,9 @@ export default class Meta extends Component {
const nextMsgKeys = Object.keys(nextProps.messages);
if(JSON.stringify(nextMsgKeys) != JSON.stringify(Object.keys(this.props.messages))) {
let {selectedMessages} = this.state;
let {selectedMessages} = this.props;
selectedMessages = selectedMessages.filter((m) => nextMsgKeys.indexOf(m) !== -1);
this.setState({selectedMessages, hoveredMessages: []});
this.setState({hoveredMessages: []});
}
}
@ -123,10 +120,6 @@ export default class Meta extends Component {
this.setState({hoveredMessages});
}
onMsgEditClick(key) {
this.props.showEditMessageModal(key);
}
onMsgRemoveClick(key) {
let {selectedMessages} = this.state;
selectedMessages = selectedMessages.filter((m) => m != key);
@ -134,50 +127,12 @@ export default class Meta extends Component {
this.setState({selectedMessages});
}
hoverButtons(key) {
return ([<div key={"edit"}
className={css(Styles.hoverButton, Styles.editButton)}
onClick={() => this.onMsgEditClick(key)}>
<p>Edit</p>
</div>,
<div key={"remove"}
className={css(Styles.hoverButton, Styles.removeButton)}
onClick={() => this.onMsgRemoveClick(key)}>
<p>Remove</p>
</div>]);
}
selectedMessagesList() {
const {selectedMessages, hoveredMessages} = this.state;
if(selectedMessages.length === 0) return null;
const messages = selectedMessages
.sort()
.map((key) => {
const msg = this.props.messages[key];
return <li key={key}
className={css(Styles.message,
Styles.selectedMessage)}
onMouseEnter={() => this.onMessageHover(key)}
onMouseLeave={() => this.onMessageHoverEnd(key)}>
{msg.frame ? msg.frame.name : ''}&nbsp;{key}
{hoveredMessages.indexOf(key) !== -1 ? this.hoverButtons(key): null}
</li>
});
return (<div className={css(Styles.messagesList)}>
<p>Selected Message</p>
<ul className={css(Styles.messageList)}>
{messages}
</ul>
</div>);
}
onMessageSelected(key) {
// uncomment when we support multiple messages
// const selectedMessages = this.state.selectedMessages.filter((m) => m != key);
const selectedMessages = [];
selectedMessages.push(key);
this.setState({selectedMessages});
this.props.updateSelectedMessages(selectedMessages);
this.props.onMessageSelected(key);
}
@ -223,75 +178,45 @@ export default class Meta extends Component {
}
selectedMessageClass(messageId) {
return (this.state.selectedMessages.includes(messageId) ? Styles.messageIsSelected : null);
return (this.props.selectedMessages.includes(messageId) ? 'is-selected' : null);
}
availableMessagesList() {
renderAvailableMessagesList() {
if(Object.keys(this.props.messages).length === 0) {
return null;
return <p>Loading messages...</p>;
}
const defaultTextVisible = this.state.filterText.trim() === 'Filter';
return (<div className={css(Styles.messagesList)}>
<p>Available Messages</p>
<div className={css(Styles.filter)}>
<input type="text"
value={this.state.filterText}
onFocus={this.onFilterFocus}
onBlur={this.onFilterUnfocus}
onChange={this.onFilterChanged}
className={css(defaultTextVisible ? Styles.defaultFilterText: null)}
/>
{this.state.filterText.trim().length > 0 && this.state.filterText !== 'Filter' ?
<Images.clear onClick={() => this.setState({filterText: 'Filter'})} />
: null}
</div>
<table className={css(Styles.messageTable)}>
<thead>
<tr className={css(Styles.messageHeader)}>
<td></td>
<td></td>
<td>Signals</td>
<td>Count</td>
<td>Bytes</td>
</tr>
</thead>
<tbody>
{this.orderedMessages()
.map((msg) => {
return <tr onClick={() => {this.onMessageSelected(msg.id)}}
key={msg.id}
className={css(Styles.message, this.selectedMessageClass(msg.id))}>
<td>{msg.frame ? msg.frame.name : ''}</td>
<td>{msg.id}</td>
<td>{Object.keys(msg.signals).length}</td>
<td>{msg.entries.length}</td>
<td>
<MessageBytes
message={msg}
seekTime={this.props.seekTime}
maxByteStateChangeCount={this.props.maxByteStateChangeCount} />
</td>
</tr>
})}
</tbody>
</table>
</div>);
}
timeWindow() {
const {route, currentParts} = this.props;
if(route) {
const partStartOffset = currentParts[0] * 60,
partEndOffset = (currentParts[1] + 1) * 60;
const windowStartTime = Moment(route.start_time).add(partStartOffset, 's').format('HH:mm:ss');
const windowEndTime = Moment(route.start_time).add(partEndOffset, 's').format('HH:mm:ss');
return `${windowStartTime} - ${windowEndTime}`;
} else return '';
return (
<table cellPadding='5'>
<thead>
<tr>
<td>Name</td>
<td>ID</td>
<td>Count</td>
<td>Bytes</td>
</tr>
</thead>
<tbody>
{this.orderedMessages()
.map((msg) => {
return (
<tr onClick={() => {this.onMessageSelected(msg.id)}}
key={msg.id}
className={cx('cabana-meta-messages-list-item', this.selectedMessageClass(msg.id))}>
<td>{msg.frame ? msg.frame.name : 'undefined'}</td>
<td>{msg.id}</td>
<td>{msg.entries.length}</td>
<td>
<MessageBytes
message={msg}
seekTime={this.props.seekTime}
maxByteStateChangeCount={this.props.maxByteStateChangeCount} />
</td>
</tr>
)
})}
</tbody>
</table>
);
}
shareUrl() {
@ -303,182 +228,53 @@ export default class Meta extends Component {
}
render() {
return (
<div className="cabana-meta">
{this.props.isDemo ?
<div className={css(Styles.chffrPanda)}>
<Images.panda styles={[Styles.panda]} />
<div className={css(Styles.chffrPandaDesc)}>
<p>Data collected with chffr + panda</p>
<a href="http://panda.comma.ai" className={css(Styles.chffrPandaGet)}>buy panda</a>
<a href="http://chffr.comma.ai" className={css(Styles.chffrPandaGet)}>get chffr</a>
</div>
</div> : null}
<div className={css(Styles.scrollContainer)}>
<div>
<span className={css(Styles.titleText)}>
comma cabana
</span>
</div>
<div>
{this.props.githubAuthToken ?
<p className={css(Styles.githubAuth)}>GitHub Authenticated</p>
:
this.props.loginWithGithub
}
</div>
<div>
<p className={css(Styles.loadDbc)}
onClick={this.props.showLoadDbc}>Load DBC</p>
&nbsp;/&nbsp;
<p className={css(Styles.loadDbc)}
onClick={this.props.showSaveDbc}>Save DBC</p>
{this.props.dbcLastSaved !== null ?
<div className='cabana-meta'>
<div className='cabana-meta-header'>
<span className='cabana-meta-header-label'>Currently editing:</span>
<strong className='cabana-meta-header-filename'>{this.props.dbcFilename}</strong>
{this.props.dbcLastSaved !== null ?
<div className='cabana-meta-header-last-saved'>
<p>Last saved: {this.lastSavedPretty()}</p>
: null
}
{this.props.dbcFilename ? <p>Editing: {this.props.dbcFilename}</p>: null}
<p data-clipboard-text={this.shareUrl()}
data-clipboard-action="copy"
ref={(ref) => ref ? new Clipboard(ref) : null}>
<a href={this.shareUrl()}
className={css(Styles.copyShareLink)}
onClick={(e) => e.preventDefault()}>Copy share link</a></p>
</div>
: null
}
<div className='cabana-meta-header-actions'>
<div className='cabana-meta-header-action'>
<button onClick={this.props.showLoadDbc}>Load DBC</button>
</div>
<div className='cabana-meta-header-action'
data-clipboard-text={this.shareUrl()}
data-clipboard-action='copy'
ref={(ref) => ref ? new Clipboard(ref) : null}>
<a className='button'
href={this.shareUrl()}
onClick={(e) => e.preventDefault()}>Copy Share Link</a>
</div>
<div className='cabana-meta-header-action'>
<button onClick={this.props.showSaveDbc}>Save DBC</button>
</div>
</div>
<div>
<p className={css(Styles.timeWindow)}>{this.timeWindow()}</p>
</div>
<div className='cabana-meta-messages'>
<div className='cabana-meta-messages-header'>
<p>Available messages</p>
</div>
<div className='cabana-meta-messages-window'>
<div className='cabana-meta-messages-filter'>
<div className='form-field form-field--small'>
<input type="text"
value={this.state.filterText}
onFocus={this.onFilterFocus}
onBlur={this.onFilterUnfocus}
onChange={this.onFilterChanged} />
</div>
</div>
<div className='cabana-meta-messages-list'>
{this.renderAvailableMessagesList()}
</div>
</div>
<PartSelector
onPartChange={this.props.onPartChange}
partsCount={this.props.partsCount}
/>
{this.selectedMessagesList()}
{this.availableMessagesList()}
</div>
</div>
);
}
}
const Styles = StyleSheet.create({
chffrPanda: {
minWidth: 450,
width: '100%',
flexDirection: 'row',
display: 'flex',
alignItems: 'center',
borderBottom: '1px solid rgba(0,0,0,0.8)',
borderRight: '1px solid rgba(0,0,0,0.8)',
backgroundColor: 'white',
padding: 10
},
chffrPandaDesc: {
textAlign: 'center',
justifyContent: 'center',
alignItems: 'center',
paddingLeft: 10
},
chffrPandaGet: {
display: 'block',
fontWeight: 'bold',
textDecoration: 'none',
':hover': {
textDecoration: 'underline'
},
color: 'rgba(0,0,0,0.8)'
},
panda: {
width: 150,
height: 138
},
scrollContainer: {
display: 'block',
height: '100%',
overflowY: 'scroll',
overflowX: 'hidden',
padding: 10,
},
githubAuth: {
marginTop: 10,
marginBottom: 10
},
titleText: {
fontFamily: 'monospace',
paddingRight: 10,
fontSize: 24
},
routeMeta: {
borderBottomWidth: '1px',
borderColor: 'grey',
'*': {
display: 'inline-block'
}
},
message: {
cursor: 'pointer',
':hover' : {
backgroundColor: 'rgba(0,0,0,0.1)'
},
marginTop: 5,
fontSize: 12,
},
selectedMessage: {
display: 'flex',
flexDirection: 'row'
},
messageIsSelected: {
backgroundColor: 'blue',
color: 'rgb(233, 233, 233)',
fontWeight: 'bold'
},
messageList: {
margin: 0,
padding: 0
},
loadDbc: {
cursor: 'pointer',
':hover': {
textDecoration: 'underline'
},
display: 'inline',
fontWeight: 'bold'
},
timeWindow: {
marginTop: 10,
marginBottom: 10,
},
hoverButton: {
height: 15,
padding: 8,
borderRadius: 4,
justifyContent: 'center',
alignItems: 'center',
display: 'flex',
marginLeft: 15
},
editButton: {
backgroundColor: 'RGBA(105, 69, 33, 1.00)',
color: 'RGBA(251, 253, 242, 1.00)'
},
removeButton: {
backgroundColor: 'RGBA(255, 34, 59, 0.83)',
color: 'RGBA(251, 253, 242, 1.00)'
},
defaultFilterText: {
color: 'rgb(205,205,205)'
},
filter: {
display: 'flex',
flexDirection: 'row',
height: 24
},
messagesList: {
marginTop: 10
},
messageHeader: {
fontSize: 12
},
copyShareLink: {
color: 'black'
}
});

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,268 +0,0 @@
/*
Comma Cabana Styles
~~~~~~~~~~~~~~~~~~~
*/
html, body {
min-width: 1340px;
margin: 0;
padding: 0;
height: 100%;
width: 100%;
font-family: sans-serif; }
p {
margin: 0;
padding: 0; }
#root {
width: 100%;
height: 100%; }
* {
box-sizing: border-box; }
.g-container {
width: 90%;
margin-left: auto;
margin-right: auto; }
@media only screen and (min-width: 540px) {
.g-container {
width: 80%; } }
@media only screen and (min-width: 960px) {
.g-container {
width: 75%; } }
.g-row {
position: relative;
width: 100%; }
.g-row::after {
content: "";
display: table;
clear: both; }
[class*="g-col"] {
float: left;
min-height: 0.125rem; }
.g-col-1,
.g-col-2,
.g-col-3,
.g-col-4,
.g-col-5,
.g-col-6,
.g-col-7,
.g-col-8,
.g-col-9,
.g-col-10,
.g-col-11,
.g-col-12 {
width: 100%; }
@media only screen and (min-width: 720px) {
.g-col-1 {
width: 8.3333333333%; }
.g-col-2 {
width: 16.6666666667%; }
.g-col-3 {
width: 25%; }
.g-col-4 {
width: 33.3333333333%; }
.g-col-5 {
width: 41.6666666667%; }
.g-col-6 {
width: 50%; }
.g-col-7 {
width: 58.3333333333%; }
.g-col-8 {
width: 66.6666666667%; }
.g-col-9 {
width: 75%; }
.g-col-10 {
width: 83.3333333333%; }
.g-col-11 {
width: 91.6666666667%; }
.g-col-12 {
width: 100%; }
.hidden-sm {
display: block; } }
/*
VG Tooltip Library
~~~~~~~~~~~~~~~~~~
*/
.vg-tooltip {
visibility: hidden;
padding: 6px;
border-radius: 3px;
position: fixed;
z-index: 2000;
font-family: sans-serif;
font-size: 1em;
/* The default look of the tooltip is the same as .light-theme
but it can be overwritten by .dark-theme */
background-color: rgba(255, 255, 255, 0.9);
border: 1px solid #d9d9d9;
color: black; }
.vg-tooltip td.key, .vg-tooltip td.value {
overflow: hidden;
text-overflow: ellipsis; }
.vg-tooltip td.key {
max-width: 150px;
text-align: right;
padding-right: 1px;
font-weight: bold; }
.vg-tooltip td.value {
max-width: 200px;
text-align: left; }
/* Dark and light color themes */
.vg-tooltip.dark-theme {
background-color: rgba(64, 64, 64, 0.9);
color: rgba(255, 255, 255, 0.95); }
/*
Meta Component Styles
~~~~~~~~~~~~~~~~~~~~~
*/
.cabana-meta {
background: #e5e5e5;
flex: .5;
flex-direction: column;
height: 100vh;
overflow-y: scroll; }
.cabana-meta table {
width: 100%; }
/*
Explorer Component Styles
~~~~~~~~~~~~~~~~~~~~~~~~~
*/
.cabana {
display: flex; }
.cabana-explorer {
display: flex;
flex: 1; }
.cabana-explorer-signals {
flex: 1;
height: 100vh;
overflow-x: hidden;
overflow-y: scroll;
padding: 3%; }
.cabana-explorer-signals-matrix table {
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 5px;
font-family: monospace;
margin: 2% 0;
width: 100%; }
.cabana-explorer-signals-log {
background: #fafafa;
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 5px;
padding: 3%; }
.cabana-explorer-visuals {
flex: 1;
height: 100vh;
overflow-x: hidden;
overflow-y: scroll;
padding: 1.5%;
padding-left: 0; }
.cabana-explorer-visuals-camera {
border: 5px solid #000;
border-radius: 5px;
max-height: 480px;
max-width: 640px;
overflow: hidden;
position: relative; }
.cabana-explorer-visuals-camera-seeker {
background: rgba(233, 233, 233, 0.2);
bottom: 0;
position: absolute;
width: 100%;
z-index: 4; }
.cabana-explorer-visuals-camera video {
cursor: pointer;
width: 100%; }
.cabana-explorer-visuals-plots {
border: 1px solid #d3d3d3;
border-radius: 5px;
margin: 3% 0; }
.cabana-explorer-visuals-plot {
padding: 1.5% 3%; }
.cabana-explorer-visuals-plot:first-child, .cabana-explorer-visuals-plot:last-child {
padding-top: 3%;
padding-bottom: 3%; }
.cabana-explorer-visuals-plot-message {
color: #a2a2a2;
font-size: 12px; }
.cabana-explorer-visuals-plot-signal strong {
padding-right: 5px; }
.cabana-explorer-visuals-plot-signal a {
color: #a2a2a2;
cursor: pointer; }
.cabana-explorer-visuals-plot-canvas svg {
height: auto;
width: 100%; }
/*
Signal Legend Component
~~~~~~~~~~~~~~~~~~~~~~~
*/
.cabana-explorer-signals-legend {
background: #fafafa;
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 5px;
margin: 5% 0;
padding: 2%; }
.signals-legend-entry {
overflow: auto; }
.signals-legend-entry:not(:last-child) {
border-bottom: 1px solid rgba(0, 0, 0, 0.05); }
.signals-legend-entry-header {
cursor: pointer;
line-height: 42px; }
.signals-legend-entry-header-name {
float: left;
width: 70%; }
.signals-legend-entry-header-plotted {
float: left;
text-align: right;
width: 30%; }
.signals-legend-entry-body {
padding: 2%; }
/*
ExplorerSignalLog Component Styles
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Also known as CanLog
*/
/*# sourceMappingURL=index.css.map */

View File

@ -8,9 +8,11 @@
// Globals
@import './styles/base/base';
@import './styles/base/grid';
@import './styles/base/forms';
// Libraries
@import './styles/lib/index';
@import './styles/lib/font-awesome';
// Components
@import './components/Meta/meta';

View File

@ -0,0 +1,4 @@
// All Global Variables and Mixins
@import 'colors';
@import 'mixins';
@import '../../../node_modules/font-awesome/scss/variables';

View File

@ -1,5 +1,10 @@
// Cabana Color Variables
$color-grey-10: #fafafa;
$color-grey-20: #e5e5e5;
$color-grey-30: #d3d3d3;
$color-grey-20: #ededed;
$color-grey-30: #e1e1e1;
$color-grey-40: #d4d4d4;
$color-grey-50: #a2a2a2;
$color-grey-60: #979797;
$color-grey-70: #6f6f6f;
$color-grey-80: #484848;
$color-grey-90: #2d2d2d;

View File

@ -1,3 +0,0 @@
// Global Cabana Variables
@import './colors';

View File

@ -0,0 +1,9 @@
// Mixins
%clearfix {
&:after {
content: "";
display: table;
clear: both;
}
}

View File

@ -2,6 +2,7 @@
html, body {
min-width: 1340px;
margin: 0;
overflow-x: hidden;
padding: 0;
height: 100%;
width: 100%;

View File

@ -0,0 +1,88 @@
/*
Base Form Elements
~~~~~~~~~~~~~~~~~~
*/
@import '../../styles/_global/all';
$input-height: 42px;
$input-height--small: 34px;
$input-height--tiny: 22px;
$input-radius: 3px;
$input-background: #fff;
$button-background: $color-grey-50;
$button-background--alpha: rgba(0,0,0,.02);
.form-field {
&.form-field--small {
input,
select {
font-size: 12px;
height: $input-height--small;
}
}
}
input[type='text'],
input[type='number'],
select {
background: $input-background;
border: 1px solid $color-grey-50;
border-radius: $input-radius;
font-size: 14px;
height: $input-height;
outline: none;
padding: 0 14px;
width: 100%;
&:focus {
border-color: $color-grey-60;
}
}
input[type='button'],
button,
a.button {
background: $color-grey-50;
border: 1px solid $color-grey-60;
border-bottom-width: 3px;
border-radius: $input-radius;
color: #fff;
cursor: pointer;
display: block;
font-size: 14px;
font-weight: 600;
height: $input-height;
line-height: $input-height - 2px;
outline: 0;
padding: 0 5%;
text-align: center;
text-decoration: none;
text-shadow: 0 1px rgba(0,0,0,.2);
&:active {
background: $color-grey-60;
border-bottom-width: 1px;
}
&.button--small {
height: $input-height--small;
line-height: $input-height--small - 2px;
}
&.button--tiny {
height: $input-height--tiny;
font-size: 12px;
line-height: $input-height--tiny - 2px;
}
&.button--alpha {
background: $button-background--alpha;
border-color: $color-grey-30;
border-width: 1px;
color: $color-grey-70;
text-shadow: none;
}
&.button--inverted {
background: transparent;
border-width: 1px;
color: $button-background;
}
}

View File

@ -0,0 +1,9 @@
/*
Font Awesome Icons
~~~~~~~~~~~~~~~~~~
*/
$fa-font-path: './fonts';
@import '../../../node_modules/font-awesome/scss/font-awesome';