@t1mmen/srtd
Version:
Supabase Repeatable Template Definitions (srtd): 🪄 Live-reloading SQL templates for Supabase DX. Make your database changes reviewable and migrations maintainable! 🚀
140 lines • 7.96 kB
JavaScript
// src/components/watch.tsx
import path from 'node:path';
import { Spinner } from '@inkjs/ui';
import figures from 'figures';
import { Box, Text, useInput } from 'ink';
import React, { useMemo } from 'react';
import Branding from '../components/Branding.js';
import Quittable from '../components/Quittable.js';
import { StatBadge } from '../components/StatBadge.js';
import { TimeSince } from '../components/TimeSince.js';
import { COLOR_ACCENT, COLOR_ERROR, COLOR_SUCCESS, COLOR_WARNING, } from '../components/customTheme.js';
import { useFullscreen } from '../hooks/useFullscreen.js';
import { useTemplateManager } from '../hooks/useTemplateManager.js';
import { store } from '../utils/store.js';
const MAX_FILES = 10;
const MAX_CHANGES = 15;
const PATH_DISPLAY_LENGTH = 15;
function formatTemplateDisplay(templatePath, templateDir) {
const parts = templatePath.split(path.sep);
const filename = parts.pop() || '';
const dirPath = parts.join(path.sep);
if (dirPath && templateDir && dirPath.includes(templateDir)) {
const relativePath = dirPath.substring(dirPath.indexOf(templateDir) + templateDir.length + 1);
if (relativePath) {
const truncatedPath = relativePath.slice(-PATH_DISPLAY_LENGTH);
return `${truncatedPath}/${filename}`;
}
}
return filename;
}
const TemplateRow = React.memo(({ template, isLatest, templateDir, }) => {
const displayName = formatTemplateDisplay(template.path, templateDir ?? '');
const needsBuild = !template.buildState.lastBuildDate ||
template.currentHash !== template.buildState.lastBuildHash;
const color = template.buildState.lastAppliedError ? COLOR_ERROR : COLOR_SUCCESS;
return (React.createElement(Box, { marginLeft: 2, gap: 1 },
React.createElement(Box, { width: 2, justifyContent: "center" },
React.createElement(Text, { color: color }, template.buildState.lastAppliedError
? figures.cross
: isLatest
? figures.play
: figures.tick)),
React.createElement(Box, { width: 35 },
React.createElement(Text, null, displayName)),
React.createElement(Box, { minWidth: 18 },
React.createElement(Text, { dimColor: true },
"applied ",
React.createElement(TimeSince, { date: template.buildState.lastAppliedDate }))),
React.createElement(Box, null,
React.createElement(Text, { dimColor: true }, template.wip ? (React.createElement(React.Fragment, null, "wip")) : needsBuild ? (React.createElement(React.Fragment, null, "needs build")) : (React.createElement(React.Fragment, null,
"built ",
React.createElement(TimeSince, { date: template.buildState.lastBuildDate }),
" ago"))))));
});
TemplateRow.displayName = 'TemplateRow';
const UpdateLog = React.memo(({ updates, templateDir, }) => {
const sortedUpdates = useMemo(() => {
return [...updates]
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()) // Newest first
.slice(0, MAX_CHANGES) // Take most recent
.reverse(); // Display oldest to newest
}, [updates]);
const formatError = (error) => {
if (error instanceof Error)
return error.message;
if (typeof error === 'string')
return error;
if (typeof error === 'object' && error && 'error' in error) {
return String(error.error);
}
return String(error);
};
return (React.createElement(Box, { flexDirection: "column", marginTop: 1 },
React.createElement(Text, { bold: true }, "Changelog:"),
!sortedUpdates.length && React.createElement(Spinner, { label: "No updates yet, watching..." }),
sortedUpdates.map(update => (React.createElement(Box, { key: `${update.template.path}-${update.timestamp}`, marginLeft: 2 },
React.createElement(Text, { color: update.type === 'error'
? COLOR_ERROR
: update.type === 'applied'
? COLOR_SUCCESS
: COLOR_ACCENT },
update.type === 'error'
? figures.cross
: update.type === 'applied'
? figures.play
: figures.info,
' ',
formatTemplateDisplay(update.template.path, templateDir ?? ''),
":",
' ',
update.type === 'error'
? formatError(update.error)
: update.type === 'applied'
? 'applied successfully'
: 'changed'))))));
});
UpdateLog.displayName = 'UpdateLog';
export default function Watch() {
useFullscreen();
const { templates, updates, stats, isLoading, errors, latestPath, templateDir } = useTemplateManager();
const [showUpdates, setShowUpdates] = React.useState(store.get('showWatchLogs'));
const activeTemplates = useMemo(() => templates.slice(-MAX_FILES), [templates]);
const hasErrors = errors.size > 0;
useInput(input => {
if (input === 'u') {
const show = !showUpdates;
setShowUpdates(show);
store.set('showWatchLogs', show);
}
});
return (React.createElement(Box, { flexDirection: "column" },
React.createElement(Branding, { subtitle: "\uD83D\uDC40 Watch Mode" }),
React.createElement(Box, { marginBottom: 1 },
React.createElement(StatBadge, { label: "Total", value: stats.total, color: COLOR_SUCCESS }),
stats.needsBuild > 0 && (React.createElement(StatBadge, { label: "Needs Build", value: stats.needsBuild, color: COLOR_WARNING })),
stats.recentlyChanged > 0 && (React.createElement(StatBadge, { label: "Recent Changes", value: stats.recentlyChanged, color: COLOR_ACCENT })),
hasErrors && React.createElement(StatBadge, { label: "Errors", value: stats.errors, color: COLOR_ERROR })),
isLoading ? (React.createElement(Text, null, "\uD83D\uDD0D Finding templates...")) : (React.createElement(Box, { flexDirection: "column" },
React.createElement(Text, { bold: true }, "Recently modified templates:"),
activeTemplates.length === 0 && React.createElement(Text, { dimColor: true }, "No templates found"),
activeTemplates.map(template => (React.createElement(TemplateRow, { key: template.path, template: template, isLatest: template.path === latestPath, templateDir: templateDir }))),
showUpdates && React.createElement(UpdateLog, { updates: updates, templateDir: templateDir }),
hasErrors && (React.createElement(Box, { flexDirection: "column", marginTop: 1 },
React.createElement(Text, { bold: true, color: COLOR_ERROR }, "Errors:"),
Array.from(errors.entries()).map(([path, error]) => (React.createElement(Box, { key: path, marginLeft: 2, marginTop: 1 },
React.createElement(Text, { color: COLOR_ERROR, wrap: "wrap" },
formatTemplateDisplay(path, templateDir ?? ''),
": ",
String(error))))))))),
React.createElement(Box, { marginY: 1, flexDirection: "row", gap: 1 },
React.createElement(Quittable, null),
React.createElement(React.Fragment, null,
React.createElement(Box, { marginY: 1 },
React.createElement(Text, { dimColor: true }, "\u2022")),
React.createElement(Box, { marginY: 1 },
React.createElement(Text, { dimColor: true }, "Press "),
React.createElement(Text, { underline: showUpdates }, "u"),
React.createElement(Text, { dimColor: true }, " to toggle updates"))))));
}
//# sourceMappingURL=watch.js.map