mirror of
https://github.com/cupcakearmy/ora.git
synced 2026-04-02 12:05:23 +00:00
limits
This commit is contained in:
111
src/dashboard/components/Chart.svelte
Normal file
111
src/dashboard/components/Chart.svelte
Normal file
@@ -0,0 +1,111 @@
|
||||
<script>
|
||||
import * as d3 from 'd3'
|
||||
import { map, min, max } from 'lodash'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
let wrapper
|
||||
|
||||
export let data = [
|
||||
// { lang: 'ts', popularity: 10 },
|
||||
// { lang: 'js', popularity: 7 },
|
||||
// { lang: 'py', popularity: 9 },
|
||||
// { lang: 'rs', popularity: 8 },
|
||||
|
||||
// { year: 2018, value: 8 },
|
||||
// { year: 2019, value: 9 },
|
||||
// { year: 2020, value: 3 },
|
||||
|
||||
{ cat: 'Phillip', value: 10 },
|
||||
{ cat: 'Rita', value: 12 },
|
||||
{ cat: 'Tom', value: 20 },
|
||||
{ cat: 'Oscar', value: 19 },
|
||||
{ cat: 'Lulu', value: 8 },
|
||||
{ cat: 'Keko', value: 14 },
|
||||
{ cat: 'Lena', value: 9 },
|
||||
]
|
||||
|
||||
onMount(async () => {
|
||||
// Dynamic left padding depending on the labels
|
||||
const longestKey = max(map(data, (d) => d.name.length))
|
||||
const mt = Math.min(longestKey * 6, 120)
|
||||
const margin = { left: mt, top: 50, bottom: 50, right: 50 }
|
||||
const styles = window.getComputedStyle(wrapper)
|
||||
const barHeight = 20
|
||||
const width = parseInt(styles.width)
|
||||
const height = Math.ceil(data.length * 1.5 * barHeight)
|
||||
|
||||
const svg = d3.select(wrapper).attr('viewBox', [0, 0, width, height])
|
||||
|
||||
const yAxis = (g) =>
|
||||
g.attr('transform', `translate(${margin.left},0)`).call(
|
||||
d3
|
||||
.axisLeft(y)
|
||||
.tickFormat((i) => data[i].name)
|
||||
.tickSizeOuter(0)
|
||||
)
|
||||
const xAxis = (g) =>
|
||||
g
|
||||
.attr('transform', `translate(0,${margin.top})`)
|
||||
.call(d3.axisTop(x).ticks(width / 100, 's'))
|
||||
.call((g) => g.select('.domain').remove())
|
||||
|
||||
const y = d3
|
||||
.scaleBand()
|
||||
.domain(d3.range(data.length))
|
||||
.rangeRound([margin.bottom, height - margin.top])
|
||||
.padding(0.2)
|
||||
const x = d3
|
||||
.scaleLinear()
|
||||
.domain([0, d3.max(data, (d) => d.value)])
|
||||
.range([margin.left, width - margin.right])
|
||||
|
||||
const format = x.tickFormat(20, 's')
|
||||
|
||||
// Bars
|
||||
svg
|
||||
.append('g')
|
||||
.attr('fill', 'steelblue')
|
||||
.selectAll('rect')
|
||||
.data(data)
|
||||
.join('rect')
|
||||
.attr('x', x(0))
|
||||
.attr('y', (d, i) => y(i))
|
||||
.attr('width', (d) => x(d.value) - x(0))
|
||||
.attr('height', y.bandwidth())
|
||||
|
||||
svg
|
||||
.append('g')
|
||||
.attr('fill', 'white')
|
||||
.attr('text-anchor', 'end')
|
||||
.attr('font-family', 'sans-serif')
|
||||
.attr('font-size', 12)
|
||||
.selectAll('text')
|
||||
.data(data)
|
||||
.join('text')
|
||||
.attr('x', (d) => x(d.value))
|
||||
.attr('y', (d, i) => y(i) + y.bandwidth() / 2)
|
||||
.attr('dy', '0.35em')
|
||||
.attr('dx', -4)
|
||||
.text((d) => d.human)
|
||||
.call((text) =>
|
||||
text
|
||||
.filter((d) => x(d.value) - x(0) < d.human.length * 7) // short bars
|
||||
.attr('dx', +4)
|
||||
.attr('fill', 'black')
|
||||
.attr('text-anchor', 'start')
|
||||
)
|
||||
|
||||
svg.append('g').call(xAxis)
|
||||
|
||||
svg.append('g').call(yAxis)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
svg {
|
||||
width: 100%;
|
||||
/* height: 25em; */
|
||||
}
|
||||
</style>
|
||||
|
||||
<svg bind:this={wrapper} preserveAspectRatio="xMidYMid meet" />
|
||||
@@ -1,115 +0,0 @@
|
||||
<script>
|
||||
import * as am4core from '@amcharts/amcharts4/core'
|
||||
import * as am4charts from '@amcharts/amcharts4/charts'
|
||||
import am4themes_frozen from '@amcharts/amcharts4/themes/frozen'
|
||||
import am4themes_animated from '@amcharts/amcharts4/themes/animated'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
/* Chart code */
|
||||
// Themes begin
|
||||
am4core.useTheme(am4themes_frozen)
|
||||
am4core.useTheme(am4themes_animated)
|
||||
|
||||
let el
|
||||
let chart
|
||||
|
||||
export let data = [
|
||||
{
|
||||
network: 'Facebook',
|
||||
MAU: 2255250000,
|
||||
},
|
||||
{
|
||||
network: 'Google+',
|
||||
MAU: 430000000,
|
||||
},
|
||||
{
|
||||
network: 'Instagram',
|
||||
MAU: 1000000000,
|
||||
},
|
||||
{
|
||||
network: 'Pinterest',
|
||||
MAU: 246500000,
|
||||
},
|
||||
{
|
||||
network: 'Reddit',
|
||||
MAU: 355000000,
|
||||
},
|
||||
{
|
||||
network: 'TikTok',
|
||||
MAU: 500000000,
|
||||
},
|
||||
{
|
||||
network: 'Tumblr',
|
||||
MAU: 624000000,
|
||||
},
|
||||
{
|
||||
network: 'Twitter',
|
||||
MAU: 329500000,
|
||||
},
|
||||
{
|
||||
network: 'WeChat',
|
||||
MAU: 1000000000,
|
||||
},
|
||||
{
|
||||
network: 'Weibo',
|
||||
MAU: 431000000,
|
||||
},
|
||||
{
|
||||
network: 'Whatsapp',
|
||||
MAU: 1433333333,
|
||||
},
|
||||
{
|
||||
network: 'YouTube',
|
||||
MAU: 1900000000,
|
||||
},
|
||||
]
|
||||
|
||||
$: if (chart) {
|
||||
chart.data = data
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
chart = am4core.create(el, am4charts.XYChart)
|
||||
|
||||
let categoryAxis = chart.yAxes.push(new am4charts.CategoryAxis())
|
||||
categoryAxis.renderer.grid.template.location = 0
|
||||
categoryAxis.dataFields.category = 'host'
|
||||
categoryAxis.renderer.minGridDistance = 1
|
||||
categoryAxis.renderer.inversed = true
|
||||
categoryAxis.renderer.grid.template.disabled = true
|
||||
|
||||
let valueAxis = chart.xAxes.push(new am4charts.ValueAxis())
|
||||
valueAxis.min = 0
|
||||
|
||||
let series = chart.series.push(new am4charts.ColumnSeries())
|
||||
series.dataFields.categoryY = 'host'
|
||||
series.dataFields.valueX = 'total'
|
||||
series.columns.template.strokeOpacity = 0
|
||||
series.columns.template.column.cornerRadiusBottomRight = 5
|
||||
series.columns.template.column.cornerRadiusTopRight = 5
|
||||
|
||||
let labelBullet = series.bullets.push(new am4charts.LabelBullet())
|
||||
labelBullet.label.horizontalCenter = 'left'
|
||||
labelBullet.label.dx = 10
|
||||
labelBullet.label.text = '{values.valueX.workingValue}'
|
||||
// labelBullet.label.text = "{values.valueX.workingValue.formatNumber('#.0as')}"
|
||||
labelBullet.locationX = 1
|
||||
|
||||
// as by default columns of the same series are of the same color, we add adapter which takes colors from chart.colors color set
|
||||
series.columns.template.adapter.add('fill', function (fill, target) {
|
||||
return chart.colors.getIndex(target.dataItem.index)
|
||||
})
|
||||
|
||||
categoryAxis.sortBySeries = series
|
||||
chart.data = data
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
div {
|
||||
width: 100%;
|
||||
height: 32em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div bind:this={el}>chart</div>
|
||||
@@ -1,68 +0,0 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
import Chart from 'chart.js'
|
||||
import palette from 'google-palette'
|
||||
|
||||
Chart.defaults.global.legend.display = false
|
||||
|
||||
export let type = 'horizontalBar'
|
||||
export let data = {
|
||||
labels: [],
|
||||
data: [],
|
||||
}
|
||||
export let options = {
|
||||
scales: {
|
||||
xAxes: [
|
||||
{
|
||||
// type: 'logarithmic',
|
||||
ticks: {
|
||||
beginAtZero: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
let ctx
|
||||
let mounted
|
||||
|
||||
function draw() {
|
||||
const backgroundColor = palette('rainbow', data.data.length).map((color) => '#' + color + '88')
|
||||
|
||||
new Chart(ctx, {
|
||||
type,
|
||||
options,
|
||||
data: {
|
||||
labels: data.labels,
|
||||
datasets: [
|
||||
{
|
||||
data: data.data,
|
||||
// backgroundColor: backgroundColor,
|
||||
backgroundColor: '#dddddd',
|
||||
borderColor: '#000000',
|
||||
borderWidth: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
$: if (mounted) {
|
||||
data
|
||||
draw()
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
mounted = true
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
canvas {
|
||||
width: 100%;
|
||||
height: 20em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<canvas bind:this={ctx} />
|
||||
@@ -3,7 +3,7 @@
|
||||
import day from 'dayjs'
|
||||
import { range, random } from 'lodash'
|
||||
|
||||
import { insertLog, normalizeTimestamp } from '../../shared/db'
|
||||
import { insertLog, normalizeTimestamp, clear as clearDB } from '../../shared/db'
|
||||
|
||||
let loading = false
|
||||
|
||||
@@ -30,14 +30,22 @@
|
||||
async function clear() {
|
||||
try {
|
||||
loading = true
|
||||
await Logs.remove({}, { multi: true })
|
||||
await clearDB()
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
div {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="p-2">
|
||||
<button class="btn" class:loading disabled={loading} on:click={fill}>Add Random Data</button>
|
||||
<button class="btn btn-error" class:loading disabled={loading} on:click={clear}>Delete data</button>
|
||||
<button class="btn btn-sm" class:loading disabled={loading} on:click={fill}>Add Random Data</button>
|
||||
<button class="btn btn-sm btn-error" class:loading disabled={loading} on:click={clear}>Delete data</button>
|
||||
</div>
|
||||
|
||||
10
src/dashboard/components/Rules.svelte
Normal file
10
src/dashboard/components/Rules.svelte
Normal file
@@ -0,0 +1,10 @@
|
||||
<script>
|
||||
import dj from 'dayjs'
|
||||
import { Logs } from '../../shared/db.js'
|
||||
|
||||
export let rules = []
|
||||
</script>
|
||||
|
||||
{#each rules as rule}
|
||||
<div>{dj.duration(...rule.limit).humanize()} / {dj.duration(...rule.every).humanize()}</div>
|
||||
{/each}
|
||||
80
src/dashboard/components/RulesEditor.svelte
Normal file
80
src/dashboard/components/RulesEditor.svelte
Normal file
@@ -0,0 +1,80 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { cloneDeep } from 'lodash'
|
||||
|
||||
import { Limits } from '../../shared/db'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const init = { limit: ['1', 'h'], every: [1, 'd'] }
|
||||
|
||||
export let limit = null
|
||||
$: active = limit !== null
|
||||
|
||||
function add() {
|
||||
limit.rules = [...limit.rules, cloneDeep(init)]
|
||||
}
|
||||
|
||||
function del(i) {
|
||||
return () => (limit.rules = limit.rules.filter((_, n) => n !== i))
|
||||
}
|
||||
|
||||
function close() {
|
||||
limit = null
|
||||
}
|
||||
|
||||
async function save() {
|
||||
await Limits.update({ host: limit.host }, limit, { upsert: true })
|
||||
dispatch('update')
|
||||
close()
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class:active class="modal">
|
||||
<a on:click={close} class="modal-overlay" aria-label="Close" />
|
||||
<div class="modal-container">
|
||||
<div class="modal-header">
|
||||
<a on:click={close} class="btn btn-clear float-right" aria-label="Close" />
|
||||
<div class="modal-title h5">{limit?._id ? 'Edit limit' : 'Create new limit'}</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="content">
|
||||
{#if limit}
|
||||
<label class="form-label">
|
||||
Host <input type="text" class="form-input" placeholder="google.com" bind:value={limit.host} />
|
||||
</label>
|
||||
|
||||
<div class="form-label">Rules</div>
|
||||
{#each limit.rules as { limit, every }, i}
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" class="form-input" placeholder="1" bind:value={limit[0]} />
|
||||
<select class="form-select" bind:value={limit[1]}>
|
||||
<option value="m">Minutes</option>
|
||||
<option value="h">Hours</option>
|
||||
<option value="d">Days</option>
|
||||
<option value="w">Weeks</option>
|
||||
<option value="M">Months</option>
|
||||
</select>
|
||||
<span class="input-group-addon">every</span>
|
||||
<input type="text" class="form-input" bind:value={every[0]} />
|
||||
<select class="form-select" bind:value={every[1]}>
|
||||
<option value="m">Minutes</option>
|
||||
<option value="h">Hours</option>
|
||||
<option value="d">Days</option>
|
||||
<option value="w">Weeks</option>
|
||||
<option value="M">Months</option>
|
||||
</select>
|
||||
<button class="btn btn-error input-group-btn" on:click={del(i)}>X</button>
|
||||
</div>
|
||||
{/each}
|
||||
<button class="btn" on:click={add}>Add</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div>
|
||||
<button on:click={close} class="btn">Cancel</button>
|
||||
<button on:click={save} class="btn btn-primary">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user