fix(qb-core): post-update recovery + centralizare notify 17mov_Hud
Restaurat jobs.lua din git (Quasar fork a suprascris joburile 17mov). Adăugat item map în items.lua (lipsea, rupt rv-maphold). Setat licences.driver = false în config.lua. Override QBCore.Functions.Notify + QBCore:Notify event → 17mov_Hud:ShowNotification (toate notificările merg automat prin 17mov_Hud).
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
build
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"singleQuote": true,
|
||||
"useTabs": false,
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"bracketSpacing": true,
|
||||
"trailingComma": "es5",
|
||||
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
||||
"pluginSearchDirs": false
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + Svelte + TS</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "oxmysql",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"start:game": "vite build --watch",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^2.5.3",
|
||||
"@tsconfig/svelte": "^3.0.0",
|
||||
"svelte": "^3.59.2",
|
||||
"svelte-check": "^2.10.3",
|
||||
"tslib": "^2.7.0",
|
||||
"typescript": "^4.9.5",
|
||||
"vite": "^4.5.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tabler/icons-svelte": "^2.47.0",
|
||||
"@tanstack/svelte-table": "8.8.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"chart.js": "^4.4.4",
|
||||
"postcss": "^8.4.47",
|
||||
"prettier": "^2.8.8",
|
||||
"prettier-plugin-svelte": "^2.10.1",
|
||||
"prettier-plugin-tailwindcss": "^0.2.8",
|
||||
"svelte-chartjs": "^3.1.5",
|
||||
"svelte-floating-ui": "^1.5.9",
|
||||
"svelte-portal": "2.2.0",
|
||||
"tailwindcss": "^3.4.13",
|
||||
"tinro": "^0.6.12"
|
||||
}
|
||||
}
|
||||
+1865
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,85 @@
|
||||
<script lang="ts">
|
||||
import { Route, router } from 'tinro';
|
||||
import Resource from './pages/resource/Resource.svelte';
|
||||
import Root from './pages/root/Root.svelte';
|
||||
import { useNuiEvent } from './utils/useNuiEvent';
|
||||
import { resources, generalData, chartData } from './store';
|
||||
import { debugData } from './utils/debugData';
|
||||
import { visible } from './store';
|
||||
import { scale } from 'svelte/transition';
|
||||
import { fetchNui } from './utils/fetchNui';
|
||||
|
||||
interface OpenData {
|
||||
resources: string[];
|
||||
totalQueries: number;
|
||||
slowQueries: number;
|
||||
totalTime: number;
|
||||
chartData: {
|
||||
labels: string[];
|
||||
data: { queries: number; time: number }[];
|
||||
};
|
||||
}
|
||||
|
||||
router.mode.hash();
|
||||
router.goto('/');
|
||||
|
||||
useNuiEvent('openUI', (data: OpenData) => {
|
||||
$visible = true;
|
||||
$resources = data.resources;
|
||||
$generalData = {
|
||||
queries: data.totalQueries,
|
||||
slowQueries: data.slowQueries,
|
||||
timeQuerying: data.totalTime,
|
||||
};
|
||||
$chartData = {
|
||||
labels: data.chartData.labels,
|
||||
data: data.chartData.data,
|
||||
};
|
||||
});
|
||||
|
||||
debugData<OpenData>([
|
||||
{
|
||||
action: 'openUI',
|
||||
data: {
|
||||
resources: ['ox_core', 'oxmysql', 'ox_inventory', 'ox_doorlock', 'ox_lib', 'ox_vehicleshop', 'ox_target'],
|
||||
slowQueries: 13,
|
||||
totalQueries: 332,
|
||||
totalTime: 230123,
|
||||
chartData: {
|
||||
labels: ['oxmysql', 'ox_core', 'ox_inventory', 'ox_doorlock'],
|
||||
data: [
|
||||
{ queries: 25, time: 133 },
|
||||
{ queries: 5, time: 12 },
|
||||
{ queries: 3, time: 2 },
|
||||
{ queries: 72, time: 133 },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const handleESC = (e: KeyboardEvent) => {
|
||||
if (e.key !== 'Escape') return;
|
||||
|
||||
$visible = false;
|
||||
fetchNui('exit');
|
||||
};
|
||||
|
||||
$: $visible ? window.addEventListener('keydown', handleESC) : window.removeEventListener('keydown', handleESC);
|
||||
</script>
|
||||
|
||||
{#if $visible}
|
||||
<main
|
||||
transition:scale={{ start: 0.95, duration: 150 }}
|
||||
class="font-main flex h-full w-full items-center justify-center"
|
||||
>
|
||||
<div class="bg-dark-800 flex h-[700px] w-[1200px] rounded-md text-white">
|
||||
<Route path="/">
|
||||
<Root />
|
||||
</Route>
|
||||
<Route path="/:resource">
|
||||
<Resource />
|
||||
</Route>
|
||||
</div>
|
||||
</main>
|
||||
{/if}
|
||||
@@ -0,0 +1,31 @@
|
||||
@import url('https://use.typekit.net/qgr5ebd.css');
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 2px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #25262b;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #909296;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #a6a7ab;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import './index.css';
|
||||
import App from './App.svelte';
|
||||
import { isEnvBrowser } from './utils/misc';
|
||||
|
||||
const app = new App({
|
||||
target: document.getElementById('app'),
|
||||
});
|
||||
|
||||
if (isEnvBrowser()) {
|
||||
const root = document.getElementById('app');
|
||||
|
||||
// https://i.imgur.com/iPTAdYV.png - Night time img
|
||||
root!.style.backgroundImage = 'url("https://i.imgur.com/3pzRj9n.png")';
|
||||
root!.style.backgroundSize = 'cover';
|
||||
root!.style.backgroundRepeat = 'no-repeat';
|
||||
root!.style.backgroundPosition = 'center';
|
||||
}
|
||||
|
||||
export default app;
|
||||
@@ -0,0 +1,67 @@
|
||||
<script lang="ts">
|
||||
import { fetchNui } from '../../utils/fetchNui';
|
||||
import Pagination from './components/Pagination.svelte';
|
||||
import QueryTable from './components/QueryTable.svelte';
|
||||
import ResourceHeader from './components/ResourceHeader.svelte';
|
||||
import { meta } from 'tinro';
|
||||
import { useNuiEvent } from '../../utils/useNuiEvent';
|
||||
import { queries, resourceData, type QueryData } from '../../store';
|
||||
import { debugData } from '../../utils/debugData';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { filterData } from '../../store';
|
||||
import QuerySearch from './components/QuerySearch.svelte';
|
||||
import IconSearch from '@tabler/icons-svelte/dist/svelte/icons/IconSearch.svelte';
|
||||
|
||||
let maxPage = 0;
|
||||
|
||||
onDestroy(() => {
|
||||
$queries = [];
|
||||
$filterData.page = 0;
|
||||
});
|
||||
|
||||
interface ResourceData {
|
||||
queries: QueryData[];
|
||||
pageCount: number;
|
||||
resourceQueriesCount: number;
|
||||
resourceSlowQueries: number;
|
||||
resourceTime: number;
|
||||
}
|
||||
|
||||
debugData<ResourceData>([
|
||||
{
|
||||
action: 'loadResource',
|
||||
data: {
|
||||
queries: [
|
||||
{ query: 'SELECT * FROM users WHERE ID = 1', executionTime: 3, slow: false, date: Date.now() },
|
||||
{ query: 'SELECT * FROM users WHERE ID = 1', executionTime: 23, slow: true, date: Date.now() },
|
||||
{ query: 'SELECT * FROM users WHERE ID = 1', executionTime: 15, slow: false, date: Date.now() },
|
||||
{ query: 'SELECT * FROM users WHERE ID = 1', executionTime: 122, slow: true, date: Date.now() },
|
||||
],
|
||||
resourceQueriesCount: 3,
|
||||
resourceSlowQueries: 2,
|
||||
resourceTime: 1342,
|
||||
pageCount: 3,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// I miss callbacks :(
|
||||
useNuiEvent('loadResource', (data: ResourceData) => {
|
||||
maxPage = data.pageCount;
|
||||
$queries = data.queries;
|
||||
$resourceData = {
|
||||
resourceQueriesCount: data.resourceQueriesCount,
|
||||
resourceSlowQueries: data.resourceSlowQueries,
|
||||
resourceTime: data.resourceTime,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex w-full flex-col justify-between">
|
||||
<div>
|
||||
<ResourceHeader />
|
||||
<QuerySearch icon={IconSearch} />
|
||||
<QueryTable />
|
||||
</div>
|
||||
<Pagination {maxPage} />
|
||||
</div>
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
<script lang="ts">
|
||||
import IconChevronLeft from '@tabler/icons-svelte/dist/svelte/icons/IconChevronLeft.svelte';
|
||||
import IconChevronRight from '@tabler/icons-svelte/dist/svelte/icons/IconChevronRight.svelte';
|
||||
import IconChevronsLeft from '@tabler/icons-svelte/dist/svelte/icons/IconChevronsLeft.svelte';
|
||||
import IconChevronsRight from '@tabler/icons-svelte/dist/svelte/icons/IconChevronsRight.svelte';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { filterData } from '../../../store';
|
||||
|
||||
export let maxPage: number;
|
||||
|
||||
onDestroy(() => (maxPage = 0));
|
||||
</script>
|
||||
|
||||
<div class="flex items-center justify-center gap-6 pb-5">
|
||||
<button
|
||||
disabled={$filterData.page === 0}
|
||||
on:click={() => ($filterData.page = 0)}
|
||||
class="bg-dark-600 disabled:bg-dark-300 disabled:text-dark-400 text-dark-100 hover:bg-dark-500 rounded-md border-[1px] border-transparent p-2 outline-none hover:text-white focus-visible:border-cyan-600 focus-visible:text-white active:translate-y-[3px] disabled:cursor-not-allowed"
|
||||
>
|
||||
<IconChevronsLeft />
|
||||
</button>
|
||||
<button
|
||||
disabled={$filterData.page === 0}
|
||||
on:click={() => $filterData.page--}
|
||||
class="bg-dark-600 disabled:bg-dark-300 disabled:text-dark-400 text-dark-100 hover:bg-dark-500 rounded-md border-[1px] border-transparent p-2 outline-none hover:text-white focus-visible:border-cyan-600 focus-visible:text-white active:translate-y-[3px] disabled:cursor-not-allowed"
|
||||
>
|
||||
<IconChevronLeft />
|
||||
</button>
|
||||
<p>Page {$filterData.page + 1} of {maxPage}</p>
|
||||
<button
|
||||
disabled={$filterData.page >= maxPage - 1}
|
||||
on:click={() => $filterData.page++}
|
||||
class="bg-dark-600 disabled:bg-dark-300 disabled:text-dark-400 text-dark-100 hover:bg-dark-500 rounded-md border-[1px] border-transparent p-2 outline-none hover:text-white focus-visible:border-cyan-600 focus-visible:text-white active:translate-y-[3px] disabled:cursor-not-allowed"
|
||||
>
|
||||
<IconChevronRight />
|
||||
</button>
|
||||
<button
|
||||
disabled={$filterData.page === maxPage - 1}
|
||||
on:click={() => ($filterData.page = maxPage - 1)}
|
||||
class="bg-dark-600 disabled:bg-dark-300 disabled:text-dark-400 text-dark-100 hover:bg-dark-500 rounded-md border-[1px] border-transparent p-2 outline-none hover:text-white focus-visible:border-cyan-600 focus-visible:text-white active:translate-y-[3px] disabled:cursor-not-allowed"
|
||||
>
|
||||
<IconChevronsRight />
|
||||
</button>
|
||||
</div>
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { filterData } from '../../../store/resource';
|
||||
|
||||
let value = '';
|
||||
|
||||
$: {
|
||||
$filterData.search = value;
|
||||
$filterData.page = 0;
|
||||
}
|
||||
|
||||
export let icon: ConstructorOfATypedSvelteComponent;
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="bg-dark-600 m-4 mt-0 flex items-center rounded-md border-[1px] border-transparent p-2 outline-none transition-all duration-100 focus-within:border-cyan-600"
|
||||
>
|
||||
<div class="pr-2">
|
||||
<svelte:component this={icon} class="text-dark-300" />
|
||||
</div>
|
||||
<input type="text" bind:value class="w-full bg-transparent outline-none" placeholder="Search queries..." />
|
||||
</div>
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
<script lang="ts">
|
||||
import IconChevronDown from '@tabler/icons-svelte/dist/svelte/icons/IconChevronDown.svelte';
|
||||
import IconChevronUp from '@tabler/icons-svelte/dist/svelte/icons/IconChevronUp.svelte';
|
||||
import { queries, type QueryData } from '../../../store';
|
||||
import {
|
||||
createSvelteTable,
|
||||
getCoreRowModel,
|
||||
type TableOptions,
|
||||
type ColumnDef,
|
||||
flexRender,
|
||||
type SortingState,
|
||||
} from '@tanstack/svelte-table';
|
||||
import { writable } from 'svelte/store';
|
||||
import { meta } from 'tinro';
|
||||
import { fetchNui } from '../../../utils/fetchNui';
|
||||
import { filterData } from '../../../store';
|
||||
import QueryTooltip from './QueryTooltip.svelte';
|
||||
|
||||
const route = meta();
|
||||
|
||||
const columns: ColumnDef<QueryData, number>[] = [
|
||||
{
|
||||
accessorKey: 'query',
|
||||
header: 'Query',
|
||||
cell: (info) => info.getValue(),
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: 'executionTime',
|
||||
header: 'Time (ms)',
|
||||
cell: (info) => info.getValue().toFixed(4),
|
||||
enableSorting: true,
|
||||
},
|
||||
];
|
||||
|
||||
let sorting: SortingState = [];
|
||||
|
||||
const setSorting = (updater) => {
|
||||
if (updater instanceof Function) {
|
||||
sorting = updater(sorting);
|
||||
} else {
|
||||
sorting = updater;
|
||||
}
|
||||
options.update((old) => ({
|
||||
...old,
|
||||
state: {
|
||||
...old.state,
|
||||
sorting,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const options = writable<TableOptions<QueryData>>({
|
||||
data: $queries,
|
||||
columns,
|
||||
manualPagination: true,
|
||||
manualSorting: true,
|
||||
pageCount: -1,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onSortingChange: setSorting,
|
||||
state: {
|
||||
sorting,
|
||||
},
|
||||
});
|
||||
|
||||
$: options.update((prev) => ({ ...prev, data: $queries }));
|
||||
|
||||
const table = createSvelteTable(options);
|
||||
|
||||
let timer: NodeJS.Timeout;
|
||||
$: {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
fetchNui('fetchResource', {
|
||||
resource: route.params.resource,
|
||||
pageIndex: $filterData.page,
|
||||
search: $filterData.search,
|
||||
sortBy: sorting,
|
||||
});
|
||||
}, 300);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="px-4">
|
||||
<table class="w-full">
|
||||
<thead class="bg-dark-600">
|
||||
{#each $table.getHeaderGroups() as headerGroup}
|
||||
<tr>
|
||||
{#each headerGroup.headers as header}
|
||||
<th class={`bg-dark-600 select-none p-1 ${header.id === 'executionTime' ? 'w-1/4' : 'w-3/4'}`}>
|
||||
{#if !header.isPlaceholder}
|
||||
<button
|
||||
class:cursor-pointer={header.column.getCanSort()}
|
||||
class:select-none={header.column.getCanSort()}
|
||||
class="flex w-full items-center justify-center gap-1"
|
||||
on:click={header.column.getToggleSortingHandler()}
|
||||
>
|
||||
<svelte:component this={flexRender(header.column.columnDef.header, header.getContext())} />
|
||||
{#if header.column.getIsSorted() === 'asc'}
|
||||
<IconChevronUp />
|
||||
{:else if header.column.getIsSorted() === 'desc'}
|
||||
<IconChevronDown />
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</th>
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each $table.getRowModel().rows as row}
|
||||
<tr>
|
||||
{#each row.getVisibleCells() as cell}
|
||||
<QueryTooltip
|
||||
content={cell.getValue()}
|
||||
let:floatingRef
|
||||
let:displayTooltip
|
||||
let:hideTooltip
|
||||
disabled={cell.column.id !== 'query'}
|
||||
>
|
||||
<td
|
||||
use:floatingRef
|
||||
on:mouseenter={displayTooltip}
|
||||
on:mouseleave={hideTooltip}
|
||||
class={`${cell.column.id === 'executionTime' && 'text-center'} bg-dark-700 p-2 ${
|
||||
row.original.slow && 'text-yellow-500'
|
||||
} max-w-[200px] truncate`}
|
||||
>
|
||||
<svelte:component this={flexRender(cell.column.columnDef.cell, cell.getContext())} />
|
||||
</td>
|
||||
</QueryTooltip>
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
<script lang="ts">
|
||||
import { offset, flip, shift } from 'svelte-floating-ui/dom';
|
||||
import { createFloatingActions } from 'svelte-floating-ui';
|
||||
import { fade } from 'svelte/transition';
|
||||
import Portal from 'svelte-portal';
|
||||
|
||||
export let content: unknown;
|
||||
export let disabled: boolean;
|
||||
|
||||
const [floatingRef, floatingContent] = createFloatingActions({
|
||||
strategy: 'absolute',
|
||||
placement: 'top',
|
||||
middleware: [offset(6), flip(), shift()],
|
||||
});
|
||||
|
||||
let display = false;
|
||||
|
||||
let timer: NodeJS.Timer;
|
||||
|
||||
const displayTooltip = () => {
|
||||
if (disabled) return;
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
display = true;
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const hideTooltip = () => {
|
||||
if (disabled) return;
|
||||
clearTimeout(timer);
|
||||
display = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<slot {floatingRef} {displayTooltip} {hideTooltip} />
|
||||
{#if display && !disabled}
|
||||
<Portal target="body">
|
||||
<div
|
||||
transition:fade={{ duration: 150 }}
|
||||
use:floatingContent
|
||||
class="absolute p-2 text-sm bg-dark-50 text-dark-400 rounded-md max-w-xl font-main"
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
</Portal>
|
||||
{/if}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import IconChevronLeft from '@tabler/icons-svelte/dist/svelte/icons/IconChevronLeft.svelte';
|
||||
|
||||
import { meta, router } from 'tinro';
|
||||
import { resourceData } from '../../../store';
|
||||
|
||||
const route = meta();
|
||||
</script>
|
||||
|
||||
<div class="p-4 grid grid-flow-col grid-cols-3 items-center ">
|
||||
<button
|
||||
on:click={() => router.goto('/')}
|
||||
class="flex p-2 w-12 bg-dark-600 text-dark-100 hover:text-white rounded-md justify-center items-center hover:bg-dark-500 outline-none border-[1px] border-transparent focus-visible:border-cyan-600"
|
||||
>
|
||||
<IconChevronLeft />
|
||||
</button>
|
||||
<p class="text-center text-lg">{route.params.resource}</p>
|
||||
<div class="text-end text-dark-100 flex flex-col text-xs">
|
||||
<p>Queries: {$resourceData.resourceQueriesCount}</p>
|
||||
<p>Time: {$resourceData.resourceTime.toFixed(4)} ms</p>
|
||||
<p class="text-yellow-500">Slow queries: {$resourceData.resourceSlowQueries}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,45 @@
|
||||
<script lang="ts">
|
||||
import Search from './components/Search.svelte';
|
||||
import IconFileAnalytics from '@tabler/icons-svelte/dist/svelte/icons/IconFileAnalytics.svelte';
|
||||
import IconSearch from '@tabler/icons-svelte/dist/svelte/icons/IconSearch.svelte';
|
||||
import IconSourceCode from '@tabler/icons-svelte/dist/svelte/icons/IconSourceCode.svelte';
|
||||
import { router } from 'tinro';
|
||||
import { search, filteredResources, generalData } from '../../store';
|
||||
import Chart from './components/Chart.svelte';
|
||||
</script>
|
||||
|
||||
<div class="p-2 w-full h-full flex justify-between gap-2">
|
||||
<div class="bg-dark-700 p-4 pr-0 flex flex-col w-2/3 rounded-md">
|
||||
<div class="pr-4 flex justify-between items-center">
|
||||
<div class="flex gap-3 items-center ">
|
||||
<p class="text-2xl">Resources</p>
|
||||
<IconSourceCode />
|
||||
</div>
|
||||
<Search icon={IconSearch} bind:value={$search} />
|
||||
</div>
|
||||
<div class="flex flex-col gap-3 mt-6 overflow-y-auto pr-4">
|
||||
{#each $filteredResources as resource}
|
||||
<button
|
||||
on:click={() => router.goto(`/${resource}`)}
|
||||
class="bg-dark-600 p-3 border-[1px] outline-none border-transparent focus-visible:border-cyan-600 text-left hover:bg-dark-400 rounded-md"
|
||||
>
|
||||
{resource}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-dark-700 p-4 flex flex-col justify-between w-1/3 rounded-md">
|
||||
<div class="flex flex-col">
|
||||
<div class="flex gap-3 items-center mb-4">
|
||||
<p class="text-2xl">General data</p>
|
||||
<IconFileAnalytics />
|
||||
</div>
|
||||
<div class="flex flex-col text-dark-50">
|
||||
<p>Queries: {$generalData.queries}</p>
|
||||
<p>Time querying: {$generalData.timeQuerying.toFixed(4)} ms</p>
|
||||
<p class="text-yellow-500">Slow queries: {$generalData.slowQueries}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Chart />
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,40 @@
|
||||
<script lang="ts">
|
||||
import { Pie } from 'svelte-chartjs';
|
||||
import { Chart as ChartJS, Title, Tooltip, ArcElement, CategoryScale, Colors, type TooltipItem } from 'chart.js';
|
||||
import { chartData } from '../../../store';
|
||||
|
||||
ChartJS.register(Title, Tooltip, ArcElement, CategoryScale, Colors);
|
||||
</script>
|
||||
|
||||
<Pie
|
||||
class="self-center"
|
||||
width={256}
|
||||
height={256}
|
||||
data={{
|
||||
labels: $chartData.labels,
|
||||
datasets: [
|
||||
{
|
||||
// @ts-ignore
|
||||
data: $chartData.data,
|
||||
},
|
||||
],
|
||||
}}
|
||||
options={{
|
||||
maintainAspectRatio: false,
|
||||
responsive: false,
|
||||
parsing: {
|
||||
key: 'time',
|
||||
},
|
||||
animation: false,
|
||||
plugins: {
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (context) => {
|
||||
// @ts-ignore
|
||||
return `Queries: ${context.raw.queries}, Time: ${context.raw.time.toFixed(4)} ms`;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
@@ -0,0 +1,13 @@
|
||||
<script lang="ts">
|
||||
export let icon: ConstructorOfATypedSvelteComponent;
|
||||
export let value: string;
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="p-2 flex items-center outline-none border-[1px] border-transparent transition-all duration-100 focus-within:border-cyan-600 rounded-md bg-dark-600"
|
||||
>
|
||||
<div class="pr-2">
|
||||
<svelte:component this={icon} class="text-dark-300" />
|
||||
</div>
|
||||
<input type="text" bind:value class="bg-transparent outline-none w-full" placeholder="Search resources..." />
|
||||
</div>
|
||||
@@ -0,0 +1,34 @@
|
||||
import { derived, writable } from 'svelte/store';
|
||||
|
||||
export const visible = writable(false);
|
||||
|
||||
export const search = writable('');
|
||||
let timeout: NodeJS.Timeout;
|
||||
export const debouncedSearch = derived(search, (value, set: (value: string) => void) => {
|
||||
timeout = setTimeout(() => set(value), 500);
|
||||
return () => clearTimeout(timeout);
|
||||
});
|
||||
|
||||
export const resources = writable<string[]>([]);
|
||||
|
||||
export const filteredResources = derived(
|
||||
[resources, debouncedSearch],
|
||||
([$resources, $debouncedSearch], set: (value: string[]) => void) => {
|
||||
if ($debouncedSearch === '' || !$debouncedSearch) return set($resources);
|
||||
|
||||
const query = $debouncedSearch.toLowerCase();
|
||||
|
||||
return set($resources.filter((resource) => resource.toLowerCase().includes(query)));
|
||||
}
|
||||
);
|
||||
|
||||
export const generalData = writable({
|
||||
queries: 0,
|
||||
timeQuerying: 0,
|
||||
slowQueries: 0,
|
||||
});
|
||||
|
||||
export const chartData = writable<{ labels: string[]; data: { queries: number; time: number }[] }>({
|
||||
labels: [],
|
||||
data: [],
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './general';
|
||||
export * from './resource';
|
||||
@@ -0,0 +1,22 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export interface QueryData {
|
||||
date: number;
|
||||
query: string;
|
||||
executionTime: number;
|
||||
slow?: boolean;
|
||||
}
|
||||
|
||||
export const queries = writable<QueryData[]>([]);
|
||||
|
||||
export const resourceData = writable<{
|
||||
resourceQueriesCount: number;
|
||||
resourceSlowQueries: number;
|
||||
resourceTime: number;
|
||||
}>({
|
||||
resourceQueriesCount: 0,
|
||||
resourceSlowQueries: 0,
|
||||
resourceTime: 0,
|
||||
});
|
||||
|
||||
export const filterData = writable({ search: '', page: 0 });
|
||||
@@ -0,0 +1,30 @@
|
||||
import { isEnvBrowser } from './misc';
|
||||
|
||||
interface DebugEvent<T = any> {
|
||||
action: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emulates dispatching an event using SendNuiMessage in the lua scripts.
|
||||
* This is used when developing in browser
|
||||
*
|
||||
* @param events - The event you want to cover
|
||||
* @param timer - How long until it should trigger (ms)
|
||||
*/
|
||||
export const debugData = <P>(events: DebugEvent<P>[], timer = 1000): void => {
|
||||
if (isEnvBrowser()) {
|
||||
for (const event of events) {
|
||||
setTimeout(() => {
|
||||
window.dispatchEvent(
|
||||
new MessageEvent('message', {
|
||||
data: {
|
||||
action: event.action,
|
||||
data: event.data,
|
||||
},
|
||||
})
|
||||
);
|
||||
}, timer);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* @param eventName - The endpoint eventname to target
|
||||
* @param data - Data you wish to send in the NUI Callback
|
||||
*
|
||||
* @return returnData - A promise for the data sent back by the NuiCallbacks CB argument
|
||||
*/
|
||||
|
||||
export async function fetchNui<T = any>(eventName: string, data: unknown = {}): Promise<T> {
|
||||
const options = {
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=UTF-8',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
|
||||
const resourceName = (window as any).GetParentResourceName
|
||||
? (window as any).GetParentResourceName()
|
||||
: 'nui-frame-app';
|
||||
|
||||
const resp = await fetch(`https://${resourceName}/${eventName}`, options);
|
||||
|
||||
return await resp.json();
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export const isEnvBrowser = (): boolean => !(window as any).invokeNative;
|
||||
@@ -0,0 +1,28 @@
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
|
||||
interface NuiMessage<T = unknown> {
|
||||
action: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* A function that manage events listeners for receiving data from the client scripts
|
||||
* @param action The specific `action` that should be listened for.
|
||||
* @param handler The callback function that will handle data relayed by this function
|
||||
*
|
||||
* @example
|
||||
* useNuiEvent<{visibility: true, wasVisible: 'something'}>('setVisible', (data) => {
|
||||
* // whatever logic you want
|
||||
* })
|
||||
*
|
||||
**/
|
||||
|
||||
export function useNuiEvent<T = unknown>(action: string, handler: (data: T) => void) {
|
||||
const eventListener = (event: MessageEvent<NuiMessage<T>>) => {
|
||||
const { action: eventAction, data } = event.data;
|
||||
|
||||
eventAction === action && handler(data);
|
||||
};
|
||||
onMount(() => window.addEventListener('message', eventListener));
|
||||
onDestroy(() => window.removeEventListener('message', eventListener));
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
/// <reference types="svelte" />
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,7 @@
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
export default {
|
||||
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
|
||||
// for more information about preprocessors
|
||||
preprocess: vitePreprocess(),
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ['./index.html', './src/**/*.{js,ts,svelte}'],
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
main: 'Roboto',
|
||||
},
|
||||
colors: {
|
||||
// Mantine dark colours
|
||||
dark: {
|
||||
50: '#C1C2C5',
|
||||
100: '#A6A7AB',
|
||||
200: '#909296',
|
||||
300: '#5C5F66',
|
||||
400: '#373A40',
|
||||
500: '#2C2E33',
|
||||
600: '#25262B',
|
||||
700: '#1A1B1E',
|
||||
800: '#141517',
|
||||
900: '#101113',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"resolveJsonModule": true,
|
||||
/**
|
||||
* Typecheck JS in `.svelte` and `.js` files by default.
|
||||
* Disable checkJs if you'd like to use dynamic types in JS.
|
||||
* Note that setting allowJs false does not prevent the use
|
||||
* of JS in `.svelte` files.
|
||||
*/
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"isolatedModules": true
|
||||
},
|
||||
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node"
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [svelte()],
|
||||
base: './',
|
||||
server: {
|
||||
port: 3000,
|
||||
},
|
||||
build: {
|
||||
outDir: 'build',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user