feat: refactor di alcune cose e grafici
This commit is contained in:
+94
-10
@@ -1,11 +1,11 @@
|
|||||||
package database
|
package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"math/rand"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.phc.dm.unipi.it/phc/cluster-dashboard/backend/executor"
|
"git.phc.dm.unipi.it/phc/cluster-dashboard/backend/executor"
|
||||||
"git.phc.dm.unipi.it/phc/cluster-dashboard/backend/model"
|
"git.phc.dm.unipi.it/phc/cluster-dashboard/backend/model"
|
||||||
"golang.org/x/exp/maps"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// simpleDB è una implementazione di [database.Database] che tiene giusto una cache in memoria e
|
// simpleDB è una implementazione di [database.Database] che tiene giusto una cache in memoria e
|
||||||
@@ -37,37 +37,121 @@ func NewSimpleDatabase(ex executor.Service) Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *simpleDB) GetNode(hostname string) (*model.Node, error) {
|
func (s *simpleDB) GetNode(hostname string) (*model.Node, error) {
|
||||||
panic("todo")
|
return &model.Node{
|
||||||
|
Hostname: hostname,
|
||||||
|
StartTime: time.Now().Add(-1 * time.Hour),
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *simpleDB) GetJob(id string) (*model.Job, error) {
|
func (s *simpleDB) GetJob(id string) (*model.Job, error) {
|
||||||
panic("todo")
|
return &model.Job{
|
||||||
|
Id: id,
|
||||||
|
Name: "example-job",
|
||||||
|
Status: "active",
|
||||||
|
Nodes: []string{"node-1", "node-2"},
|
||||||
|
Resources: []string{},
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *simpleDB) AllNodes() ([]*model.Node, error) {
|
func (s *simpleDB) AllNodes() ([]*model.Node, error) {
|
||||||
return maps.Values(s.nodes), nil
|
return []*model.Node{
|
||||||
|
{
|
||||||
|
Hostname: "node-1",
|
||||||
|
StartTime: time.Now().Add(-1 * time.Hour),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Hostname: "node-2",
|
||||||
|
StartTime: time.Now().Add(-1 * time.Hour),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Hostname: "node-3",
|
||||||
|
StartTime: time.Now().Add(-1 * time.Hour),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *simpleDB) AllJobs() ([]*model.Job, error) {
|
func (s *simpleDB) AllJobs() ([]*model.Job, error) {
|
||||||
return maps.Values(s.jobs), nil
|
return []*model.Job{
|
||||||
|
{
|
||||||
|
Id: "job-1",
|
||||||
|
Name: "example-job",
|
||||||
|
Status: "active",
|
||||||
|
Nodes: []string{"node-1", "node-2"},
|
||||||
|
Resources: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "job-2",
|
||||||
|
Name: "example-job",
|
||||||
|
Status: "active",
|
||||||
|
Nodes: []string{"node-1", "node-2"},
|
||||||
|
Resources: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "job-3",
|
||||||
|
Name: "example-job",
|
||||||
|
Status: "active",
|
||||||
|
Nodes: []string{"node-1", "node-2"},
|
||||||
|
Resources: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "job-4",
|
||||||
|
Name: "example-job",
|
||||||
|
Status: "active",
|
||||||
|
Nodes: []string{"node-1", "node-2"},
|
||||||
|
Resources: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "job-5",
|
||||||
|
Name: "example-job",
|
||||||
|
Status: "active",
|
||||||
|
Nodes: []string{"node-1", "node-2"},
|
||||||
|
Resources: []string{},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *simpleDB) QueryTemperatureSamples(from, to time.Time) ([]model.Sample[float64], error) {
|
func (s *simpleDB) QueryTemperatureSamples(from, to time.Time) ([]model.Sample[float64], error) {
|
||||||
panic("todo")
|
return generateFakeFloat64Samples(100), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *simpleDB) QueryMemorySamples(from, to time.Time) ([]model.Sample[int64], error) {
|
func (s *simpleDB) QueryMemorySamples(from, to time.Time) ([]model.Sample[int64], error) {
|
||||||
panic("todo")
|
return generateFakeInt64Samples(100), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *simpleDB) QueryStorageSamples(from, to time.Time) ([]model.Sample[int64], error) {
|
func (s *simpleDB) QueryStorageSamples(from, to time.Time) ([]model.Sample[int64], error) {
|
||||||
panic("todo")
|
return generateFakeInt64Samples(100), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *simpleDB) QueryNetworkUploadSamples(from, to time.Time) ([]model.Sample[int64], error) {
|
func (s *simpleDB) QueryNetworkUploadSamples(from, to time.Time) ([]model.Sample[int64], error) {
|
||||||
panic("todo")
|
return generateFakeInt64Samples(100), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *simpleDB) QueryNetworkDownloadSamples(from, to time.Time) ([]model.Sample[int64], error) {
|
func (s *simpleDB) QueryNetworkDownloadSamples(from, to time.Time) ([]model.Sample[int64], error) {
|
||||||
panic("todo")
|
return generateFakeInt64Samples(100), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateFakeFloat64Samples(amount int) []model.Sample[float64] {
|
||||||
|
fakeSamples := make([]model.Sample[float64], amount)
|
||||||
|
|
||||||
|
for i := range fakeSamples {
|
||||||
|
fakeSamples[i] = model.Sample[float64]{
|
||||||
|
Timestamp: time.Now().Add(time.Duration(i-amount) * time.Second),
|
||||||
|
Value: rand.Float64(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fakeSamples
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateFakeInt64Samples(amount int) []model.Sample[int64] {
|
||||||
|
fakeSamples := make([]model.Sample[int64], amount)
|
||||||
|
|
||||||
|
for i := range fakeSamples {
|
||||||
|
fakeSamples[i] = model.Sample[int64]{
|
||||||
|
Timestamp: time.Now().Add(time.Duration(i-amount) * time.Second),
|
||||||
|
Value: rand.Int63n(1000),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fakeSamples
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-106
@@ -25,111 +25,6 @@
|
|||||||
<link rel="stylesheet" href="/styles/main.scss" />
|
<link rel="stylesheet" href="/styles/main.scss" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="sidebar">
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
<div class="top">
|
|
||||||
<div class="logo">
|
|
||||||
<pre><code>_______________________ _______ _______ _______ _
|
|
||||||
( ____ \__ __( ____ ( ____ ( ____ ( ____ ( )
|
|
||||||
| ( \/ ) ( | ( \/ ( \/ ( \/ ( \//
|
|
||||||
| (_____ | | | (__ | (__ | (__ | (__
|
|
||||||
(_____ ) | | | __) | __) | __) | __)
|
|
||||||
) | | | | ( | ( | ( | (
|
|
||||||
/\____) | | | | (____/\ ) | ) | (____/\
|
|
||||||
\_______) )_( (_______// |/ (_______/</code></pre>
|
|
||||||
</div>
|
|
||||||
<div class="item">
|
|
||||||
<div class="icon">
|
|
||||||
<span class="material-symbols-outlined">
|
|
||||||
dashboard
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="label">Overview</div>
|
|
||||||
</div>
|
|
||||||
<div class="item">
|
|
||||||
<div class="icon">
|
|
||||||
<span class="material-symbols-outlined">
|
|
||||||
device_thermostat
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="label">Temperature</div>
|
|
||||||
</div>
|
|
||||||
<div class="item">
|
|
||||||
<div class="icon">
|
|
||||||
<span class="material-symbols-outlined"> speed </span>
|
|
||||||
</div>
|
|
||||||
<div class="label">CPU</div>
|
|
||||||
</div>
|
|
||||||
<div class="item">
|
|
||||||
<div class="icon">
|
|
||||||
<span class="material-symbols-outlined">
|
|
||||||
swap_vert
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="label">Network</div>
|
|
||||||
</div>
|
|
||||||
<div class="item">
|
|
||||||
<div class="icon">
|
|
||||||
<span class="material-symbols-outlined"> memory </span>
|
|
||||||
</div>
|
|
||||||
<div class="label">Memory</div>
|
|
||||||
</div>
|
|
||||||
<div class="item">
|
|
||||||
<div class="icon">
|
|
||||||
<span class="material-symbols-outlined">
|
|
||||||
database
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="label">Storage</div>
|
|
||||||
</div>
|
|
||||||
<div class="item">
|
|
||||||
<div class="icon">
|
|
||||||
<span class="material-symbols-outlined">
|
|
||||||
view_list
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="label">Jobs</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="bottom">
|
|
||||||
<div class="item">
|
|
||||||
<div class="icon">
|
|
||||||
<img src="/public/logo-gitea.svg" alt="logo gitea" />
|
|
||||||
</div>
|
|
||||||
<div class="label"><a href="#">Dashboard</a></div>
|
|
||||||
</div>
|
|
||||||
<div class="item">
|
|
||||||
<div class="icon">
|
|
||||||
<img src="/public/logo-gitea.svg" alt="logo gitea" />
|
|
||||||
</div>
|
|
||||||
<div class="label"><a href="#">cpar2023</a></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="main">
|
|
||||||
<div class="card">
|
|
||||||
<div class="label">Uptime</div>
|
|
||||||
<div class="graph"></div>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<div class="label">CPU</div>
|
|
||||||
<div class="graph"></div>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<div class="label">Storage</div>
|
|
||||||
<div class="graph"></div>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<div class="label">Memory</div>
|
|
||||||
<div class="graph"></div>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<div class="label">Network</div>
|
|
||||||
<div class="graph"></div>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<div class="label">Temperature</div>
|
|
||||||
<div class="graph"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { useEffect, useState } from 'preact/hooks'
|
||||||
|
import { Plot } from './Plot.jsx'
|
||||||
|
import { server } from '../utils.js'
|
||||||
|
|
||||||
|
export const GraphCard = ({ label, url }) => {
|
||||||
|
const [samples, setSamples] = useState([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (url) {
|
||||||
|
server.get(url).then(data => setSamples(data))
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="card">
|
||||||
|
<div class="label">{label}</div>
|
||||||
|
<div class="graph">{url && <Plot samples={samples} />}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,13 +1,83 @@
|
|||||||
import d3 from 'd3'
|
import * as d3 from 'd3'
|
||||||
|
import { useEffect, useRef } from 'preact/hooks'
|
||||||
|
|
||||||
// export const Plot = ({ samples }) => {
|
// set the dimensions and margins of the graph
|
||||||
// const mountElement = el => {
|
const margin = { top: 10, right: 10, bottom: 30, left: 30 }
|
||||||
// d3.select(el).append('svg')
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return <div class="d3-plot" ref={mountElement}></div>
|
|
||||||
// }
|
|
||||||
|
|
||||||
export const Plot = ({ samples }) => {
|
export const Plot = ({ samples }) => {
|
||||||
return <div class="d3-plot">Work in progress...</div>
|
if (samples.length === 0) {
|
||||||
|
return <>Loading...</>
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = samples.map(({ timestamp, value }) => ({
|
||||||
|
timestamp: d3.isoParse(timestamp),
|
||||||
|
value: value,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const plotRef = useRef(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (plotRef.current) {
|
||||||
|
const el = plotRef.current
|
||||||
|
|
||||||
|
const width = el.offsetWidth - margin.left - margin.right
|
||||||
|
const height = el.offsetHeight - margin.top - margin.bottom
|
||||||
|
|
||||||
|
const svg = d3
|
||||||
|
.select(el)
|
||||||
|
.append('svg')
|
||||||
|
.attr('width', el.offsetWidth)
|
||||||
|
.attr('height', el.offsetHeight)
|
||||||
|
.append('g')
|
||||||
|
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
|
||||||
|
|
||||||
|
const x = d3
|
||||||
|
.scaleTime()
|
||||||
|
.domain(
|
||||||
|
d3.extent(data, function (d) {
|
||||||
|
return d.timestamp
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.range([0, width])
|
||||||
|
|
||||||
|
const y = d3
|
||||||
|
.scaleLinear()
|
||||||
|
.domain([
|
||||||
|
0,
|
||||||
|
d3.max(data, function (d) {
|
||||||
|
return +d.value
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
.range([height, 0])
|
||||||
|
|
||||||
|
svg.append('g')
|
||||||
|
.attr('transform', 'translate(0,' + height + ')')
|
||||||
|
.call(d3.axisBottom(x))
|
||||||
|
|
||||||
|
svg.append('g').call(d3.axisLeft(y))
|
||||||
|
|
||||||
|
const line = svg.append('g')
|
||||||
|
|
||||||
|
line.append('path')
|
||||||
|
.datum(data)
|
||||||
|
.attr('fill', 'none')
|
||||||
|
.attr('stroke', 'steelblue')
|
||||||
|
.attr('stroke-width', 2)
|
||||||
|
.attr(
|
||||||
|
'd',
|
||||||
|
d3
|
||||||
|
.line()
|
||||||
|
.x(d => x(d.timestamp))
|
||||||
|
.y(d => y(d.value))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (plotRef.current) {
|
||||||
|
plotRef.current.firstChild.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [plotRef])
|
||||||
|
|
||||||
|
return <div class="d3-plot" ref={plotRef}></div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { DashboardPage, JobsPage } from './pages.jsx'
|
|||||||
|
|
||||||
const Redirect = ({ to }) => {
|
const Redirect = ({ to }) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
console.log('Redirecting...')
|
||||||
route(to, true)
|
route(to, true)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|||||||
+95
-1
@@ -1,6 +1,100 @@
|
|||||||
|
import { GraphCard } from './components/Graph.jsx'
|
||||||
|
|
||||||
|
const LOGO = String.raw`
|
||||||
|
_______________________ _______ _______ _______ _
|
||||||
|
( ____ \__ __( ____ ( ____ ( ____ ( ____ ( )
|
||||||
|
| ( \/ ) ( | ( \/ ( \/ ( \/ ( \//
|
||||||
|
| (_____ | | | (__ | (__ | (__ | (__
|
||||||
|
(_____ ) | | | __) | __) | __) | __)
|
||||||
|
) | | | | ( | ( | ( | (
|
||||||
|
/\____) | | | | (____/\ ) | ) | (____/\
|
||||||
|
\_______) )_( (_______// |/ (_______/
|
||||||
|
`.trim()
|
||||||
|
|
||||||
export const DashboardPage = ({}) => {
|
export const DashboardPage = ({}) => {
|
||||||
return <>Dashboard Page</>
|
return (
|
||||||
|
<>
|
||||||
|
<div class="sidebar">
|
||||||
|
<div class="top">
|
||||||
|
<div class="logo">
|
||||||
|
<pre>
|
||||||
|
<code>{LOGO}</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
<div class="icon">
|
||||||
|
<span class="material-symbols-outlined">dashboard</span>
|
||||||
|
</div>
|
||||||
|
<div class="label">Overview</div>
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
<div class="icon">
|
||||||
|
<span class="material-symbols-outlined">device_thermostat</span>
|
||||||
|
</div>
|
||||||
|
<div class="label">Temperature</div>
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
<div class="icon">
|
||||||
|
<span class="material-symbols-outlined"> speed </span>
|
||||||
|
</div>
|
||||||
|
<div class="label">CPU</div>
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
<div class="icon">
|
||||||
|
<span class="material-symbols-outlined">swap_vert</span>
|
||||||
|
</div>
|
||||||
|
<div class="label">Network</div>
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
<div class="icon">
|
||||||
|
<span class="material-symbols-outlined"> memory </span>
|
||||||
|
</div>
|
||||||
|
<div class="label">Memory</div>
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
<div class="icon">
|
||||||
|
<span class="material-symbols-outlined">database</span>
|
||||||
|
</div>
|
||||||
|
<div class="label">Storage</div>
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
<div class="icon">
|
||||||
|
<span class="material-symbols-outlined">view_list</span>
|
||||||
|
</div>
|
||||||
|
<div class="label">Jobs</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bottom">
|
||||||
|
<div class="item link">
|
||||||
|
<div class="icon">
|
||||||
|
<img src="/public/logo-gitea.svg" alt="logo gitea" />
|
||||||
|
</div>
|
||||||
|
<div class="label">
|
||||||
|
<a href="https://git.phc.dm.unipi.it/phc/cluster-dashboard">Dashboard</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="item link">
|
||||||
|
<div class="icon">
|
||||||
|
<img src="/public/logo-gitea.svg" alt="logo gitea" />
|
||||||
|
</div>
|
||||||
|
<div class="label">
|
||||||
|
<a href="https://git.phc.dm.unipi.it/fdurastante/cpar2023">cpar2023</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="main">
|
||||||
|
<GraphCard label="Uptime" />
|
||||||
|
<GraphCard label="CPU" />
|
||||||
|
<GraphCard label="Storage" />
|
||||||
|
<GraphCard label="Memory" />
|
||||||
|
<GraphCard label="Network" />
|
||||||
|
<GraphCard label="Temperature" url="/api/stats/temperature" />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const JobsPage = ({}) => {
|
export const JobsPage = ({}) => {
|
||||||
return <>Jobs Page</>
|
return <>Jobs Page</>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
export const server = {
|
||||||
|
async get(url) {
|
||||||
|
const res = await fetch(url)
|
||||||
|
return await res.json()
|
||||||
|
},
|
||||||
|
async post(url, data) {
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
})
|
||||||
|
|
||||||
|
return await res.json()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
window.server = server
|
||||||
@@ -10,7 +10,7 @@ body {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
|
||||||
font-family: "Open Sans", "Segoe UI", "Helvetica", "Arial", sans-serif;
|
font-family: 'Open Sans', 'Segoe UI', 'Helvetica', 'Arial', sans-serif;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
|
||||||
color: #284a36;
|
color: #284a36;
|
||||||
@@ -57,7 +57,7 @@ body {
|
|||||||
|
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
|
||||||
font-family: "Ubuntu Mono", monospace;
|
font-family: 'Ubuntu Mono', monospace;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -99,6 +99,22 @@ body {
|
|||||||
&:hover {
|
&:hover {
|
||||||
background-color: #e8ede9;
|
background-color: #e8ede9;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.link {
|
||||||
|
padding: 0 16px;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -128,9 +144,13 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.graph {
|
.graph {
|
||||||
background-color: #f4f5f9;
|
|
||||||
|
|
||||||
min-height: 300px;
|
min-height: 300px;
|
||||||
|
|
||||||
|
.d3-plot {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user