diff options
author | Emiliano Ciavatta | 2020-09-30 20:58:05 +0000 |
---|---|---|
committer | Emiliano Ciavatta | 2020-09-30 20:58:05 +0000 |
commit | 55afd62a8cfe2cde6e627f1905ab8fe77965afd6 (patch) | |
tree | 57545a722a62d2279bfcd2e36f1cbd1da5a5736a /frontend/src/views | |
parent | 4cfdf6e2dfe9184e988a145495e072571d512cdc (diff) | |
parent | d6e2aaad41f916c2080c59cf7b4e42bf87a1a03f (diff) |
Merge branch 'feature/frontend' into develop
Diffstat (limited to 'frontend/src/views')
-rw-r--r-- | frontend/src/views/App.js | 51 | ||||
-rw-r--r-- | frontend/src/views/App.scss | 15 | ||||
-rw-r--r-- | frontend/src/views/Connections.js | 83 | ||||
-rw-r--r-- | frontend/src/views/Filters.js | 6 | ||||
-rw-r--r-- | frontend/src/views/Footer.js | 4 | ||||
-rw-r--r-- | frontend/src/views/Header.js | 30 | ||||
-rw-r--r-- | frontend/src/views/Header.scss | 19 | ||||
-rw-r--r-- | frontend/src/views/MainPane.js | 46 | ||||
-rw-r--r-- | frontend/src/views/MainPane.scss | 6 | ||||
-rw-r--r-- | frontend/src/views/Rules.js | 118 | ||||
-rw-r--r-- | frontend/src/views/Services.js | 210 | ||||
-rw-r--r-- | frontend/src/views/Services.scss | 32 |
12 files changed, 123 insertions, 497 deletions
diff --git a/frontend/src/views/App.js b/frontend/src/views/App.js index 6c101fa..fb4454c 100644 --- a/frontend/src/views/App.js +++ b/frontend/src/views/App.js @@ -1,49 +1,46 @@ import React, {Component} from 'react'; +import './App.scss'; import Header from "./Header"; -import MainPane from "./MainPane"; +import MainPane from "../components/panels/MainPane"; import Footer from "./Footer"; -import {BrowserRouter as Router, Route, Switch} from "react-router-dom"; -import Services from "./Services"; +import {BrowserRouter as Router} from "react-router-dom"; import Filters from "./Filters"; -import Rules from "./Rules"; +import backend from "../backend"; +import ConfigurationPane from "../components/panels/ConfigurationPane"; class App extends Component { constructor(props) { super(props); - this.state = { - servicesWindowOpen: false, - filterWindowOpen: false, - rulesWindowOpen: false - }; + + this.state = {}; + } + + componentDidMount() { + backend.get("/api/services").then(_ => this.setState({configured: true})); } render() { let modal; - if (this.state.servicesWindowOpen) { - modal = <Services onHide={() => this.setState({servicesWindowOpen: false})}/>; - } - if (this.state.filterWindowOpen) { + if (this.state.filterWindowOpen && this.state.configured) { modal = <Filters onHide={() => this.setState({filterWindowOpen: false})}/>; } - if (this.state.rulesWindowOpen) { - modal = <Rules onHide={() => this.setState({rulesWindowOpen: false})}/>; - } return ( - <div className="app"> + <div className="main"> <Router> - <Header onOpenServices={() => this.setState({servicesWindowOpen: true})} - onOpenFilters={() => this.setState({filterWindowOpen: true})} - onOpenRules={() => this.setState({rulesWindowOpen: true})} /> - <Switch> - <Route path="/connections/:id" children={<MainPane/>}/> - <Route path="/" children={<MainPane/>}/> - </Switch> - {modal} - <Footer/> + <div className="main-header"> + <Header onOpenFilters={() => this.setState({filterWindowOpen: true})} /> + </div> + <div className="main-content"> + {this.state.configured ? <MainPane /> : + <ConfigurationPane onConfigured={() => this.setState({configured: true})} />} + {modal} + </div> + <div className="main-footer"> + {this.state.configured && <Footer/>} + </div> </Router> - </div> ); } diff --git a/frontend/src/views/App.scss b/frontend/src/views/App.scss new file mode 100644 index 0000000..b25d4c9 --- /dev/null +++ b/frontend/src/views/App.scss @@ -0,0 +1,15 @@ + +.main { + display: flex; + flex-direction: column; + height: 100vh; + + .main-content { + flex: 1 1; + overflow: hidden; + } + + .main-header, .main-footer { + flex: 0 0; + } +} diff --git a/frontend/src/views/Connections.js b/frontend/src/views/Connections.js index 62733d7..73979c4 100644 --- a/frontend/src/views/Connections.js +++ b/frontend/src/views/Connections.js @@ -1,10 +1,11 @@ import React, {Component} from 'react'; import './Connections.scss'; -import axios from 'axios'; import Connection from "../components/Connection"; import Table from 'react-bootstrap/Table'; import {Redirect} from 'react-router'; import {withRouter} from "react-router-dom"; +import backend from "../backend"; +import ConnectionMatchedRules from "../components/ConnectionMatchedRules"; class Connections extends Component { @@ -25,21 +26,21 @@ class Connections extends Component { this.scrollBottomThreashold = 0.99999; this.maxConnections = 500; this.queryLimit = 50; - - this.handleScroll = this.handleScroll.bind(this); - this.connectionSelected = this.connectionSelected.bind(this); - this.addServicePortFilter = this.addServicePortFilter.bind(this); } componentDidMount() { this.loadConnections({limit: this.queryLimit}) .then(() => this.setState({loaded: true})); + if (this.props.initialConnection != null) { + this.setState({selected: this.props.initialConnection.id}); + // TODO: scroll to initial connection + } } - connectionSelected(c) { + connectionSelected = (c) => { this.setState({selected: c.id}); this.props.onSelected(c); - } + }; componentDidUpdate(prevProps, prevState, snapshot) { if (this.state.loaded && prevProps.location.search !== this.props.location.search) { @@ -49,7 +50,7 @@ class Connections extends Component { } } - handleScroll(e) { + handleScroll = (e) => { let relativeScroll = e.currentTarget.scrollTop / (e.currentTarget.scrollHeight - e.currentTarget.clientHeight); if (!this.state.loading && relativeScroll > this.scrollBottomThreashold) { this.loadConnections({from: this.state.lastConnection.id, limit: this.queryLimit,}) @@ -59,13 +60,23 @@ class Connections extends Component { this.loadConnections({to: this.state.firstConnection.id, limit: this.queryLimit,}) .then(() => console.log("Previous connections loaded")); } - } + }; - addServicePortFilter(port) { - let urlParams = new URLSearchParams(this.props.location.search); + addServicePortFilter = (port) => { + const urlParams = new URLSearchParams(this.props.location.search); urlParams.set("service_port", port); this.setState({queryString: "?" + urlParams}); - } + }; + + addMatchedRulesFilter = (matchedRule) => { + const urlParams = new URLSearchParams(this.props.location.search); + const oldMatchedRules = urlParams.getAll("matched_rules") || []; + + if (!oldMatchedRules.includes(matchedRule)) { + urlParams.append("matched_rules", matchedRule); + this.setState({queryString: "?" + urlParams}); + } + }; async loadConnections(params) { let url = "/api/connections"; @@ -75,15 +86,15 @@ class Connections extends Component { } this.setState({loading: true, prevParams: params}); - let res = await axios.get(`${url}?${urlParams}`); + let res = (await backend.get(`${url}?${urlParams}`)).json; let connections = this.state.connections; let firstConnection = this.state.firstConnection; let lastConnection = this.state.lastConnection; if (params !== undefined && params.from !== undefined) { - if (res.data.length > 0) { - connections = this.state.connections.concat(res.data); + if (res.length > 0) { + connections = this.state.connections.concat(res); lastConnection = connections[connections.length - 1]; if (connections.length > this.maxConnections) { connections = connections.slice(connections.length - this.maxConnections, @@ -92,8 +103,8 @@ class Connections extends Component { } } } else if (params !== undefined && params.to !== undefined) { - if (res.data.length > 0) { - connections = res.data.concat(this.state.connections); + if (res.length > 0) { + connections = res.concat(this.state.connections); firstConnection = connections[0]; if (connections.length > this.maxConnections) { connections = connections.slice(0, this.maxConnections); @@ -101,8 +112,8 @@ class Connections extends Component { } } } else { - if (res.data.length > 0) { - connections = res.data; + if (res.length > 0) { + connections = res; firstConnection = connections[0]; lastConnection = connections[connections.length - 1]; } else { @@ -112,21 +123,15 @@ class Connections extends Component { } } - let flagRule = this.state.flagRule; let rules = this.state.rules; - if (flagRule === null) { - let res = await axios.get("/api/rules"); - rules = res.data; - flagRule = rules.filter(rule => { - return rule.name === "flag"; - })[0]; + if (rules === null) { + rules = (await backend.get("/api/rules")).json; } this.setState({ loading: false, connections: connections, - rules: res.data, - flagRule: flagRule, + rules: rules, firstConnection: firstConnection, lastConnection: lastConnection }); @@ -134,7 +139,7 @@ class Connections extends Component { render() { let redirect; - let queryString = this.state.queryString !== null ? this.state.queryString : "" + let queryString = this.state.queryString !== null ? this.state.queryString : ""; if (this.state.selected) { let format = this.props.match.params.format; format = format !== undefined ? "/" + format : ""; @@ -159,6 +164,7 @@ class Connections extends Component { <th>srcport</th> <th>dstip</th> <th>dstport</th> + <th>started_at</th> <th>duration</th> <th>up</th> <th>down</th> @@ -167,13 +173,18 @@ class Connections extends Component { </thead> <tbody> { - this.state.connections.map(c => - <Connection key={c.id} data={c} onSelected={() => this.connectionSelected(c)} - selected={this.state.selected === c.id} onMarked={marked => c.marked = marked} - onEnabled={enabled => c.hidden = !enabled} - containsFlag={c.matched_rules.includes(this.state.flagRule.id)} - addServicePortFilter={this.addServicePortFilter}/> - ) + this.state.connections.flatMap(c => { + return [<Connection key={c.id} data={c} onSelected={() => this.connectionSelected(c)} + selected={this.state.selected === c.id} + onMarked={marked => c.marked = marked} + onEnabled={enabled => c.hidden = !enabled} + addServicePortFilter={this.addServicePortFilter} />, + c.matched_rules.length > 0 && + <ConnectionMatchedRules key={c.id + "_m"} matchedRules={c.matched_rules} + rules={this.state.rules} + addMatchedRulesFilter={this.addMatchedRulesFilter} /> + ]; + }) } {loading} </tbody> diff --git a/frontend/src/views/Filters.js b/frontend/src/views/Filters.js index 78f0342..ba7d467 100644 --- a/frontend/src/views/Filters.js +++ b/frontend/src/views/Filters.js @@ -1,7 +1,7 @@ import React, {Component} from 'react'; -import './Services.scss'; -import {Button, Col, Container, Modal, Row, Table} from "react-bootstrap"; +import {Col, Container, Modal, Row, Table} from "react-bootstrap"; import {filtersDefinitions, filtersNames} from "../components/filters/FiltersDefinitions"; +import ButtonField from "../components/fields/ButtonField"; class Filters extends Component { @@ -89,7 +89,7 @@ class Filters extends Component { </Container> </Modal.Body> <Modal.Footer className="dialog-footer"> - <Button variant="red" onClick={this.props.onHide}>close</Button> + <ButtonField variant="red" bordered onClick={this.props.onHide} name="close" /> </Modal.Footer> </Modal> ); diff --git a/frontend/src/views/Footer.js b/frontend/src/views/Footer.js index b6ffd9d..0a3c5a3 100644 --- a/frontend/src/views/Footer.js +++ b/frontend/src/views/Footer.js @@ -8,11 +8,11 @@ class Footer extends Component { <footer className="footer container-fluid"> <div className="row"> <div className="col-12"> - <div className="footer-timeline">timeline</div> + <div className="footer-timeline">timeline - <a href="https://github.com/eciavatta/caronte/issues/12">to be implemented</a></div> </div> </div> </footer> - ) + ); } } diff --git a/frontend/src/views/Header.js b/frontend/src/views/Header.js index 5d0f690..944f1d5 100644 --- a/frontend/src/views/Header.js +++ b/frontend/src/views/Header.js @@ -1,8 +1,9 @@ import React, {Component} from 'react'; import Typed from 'typed.js'; import './Header.scss'; -import {Button} from "react-bootstrap"; import {filtersDefinitions, filtersNames} from "../components/filters/FiltersDefinitions"; +import {Link} from "react-router-dom"; +import ButtonField from "../components/fields/ButtonField"; class Header extends Component { @@ -45,7 +46,7 @@ class Header extends Component { render() { let quickFilters = filtersNames.filter(name => this.state[`${name}_active`]) - .map(name => filtersDefinitions[name]) + .map(name => <React.Fragment key={name} >{filtersDefinitions[name]}</React.Fragment>) .slice(0, 5); return ( @@ -60,21 +61,26 @@ class Header extends Component { </div> <div className="col-auto"> - <div className="filters-bar-wrapper"> - <div className="filters-bar"> - {quickFilters} - </div> + <div className="filters-bar"> + {quickFilters} </div> </div> <div className="col"> <div className="header-buttons"> - <Button onClick={this.props.onOpenFilters}>filters</Button> - <Button variant="yellow" size="sm">pcaps</Button> - <Button variant="blue" onClick={this.props.onOpenRules}>rules</Button> - <Button variant="red" onClick={this.props.onOpenServices}> - services - </Button> + <ButtonField variant="pink" onClick={this.props.onOpenFilters} name="filters" bordered /> + <Link to="/pcaps"> + <ButtonField variant="purple" name="pcaps" bordered /> + </Link> + <Link to="/rules"> + <ButtonField variant="deep-purple" name="rules" bordered /> + </Link> + <Link to="/services"> + <ButtonField variant="indigo" name="services" bordered /> + </Link> + <Link to="/config"> + <ButtonField variant="blue" name="config" bordered /> + </Link> </div> </div> </div> diff --git a/frontend/src/views/Header.scss b/frontend/src/views/Header.scss index e84e758..15d1375 100644 --- a/frontend/src/views/Header.scss +++ b/frontend/src/views/Header.scss @@ -14,12 +14,21 @@ } .header-buttons { - margin: 5px 0; - text-align: right; + margin: 7px 0; + display: flex; + justify-content: flex-end; + + .button-field { + margin-left: 7px; + } } - .filters-bar-wrapper { - height: 50px; - padding: 8px 0; + .filters-bar { + padding: 3px 0; + + .filter { + display: inline-block; + margin-right: 10px; + } } } diff --git a/frontend/src/views/MainPane.js b/frontend/src/views/MainPane.js deleted file mode 100644 index 69de725..0000000 --- a/frontend/src/views/MainPane.js +++ /dev/null @@ -1,46 +0,0 @@ -import React, {Component} from 'react'; -import './MainPane.scss'; -import Connections from "./Connections"; -import ConnectionContent from "../components/ConnectionContent"; -import {withRouter} from "react-router-dom"; -import axios from 'axios'; - -class MainPane extends Component { - - constructor(props) { - super(props); - this.state = { - selectedConnection: null - }; - } - - componentDidMount() { - if ('id' in this.props.match.params) { - const id = this.props.match.params.id; - axios.get(`/api/connections/${id}`).then(res => { - if (res.status === 200) { - this.setState({selectedConnection: res.data}); - } - }); - } - } - - render() { - return ( - <div className="main-pane"> - <div className="container-fluid"> - <div className="row"> - <div className="col-md-6 pane"> - <Connections onSelected={(c) => this.setState({selectedConnection: c})} /> - </div> - <div className="col-md-6 pl-0 pane"> - <ConnectionContent connection={this.state.selectedConnection}/> - </div> - </div> - </div> - </div> - ); - } -} - -export default withRouter(MainPane); diff --git a/frontend/src/views/MainPane.scss b/frontend/src/views/MainPane.scss deleted file mode 100644 index 20720ba..0000000 --- a/frontend/src/views/MainPane.scss +++ /dev/null @@ -1,6 +0,0 @@ -.main-pane { - .pane { - height: calc(100vh - 210px); - position: relative; - } -} diff --git a/frontend/src/views/Rules.js b/frontend/src/views/Rules.js deleted file mode 100644 index 3424410..0000000 --- a/frontend/src/views/Rules.js +++ /dev/null @@ -1,118 +0,0 @@ -import React, {Component} from 'react'; -import './Services.scss'; -import {Button, ButtonGroup, Col, Container, Form, FormControl, InputGroup, Modal, Row, Table} from "react-bootstrap"; -import axios from "axios"; - -class Rules extends Component { - - constructor(props) { - super(props); - - this.state = { - rules: [] - }; - } - - componentDidMount() { - this.loadRules(); - } - - loadRules() { - axios.get("/api/rules").then(res => this.setState({rules: res.data})); - } - - render() { - let rulesRows = this.state.rules.map(rule => - <tr key={rule.id}> - <td><Button variant="btn-edit" size="sm" - style={{"backgroundColor": rule.color}}>edit</Button></td> - <td>{rule.name}</td> - </tr> - ); - - - return ( - <Modal - {...this.props} - show="true" - size="lg" - aria-labelledby="rules-dialog" - centered - > - <Modal.Header> - <Modal.Title id="rules-dialog"> - ~/rules - </Modal.Title> - </Modal.Header> - <Modal.Body> - <Container> - <Row> - <Col md={7}> - <Table borderless size="sm" className="rules-list"> - <thead> - <tr> - <th><Button size="sm" >new</Button></th> - <th>name</th> - </tr> - </thead> - <tbody> - {rulesRows} - - </tbody> - </Table> - </Col> - <Col md={5}> - <Form> - <Form.Group controlId="servicePort"> - <Form.Label>port:</Form.Label> - <Form.Control type="text" /> - </Form.Group> - - <Form.Group controlId="serviceName"> - <Form.Label>name:</Form.Label> - <Form.Control type="text" /> - </Form.Group> - - <Form.Group controlId="serviceColor"> - <Form.Label>color:</Form.Label> - <ButtonGroup aria-label="Basic example"> - - </ButtonGroup> - <ButtonGroup aria-label="Basic example"> - - </ButtonGroup> - </Form.Group> - - <Form.Group controlId="serviceNotes"> - <Form.Label>notes:</Form.Label> - <Form.Control as="textarea" rows={3} /> - </Form.Group> - </Form> - - - </Col> - - </Row> - - <Row> - <Col md={12}> - <InputGroup> - <FormControl as="textarea" rows={4} className="curl-output" readOnly={true} - /> - </InputGroup> - - </Col> - </Row> - - - </Container> - </Modal.Body> - <Modal.Footer className="dialog-footer"> - <Button variant="red" onClick={this.props.onHide}>close</Button> - </Modal.Footer> - </Modal> - ); - } -} - -export default Rules; diff --git a/frontend/src/views/Services.js b/frontend/src/views/Services.js deleted file mode 100644 index b95b01c..0000000 --- a/frontend/src/views/Services.js +++ /dev/null @@ -1,210 +0,0 @@ -import React, {Component} from 'react'; -import './Services.scss'; -import {Button, ButtonGroup, Col, Container, Form, FormControl, InputGroup, Modal, Row, Table} from "react-bootstrap"; -import axios from 'axios' -import {createCurlCommand} from '../utils'; - -class Services extends Component { - - constructor(props) { - super(props); - this.alphabet = 'abcdefghijklmnopqrstuvwxyz'; - this.colors = ["#E53935", "#D81B60", "#8E24AA", "#5E35B1", "#3949AB", "#1E88E5", "#039BE5", "#00ACC1", - "#00897B", "#43A047", "#7CB342", "#9E9D24", "#F9A825", "#FB8C00", "#F4511E", "#6D4C41"]; - - this.state = { - services: {}, - port: 0, - portValid: false, - name: "", - nameValid: false, - color: this.colors[0], - colorValid: false, - notes: "" - }; - - this.portChanged = this.portChanged.bind(this); - this.nameChanged = this.nameChanged.bind(this); - this.notesChanged = this.notesChanged.bind(this); - this.newService = this.newService.bind(this); - this.editService = this.editService.bind(this); - this.saveService = this.saveService.bind(this); - this.loadServices = this.loadServices.bind(this); - } - - componentDidMount() { - this.loadServices(); - } - - portChanged(event) { - let value = event.target.value.replace(/[^\d]/gi, ''); - let port = 0; - if (value !== "") { - port = parseInt(value); - } - this.setState({port: port}); - } - - nameChanged(event) { - let value = event.target.value.replace(/[\s]/gi, '_').replace(/[^\w]/gi, '').toLowerCase(); - this.setState({name: value}); - } - - notesChanged(event) { - this.setState({notes: event.target.value}); - } - - newService() { - this.setState({name: "", port: 0, notes: ""}); - } - - editService(service) { - this.setState({name: service.name, port: service.port, color: service.color, notes: service.notes}); - } - - saveService() { - if (this.state.portValid && this.state.nameValid) { - axios.put("/api/services", { - name: this.state.name, - port: this.state.port, - color: this.state.color, - notes: this.state.notes - }); - - this.newService(); - this.loadServices(); - } - } - - loadServices() { - axios.get("/api/services").then(res => this.setState({services: res.data})); - } - - componentDidUpdate(prevProps, prevState, snapshot) { - if (this.state.name != null && prevState.name !== this.state.name) { - this.setState({ - nameValid: this.state.name.length >= 3 - }); - } - if (prevState.port !== this.state.port) { - this.setState({ - portValid: this.state.port > 0 && this.state.port <= 65565 - }); - } - } - - render() { - let output = ""; - if (!this.state.portValid) { - output += "assert(1 <= port <= 65565)\n"; - } - if (!this.state.nameValid) { - output += "assert(len(name) >= 3)\n"; - } - if (output === "") { - output = createCurlCommand("/services", { - "port": this.state.port, - "name": this.state.name, - "color": this.state.color, - "notes": this.state.notes - }); - } - let rows = Object.values(this.state.services).map(s => - <tr> - <td><Button variant="btn-edit" size="sm" - onClick={() => this.editService(s)} style={{"backgroundColor": s.color}}>edit</Button></td> - <td>{s.port}</td> - <td>{s.name}</td> - </tr> - ); - - let colorButtons = this.colors.map((color, i) => - <Button size="sm" className="btn-color" key={"button" + this.alphabet[i]} - style={{"backgroundColor": color, "borderColor": this.state.color === color ? "#fff" : color}} - onClick={() => this.setState({color: color})}>{this.alphabet[i]}</Button>); - - return ( - <Modal - {...this.props} - show="true" - size="lg" - aria-labelledby="services-dialog" - centered - > - <Modal.Header> - <Modal.Title id="services-dialog"> - ~/services - </Modal.Title> - </Modal.Header> - <Modal.Body> - <Container> - <Row> - <Col md={7}> - <Table borderless size="sm" className="services-list"> - <thead> - <tr> - <th><Button size="sm" onClick={this.newService}>new</Button></th> - <th>port</th> - <th>name</th> - </tr> - </thead> - <tbody> - {rows} - </tbody> - </Table> - </Col> - <Col md={5}> - <Form> - <Form.Group controlId="servicePort"> - <Form.Label>port:</Form.Label> - <Form.Control type="text" onChange={this.portChanged} value={this.state.port}/> - </Form.Group> - - <Form.Group controlId="serviceName"> - <Form.Label>name:</Form.Label> - <Form.Control type="text" onChange={this.nameChanged} value={this.state.name}/> - </Form.Group> - - <Form.Group controlId="serviceColor"> - <Form.Label>color:</Form.Label> - <ButtonGroup aria-label="Basic example"> - {colorButtons.slice(0, 8)} - </ButtonGroup> - <ButtonGroup aria-label="Basic example"> - {colorButtons.slice(8, 18)} - </ButtonGroup> - </Form.Group> - - <Form.Group controlId="serviceNotes"> - <Form.Label>notes:</Form.Label> - <Form.Control as="textarea" rows={3} onChange={this.notesChanged} - value={this.state.notes}/> - </Form.Group> - </Form> - - - </Col> - - </Row> - - <Row> - <Col md={12}> - <InputGroup> - <FormControl as="textarea" rows={4} className="curl-output" readOnly={true} - value={output}/> - </InputGroup> - - </Col> - </Row> - </Container> - </Modal.Body> - <Modal.Footer className="dialog-footer"> - <Button variant="green" onClick={this.saveService}>save</Button> - <Button variant="red" onClick={this.props.onHide}>close</Button> - </Modal.Footer> - </Modal> - ); - } -} - -export default Services; diff --git a/frontend/src/views/Services.scss b/frontend/src/views/Services.scss deleted file mode 100644 index 2abb55e..0000000 --- a/frontend/src/views/Services.scss +++ /dev/null @@ -1,32 +0,0 @@ -@import '../colors.scss'; - -.curl-output { - width: 100%; - font-size: 13px; -} - -.services-list { - .btn { - width: 70px; - } - - td { - background-color: $color-primary-2; - border-top: 2px solid $color-primary-0; - vertical-align: middle; - } - - th { - background-color: $color-primary-1; - } -} - -.btn-color { - border: 3px solid #fff; -} - -.dialog-footer { - .btn { - width: 80px; - } -} |