mirror of
https://github.com/diced/zipline.git
synced 2025-12-05 20:40:12 -08:00
feat: asciinema in dashboard rendering
This commit is contained in:
@@ -122,6 +122,7 @@
|
||||
["calx", ["application/vnd.ms-office.calx"]],
|
||||
["cap", ["application/vnd.tcpdump.pcap"]],
|
||||
["car", ["application/vnd.curl.car"]],
|
||||
["cast", ["application/x-asciicast"]],
|
||||
["cat", ["application/vnd.ms-pki.seccat"]],
|
||||
["cb7", ["application/x-cbr"]],
|
||||
["cba", ["application/x-cbr"]],
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"@smithy/node-http-handler": "^4.1.0",
|
||||
"@tabler/icons-react": "^3.34.1",
|
||||
"argon2": "^0.43.1",
|
||||
"asciinema-player": "^3.10.0",
|
||||
"bytes": "^3.1.2",
|
||||
"clsx": "^2.1.1",
|
||||
"colorette": "^2.0.20",
|
||||
|
||||
36
pnpm-lock.yaml
generated
36
pnpm-lock.yaml
generated
@@ -83,6 +83,9 @@ importers:
|
||||
argon2:
|
||||
specifier: ^0.43.1
|
||||
version: 0.43.1
|
||||
asciinema-player:
|
||||
specifier: ^3.10.0
|
||||
version: 3.10.0
|
||||
bytes:
|
||||
specifier: ^3.1.2
|
||||
version: 3.1.2
|
||||
@@ -2164,6 +2167,9 @@ packages:
|
||||
resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
asciinema-player@3.10.0:
|
||||
resolution: {integrity: sha512-shoOK6F606nDKZxDVM7JuGSCAyWLePoGRFNlV+FqiP5Sqvyn0BlE7wlbjZyd2X4P1iRhv/HKfVNtnQIxmgphRA==}
|
||||
|
||||
ast-types-flow@0.0.8:
|
||||
resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==}
|
||||
|
||||
@@ -4248,6 +4254,16 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
seroval-plugins@1.3.2:
|
||||
resolution: {integrity: sha512-0QvCV2lM3aj/U3YozDiVwx9zpH0q8A60CTWIv4Jszj/givcudPb48B+rkU5D51NJ0pTpweGMttHjboPa9/zoIQ==}
|
||||
engines: {node: '>=10'}
|
||||
peerDependencies:
|
||||
seroval: ^1.0
|
||||
|
||||
seroval@1.3.2:
|
||||
resolution: {integrity: sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
set-blocking@2.0.0:
|
||||
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
|
||||
|
||||
@@ -4311,6 +4327,9 @@ packages:
|
||||
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
solid-js@1.9.9:
|
||||
resolution: {integrity: sha512-A0ZBPJQldAeGCTW0YRYJmt7RCeh5rbFfPZ2aOttgYnctHE7HgKeHCBB/PVc2P7eOfmNXqMFFFoYYdm3S4dcbkA==}
|
||||
|
||||
sonic-boom@4.2.0:
|
||||
resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==}
|
||||
|
||||
@@ -7177,6 +7196,11 @@ snapshots:
|
||||
get-intrinsic: 1.3.0
|
||||
is-array-buffer: 3.0.5
|
||||
|
||||
asciinema-player@3.10.0:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.2
|
||||
solid-js: 1.9.9
|
||||
|
||||
ast-types-flow@0.0.8: {}
|
||||
|
||||
async-function@1.0.0: {}
|
||||
@@ -9699,6 +9723,12 @@ snapshots:
|
||||
|
||||
semver@7.7.2: {}
|
||||
|
||||
seroval-plugins@1.3.2(seroval@1.3.2):
|
||||
dependencies:
|
||||
seroval: 1.3.2
|
||||
|
||||
seroval@1.3.2: {}
|
||||
|
||||
set-blocking@2.0.0: {}
|
||||
|
||||
set-cookie-parser@2.7.1: {}
|
||||
@@ -9800,6 +9830,12 @@ snapshots:
|
||||
|
||||
slash@3.0.0: {}
|
||||
|
||||
solid-js@1.9.9:
|
||||
dependencies:
|
||||
csstype: 3.1.3
|
||||
seroval: 1.3.2
|
||||
seroval-plugins: 1.3.2(seroval@1.3.2)
|
||||
|
||||
sonic-boom@4.2.0:
|
||||
dependencies:
|
||||
atomic-sleep: 1.0.0
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useEffect, useState, useCallback, useMemo } from 'react';
|
||||
import { renderMode } from '../pages/upload/renderMode';
|
||||
import Render from '../render/Render';
|
||||
import fileIcon from './fileIcon';
|
||||
import Asciinema from '../render/Asciinema';
|
||||
|
||||
function PlaceholderContent({ text, Icon }: { text: string; Icon: Icon }) {
|
||||
return (
|
||||
@@ -83,7 +84,7 @@ export default function DashboardFileType({
|
||||
const renderIn = useMemo(() => renderMode(file.name.split('.').pop() || ''), [file.name]);
|
||||
|
||||
const [fileContent, setFileContent] = useState('');
|
||||
const [type, setType] = useState<string>(file.type.split('/')[0]);
|
||||
const [type, setType] = useState(file.type.split('/')[0]);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
@@ -164,8 +165,10 @@ export default function DashboardFileType({
|
||||
</Paper>
|
||||
);
|
||||
|
||||
switch (type) {
|
||||
case 'video':
|
||||
const isAsciicast = file.type === 'application/x-asciicast' || file.name.endsWith('.cast');
|
||||
|
||||
switch (true) {
|
||||
case type === 'video':
|
||||
return show ? (
|
||||
<video
|
||||
width='100%'
|
||||
@@ -201,7 +204,7 @@ export default function DashboardFileType({
|
||||
) : (
|
||||
<Placeholder text={`Click to play video ${file.name}`} Icon={fileIcon(file.type)} />
|
||||
);
|
||||
case 'image':
|
||||
case type === 'image':
|
||||
return show ? (
|
||||
<Center>
|
||||
<MantineImage
|
||||
@@ -240,7 +243,7 @@ export default function DashboardFileType({
|
||||
alt={file.name || 'Image'}
|
||||
/>
|
||||
);
|
||||
case 'audio':
|
||||
case type === 'audio':
|
||||
return show ? (
|
||||
<audio
|
||||
autoPlay
|
||||
@@ -252,7 +255,7 @@ export default function DashboardFileType({
|
||||
) : (
|
||||
<Placeholder text={`Click to play audio ${file.name}`} Icon={fileIcon(file.type)} />
|
||||
);
|
||||
case 'text':
|
||||
case type === 'text':
|
||||
return show ? (
|
||||
fileContent.trim() === '' ? (
|
||||
<LoadingOverlay
|
||||
@@ -276,6 +279,15 @@ export default function DashboardFileType({
|
||||
) : (
|
||||
<Placeholder text={`Click to view text ${file.name}`} Icon={fileIcon(file.type)} />
|
||||
);
|
||||
case isAsciicast === true:
|
||||
return show && dbFile ? (
|
||||
<Asciinema src={`/raw/${file.name}`} />
|
||||
) : (
|
||||
<Placeholder
|
||||
text={`Click to download asciinema cast ${file.name}`}
|
||||
Icon={fileIcon('application/x-asciicast')}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
if (dbFile && !show)
|
||||
return <Placeholder text={`Click to view file ${file.name}`} Icon={fileIcon(file.type)} />;
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
IconFileTypeHtml,
|
||||
IconFileTypeJs,
|
||||
IconFileTypeJsx,
|
||||
IconFileTypePdf,
|
||||
IconFileTypePhp,
|
||||
IconFileTypePpt,
|
||||
IconFileTypeRs,
|
||||
@@ -49,7 +50,7 @@ const icons: Record<string, Icon> = {
|
||||
'application/x-gzip': IconFileZip,
|
||||
|
||||
// common text/document files that are not detected by the 'text' type
|
||||
'application/pdf': IconFileText,
|
||||
'application/pdf': IconFileTypePdf,
|
||||
'application/msword': IconFileTypeDocx,
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': IconFileTypeDocx,
|
||||
'application/vnd.ms-excel': IconFileTypeXls,
|
||||
@@ -67,6 +68,7 @@ const icons: Record<string, Icon> = {
|
||||
'text/javascript': IconFileTypeJs,
|
||||
'application/json': IconBracketsContain,
|
||||
'text/xml': IconFileTypeXml,
|
||||
'application/x-asciicast': IconTerminal2,
|
||||
|
||||
// zipline text uploads
|
||||
'text/x-zipline-html': IconFileTypeHtml,
|
||||
|
||||
7
src/components/render/Asciinema/asciinema-player.d.ts
vendored
Normal file
7
src/components/render/Asciinema/asciinema-player.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
declare module 'asciinema-player' {
|
||||
export function create(
|
||||
src: string,
|
||||
container: HTMLElement,
|
||||
options?: { autoplay?: boolean; cols?: number; rows?: number },
|
||||
): void;
|
||||
}
|
||||
47
src/components/render/Asciinema/index.tsx
Normal file
47
src/components/render/Asciinema/index.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Box, LoadingOverlay } from '@mantine/core';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
export default function Asciinema({ src }: { src: string }) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
const loadPlayer = async () => {
|
||||
const AsciinemaPlayer = await import('asciinema-player');
|
||||
await import('asciinema-player/dist/bundle/asciinema-player.css');
|
||||
|
||||
if (ref.current && !cancelled) {
|
||||
ref.current.innerHTML = '';
|
||||
|
||||
AsciinemaPlayer.create(src, ref.current);
|
||||
setLoaded(true);
|
||||
}
|
||||
};
|
||||
|
||||
loadPlayer();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (ref.current) {
|
||||
ref.current.innerHTML = '';
|
||||
}
|
||||
};
|
||||
}, [src]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!loaded && (
|
||||
<Box pos='relative' h={400}>
|
||||
<LoadingOverlay visible />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<div style={location.pathname.startsWith('/view') ? { width: '70vw' } : undefined} ref={ref} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user