Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[![npm](https://img.shields.io/npm/v/hyperparam)](https://www.npmjs.com/package/hyperparam)
[![workflow status](https://github.com/hyparam/hyperparam-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/hyparam/hyperparam-cli/actions)
[![mit license](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
![coverage](https://img.shields.io/badge/Coverage-35-darkred)
![coverage](https://img.shields.io/badge/Coverage-51-darkred)

This is the hyperparam cli tool.

Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
"build": "run-s build:lib build:types build:app",
"coverage": "vitest run -c vite.lib.config.js --coverage --coverage.include=src --coverage.include=bin",
"dev": "run-p -l watch:ts watch:vite watch:serve",
"lint": "eslint .",
"lint": "eslint",
"lint:fix": "eslint --fix",
"prepublishOnly": "npm run build",
"serve": "node bin/cli.js",
"preserve": "npm run build",
Expand All @@ -47,7 +48,7 @@
},
"dependencies": {
"hightable": "0.12.1",
"hyparquet": "1.8.6",
"hyparquet": "1.8.7",
"hyparquet-compressors": "1.0.0",
"react": "18.3.1",
"react-dom": "18.3.1"
Expand Down
17 changes: 17 additions & 0 deletions src/components/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ReactNode, useEffect, useState } from 'react'
import { cn } from '../lib/utils.js'
import Welcome from './Welcome.js'

interface LayoutProps {
children: ReactNode
Expand All @@ -21,10 +22,25 @@ interface LayoutProps {
* @param props.title - page title
*/
export default function Layout({ children, className, progress, error, title }: LayoutProps) {
const [showWelcome, setShowWelcome] = useState(false)

// Check localStorage on mount to see if the user has seen the welcome popup
useEffect(() => {
const dismissed = localStorage.getItem('welcome:dismissed') === 'true'
setShowWelcome(!dismissed)
}, [])

// Handle closing the welcome popup
function handleCloseWelcome() {
setShowWelcome(false)
localStorage.setItem('welcome:dismissed', 'true')
}

// Update title
useEffect(() => {
document.title = title ? `${title} - hyperparam` : 'hyperparam'
}, [title])

return <main className='main'>
<Sidebar />
<div className='content-container'>
Expand All @@ -38,6 +54,7 @@ export default function Layout({ children, className, progress, error, title }:
<div style={{ width: `${100 * progress}%` }} />
</div>
}
{showWelcome && <Welcome onClose={handleCloseWelcome} />}
</main>
}

Expand Down
50 changes: 50 additions & 0 deletions src/components/Welcome.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { MouseEvent, useEffect } from 'react'

interface WelcomePopupProps {
onClose: () => void
}

/**
* Welcome popup component shown to first-time users.
* Clicking outside the popup or pressing Escape will dismiss it.
*/
export default function Welcome({ onClose }: WelcomePopupProps) {
// Close popup when clicking outside
function handleBackdropClick(e: MouseEvent) {
if (e.target === e.currentTarget) {
onClose()
}
}

// Close popup when pressing Escape key
useEffect(() => {
function handleEscKey(e: KeyboardEvent) {
if (e.key === 'Escape') {
onClose()
}
}

window.addEventListener('keydown', handleEscKey)
return () => { window.removeEventListener('keydown', handleEscKey) }
}, [onClose])

return (
<div className="welcome" onClick={handleBackdropClick}>
<div>
<h2>npx hyperparam</h2>
<p>
This is the <a href="https://hyperparam.app">Hyperparam</a> cli for local data viewing.
</p>
<p>
This tool lets you browse and explore large datasets particularly in parquet format.
</p>
<p>
Supported file types include Parquet, CSV, JSON, Markdown, and Text.
</p>
<button onClick={onClose}>
Got it
</button>
</div>
</div>
)
}
63 changes: 62 additions & 1 deletion src/styles/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,26 @@
padding: 0;
}

button {
background-color: #111;
color: #fff;
border: none;
border-radius: 8px;
padding: 8px 16px;
cursor: pointer;
outline: none;
transition: background-color 0.2s;
}
button:active,
button:focus,
button:hover {
background-color: #333;
}

code {
font-family: monospace;
}

#app {
display: flex;
flex-direction: column;
Expand Down Expand Up @@ -499,4 +519,45 @@ button.close-button:hover {
width: 100%;
overflow-x: auto;
white-space: pre-wrap;
}
}

/* Welcome popup */
.welcome {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
animation: fadeIn 0.3s ease-in;
}

.welcome > div {
background-color: white;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
display: flex;
flex-direction: column;
padding: 30px;
width: 450px;
max-width: 90%;
}

.welcome h2 {
color: #342267;
margin-bottom: 16px;
font-size: 24px;
}

.welcome p {
color: #444;
line-height: 1.5;
margin-bottom: 24px;
}

.welcome button {
margin-left: auto;
}
68 changes: 68 additions & 0 deletions test/components/Welcome.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { fireEvent, render } from '@testing-library/react'
import React from 'react'
import { describe, expect, it, vi } from 'vitest'
import Welcome from '../../src/components/Welcome.js'

describe('Welcome Component', () => {
it('renders welcome content', () => {
const onClose = vi.fn()
const { getByRole, getByText } = render(<Welcome onClose={onClose} />)

expect(getByText('npx hyperparam')).toBeDefined()
expect(getByText('Got it')).toBeDefined()
const button = getByRole('button')
expect(button).toBeDefined()
expect(button.textContent).toBe('Got it')
})

it('calls onClose when button is clicked', () => {
const onClose = vi.fn()
const { getByRole } = render(<Welcome onClose={onClose} />)

fireEvent.click(getByRole('button'))
expect(onClose).toHaveBeenCalledTimes(1)
})

it('calls onClose when clicking outside the popup', () => {
const onClose = vi.fn()
const { container } = render(<Welcome onClose={onClose} />)

// Find the backdrop element
const backdropElement = container.querySelector('.welcome')
expect(backdropElement).toBeDefined()

if (backdropElement) {
fireEvent.click(backdropElement)
expect(onClose).toHaveBeenCalledTimes(1)
}
})

it('does not call onClose when clicking inside the popup', () => {
const onClose = vi.fn()
const { getByText } = render(<Welcome onClose={onClose} />)

// Find and click on an element inside the popup content
const paragraphElement = getByText('Supported file types include Parquet, CSV, JSON, Markdown, and Text.')
fireEvent.click(paragraphElement)

expect(onClose).not.toHaveBeenCalled()
})

it('calls onClose when pressing Escape key', () => {
const onClose = vi.fn()
render(<Welcome onClose={onClose} />)

// Simulate pressing the Escape key
fireEvent.keyDown(window, { key: 'Escape' })
expect(onClose).toHaveBeenCalledTimes(1)
})

it('does not call onClose when pressing other keys', () => {
const onClose = vi.fn()
render(<Welcome onClose={onClose} />)

// Simulate pressing a different key
fireEvent.keyDown(window, { key: 'Enter' })
expect(onClose).not.toHaveBeenCalled()
})
})