Skip to content

Commit

Permalink
feat(ui): add slider to sync multiples layer at a time
Browse files Browse the repository at this point in the history
  • Loading branch information
LucasMrqes committed Oct 4, 2024
1 parent 49b6d3b commit db1772b
Show file tree
Hide file tree
Showing 7 changed files with 370 additions and 11 deletions.
1 change: 1 addition & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"axios": "^1.5.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-focus-lock": "^2.13.2",
"react-router-dom": "^6.16.0",
"react-tooltip": "^5.21.6",
"tailwind-merge": "^2.0.0"
Expand Down
98 changes: 98 additions & 0 deletions ui/src/components/tools/LayerChecklist.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import React, { useState, useEffect, useRef } from 'react';
import Checkbox from '@/components/core/Checkbox';
import Button from '@/components/core/Button';
import Tag from '@/components/widgets/Tag';
import { Layer } from '@/clients/layers/types';

// Define the props for the LayerChecklist component
interface LayerChecklistProps {
layers: Layer[];
variant?: 'light' | 'dark'; // Optional: To pass variant to Checkbox
onSelectionChange?: (selectedLayers: { name: string; namespace: string }[]) => void; // Updated callback prop
}

const LayerChecklist: React.FC<LayerChecklistProps> = ({
layers,
variant = 'light',
onSelectionChange,
}) => {
// State to keep track of selected layers using unique keys
const [selectedLayers, setSelectedLayers] = useState<{ name: string; namespace: string }[]>([]);
const selectAllRef = useRef<HTMLInputElement>(null);

// Function to generate a unique key for each layer
const getLayerKey = (layer: Layer): string => `${layer.namespace}-${layer.name}`;

// Update the indeterminate state based on selection
useEffect(() => {
if (selectAllRef.current) {
const isIndeterminate =
selectedLayers.length > 0 && selectedLayers.length < layers.length;
selectAllRef.current.indeterminate = isIndeterminate;
}
}, [selectedLayers, layers.length]);

// Handler for individual layer checkbox toggle
const handleToggle = (layer: Layer) => {
setSelectedLayers((prevSelected) =>
prevSelected.some((selectedLayer) => selectedLayer.name === layer.name && selectedLayer.namespace === layer.namespace)
? prevSelected.filter((selectedLayer) => selectedLayer.name !== layer.name || selectedLayer.namespace !== layer.namespace)
: [...prevSelected, { name: layer.name, namespace: layer.namespace }]
);
};

// Handler to select all layers
const handleSelectAll = () => {
setSelectedLayers(layers.map(layer => ({ name: layer.name, namespace: layer.namespace })));
};

// Handler to unselect all layers
const handleUnselectAll = () => {
setSelectedLayers([]);
};

useEffect(() => {
if (onSelectionChange) {
onSelectionChange(selectedLayers);
}
}, [selectedLayers, onSelectionChange]);

return (
<div className="max-w-md mx-auto p-4 bg-white shadow-md rounded-md">
<div className="flex justify-start items-center mb-4 space-x-2">
<Button
variant={"tertiary"}
className='text-sm px-0'
onClick={handleSelectAll}>
Select All
</Button>
<Button
variant={"tertiary"}
className='text-sm'
onClick={handleUnselectAll}>
Unselect All
</Button>
</div>
<ul className="space-y-2">
{layers.map((layer) => {
const key = getLayerKey(layer);
return (
<li key={key}>
<div className="flex justify-between">
<Checkbox
label={`${layer.namespace}/${layer.name}`}
checked={selectedLayers.some(selectedLayer => selectedLayer.name === layer.name && selectedLayer.namespace === layer.namespace)}
onChange={() => handleToggle(layer)}
variant={variant}
/>
<Tag variant={layer.state}/>
</div>
</li>
);
})}
</ul>
</div>
);
};

export default LayerChecklist;
42 changes: 42 additions & 0 deletions ui/src/components/widgets/ProgressBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react';

interface ProgressBarProps {
/**
* Progress value between 0 and 100
*/
value: number;
label?: string;
color?: string;
className?: string;
}

const ProgressBar: React.FC<ProgressBarProps> = ({
value,
label,
color = 'bg-blue-500',
className = '',
}) => {
// Ensure the value is between 0 and 100
const normalizedValue = Math.min(Math.max(value, 0), 100);

return (
<div className={`w-full bg-gray-200 rounded-full h-4 ${className}`} aria-label="Progress Bar">
<div
className={`${color} h-4 rounded-full transition-width duration-300 ease-in-out`}
style={{ width: `${normalizedValue}%` }}
role="progressbar"
aria-valuenow={normalizedValue}
aria-valuemin={0}
aria-valuemax={100}
>
{label && (
<span className="sr-only">
{label}
</span>
)}
</div>
</div>
);
};

export default ProgressBar;
79 changes: 79 additions & 0 deletions ui/src/modals/SlidingPane.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React, { useEffect } from 'react';
import ReactDOM from 'react-dom';
import FocusLock from 'react-focus-lock';

interface SlidingPaneProps {
isOpen: boolean;
onClose: () => void;
children?: React.ReactNode;
width?: string;
}

const SlidingPane: React.FC<SlidingPaneProps> = ({
isOpen,
onClose,
children,
width = 'w-1/3',
}) => {
// Handle Escape key to close the pane
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isOpen) {
onClose();
}
};

document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [isOpen, onClose]);

