wip: trpc

This commit is contained in:
Andras Bacsai
2022-12-21 15:06:33 +01:00
parent e3e39af6fb
commit c8f7ca920e
36 changed files with 1761 additions and 10 deletions

View File

@@ -183,3 +183,19 @@ export function put(
): Promise<Record<string, any>> {
return send({ method: 'PUT', path, data, headers });
}
export function changeQueryParams(buildId: string) {
const queryParams = new URLSearchParams(window.location.search);
queryParams.set('buildId', buildId);
// @ts-ignore
return history.pushState(null, null, '?' + queryParams.toString());
}
export const dateOptions: any = {
year: 'numeric',
month: 'short',
day: '2-digit',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
hour12: false
};

View File

@@ -0,0 +1,7 @@
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc.js';
import relativeTime from 'dayjs/plugin/relativeTime.js';
dayjs.extend(utc);
dayjs.extend(relativeTime);
export { dayjs as day };

View File

@@ -170,3 +170,4 @@ export const setLocation = (resource: any, settings?: any) => {
disabledButton.set(false);
}
};
export const selectedBuildId: any = writable(null)

View File

@@ -11,7 +11,7 @@
import DatabaseIcons from '$lib/components/icons/databases/DatabaseIcons.svelte';
import ServiceIcons from '$lib/components/icons/services/ServiceIcons.svelte';
import * as Icons from '$lib/components/icons';
import NewResource from './_components/NewResource.svelte';
import NewResource from './components/NewResource.svelte';
const {
applications,

View File

@@ -3,10 +3,10 @@
import { status, trpc } from '$lib/store';
import { onDestroy, onMount } from 'svelte';
import type { LayoutData } from './$types';
import * as Buttons from './_components/Buttons';
import * as States from './_components/States';
import * as Buttons from './components/Buttons';
import * as States from './components/States';
import Menu from './_components/Menu.svelte';
import Menu from './components/Menu.svelte';
export let data: LayoutData;
const id = $page.params.id;

View File

@@ -0,0 +1,204 @@
<script lang="ts">
import type { PageData } from '../build/$types';
export let data: PageData;
console.log(data);
let builds = data.builds;
const application = data.application.data;
const buildCount = data.buildCount;
import { page } from '$app/stores';
import { addToast, selectedBuildId, trpc } from '$lib/store';
import BuildLog from './BuildLog.svelte';
import { changeQueryParams, dateOptions, errorNotification, asyncSleep } from '$lib/common';
import Tooltip from '$lib/components/Tooltip.svelte';
import { day } from '$lib/dayjs';
import { onDestroy, onMount } from 'svelte';
const { id } = $page.params;
let debug = application.settings.debug;
let loadBuildLogsInterval: any = null;
let skip = 0;
let noMoreBuilds = buildCount < 5 || buildCount <= skip;
let preselectedBuildId = $page.url.searchParams.get('buildId');
if (preselectedBuildId) $selectedBuildId = preselectedBuildId;
onMount(async () => {
getBuildLogs();
loadBuildLogsInterval = setInterval(() => {
getBuildLogs();
}, 2000);
});
onDestroy(() => {
clearInterval(loadBuildLogsInterval);
});
async function getBuildLogs() {
const response = await trpc.applications.getBuilds.query({ id, skip });
builds = response.builds;
}
async function loadMoreBuilds() {
if (buildCount >= skip) {
skip = skip + 5;
noMoreBuilds = buildCount <= skip;
try {
const data = await trpc.applications.getBuilds.query({ id, skip });
builds = data.builds;
return;
} catch (error) {
return errorNotification(error);
}
} else {
noMoreBuilds = true;
}
}
function loadBuild(build: any) {
$selectedBuildId = build;
return changeQueryParams($selectedBuildId);
}
async function resetQueue() {
const sure = confirm(
'It will reset all build queues for all applications. If something is queued, it will be canceled automatically. Are you sure? '
);
if (sure) {
try {
await trpc.applications.resetQueue.mutate();
addToast({
message: 'Queue reset done.',
type: 'success'
});
await asyncSleep(500);
return window.location.reload();
} catch (error) {
return errorNotification(error);
}
}
}
function generateBadgeColors(status: string) {
if (status === 'failed') {
return 'text-red-500';
} else if (status === 'running') {
return 'text-yellow-300';
} else if (status === 'success') {
return 'text-green-500';
} else if (status === 'canceled') {
return 'text-orange-500';
} else {
return 'text-white';
}
}
async function changeSettings(name: any) {
if (name === 'debug') {
debug = !debug;
}
try {
trpc.applications.saveSettings.mutate({
id,
debug
});
return addToast({
message: 'Settings saved.',
type: 'success'
});
} catch (error) {
if (name === 'debug') {
debug = !debug;
}
return errorNotification(error);
}
}
</script>
<div class="mx-auto w-full lg:px-0 px-1">
<div class="flex lg:flex-row flex-col border-b border-coolgray-500 mb-6 space-x-2">
<div class="flex flex-row">
<div class="title font-bold pb-3 pr-3">Build Logs</div>
<button class="btn btn-sm bg-error" on:click={resetQueue}>Reset Build Queue</button>
</div>
<div class=" flex-1" />
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text text-white pr-4 font-bold">Enable Debug Logs</span>
<input
type="checkbox"
checked={debug}
class="checkbox checkbox-success"
on:click={() => changeSettings('debug')}
/>
</label>
</div>
</div>
</div>
<div class="justify-start space-x-5 flex flex-col-reverse lg:flex-row">
<div class="flex-1 md:w-96">
{#if $selectedBuildId}
{#key $selectedBuildId}
<svelte:component this={BuildLog} />
{/key}
{:else if buildCount === 0}
Not build logs found.
{:else}
Select a build to see the logs.
{/if}
</div>
<div class="mb-4 min-w-[16rem] space-y-2 md:mb-0 ">
<div class="top-4 md:sticky">
<div class="flex space-x-2 pb-2">
<button
disabled={noMoreBuilds}
class:btn-primary={!noMoreBuilds}
class=" btn btn-sm w-full"
on:click={loadMoreBuilds}>Load more</button
>
</div>
{#each builds as build, index (build.id)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
id={`building-${build.id}`}
on:click={() => loadBuild(build.id)}
class:rounded-tr={index === 0}
class:rounded-br={index === builds.length - 1}
class="flex cursor-pointer items-center justify-center py-4 no-underline transition-all duration-150 hover:bg-coolgray-300 hover:shadow-xl"
class:bg-coolgray-200={$selectedBuildId === build.id}
>
<div class="flex-col px-2 text-center">
<div class="text-sm font-bold truncate">
{build.branch || application.branch}
</div>
<div class="text-xs">
{build.type}
</div>
<div
class={`badge badge-sm text-xs uppercase rounded bg-coolgray-300 border-none font-bold ${generateBadgeColors(
build.status
)}`}
>
{build.status}
</div>
</div>
<div class="w-32 text-center text-xs">
{#if build.status === 'running'}
<div>
<span class="font-bold text-xl">{build.elapsed}s</span>
</div>
{:else if build.status !== 'queued'}
<div>{day(build.updatedAt).utc().fromNow()}</div>
<div>
Finished in
<span class="font-bold"
>{day(build.updatedAt).utc().diff(day(build.createdAt)) / 1000}s</span
>
</div>
{/if}
</div>
</div>
<Tooltip triggeredBy={`#building-${build.id}`}
>{new Intl.DateTimeFormat('default', dateOptions).format(new Date(build.createdAt)) +
`\n`}</Tooltip
>
{/each}
</div>
</div>
</div>

View File

@@ -0,0 +1,16 @@
import { error } from '@sveltejs/kit';
import { trpc } from '$lib/store';
import type { PageLoad } from './$types';
export const ssr = false;
export const load: PageLoad = async ({ params }) => {
try {
const { id } = params;
const data = await trpc.applications.getBuilds.query({ id, skip: 0 });
return data;
} catch (err) {
throw error(500, {
message: 'An unexpected error occurred, please try again later.'
});
}
};

View File

@@ -0,0 +1,215 @@
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import { page } from '$app/stores';
import { errorNotification } from '$lib/common';
import Tooltip from '$lib/components/Tooltip.svelte';
import { day } from '$lib/dayjs';
import { selectedBuildId, trpc } from '$lib/store';
import { dev } from '$app/environment';
let logs: any = [];
let currentStatus: any;
let streamInterval: any;
let followingLogs: any;
let followingInterval: any;
let logsEl: any;
let fromDb = false;
let cancelInprogress = false;
let position = 0;
let loading = true;
const { id } = $page.params;
const cleanAnsiCodes = (str: string) => str.replace(/\x1B\[(\d+)m/g, '');
function detect() {
if (position < logsEl.scrollTop) {
position = logsEl.scrollTop;
} else {
if (followingLogs) {
clearInterval(followingInterval);
followingLogs = false;
}
position = logsEl.scrollTop;
}
}
function followBuild() {
followingLogs = !followingLogs;
if (followingLogs) {
followingInterval = setInterval(() => {
logsEl.scrollTop = logsEl.scrollHeight;
window.scrollTo(0, document.body.scrollHeight);
}, 100);
} else {
window.clearInterval(followingInterval);
}
}
async function streamLogs(sequence = 0) {
try {
loading = true;
let {
logs: responseLogs,
status,
fromDb: from
} = await trpc.applications.getBuildLogs.query({ id, buildId: $selectedBuildId, sequence });
currentStatus = status;
logs = logs.concat(
responseLogs.map((log: any) => ({ ...log, line: cleanAnsiCodes(log.line) }))
);
fromDb = from;
streamInterval = setInterval(async () => {
const nextSequence = logs[logs.length - 1]?.time || 0;
if (status !== 'running' && status !== 'queued') {
loading = false;
try {
const data = await trpc.applications.getBuildLogs.query({
id,
buildId: $selectedBuildId,
sequence: nextSequence
});
status = data.status;
currentStatus = status;
fromDb = data.fromDb;
logs = logs.concat(
data.logs.map((log: any) => ({ ...log, line: cleanAnsiCodes(log.line) }))
);
loading = false;
} catch (error) {
return errorNotification(error);
}
clearInterval(streamInterval);
return;
}
try {
const data = await trpc.applications.getBuildLogs.query({
id,
buildId: $selectedBuildId,
sequence: nextSequence
});
status = data.status;
currentStatus = status;
fromDb = data.fromDb;
logs = logs.concat(
data.logs.map((log: any) => ({ ...log, line: cleanAnsiCodes(log.line) }))
);
loading = false;
} catch (error) {
return errorNotification(error);
}
}, 1000);
} catch (error) {
return errorNotification(error);
}
}
async function cancelBuild() {
if (cancelInprogress) return;
try {
cancelInprogress = true;
await trpc.applications.cancelBuild.mutate({
buildId: $selectedBuildId,
applicationId: id
});
} catch (error) {
return errorNotification(error);
}
}
onDestroy(() => {
clearInterval(streamInterval);
clearInterval(followingInterval);
});
onMount(async () => {
window.scrollTo(0, 0);
await streamLogs();
});
</script>
<div class="flex justify-start top-0 pb-2 space-x-2">
<button
on:click={followBuild}
class="btn btn-sm bg-coollabs"
disabled={currentStatus !== 'running'}
class:bg-coolgray-300={followingLogs || currentStatus !== 'running'}
class:text-applications={followingLogs}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 mr-2"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="12" cy="12" r="9" />
<line x1="8" y1="12" x2="12" y2="16" />
<line x1="12" y1="8" x2="12" y2="16" />
<line x1="16" y1="12" x2="12" y2="16" />
</svg>
{followingLogs ? 'Following Logs...' : 'Follow Logs'}
</button>
<button
on:click={cancelBuild}
class:animation-spin={cancelInprogress}
class="btn btn-sm"
disabled={currentStatus !== 'running'}
class:bg-coolgray-300={cancelInprogress || currentStatus !== 'running'}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 mr-2"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="12" cy="12" r="9" />
<path d="M10 10l4 4m0 -4l-4 4" />
</svg>
{cancelInprogress ? 'Cancelling...' : 'Cancel Build'}
</button>
{#if currentStatus === 'running'}
<button id="streaming" class="btn btn-sm bg-transparent border-none loading" />
<Tooltip triggeredBy="#streaming">Streaming logs</Tooltip>
{/if}
</div>
{#if currentStatus === 'queued'}
<div
class="font-mono w-full bg-coolgray-200 p-5 overflow-x-auto overflox-y-auto max-h-[80vh] rounded mb-20 flex flex-col whitespace-nowrap scrollbar-thumb-coollabs scrollbar-track-coolgray-200 scrollbar-w-1"
>
Queued and waiting for execution.
</div>
{:else if logs.length > 0}
<div
bind:this={logsEl}
on:scroll={detect}
class="font-mono w-full bg-coolgray-100 border border-coolgray-200 p-5 overflow-x-auto overflox-y-auto max-h-[80vh] rounded mb-20 flex flex-col scrollbar-thumb-coollabs scrollbar-track-coolgray-200 scrollbar-w-1 whitespace-pre"
>
{#each logs as log}
{#if fromDb}
{log.line + '\n'}
{:else}
[{day.unix(log.time).format('HH:mm:ss.SSS')}] {log.line + '\n'}
{/if}
{/each}
</div>
{:else}
<div
class="font-mono w-full bg-coolgray-200 p-5 overflow-x-auto overflox-y-auto max-h-[80vh] rounded mb-20 flex flex-col whitespace-nowrap scrollbar-thumb-coollabs scrollbar-track-coolgray-200 scrollbar-w-1"
>
{loading
? 'Loading logs...'
: dev
? 'In development, logs are shown in the console.'
: 'No logs found yet.'}
</div>
{/if}

View File

@@ -149,9 +149,9 @@
</li>
<li
class="rounded"
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/logs/build`}
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/builds`}
>
<a href={`/applications/${$page.params.id}/logs/build`} class="no-underline w-full"
<a href={`/applications/${$page.params.id}/builds`} class="no-underline w-full"
><svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"

View File

@@ -0,0 +1,176 @@
<script lang="ts">
import { page } from '$app/stores';
import { errorNotification } from '$lib/common';
import { trpc } from '$lib/store';
import { onMount, onDestroy } from 'svelte';
let application: any = {};
let logsLoading = false;
let loadLogsInterval: any = null;
let logs: any = [];
let lastLog: any = null;
let followingInterval: any;
let followingLogs: any;
let logsEl: any;
let position = 0;
let services: any = [];
let selectedService: any = null;
let noContainer = false;
const { id } = $page.params;
onMount(async () => {
const { data } = await trpc.applications.getApplicationById.query({ id });
application = data;
if (data.dockerComposeFile) {
services = normalizeDockerServices(JSON.parse(data.dockerComposeFile).services);
} else {
services = [
{
name: ''
}
];
await selectService('');
}
});
onDestroy(() => {
clearInterval(loadLogsInterval);
clearInterval(followingInterval);
});
function normalizeDockerServices(services: any[]) {
const tempdockerComposeServices = [];
for (const [name, data] of Object.entries(services)) {
tempdockerComposeServices.push({
name,
data
});
}
return tempdockerComposeServices;
}
async function loadLogs() {
if (logsLoading) return;
try {
const newLogs = await trpc.applications.loadLogs.query({
id,
containerId: selectedService,
since: Number(lastLog?.split(' ')[0]) || 0
});
if (newLogs.noContainer) {
noContainer = true;
} else {
noContainer = false;
}
if (newLogs?.logs && newLogs.logs[newLogs.logs.length - 1] !== logs[logs.length - 1]) {
logs = logs.concat(newLogs.logs);
lastLog = newLogs.logs[newLogs.logs.length - 1];
}
} catch (error) {
return errorNotification(error);
}
}
function detect() {
if (position < logsEl.scrollTop) {
position = logsEl.scrollTop;
} else {
if (followingLogs) {
clearInterval(followingInterval);
followingLogs = false;
}
position = logsEl.scrollTop;
}
}
function followBuild() {
followingLogs = !followingLogs;
if (followingLogs) {
followingInterval = setInterval(() => {
logsEl.scrollTop = logsEl.scrollHeight;
window.scrollTo(0, document.body.scrollHeight);
}, 1000);
} else {
clearInterval(followingInterval);
}
}
async function selectService(service: any, init: boolean = false) {
if (loadLogsInterval) clearInterval(loadLogsInterval);
if (followingInterval) clearInterval(followingInterval);
logs = [];
lastLog = null;
followingLogs = false;
selectedService = `${application.id}${service.name ? `-${service.name}` : ''}`;
loadLogs();
loadLogsInterval = setInterval(() => {
loadLogs();
}, 1000);
}
</script>
<div class="mx-auto w-full">
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="title font-bold pb-3">Application Logs</div>
</div>
</div>
<div class="flex gap-2 lg:gap-8 pb-4">
{#each services as service}
<button
on:click={() => selectService(service, true)}
class:bg-primary={selectedService ===
`${application.id}${service.name ? `-${service.name}` : ''}`}
class:bg-coolgray-200={selectedService !==
`${application.id}${service.name ? `-${service.name}` : ''}`}
class="w-full rounded p-5 hover:bg-primary font-bold"
>
{application.id}{service.name ? `-${service.name}` : ''}</button
>
{/each}
</div>
{#if selectedService}
<div class="flex flex-row justify-center space-x-2">
{#if logs.length === 0}
{#if noContainer}
<div class="text-xl font-bold tracking-tighter">Container not found / exited.</div>
{/if}
{:else}
<div class="relative w-full">
<div class="flex justify-start sticky space-x-2 pb-2">
<button on:click={followBuild} class="btn btn-sm " class:bg-coollabs={followingLogs}>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 mr-2"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="12" cy="12" r="9" />
<line x1="8" y1="12" x2="12" y2="16" />
<line x1="12" y1="8" x2="12" y2="16" />
<line x1="16" y1="12" x2="12" y2="16" />
</svg>
{followingLogs ? 'Following Logs...' : 'Follow Logs'}
</button>
{#if loadLogsInterval}
<button id="streaming" class="btn btn-sm bg-transparent border-none loading"
>Streaming logs</button
>
{/if}
</div>
<div
bind:this={logsEl}
on:scroll={detect}
class="font-mono w-full bg-coolgray-100 border border-coolgray-200 p-5 overflow-x-auto overflox-y-auto max-h-[80vh] rounded mb-20 flex flex-col scrollbar-thumb-coollabs scrollbar-track-coolgray-200 scrollbar-w-1"
>
{#each logs as log}
<p>{log + '\n'}</p>
{/each}
</div>
</div>
{/if}
</div>
{/if}

View File

@@ -9,8 +9,8 @@
import pLimit from 'p-limit';
import { page } from '$app/stores';
import { addToast, trpc } from '$lib/store';
import Secret from './_components/Secret.svelte';
import PreviewSecret from './_components/PreviewSecret.svelte';
import Secret from './components/Secret.svelte';
import PreviewSecret from './components/PreviewSecret.svelte';
import { errorNotification } from '$lib/common';
import Explainer from '$lib/components/Explainer.svelte';