Implementation:TobikoData Sqlmesh FileExplorer Context
| Knowledge Sources | |
|---|---|
| Domains | Web_UI, File_Management, State_Management |
| Last Updated | 2026-02-07 20:00 GMT |
Overview
Concrete tool for managing file explorer state and operations through React Context in the SQLMesh web client.
Description
This module provides a React Context provider that centralizes all file explorer operations including creating, renaming, moving, and deleting files and directories. It manages the artifact rename state, range selection logic, and confirmation dialogs for destructive operations. The context integrates with the backend API for persistence and handles duplicate name detection and loading states.
Key features include:
- FileExplorerContext with comprehensive file operation methods
- createDirectory and createFile with unique name generation
- renameArtifact with duplicate detection and path updates
- removeArtifacts with confirmation dialog for destructive actions
- moveArtifacts with drag-drop support and duplicate handling
- selectArtifactsInRange for Shift+click multi-selection
- Position detection (top/middle/bottom) for visual feedback in range selections
- Loading state management to prevent concurrent operations
- Error handling integrated with notification system
Usage
Wrap the file explorer UI with FileExplorerProvider and access operations via the useFileExplorer hook. The context handles all state updates and API calls, providing a clean interface for file operations.
Code Reference
Source Location
- Repository: TobikoData_Sqlmesh
- File: web/client/src/library/components/fileExplorer/context.tsx
Signature
// Provider component
function FileExplorerProvider({ children }: { children: React.ReactNode }): JSX.Element
// Hook to access context
function useFileExplorer(): FileExplorer
// Context interface
interface FileExplorer {
artifactRename?: ModelArtifact
setArtifactRename: (artifact?: ModelArtifact) => void
selectArtifactsInRange: (to: ModelArtifact) => void
createDirectory: (parent: ModelDirectory) => void
createFile: (parent: ModelDirectory, extension?: string) => void
renameArtifact: (artifact: ModelArtifact, newName?: string) => void
removeArtifacts: (artifacts: ModelArtifact[]) => void
removeArtifactWithConfirmation: (artifact: ModelArtifact) => void
moveArtifacts: (artifacts: ModelArtifact[], target: ModelDirectory) => void
isTopGroupInActiveRange: (artifact: ModelArtifact) => boolean
isBottomGroupInActiveRange: (artifact: ModelArtifact) => boolean
isMiddleGroupInActiveRange: (artifact: ModelArtifact) => boolean
}
// Constants
const EnumFileExplorerChange = {
Added: 1,
Modified: 2,
Deleted: 3,
} as const
Import
import FileExplorerProvider, { useFileExplorer } from '@components/fileExplorer/context'
I/O Contract
Inputs
| Name | Type | Required | Description |
|---|---|---|---|
| parent | ModelDirectory | Yes (for create ops) | Parent directory for new files/folders |
| artifact | ModelArtifact | Yes (for ops) | Target artifact for rename/remove/move |
| artifacts | ModelArtifact[] | Yes (for move/remove) | Array of artifacts to operate on |
| target | ModelDirectory | Yes (for move) | Destination directory for move operation |
| newName | string | No | New name for rename operation |
| extension | string | No | File extension for createFile (auto-detected if omitted) |
Outputs
| Name | Type | Description |
|---|---|---|
| context | FileExplorer | Object with all file operation methods and state |
Usage Examples
import FileExplorerProvider, { useFileExplorer } from '@components/fileExplorer/context'
import { ModelDirectory } from '~/models/directory'
// Example 1: Setup with provider
function App() {
return (
<FileExplorerProvider>
<FileExplorerUI />
</FileExplorerProvider>
)
}
// Example 2: Using context in components
function FileExplorerUI() {
const {
createFile,
createDirectory,
renameArtifact,
removeArtifactWithConfirmation,
moveArtifacts,
selectArtifactsInRange,
artifactRename,
setArtifactRename,
} = useFileExplorer()
const handleNewFile = (parent: ModelDirectory) => {
// Auto-detects extension based on parent type
// models/ -> .sql, tests/ -> .yaml, others -> .py
createFile(parent)
}
const handleNewFolder = (parent: ModelDirectory) => {
createDirectory(parent) // Creates "new_directory" or "new_directory_1" etc.
}
const handleRename = (artifact: ModelArtifact, newName: string) => {
renameArtifact(artifact, newName)
// Handles duplicate detection
// Updates file paths in backend
// Refreshes file tree
}
const handleDelete = (artifact: ModelArtifact) => {
// Shows confirmation dialog automatically
removeArtifactWithConfirmation(artifact)
}
return (
<div>
<button onClick={() => handleNewFile(rootDir)}>New File</button>
<button onClick={() => handleNewFolder(rootDir)}>New Folder</button>
</div>
)
}
// Example 3: Range selection for multi-select
function DirectoryItem({ directory }: { directory: ModelDirectory }) {
const { selectArtifactsInRange } = useFileExplorer()
const handleClick = (e: React.MouseEvent) => {
if (e.shiftKey) {
selectArtifactsInRange(directory)
// Selects all artifacts between first selected and current
}
}
return <div onClick={handleClick}>{directory.name}</div>
}
// Example 4: Move artifacts with drag-drop
function DraggableFile({ file }: { file: ModelFile }) {
const { moveArtifacts } = useFileExplorer()
const activeRange = useStoreProject(s => s.activeRange)
const inActiveRange = useStoreProject(s => s.inActiveRange)
const handleDrop = (target: ModelDirectory) => {
const artifacts = inActiveRange(file) ? activeRange : [file]
moveArtifacts(artifacts, target)
// Checks for duplicates
// Shows confirmation if needed
// Updates paths in backend
}
return <div>{file.name}</div>
}
// Example 5: Rename flow
function RenameFlow() {
const { artifactRename, setArtifactRename, renameArtifact } = useFileExplorer()
// User right-clicks directory, selects "Rename"
// -> setArtifactRename(directory) called
// -> Input field shown inline
const handleSubmit = (newName: string) => {
if (artifactRename) {
renameArtifact(artifactRename, newName)
setArtifactRename(undefined) // Exit rename mode
}
}
return artifactRename ? (
<input
defaultValue={artifactRename.name}
onBlur={(e) => handleSubmit(e.target.value)}
/>
) : null
}
// Example 6: Delete with confirmation
function DeleteWithConfirmation({ artifact }: { artifact: ModelArtifact }) {
const { removeArtifactWithConfirmation } = useFileExplorer()
// Clicking delete shows modal:
// "Are you sure you want to remove the file 'my_model.sql'?"
// [Yes, Remove] [No, Cancel]
return (
<button onClick={() => removeArtifactWithConfirmation(artifact)}>
Delete
</button>
)
}
// Example 7: Position detection for visual feedback
function ArtifactItem({ artifact }: { artifact: ModelArtifact }) {
const {
isTopGroupInActiveRange,
isMiddleGroupInActiveRange,
isBottomGroupInActiveRange,
} = useFileExplorer()
const isTop = isTopGroupInActiveRange(artifact)
const isMiddle = isMiddleGroupInActiveRange(artifact)
const isBottom = isBottomGroupInActiveRange(artifact)
// Use for border radius styling:
// - Top: rounded top corners
// - Middle: no rounded corners
// - Bottom: rounded bottom corners
return (
<div className={clsx(
isTop && 'rounded-t',
isMiddle && 'rounded-none',
isBottom && 'rounded-b'
)}>
{artifact.name}
</div>
)
}