// Prevent background scrolling when pane is open
useEffect(() => {
if (isOpen) {
document.body.classList.add('overflow-hidden');
} else {
document.body.classList.remove('overflow-hidden');
}

return () => {
document.body.classList.remove('overflow-hidden');
};
}, [isOpen]);


return ReactDOM.createPortal(
<>
{/* Background */}
<div
className={`fixed inset-0 flex bg-nuances-400 bg-opacity-50 z-9 duration-300 ease-in-out ${
isOpen ? 'opacity-100 visible' : 'opacity-0 invisible'
}`}
onClick={onClose}
aria-hidden={!isOpen}
></div>

{/* Sliding Pane */}
<FocusLock disabled={!isOpen}>
<div
className={`fixed top-0 right-0 h-screen bg-primary-100 z-10 shadow-lg transform transition-transform duration-300 ease-in-out ${
isOpen ? 'translate-x-0' : 'translate-x-full'
} ${width}`}
>
{/* Close Button */}
<button
aria-label="Close"
className="absolute top-4 right-8 text-2xl text-gray-600 focus:outline-none"
onClick={onClose}
>
&times;
</button>
{/* Content */}
<div className="p-8 pt-12 overflow-y-auto h-full">{children}</div>
</div>
</FocusLock>
</>,
document.body
);
};

export default SlidingPane;
83 changes: 75 additions & 8 deletions ui/src/pages/Layers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useState, useContext, useCallback, useMemo } from "react";
import { useSearchParams } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";

import { fetchLayers } from "@/clients/layers/client";
import { fetchLayers, syncLayer } from "@/clients/layers/client";
import { reactQueryKeys } from "@/clients/reactQueryConfig";

import { ThemeContext } from "@/contexts/ThemeContext";
Expand All @@ -23,6 +23,9 @@ import CardLoader from "@/components/loaders/CardLoader";

import { LayerState } from "@/clients/layers/types";
import PaginationDropdown from "@/components/dropdowns/PaginationDropdown";
import SlidingPane from "@/modals/SlidingPane";
import LayerChecklist from "@/components/tools/LayerChecklist";
import ProgressBar from "@/components/widgets/ProgressBar";

const Layers: React.FC = () => {
const { theme } = useContext(ThemeContext);
Expand Down Expand Up @@ -83,6 +86,8 @@ const Layers: React.FC = () => {
[searchParams, setSearchParams]
);

const [showRefreshPane, setShowRefreshPane] = useState(false);

const layersQuery = useQuery({
queryKey: reactQueryKeys.layers,
queryFn: fetchLayers,
Expand Down Expand Up @@ -117,8 +122,59 @@ const Layers: React.FC = () => {
[layerOffset, layersQuery]
);

const [selectedLayersForSync, setSelectedLayersForSync] = useState<{ name: string; namespace: string }[]>([]);
const [syncProgressValue, setSyncProgressValue] = useState(0);
const syncSelectedLayers = async () => {
const totalLayers = selectedLayersForSync.length;
for (const layer of selectedLayersForSync) {
try {
await syncLayer(layer.namespace, layer.name);
} catch (error) {
console.error(`Failed to sync layer ${layer.name}:`, error);
}
setSyncProgressValue((prev) => prev + 100 / totalLayers)
}
setTimeout(() => {
setSyncProgressValue(0);
setShowRefreshPane(false);
layersQuery.refetch();
}, 1000);
}

return (
<div className="flex flex-col flex-1 h-screen min-w-0">
<SlidingPane isOpen={showRefreshPane} onClose={() => setShowRefreshPane(false)}>
<div className="relative h-full">
<div className="overflow-auto h-[calc(100%-90px)]">
<h2
className={`
text-lg
font-semibold
${theme === "light" ? "text-nuances-black" : "text-nuances-50"}
`}
>
Select Layers to synchronize
</h2>

{layersQuery.isSuccess && (
<LayerChecklist layers={layersQuery.data.results} variant={theme} onSelectionChange={(layers) => setSelectedLayersForSync(layers)}/>
)}
</div>
<div className="absolute bottom-0 left-0 right-0 p-4 bg-white dark:bg-black">
<Button
variant={theme === "light" ? "primary" : "secondary"}
className="w-full"
disabled={selectedLayersForSync.length === 0}
onClick={() => {
syncSelectedLayers();
}}
>
Synchronize
</Button>
<ProgressBar value={syncProgressValue} className="mt-4"/>
</div>
</div>
</SlidingPane>
<div
className={`
flex
Expand All @@ -140,13 +196,24 @@ const Layers: React.FC = () => {
>
Layers
</h1>
<Button
variant={theme === "light" ? "primary" : "secondary"}
isLoading={layersQuery.isRefetching}
onClick={() => layersQuery.refetch()}
>
Refresh layers
</Button>
<div className="space-x-2">
<Button
theme={theme}
variant={"secondary"}
onClick={() =>
setShowRefreshPane((showRefreshPane) => !showRefreshPane)
}
>
Run Sync
</Button>
<Button
variant={theme === "light" ? "primary" : "secondary"}
isLoading={layersQuery.isRefetching}
onClick={() => layersQuery.refetch()}
>
Refresh
</Button>
</div>
</div>
<Input
variant={theme}
Expand Down
2 changes: 1 addition & 1 deletion ui/src/pages/Logs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ const Logs: React.FC = () => {
isLoading={layersQuery.isRefetching}
onClick={() => layersQuery.refetch()}
>
Refresh layers
Refresh
</Button>
</div>
<Input
Expand Down
Loading

0 comments on commit db1772b

Please sign in to comment.