mirror of
https://github.com/diced/zipline.git
synced 2025-12-12 07:40:45 -08:00
feat: asciinema in dashboard rendering
This commit is contained in:
@@ -122,6 +122,7 @@
|
|||||||
["calx", ["application/vnd.ms-office.calx"]],
|
["calx", ["application/vnd.ms-office.calx"]],
|
||||||
["cap", ["application/vnd.tcpdump.pcap"]],
|
["cap", ["application/vnd.tcpdump.pcap"]],
|
||||||
["car", ["application/vnd.curl.car"]],
|
["car", ["application/vnd.curl.car"]],
|
||||||
|
["cast", ["application/x-asciicast"]],
|
||||||
["cat", ["application/vnd.ms-pki.seccat"]],
|
["cat", ["application/vnd.ms-pki.seccat"]],
|
||||||
["cb7", ["application/x-cbr"]],
|
["cb7", ["application/x-cbr"]],
|
||||||
["cba", ["application/x-cbr"]],
|
["cba", ["application/x-cbr"]],
|
||||||
|
|||||||
@@ -56,6 +56,7 @@
|
|||||||
"@smithy/node-http-handler": "^4.1.0",
|
"@smithy/node-http-handler": "^4.1.0",
|
||||||
"@tabler/icons-react": "^3.34.1",
|
"@tabler/icons-react": "^3.34.1",
|
||||||
"argon2": "^0.43.1",
|
"argon2": "^0.43.1",
|
||||||
|
"asciinema-player": "^3.10.0",
|
||||||
"bytes": "^3.1.2",
|
"bytes": "^3.1.2",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"colorette": "^2.0.20",
|
"colorette": "^2.0.20",
|
||||||
|
|||||||
36
pnpm-lock.yaml
generated
36
pnpm-lock.yaml
generated
@@ -83,6 +83,9 @@ importers:
|
|||||||
argon2:
|
argon2:
|
||||||
specifier: ^0.43.1
|
specifier: ^0.43.1
|
||||||
version: 0.43.1
|
version: 0.43.1
|
||||||
|
asciinema-player:
|
||||||
|
specifier: ^3.10.0
|
||||||
|
version: 3.10.0
|
||||||
bytes:
|
bytes:
|
||||||
specifier: ^3.1.2
|
specifier: ^3.1.2
|
||||||
version: 3.1.2
|
version: 3.1.2
|
||||||
@@ -2164,6 +2167,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==}
|
resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
asciinema-player@3.10.0:
|
||||||
|
resolution: {integrity: sha512-shoOK6F606nDKZxDVM7JuGSCAyWLePoGRFNlV+FqiP5Sqvyn0BlE7wlbjZyd2X4P1iRhv/HKfVNtnQIxmgphRA==}
|
||||||
|
|
||||||
ast-types-flow@0.0.8:
|
ast-types-flow@0.0.8:
|
||||||
resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==}
|
resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==}
|
||||||
|
|
||||||
@@ -4248,6 +4254,16 @@ packages:
|
|||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
hasBin: true
|
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:
|
set-blocking@2.0.0:
|
||||||
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
|
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
|
||||||
|
|
||||||
@@ -4311,6 +4327,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
|
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
solid-js@1.9.9:
|
||||||
|
resolution: {integrity: sha512-A0ZBPJQldAeGCTW0YRYJmt7RCeh5rbFfPZ2aOttgYnctHE7HgKeHCBB/PVc2P7eOfmNXqMFFFoYYdm3S4dcbkA==}
|
||||||
|
|
||||||
sonic-boom@4.2.0:
|
sonic-boom@4.2.0:
|
||||||
resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==}
|
resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==}
|
||||||
|
|
||||||
@@ -7177,6 +7196,11 @@ snapshots:
|
|||||||
get-intrinsic: 1.3.0
|
get-intrinsic: 1.3.0
|
||||||
is-array-buffer: 3.0.5
|
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: {}
|
ast-types-flow@0.0.8: {}
|
||||||
|
|
||||||
async-function@1.0.0: {}
|
async-function@1.0.0: {}
|
||||||
@@ -9699,6 +9723,12 @@ snapshots:
|
|||||||
|
|
||||||
semver@7.7.2: {}
|
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-blocking@2.0.0: {}
|
||||||
|
|
||||||
set-cookie-parser@2.7.1: {}
|
set-cookie-parser@2.7.1: {}
|
||||||
@@ -9800,6 +9830,12 @@ snapshots:
|
|||||||
|
|
||||||
slash@3.0.0: {}
|
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:
|
sonic-boom@4.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
atomic-sleep: 1.0.0
|
atomic-sleep: 1.0.0
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { useEffect, useState, useCallback, useMemo } from 'react';
|
|||||||
import { renderMode } from '../pages/upload/renderMode';
|
import { renderMode } from '../pages/upload/renderMode';
|
||||||
import Render from '../render/Render';
|
import Render from '../render/Render';
|
||||||
import fileIcon from './fileIcon';
|
import fileIcon from './fileIcon';
|
||||||
|
import Asciinema from '../render/Asciinema';
|
||||||
|
|
||||||
function PlaceholderContent({ text, Icon }: { text: string; Icon: Icon }) {
|
function PlaceholderContent({ text, Icon }: { text: string; Icon: Icon }) {
|
||||||
return (
|
return (
|
||||||
@@ -83,7 +84,7 @@ export default function DashboardFileType({
|
|||||||
const renderIn = useMemo(() => renderMode(file.name.split('.').pop() || ''), [file.name]);
|
const renderIn = useMemo(() => renderMode(file.name.split('.').pop() || ''), [file.name]);
|
||||||
|
|
||||||
const [fileContent, setFileContent] = useState('');
|
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);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
@@ -164,8 +165,10 @@ export default function DashboardFileType({
|
|||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
|
|
||||||
switch (type) {
|
const isAsciicast = file.type === 'application/x-asciicast' || file.name.endsWith('.cast');
|
||||||
case 'video':
|
|
||||||
|
switch (true) {
|
||||||
|
case type === 'video':
|
||||||
return show ? (
|
return show ? (
|
||||||
<video
|
<video
|
||||||
width='100%'
|
width='100%'
|
||||||
@@ -201,7 +204,7 @@ export default function DashboardFileType({
|
|||||||
) : (
|
) : (
|
||||||
<Placeholder text={`Click to play video ${file.name}`} Icon={fileIcon(file.type)} />
|
<Placeholder text={`Click to play video ${file.name}`} Icon={fileIcon(file.type)} />
|
||||||
);
|
);
|
||||||
case 'image':
|
case type === 'image':
|
||||||
return show ? (
|
return show ? (
|
||||||
<Center>
|
<Center>
|
||||||
<MantineImage
|
<MantineImage
|
||||||
@@ -240,7 +243,7 @@ export default function DashboardFileType({
|
|||||||
alt={file.name || 'Image'}
|
alt={file.name || 'Image'}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'audio':
|
case type === 'audio':
|
||||||
return show ? (
|
return show ? (
|
||||||
<audio
|
<audio
|
||||||
autoPlay
|
autoPlay
|
||||||
@@ -252,7 +255,7 @@ export default function DashboardFileType({
|
|||||||
) : (
|
) : (
|
||||||
<Placeholder text={`Click to play audio ${file.name}`} Icon={fileIcon(file.type)} />
|
<Placeholder text={`Click to play audio ${file.name}`} Icon={fileIcon(file.type)} />
|
||||||
);
|
);
|
||||||
case 'text':
|
case type === 'text':
|
||||||
return show ? (
|
return show ? (
|
||||||
fileContent.trim() === '' ? (
|
fileContent.trim() === '' ? (
|
||||||
<LoadingOverlay
|
<LoadingOverlay
|
||||||
@@ -276,6 +279,15 @@ export default function DashboardFileType({
|
|||||||
) : (
|
) : (
|
||||||
<Placeholder text={`Click to view text ${file.name}`} Icon={fileIcon(file.type)} />
|
<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:
|
default:
|
||||||
if (dbFile && !show)
|
if (dbFile && !show)
|
||||||
return <Placeholder text={`Click to view file ${file.name}`} Icon={fileIcon(file.type)} />;
|
return <Placeholder text={`Click to view file ${file.name}`} Icon={fileIcon(file.type)} />;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
IconFileTypeHtml,
|
IconFileTypeHtml,
|
||||||
IconFileTypeJs,
|
IconFileTypeJs,
|
||||||
IconFileTypeJsx,
|
IconFileTypeJsx,
|
||||||
|
IconFileTypePdf,
|
||||||
IconFileTypePhp,
|
IconFileTypePhp,
|
||||||
IconFileTypePpt,
|
IconFileTypePpt,
|
||||||
IconFileTypeRs,
|
IconFileTypeRs,
|
||||||
@@ -49,7 +50,7 @@ const icons: Record<string, Icon> = {
|
|||||||
'application/x-gzip': IconFileZip,
|
'application/x-gzip': IconFileZip,
|
||||||
|
|
||||||
// common text/document files that are not detected by the 'text' type
|
// common text/document files that are not detected by the 'text' type
|
||||||
'application/pdf': IconFileText,
|
'application/pdf': IconFileTypePdf,
|
||||||
'application/msword': IconFileTypeDocx,
|
'application/msword': IconFileTypeDocx,
|
||||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': IconFileTypeDocx,
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': IconFileTypeDocx,
|
||||||
'application/vnd.ms-excel': IconFileTypeXls,
|
'application/vnd.ms-excel': IconFileTypeXls,
|
||||||
@@ -67,6 +68,7 @@ const icons: Record<string, Icon> = {
|
|||||||
'text/javascript': IconFileTypeJs,
|
'text/javascript': IconFileTypeJs,
|
||||||
'application/json': IconBracketsContain,
|
'application/json': IconBracketsContain,
|
||||||
'text/xml': IconFileTypeXml,
|
'text/xml': IconFileTypeXml,
|
||||||
|
'application/x-asciicast': IconTerminal2,
|
||||||
|
|
||||||
// zipline text uploads
|
// zipline text uploads
|
||||||
'text/x-zipline-html': IconFileTypeHtml,
|
'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