mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 06:27:40 +00:00
Improve UI dropdown
This commit is contained in:
@@ -1,18 +1,31 @@
|
|||||||
/* eslint-disable no-nested-ternary */
|
/* eslint-disable no-nested-ternary */
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import { faExternalLink, faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons";
|
import { SingleValue } from "react-select";
|
||||||
|
import { faExternalLink } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { Button, FormControl, Input } from "@app/components/v2";
|
import { Button, FilterableSelect, FormControl } from "@app/components/v2";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useCalculateUpgradePath,
|
useCalculateUpgradePath,
|
||||||
useGetUpgradePathVersions
|
useGetUpgradePathVersions
|
||||||
} from "../../../hooks/api/upgradePath/queries";
|
} from "../../../hooks/api/upgradePath/queries";
|
||||||
|
|
||||||
|
type VersionOption = {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
isLatest: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatVersionOption = (option: VersionOption) => (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span>{option.label}</span>
|
||||||
|
{option.isLatest && <span className="text-xs text-primary">(Latest)</span>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
interface UpgradeResult {
|
interface UpgradeResult {
|
||||||
path: Array<{
|
path: Array<{
|
||||||
version: string;
|
version: string;
|
||||||
@@ -39,66 +52,9 @@ interface UpgradeResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const UpgradePathPage = () => {
|
export const UpgradePathPage = () => {
|
||||||
const [fromVersion, setFromVersion] = useState("");
|
const [fromVersion, setFromVersion] = useState<string | null>(null);
|
||||||
const [toVersion, setToVersion] = useState("");
|
const [toVersion, setToVersion] = useState<string | null>(null);
|
||||||
const [fromSearch, setFromSearch] = useState("");
|
|
||||||
const [toSearch, setToSearch] = useState("");
|
|
||||||
const [showFromDropdown, setShowFromDropdown] = useState(false);
|
|
||||||
const [showToDropdown, setShowToDropdown] = useState(false);
|
|
||||||
const [upgradeResult, setUpgradeResult] = useState<UpgradeResult | null>(null);
|
const [upgradeResult, setUpgradeResult] = useState<UpgradeResult | null>(null);
|
||||||
const [debouncedFromSearch, setDebouncedFromSearch] = useState("");
|
|
||||||
const [debouncedToSearch, setDebouncedToSearch] = useState("");
|
|
||||||
|
|
||||||
const fromDropdownRef = useRef<HTMLDivElement>(null);
|
|
||||||
const toDropdownRef = useRef<HTMLDivElement>(null);
|
|
||||||
const fromSearchTimeoutRef = useRef<NodeJS.Timeout>();
|
|
||||||
const toSearchTimeoutRef = useRef<NodeJS.Timeout>();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (fromSearchTimeoutRef.current) {
|
|
||||||
clearTimeout(fromSearchTimeoutRef.current);
|
|
||||||
}
|
|
||||||
fromSearchTimeoutRef.current = setTimeout(() => {
|
|
||||||
setDebouncedFromSearch(fromSearch);
|
|
||||||
}, 300);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (fromSearchTimeoutRef.current) {
|
|
||||||
clearTimeout(fromSearchTimeoutRef.current);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [fromSearch]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (toSearchTimeoutRef.current) {
|
|
||||||
clearTimeout(toSearchTimeoutRef.current);
|
|
||||||
}
|
|
||||||
toSearchTimeoutRef.current = setTimeout(() => {
|
|
||||||
setDebouncedToSearch(toSearch);
|
|
||||||
}, 300);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (toSearchTimeoutRef.current) {
|
|
||||||
clearTimeout(toSearchTimeoutRef.current);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [toSearch]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
|
||||||
if (fromDropdownRef.current && !fromDropdownRef.current.contains(event.target as Node)) {
|
|
||||||
setShowFromDropdown(false);
|
|
||||||
}
|
|
||||||
if (toDropdownRef.current && !toDropdownRef.current.contains(event.target as Node)) {
|
|
||||||
setShowToDropdown(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener("mousedown", handleClickOutside);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: versions,
|
data: versions,
|
||||||
@@ -128,44 +84,26 @@ export const UpgradePathPage = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const filteredFromVersions = useMemo(() => {
|
const versionOptions = useMemo(() => {
|
||||||
if (!versions?.versions) return [];
|
if (!versions?.versions) return [];
|
||||||
|
|
||||||
const filtered = versions.versions
|
return versions.versions
|
||||||
.filter((version) => !version.tagName.includes("nightly"))
|
.filter((version) => !version.tagName.includes("nightly"))
|
||||||
.filter((version) => {
|
.map((version) => ({
|
||||||
if (!debouncedFromSearch) return true;
|
label: version.tagName,
|
||||||
const searchTerm = debouncedFromSearch.toLowerCase();
|
value: version.tagName,
|
||||||
return version.tagName.toLowerCase().includes(searchTerm);
|
isLatest: versions.versions[0]?.tagName === version.tagName
|
||||||
});
|
}));
|
||||||
|
}, [versions?.versions]);
|
||||||
|
|
||||||
return filtered.slice(0, 25);
|
const handleFromVersionSelect = (value: unknown) => {
|
||||||
}, [versions?.versions, debouncedFromSearch]);
|
const selected = value as SingleValue<VersionOption>;
|
||||||
|
setFromVersion(selected?.value || null);
|
||||||
const filteredToVersions = useMemo(() => {
|
|
||||||
if (!versions?.versions) return [];
|
|
||||||
|
|
||||||
const filtered = versions.versions
|
|
||||||
.filter((version) => !version.tagName.includes("nightly"))
|
|
||||||
.filter((version) => {
|
|
||||||
if (!debouncedToSearch) return true;
|
|
||||||
const searchTerm = debouncedToSearch.toLowerCase();
|
|
||||||
return version.tagName.toLowerCase().includes(searchTerm);
|
|
||||||
});
|
|
||||||
|
|
||||||
return filtered.slice(0, 25);
|
|
||||||
}, [versions?.versions, debouncedToSearch]);
|
|
||||||
|
|
||||||
const handleFromVersionSelect = (version: string) => {
|
|
||||||
setFromVersion(version);
|
|
||||||
setFromSearch(version);
|
|
||||||
setShowFromDropdown(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToVersionSelect = (version: string) => {
|
const handleToVersionSelect = (value: unknown) => {
|
||||||
setToVersion(version);
|
const selected = value as SingleValue<VersionOption>;
|
||||||
setToSearch(version);
|
setToVersion(selected?.value || null);
|
||||||
setShowToDropdown(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCalculate = () => {
|
const handleCalculate = () => {
|
||||||
@@ -232,121 +170,36 @@ export const UpgradePathPage = () => {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||||
{/* From Version Selector */}
|
{/* From Version Selector */}
|
||||||
<div className="relative" ref={fromDropdownRef}>
|
<FormControl label="From Version" isRequired>
|
||||||
<FormControl label="From Version" isRequired>
|
<FilterableSelect
|
||||||
<div className="relative">
|
options={versionOptions}
|
||||||
<Input
|
value={versionOptions.find((opt) => opt.value === fromVersion) || null}
|
||||||
value={fromSearch}
|
onChange={handleFromVersionSelect}
|
||||||
onChange={(e) => {
|
placeholder="Search or select version..."
|
||||||
setFromSearch(e.target.value);
|
isLoading={versionsLoading || versionsFetching}
|
||||||
setFromVersion("");
|
isDisabled={versionsLoading || versionsFetching}
|
||||||
setShowFromDropdown(true);
|
isSearchable
|
||||||
}}
|
isClearable
|
||||||
onFocus={() => setShowFromDropdown(true)}
|
menuPortalTarget={document.body}
|
||||||
placeholder="Search or select version..."
|
formatOptionLabel={formatVersionOption}
|
||||||
className="border-mineshaft-600 bg-mineshaft-900"
|
/>
|
||||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
</FormControl>
|
||||||
isDisabled={versionsLoading || versionsFetching}
|
|
||||||
/>
|
|
||||||
{showFromDropdown && (
|
|
||||||
<div className="absolute z-50 mt-1 max-h-60 w-full overflow-auto rounded-md border border-mineshaft-600 bg-mineshaft-900 shadow-lg">
|
|
||||||
{(() => {
|
|
||||||
if (versionsLoading || versionsFetching) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center px-3 py-4">
|
|
||||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
|
||||||
<span className="ml-2 text-sm text-bunker-300">
|
|
||||||
Loading versions...
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (filteredFromVersions.length > 0) {
|
|
||||||
return filteredFromVersions.slice(0, 8).map((version) => (
|
|
||||||
<button
|
|
||||||
key={version.tagName}
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleFromVersionSelect(version.tagName)}
|
|
||||||
className="flex w-full items-center justify-between px-3 py-2 text-left text-sm text-bunker-200 hover:bg-mineshaft-700 focus:bg-mineshaft-700 focus:outline-none"
|
|
||||||
>
|
|
||||||
<span>{version.tagName}</span>
|
|
||||||
</button>
|
|
||||||
));
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className="px-3 py-2 text-sm text-bunker-400">
|
|
||||||
{debouncedFromSearch
|
|
||||||
? `No versions found matching "${debouncedFromSearch}"`
|
|
||||||
: "No versions available"}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* To Version Selector */}
|
{/* To Version Selector */}
|
||||||
<div className="relative" ref={toDropdownRef}>
|
<FormControl label="To Version" isRequired>
|
||||||
<FormControl label="To Version" isRequired>
|
<FilterableSelect
|
||||||
<div className="relative">
|
options={versionOptions}
|
||||||
<Input
|
value={versionOptions.find((opt) => opt.value === toVersion) || null}
|
||||||
value={toSearch}
|
onChange={handleToVersionSelect}
|
||||||
onChange={(e) => {
|
placeholder="Search or select version..."
|
||||||
setToSearch(e.target.value);
|
isLoading={versionsLoading || versionsFetching}
|
||||||
setToVersion("");
|
isDisabled={versionsLoading || versionsFetching}
|
||||||
setShowToDropdown(true);
|
isSearchable
|
||||||
}}
|
isClearable
|
||||||
onFocus={() => setShowToDropdown(true)}
|
menuPortalTarget={document.body}
|
||||||
placeholder="Search or select version..."
|
formatOptionLabel={formatVersionOption}
|
||||||
className="border-mineshaft-600 bg-mineshaft-900"
|
/>
|
||||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
</FormControl>
|
||||||
isDisabled={versionsLoading || versionsFetching}
|
|
||||||
/>
|
|
||||||
{showToDropdown && (
|
|
||||||
<div className="absolute z-50 mt-1 max-h-60 w-full overflow-auto rounded-md border border-mineshaft-600 bg-mineshaft-900 shadow-lg">
|
|
||||||
{(() => {
|
|
||||||
if (versionsLoading || versionsFetching) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center px-3 py-4">
|
|
||||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
|
||||||
<span className="ml-2 text-sm text-bunker-300">
|
|
||||||
Loading versions...
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (filteredToVersions.length > 0) {
|
|
||||||
return filteredToVersions.slice(0, 8).map((version) => (
|
|
||||||
<button
|
|
||||||
key={version.tagName}
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleToVersionSelect(version.tagName)}
|
|
||||||
className="flex w-full items-center justify-between px-3 py-2 text-left text-sm text-bunker-200 hover:bg-mineshaft-700 focus:bg-mineshaft-700 focus:outline-none"
|
|
||||||
>
|
|
||||||
<span>{version.tagName}</span>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
{versions?.versions?.[0]?.tagName === version.tagName && (
|
|
||||||
<span className="text-xs text-primary">(Latest)</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
));
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className="px-3 py-2 text-sm text-bunker-400">
|
|
||||||
{debouncedToSearch
|
|
||||||
? `No versions found matching "${debouncedToSearch}"`
|
|
||||||
: "No versions available"}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
Reference in New Issue
Block a user