pull/1/head
parent
36ad084621
commit
f76004a261
@ -1,2 +1,7 @@
|
||||
.bin
|
||||
.idea
|
||||
.idea
|
||||
|
||||
client/node_modules/
|
||||
client/public/build/
|
||||
|
||||
.DS_Store
|
||||
|
@ -0,0 +1,6 @@
|
||||
/node_modules/
|
||||
/public/build/
|
||||
.vscode/
|
||||
.DS_Store
|
||||
package-lock.json
|
||||
yarn.lock
|
@ -0,0 +1,10 @@
|
||||
# Svelte TypeScript Tailwindcss Setup
|
||||
|
||||
svelte template based on the default svelte/template with enabled typescript and tailwindcss support.
|
||||
|
||||
```bash
|
||||
npx degit munxar/svelte-template my-svelte-project
|
||||
cd my-svelte-project
|
||||
npm i
|
||||
npm run dev
|
||||
```
|
@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "svelte-app",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"build": "NODE_ENV=production rollup -c",
|
||||
"dev": "rollup -c -w",
|
||||
"start": "sirv public",
|
||||
"validate": "svelte-check"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^17.0.0",
|
||||
"@rollup/plugin-node-resolve": "^11.0.0",
|
||||
"@rollup/plugin-typescript": "^8.1.0",
|
||||
"@tsconfig/svelte": "^1.0.0",
|
||||
"autoprefixer": "^10.2.3",
|
||||
"postcss": "^8.2.4",
|
||||
"rollup": "^2.3.4",
|
||||
"rollup-plugin-css-only": "^3.1.0",
|
||||
"rollup-plugin-livereload": "^2.0.0",
|
||||
"rollup-plugin-svelte": "^7.0.0",
|
||||
"rollup-plugin-terser": "^7.0.0",
|
||||
"svelte": "^3.38.2",
|
||||
"svelte-check": "^1.0.0",
|
||||
"svelte-preprocess": "^4.6.3",
|
||||
"tailwindcss": "^2.0.2",
|
||||
"tslib": "^2.0.0",
|
||||
"typescript": "^4.1.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.3",
|
||||
"bootstrap": "^5.0.0",
|
||||
"chart.js": "^3.2.1",
|
||||
"date-fns": "^2.21.3",
|
||||
"fa-svelte": "^3.1.0",
|
||||
"normalize.css": "^8.0.1",
|
||||
"sirv-cli": "^1.0.0",
|
||||
"svelte-chartjs": "^1.0.1",
|
||||
"svelte-routing": "^1.6.0",
|
||||
"sveltestrap": "^4.2.1"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 3.1 KiB |
@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
|
||||
<title>Svelte app</title>
|
||||
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="stylesheet" href="/build/bundle.css" />
|
||||
|
||||
<script defer src="/build/bundle.js"></script>
|
||||
</head>
|
||||
|
||||
<body></body>
|
||||
</html>
|
@ -0,0 +1,74 @@
|
||||
import svelte from "rollup-plugin-svelte";
|
||||
import commonjs from "@rollup/plugin-commonjs";
|
||||
import resolve from "@rollup/plugin-node-resolve";
|
||||
import livereload from "rollup-plugin-livereload";
|
||||
import { terser } from "rollup-plugin-terser";
|
||||
import sveltePreprocess from "svelte-preprocess";
|
||||
import typescript from "@rollup/plugin-typescript";
|
||||
import css from "rollup-plugin-css-only";
|
||||
|
||||
const production = !process.env.ROLLUP_WATCH;
|
||||
|
||||
function serve() {
|
||||
let server;
|
||||
|
||||
function toExit() {
|
||||
if (server) server.kill(0);
|
||||
}
|
||||
|
||||
return {
|
||||
writeBundle() {
|
||||
if (server) return;
|
||||
server = require("child_process").spawn(
|
||||
"npm",
|
||||
["run", "start", "--", "--dev"],
|
||||
{
|
||||
stdio: ["ignore", "inherit", "inherit"],
|
||||
shell: true,
|
||||
}
|
||||
);
|
||||
|
||||
process.on("SIGTERM", toExit);
|
||||
process.on("exit", toExit);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
input: "src/main.ts",
|
||||
output: {
|
||||
sourcemap: true,
|
||||
format: "iife",
|
||||
name: "app",
|
||||
file: "public/build/bundle.js",
|
||||
},
|
||||
plugins: [
|
||||
svelte({
|
||||
// add postcss config with tailwind
|
||||
preprocess: sveltePreprocess({
|
||||
postcss: {
|
||||
plugins: [require("tailwindcss"), require("autoprefixer")],
|
||||
},
|
||||
}),
|
||||
compilerOptions: {
|
||||
dev: !production,
|
||||
},
|
||||
}),
|
||||
css({ output: "bundle.css" }),
|
||||
resolve({
|
||||
browser: true,
|
||||
dedupe: ["svelte"],
|
||||
}),
|
||||
commonjs(),
|
||||
typescript({
|
||||
sourceMap: !production,
|
||||
inlineSources: !production,
|
||||
}),
|
||||
!production && serve(),
|
||||
!production && livereload("public"),
|
||||
production && terser(),
|
||||
],
|
||||
watch: {
|
||||
clearScreen: false,
|
||||
},
|
||||
};
|
@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import "bootstrap/dist/css/bootstrap.min.css";
|
||||
import Tailwind from "./Tailwind.svelte";
|
||||
|
||||
import { Router, Link, Route } from "svelte-routing";
|
||||
import Home from "./routes/Home.svelte";
|
||||
|
||||
export let url = "";
|
||||
</script>
|
||||
|
||||
<Tailwind />
|
||||
<main class="flex w-full h-full">
|
||||
<Router {url}>
|
||||
<nav class="bg-gray-700 text-gray-300 w-1/8 py-5 px-2 shadow-md">
|
||||
<Link to="/">Home</Link>
|
||||
</nav>
|
||||
<div class="flex-column py-5 px-5 w-full h-full overflow-y-auto">
|
||||
<Route path="/" component={Home} />
|
||||
</div>
|
||||
</Router>
|
||||
</main>
|
@ -0,0 +1,14 @@
|
||||
<style global lang="postcss">
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body,
|
||||
html {
|
||||
@apply w-full h-full;
|
||||
}
|
||||
|
||||
nav {
|
||||
width: 100px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,25 @@
|
||||
import { sub } from 'date-fns'
|
||||
import { formatDate } from './metrics';
|
||||
|
||||
|
||||
export const getLogs = async function ({
|
||||
start = sub(new Date(), { hours: 12 }),
|
||||
end = new Date(),
|
||||
filter = ""
|
||||
} = {}) {
|
||||
try {
|
||||
const data = await fetch(`http://localhost:8080/api/v1/metrics/log?filter=${filter}`, {
|
||||
"method": "GET",
|
||||
"headers": { "Accept": "application/json" }
|
||||
});
|
||||
|
||||
const { success, payload } = await data.json();
|
||||
if (success) {
|
||||
return { payload, start, end, filter };
|
||||
}
|
||||
return { error: payload };
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { error };
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
import { sub, format } from 'date-fns';
|
||||
import { readable } from "svelte/store";
|
||||
|
||||
export const formatDate = date => format(date, "yyyy-MM-dd HH:mm:ss.SSS");
|
||||
|
||||
/*
|
||||
take something like `clientIp:127.0.0.1 protocol:udp domain:google.com`
|
||||
`(prop:expr)*
|
||||
*/
|
||||
export const parseTerms = (terms: string = "") :[string] | [] => {
|
||||
|
||||
const matches = terms.match(/([a-z]+[:>][\w\.]+)*/ig);
|
||||
|
||||
const exprs = matches.reduce((agg, term) => {
|
||||
|
||||
}, []);
|
||||
return [];
|
||||
};
|
||||
|
||||
export const fetchMetrics = async function ({
|
||||
start = sub(new Date(), { hours: 8 }),
|
||||
end = new Date(),
|
||||
key = "domain",
|
||||
interval = "30",
|
||||
} = {}) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`http://localhost:8080/api/v1/metrics/stats?start=${formatDate(
|
||||
start
|
||||
)}&end=${formatDate(end)}&key=${key}&interval=${interval}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: { Accept: "application/json" }
|
||||
}
|
||||
);
|
||||
|
||||
const { success, payload } = await response.json();
|
||||
|
||||
if (!success) {
|
||||
return { error: payload };
|
||||
}
|
||||
|
||||
return { payload, interval, start, end, key };
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return { error: e };
|
||||
}
|
||||
};
|
||||
|
||||
export const metricsStore = readable([], (set) => {
|
||||
const interval = setInterval(async () => {
|
||||
const { error, payload } = await fetchMetrics({});
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(payload);
|
||||
set(payload);
|
||||
}, 15 * 1000);
|
||||
return () => clearInterval(interval);
|
||||
});
|
@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
Input,
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupText,
|
||||
} from "sveltestrap";
|
||||
export let label = "label";
|
||||
</script>
|
||||
|
||||
<InputGroup class="flex flex-row">
|
||||
<InputGroupAddon addonType="prepend">
|
||||
<InputGroupText>{label}</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
<Input bsSize="sm" type="date" />
|
||||
<Input bsSize="sm" type="time" />
|
||||
</InputGroup>
|
@ -0,0 +1,52 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { Column, Table } from "sveltestrap";
|
||||
import { getLogs } from "../api/logs";
|
||||
|
||||
let rows = [];
|
||||
onMount(async () => {
|
||||
try {
|
||||
const { payload } = await getLogs();
|
||||
console.log(payload);
|
||||
rows = payload || [];
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex flex-column text-sm">
|
||||
{#if rows.length > 0}
|
||||
<Table {rows} let:row hover bordered>
|
||||
<Column header="Started">
|
||||
{row.Started}
|
||||
</Column>
|
||||
<Column header="Domain">
|
||||
{row.Domain}
|
||||
</Column>
|
||||
<Column header="IP">
|
||||
{row.ClientIP}
|
||||
</Column>
|
||||
<Column header="Status">
|
||||
{row.Status}
|
||||
</Column>
|
||||
<Column header="Protocol">
|
||||
{row.Protocol}
|
||||
</Column>
|
||||
<Column header="Error">
|
||||
{#if row.Error}
|
||||
{row.Error}
|
||||
{/if}
|
||||
</Column>
|
||||
<Column header="Round Trip Ms">
|
||||
{row.RecurseRoundTripTimeMs}
|
||||
</Column>
|
||||
<Column header="Upstream Used">
|
||||
{row.RecurseUpstreamIP}
|
||||
</Column>
|
||||
<Column header="Total Time Ms">
|
||||
{row.TotalTimeMs}
|
||||
</Column>
|
||||
</Table>
|
||||
{/if}
|
||||
</div>
|
@ -0,0 +1,85 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownMenu,
|
||||
DropdownItem,
|
||||
DropdownToggle,
|
||||
Form,
|
||||
FormGroup,
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupText,
|
||||
Label,
|
||||
Input,
|
||||
} from "sveltestrap";
|
||||
import DatetimePicker from "./DatetimePicker.svelte";
|
||||
|
||||
let menuItem = [
|
||||
{ label: "Domain Name", key: "domain" },
|
||||
{ label: "Client IP", key: "clientIp" },
|
||||
{ label: "Protocol", key: "protocol" },
|
||||
{ label: "Status", key: "status" },
|
||||
];
|
||||
|
||||
let selected = menuItem[0];
|
||||
const selectItem = (selectedItem) => {
|
||||
selected = selectedItem;
|
||||
handleOnChange();
|
||||
};
|
||||
|
||||
let searchFilter = "";
|
||||
let startDate = new Date();
|
||||
let endDate = new Date();
|
||||
|
||||
const handleOnChange = () => {
|
||||
if (onChange) {
|
||||
const event = {
|
||||
terms: searchFilter,
|
||||
start: startDate,
|
||||
end: endDate,
|
||||
key: selected.key,
|
||||
};
|
||||
|
||||
onChange(event);
|
||||
}
|
||||
};
|
||||
|
||||
export let onChange = (evt) =>
|
||||
console.log("no handler bound to SearchOptions.svelte: ", evt);
|
||||
</script>
|
||||
|
||||
<div class="w-full flex flex-row">
|
||||
<section class="w-full flex flex-row">
|
||||
<FormGroup class="flex-grow flex flex-row mx-2">
|
||||
<InputGroup>
|
||||
<InputGroupAddon addonType="prepend">
|
||||
<InputGroupText>Search</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
<Input
|
||||
id="search"
|
||||
type="search"
|
||||
on:change={handleOnChange}
|
||||
bind:value={searchFilter}
|
||||
/>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
<FormGroup class="flex flex-row">
|
||||
<DatetimePicker label="Start" />
|
||||
<DatetimePicker label="End" />
|
||||
</FormGroup>
|
||||
<FormGroup class="mx-2">
|
||||
<Dropdown>
|
||||
<DropdownToggle caret>By {selected.label}</DropdownToggle>
|
||||
<DropdownMenu>
|
||||
{#each menuItem as item}
|
||||
<DropdownItem active={selected.key === item.key}>
|
||||
<span on:click={() => selectItem(item)}>
|
||||
{item.label}
|
||||
</span>
|
||||
</DropdownItem>
|
||||
{/each}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</FormGroup>
|
||||
</section>
|
||||
</div>
|
@ -0,0 +1,66 @@
|
||||
<script>
|
||||
import { fetchMetrics } from "../api/metrics.ts";
|
||||
import { onMount } from "svelte";
|
||||
import { Chart, registerables } from "chart.js";
|
||||
Chart.register(...registerables);
|
||||
|
||||
let canvas;
|
||||
onMount(async () => {
|
||||
const ctx = canvas.getContext("2d");
|
||||
const { labels, datasets } = await fetchAndPrepareMetrics();
|
||||
|
||||
console.log(labels);
|
||||
console.log(datasets);
|
||||
var c = new Chart(ctx, {
|
||||
type: "line",
|
||||
data: { labels, datasets },
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
title: {
|
||||
display: true,
|
||||
text: `Top Requests`,
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
stacked: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const fetchAndPrepareMetrics = async () => {
|
||||
const { error, payload } = await fetchMetrics({ key: "clientIp" });
|
||||
if (error) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const chartData = payload.reduce((agg, x) => {
|
||||
let root = agg[x.Header] || {
|
||||
labels: [],
|
||||
dataset: {
|
||||
label: x.Header,
|
||||
borderColor: "rgb(75,192,192)",
|
||||
data: [],
|
||||
},
|
||||
};
|
||||
root.dataset.data = root.dataset.data.concat(x.Count);
|
||||
root.labels = root.labels.concat(x.Time);
|
||||
agg[x.Header] = root;
|
||||
return agg;
|
||||
}, {});
|
||||
|
||||
const finalChartData = Object.keys(chartData).map((x) => chartData[x]);
|
||||
const finalChartLabels = finalChartData[0].labels;
|
||||
|
||||
return {
|
||||
labels: finalChartLabels,
|
||||
datasets: finalChartData.map((x) => x.dataset),
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<div style="position: relative; max-height: 512px; height: 512px; width: 100%;">
|
||||
<canvas width="100%" height="100%" bind:this={canvas} />
|
||||
</div>
|
@ -0,0 +1,7 @@
|
||||
import App from "./App.svelte";
|
||||
|
||||
const app = new App({
|
||||
target: document.body,
|
||||
});
|
||||
|
||||
export default app;
|
@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import LogViewer from "../components/LogViewer.svelte";
|
||||
import SearchOptions from "../components/SearchOptions.svelte";
|
||||
import TimeChart from "../components/TimeChart.svelte";
|
||||
</script>
|
||||
|
||||
<section class="w-full">
|
||||
<header>
|
||||
<h1>Home</h1>
|
||||
</header>
|
||||
<section>
|
||||
<SearchOptions />
|
||||
</section>
|
||||
<section class="m-10">
|
||||
<TimeChart />
|
||||
</section>
|
||||
<section>
|
||||
<LogViewer />
|
||||
</section>
|
||||
</section>
|
@ -0,0 +1,24 @@
|
||||
const { tailwindExtractor } = require("tailwindcss/lib/lib/purgeUnusedStyles");
|
||||
|
||||
module.exports = {
|
||||
purge: {
|
||||
content: ["src/**/*.svelte", "public/index.html"],
|
||||
options: {
|
||||
defaultExtractor: (content) => [
|
||||
...tailwindExtractor(content),
|
||||
...[...content.matchAll(/(?:class:)*([\w\d-/:%.]+)/gm)].map(
|
||||
([_match, group, ..._rest]) => group
|
||||
),
|
||||
],
|
||||
keyframes: true,
|
||||
},
|
||||
},
|
||||
darkMode: false, // or 'media' or 'class'
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
variants: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules/*", "__sapper__/*", "public/*"]
|
||||
}
|
Loading…
Reference in new issue