mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-20 11:37:06 +00:00
Merge pull request #4804 from Infisical/feature/secret-reference-shortcut
feature: secret reference shortcut
This commit is contained in:
+15
@@ -249,6 +249,21 @@ export const CreateSecretForm = ({ secretPath = "/", onClose }: Props) => {
|
||||
name="value"
|
||||
render={({ field }) => (
|
||||
<FormControl
|
||||
tooltipText={
|
||||
<div>
|
||||
You can add references to other secrets using the format{" "}
|
||||
<code className="rounded-sm bg-mineshaft-600 px-1 py-0.5">
|
||||
${"{"}secret_name{"}"}
|
||||
</code>
|
||||
<br />
|
||||
<br />
|
||||
You can go to the referenced secret by holding the{" "}
|
||||
<code className="rounded-sm bg-mineshaft-600 px-1 py-0.5">Cmd</code> (Mac) or{" "}
|
||||
<code className="rounded-sm bg-mineshaft-600 px-1 py-0.5">Ctrl</code>{" "}
|
||||
(Windows/Linux) key and clicking on the secret name.
|
||||
</div>
|
||||
}
|
||||
tooltipClassName="max-w-md"
|
||||
label="Value"
|
||||
isError={Boolean(errors?.value)}
|
||||
errorText={errors?.value?.message}
|
||||
|
||||
@@ -219,22 +219,43 @@ const Page = () => {
|
||||
ProjectPermissionSub.Commits
|
||||
);
|
||||
|
||||
const defaultFilterState = {
|
||||
tags: {},
|
||||
searchFilter: (routerQueryParams.search as string) || "",
|
||||
// these should always be on by default for the UI, they will be disabled for the query below based off permissions
|
||||
include: {
|
||||
[RowType.Folder]: false,
|
||||
[RowType.Import]: false,
|
||||
[RowType.DynamicSecret]: false,
|
||||
[RowType.Secret]: false,
|
||||
[RowType.SecretRotation]: false
|
||||
}
|
||||
};
|
||||
const getFilterStateFromQueryParams = useCallback(() => {
|
||||
const filterByArray = routerQueryParams.filterBy
|
||||
? (routerQueryParams.filterBy as string).split(",").filter(Boolean)
|
||||
: [];
|
||||
|
||||
const includeFilters = {
|
||||
[RowType.Folder]: filterByArray.includes("folder") || false,
|
||||
[RowType.Import]: filterByArray.includes("import") || false,
|
||||
[RowType.DynamicSecret]: filterByArray.includes("dynamic") || false,
|
||||
[RowType.Secret]: filterByArray.includes("secret") || false,
|
||||
[RowType.SecretRotation]: filterByArray.includes("rotation") || false
|
||||
};
|
||||
|
||||
const tags = routerQueryParams.tags
|
||||
? routerQueryParams.tags.split(",").reduce(
|
||||
(acc, tag) => {
|
||||
const trimmedTag = tag.trim();
|
||||
if (trimmedTag) {
|
||||
acc[trimmedTag] = true;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, boolean>
|
||||
)
|
||||
: {};
|
||||
|
||||
return {
|
||||
tags,
|
||||
searchFilter: (routerQueryParams.search as string) || "",
|
||||
include: includeFilters
|
||||
};
|
||||
}, [routerQueryParams.search, routerQueryParams.tags, routerQueryParams.filterBy]);
|
||||
|
||||
const defaultFilterState = getFilterStateFromQueryParams();
|
||||
|
||||
const [filter, setFilter] = useState<Filter>(defaultFilterState);
|
||||
const [debouncedSearchFilter, setDebouncedSearchFilter] = useDebounce(filter.searchFilter);
|
||||
const [filterHistory, setFilterHistory] = useState<Map<string, Filter>>(new Map());
|
||||
|
||||
const createSecretPopUp = usePopUpState(PopUpNames.CreateSecretForm);
|
||||
const { togglePopUp } = usePopUpAction();
|
||||
@@ -475,34 +496,96 @@ const Page = () => {
|
||||
);
|
||||
|
||||
const handleTagToggle = useCallback(
|
||||
(tagSlug: string) =>
|
||||
(tagSlug: string) => {
|
||||
setFilter((state) => {
|
||||
const isTagPresent = Boolean(state.tags?.[tagSlug]);
|
||||
const newTagFilter = { ...state.tags };
|
||||
if (isTagPresent) delete newTagFilter[tagSlug];
|
||||
else newTagFilter[tagSlug] = true;
|
||||
|
||||
// Update URL to match filter state
|
||||
const tagsList = Object.keys(newTagFilter).filter((tag) => newTagFilter[tag]);
|
||||
navigate({
|
||||
search: (prev) => ({
|
||||
...prev,
|
||||
tags: tagsList.length > 0 ? tagsList.join(",") : ""
|
||||
})
|
||||
});
|
||||
|
||||
return { ...state, tags: newTagFilter };
|
||||
}),
|
||||
[]
|
||||
});
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
|
||||
const handleToggleRowType = useCallback(
|
||||
(rowType: RowType) =>
|
||||
(rowType: RowType) => {
|
||||
setFilter((state) => {
|
||||
const newInclude = {
|
||||
...state.include,
|
||||
[rowType]: !state.include[rowType]
|
||||
};
|
||||
|
||||
// Update URL to match filter state
|
||||
const filterByList: string[] = [];
|
||||
if (newInclude[RowType.Folder]) filterByList.push("folder");
|
||||
if (newInclude[RowType.Import]) filterByList.push("import");
|
||||
if (newInclude[RowType.DynamicSecret]) filterByList.push("dynamic");
|
||||
if (newInclude[RowType.Secret]) filterByList.push("secret");
|
||||
if (newInclude[RowType.SecretRotation]) filterByList.push("rotation");
|
||||
|
||||
navigate({
|
||||
search: (prev) => ({
|
||||
...prev,
|
||||
filterBy: filterByList.length > 0 ? filterByList.join(",") : ""
|
||||
})
|
||||
});
|
||||
|
||||
return {
|
||||
...state,
|
||||
include: {
|
||||
...state.include,
|
||||
[rowType]: !state.include[rowType]
|
||||
}
|
||||
include: newInclude
|
||||
};
|
||||
}),
|
||||
[]
|
||||
});
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
|
||||
const handleClearFilters = useCallback(() => {
|
||||
setFilter({
|
||||
searchFilter: "",
|
||||
tags: {},
|
||||
include: {
|
||||
[RowType.Folder]: false,
|
||||
[RowType.Import]: false,
|
||||
[RowType.DynamicSecret]: false,
|
||||
[RowType.Secret]: false,
|
||||
[RowType.SecretRotation]: false
|
||||
}
|
||||
});
|
||||
setDebouncedSearchFilter("");
|
||||
navigate({
|
||||
search: (prev) => ({
|
||||
...prev,
|
||||
search: "",
|
||||
tags: "",
|
||||
filterBy: ""
|
||||
})
|
||||
});
|
||||
}, [navigate]);
|
||||
|
||||
const handleSearchChange = useCallback(
|
||||
(searchFilter: string) => setFilter((state) => ({ ...state, searchFilter })),
|
||||
[]
|
||||
(searchFilter: string) => {
|
||||
setFilter((state) => ({ ...state, searchFilter }));
|
||||
|
||||
// Update URL to match filter state
|
||||
navigate({
|
||||
search: (prev) => ({
|
||||
...prev,
|
||||
search: searchFilter || ""
|
||||
})
|
||||
});
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
|
||||
const handleToggleVisibility = useCallback(() => setIsVisible((state) => !state), []);
|
||||
@@ -527,38 +610,10 @@ const Page = () => {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// restore filters for path if set
|
||||
const restore = filterHistory.get(secretPath);
|
||||
setFilter(restore ?? defaultFilterState);
|
||||
setDebouncedSearchFilter(restore?.searchFilter ?? "");
|
||||
}, [secretPath]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!routerQueryParams.search && !routerQueryParams.tags) return;
|
||||
|
||||
const queryTags = routerQueryParams.tags
|
||||
? (routerQueryParams.tags as string).split(",").filter((tag) => Boolean(tag.trim()))
|
||||
: [];
|
||||
const updatedTags: Record<string, boolean> = {};
|
||||
queryTags.forEach((tag) => {
|
||||
updatedTags[tag] = true;
|
||||
});
|
||||
|
||||
setFilter((prev) => ({
|
||||
...prev,
|
||||
...defaultFilterState,
|
||||
searchFilter: (routerQueryParams.search as string) ?? "",
|
||||
tags: updatedTags
|
||||
}));
|
||||
setDebouncedSearchFilter(routerQueryParams.search as string);
|
||||
// this is a temp workaround until we fully transition state to query params,
|
||||
navigate({
|
||||
search: (state) => {
|
||||
const { search, tags: qTags, ...query } = state;
|
||||
return query;
|
||||
}
|
||||
});
|
||||
}, [routerQueryParams.search, routerQueryParams.tags]);
|
||||
const filterState = getFilterStateFromQueryParams();
|
||||
setFilter(filterState);
|
||||
setDebouncedSearchFilter(filterState.searchFilter);
|
||||
}, [getFilterStateFromQueryParams]);
|
||||
|
||||
const selectedSecrets = useSelectedSecrets();
|
||||
const selectedSecretActions = useSelectedSecretActions();
|
||||
@@ -597,13 +652,6 @@ const Page = () => {
|
||||
}
|
||||
|
||||
const handleResetFilter = () => {
|
||||
// store for breadcrumb nav to restore previously used filters
|
||||
setFilterHistory((prev) => {
|
||||
const curr = new Map(prev);
|
||||
curr.set(secretPath, filter);
|
||||
return curr;
|
||||
});
|
||||
|
||||
setFilter(defaultFilterState);
|
||||
setDebouncedSearchFilter("");
|
||||
};
|
||||
@@ -838,19 +886,7 @@ const Page = () => {
|
||||
isPITEnabled={isPITEnabled}
|
||||
hasPathPolicies={hasPathPolicies}
|
||||
onRequestAccess={(params) => handlePopUpOpen("requestAccess", params)}
|
||||
onClearFilters={() =>
|
||||
setFilter((prev) => ({
|
||||
...prev,
|
||||
tags: {},
|
||||
include: {
|
||||
secret: false,
|
||||
import: false,
|
||||
dynamic: false,
|
||||
rotation: false,
|
||||
folder: false
|
||||
}
|
||||
}))
|
||||
}
|
||||
onClearFilters={handleClearFilters}
|
||||
/>
|
||||
<div
|
||||
ref={tableRef}
|
||||
|
||||
+15
@@ -197,6 +197,21 @@ export const CreateSecretForm = ({
|
||||
render={({ field }) => (
|
||||
<FormControl
|
||||
label="Value"
|
||||
tooltipText={
|
||||
<div>
|
||||
You can add references to other secrets using the format{" "}
|
||||
<code className="rounded-sm bg-mineshaft-600 px-1 py-0.5">
|
||||
${"{"}secret_name{"}"}
|
||||
</code>
|
||||
<br />
|
||||
<br />
|
||||
You can go to the referenced secret by holding the{" "}
|
||||
<code className="rounded-sm bg-mineshaft-600 px-1 py-0.5">Cmd</code> (Mac) or{" "}
|
||||
<code className="rounded-sm bg-mineshaft-600 px-1 py-0.5">Ctrl</code>{" "}
|
||||
(Windows/Linux) key and clicking on the secret name.
|
||||
</div>
|
||||
}
|
||||
tooltipClassName="max-w-md"
|
||||
isError={Boolean(errors?.value)}
|
||||
errorText={errors?.value?.message}
|
||||
>
|
||||
|
||||
@@ -11,6 +11,7 @@ const SecretDashboardPageQueryParamsSchema = z.object({
|
||||
secretPath: z.string().catch("/"),
|
||||
search: z.string().catch(""),
|
||||
tags: z.string().catch(""),
|
||||
filterBy: z.string().catch(""),
|
||||
connectionId: z.string().optional(),
|
||||
connectionName: z.string().optional()
|
||||
});
|
||||
@@ -20,7 +21,7 @@ export const Route = createFileRoute(
|
||||
component: SecretDashboardPage,
|
||||
validateSearch: zodValidator(SecretDashboardPageQueryParamsSchema),
|
||||
search: {
|
||||
middlewares: [stripSearchParams({ secretPath: "/", search: "", tags: "" })]
|
||||
middlewares: [stripSearchParams({ secretPath: "/", search: "", tags: "", filterBy: "" })]
|
||||
},
|
||||
beforeLoad: ({ context, params, search }) => {
|
||||
const secretPathSegments = search.secretPath.split("/").filter(Boolean);
|
||||
|
||||
Reference in New Issue
Block a user