Merge branch 'heads/main' into daniel/fips-initative

This commit is contained in:
Daniel Hougaard
2025-07-07 09:55:26 +04:00
720 changed files with 13000 additions and 13897 deletions
@@ -25,8 +25,8 @@ export const accessApprovalKeys = {
requestedBy?: string,
bypassReason?: string
) => [{ projectSlug, envSlug, requestedBy, bypassReason }, "access-approvals-requests"] as const,
getAccessApprovalRequestCount: (projectSlug: string) =>
[{ projectSlug }, "access-approval-request-count"] as const
getAccessApprovalRequestCount: (projectSlug: string, policyId?: string) =>
[{ projectSlug }, "access-approval-request-count", ...(policyId ? [policyId] : [])] as const
};
export const fetchPolicyApprovalCount = async ({
@@ -87,21 +87,22 @@ const fetchApprovalRequests = async ({
}));
};
const fetchAccessRequestsCount = async (projectSlug: string) => {
const fetchAccessRequestsCount = async (projectSlug: string, policyId?: string) => {
const { data } = await apiRequest.get<TAccessRequestCount>(
"/api/v1/access-approvals/requests/count",
{ params: { projectSlug } }
{ params: { projectSlug, policyId } }
);
return data;
};
export const useGetAccessRequestsCount = ({
projectSlug,
policyId,
options = {}
}: TGetAccessApprovalRequestsDTO & TReactQueryOptions) =>
useQuery({
queryKey: accessApprovalKeys.getAccessApprovalRequestCount(projectSlug),
queryFn: () => fetchAccessRequestsCount(projectSlug),
queryKey: accessApprovalKeys.getAccessApprovalRequestCount(projectSlug, policyId),
queryFn: () => fetchAccessRequestsCount(projectSlug, policyId),
...options,
enabled: Boolean(projectSlug) && (options?.enabled ?? true)
});
@@ -147,6 +147,7 @@ export type TCreateAccessRequestDTO = {
export type TGetAccessApprovalRequestsDTO = {
projectSlug: string;
policyId?: string;
envSlug?: string;
authorUserId?: string;
};
+13 -1
View File
@@ -10,6 +10,7 @@ import {
AdminGetUsersFilters,
AdminIntegrationsConfig,
OrganizationWithProjects,
TGetEnvOverrides,
TGetInvalidatingCacheStatus,
TGetServerRootKmsEncryptionDetails,
TServerConfig
@@ -31,7 +32,8 @@ export const adminQueryKeys = {
getAdminSlackConfig: () => ["admin-slack-config"] as const,
getServerEncryptionStrategies: () => ["server-encryption-strategies"] as const,
getInvalidateCache: () => ["admin-invalidate-cache"] as const,
getAdminIntegrationsConfig: () => ["admin-integrations-config"] as const
getAdminIntegrationsConfig: () => ["admin-integrations-config"] as const,
getEnvOverrides: () => ["env-overrides"] as const
};
export const fetchServerConfig = async () => {
@@ -163,3 +165,13 @@ export const useGetInvalidatingCacheStatus = (enabled = true) => {
refetchInterval: (data) => (data ? 3000 : false)
});
};
export const useGetEnvOverrides = () => {
return useQuery({
queryKey: adminQueryKeys.getEnvOverrides(),
queryFn: async () => {
const { data } = await apiRequest.get<TGetEnvOverrides>("/api/v1/admin/env-overrides");
return data;
}
});
};
+9
View File
@@ -49,6 +49,7 @@ export type TServerConfig = {
pageFrameContent?: string;
invalidatingCache: boolean;
fipsEnabled: boolean;
envOverrides?: Record<string, string>;
};
export type TUpdateServerConfigDTO = {
@@ -62,6 +63,7 @@ export type TUpdateServerConfigDTO = {
gitHubAppConnectionSlug?: string;
gitHubAppConnectionId?: string;
gitHubAppConnectionPrivateKey?: string;
envOverrides?: Record<string, string>;
} & Partial<TServerConfig>;
export type TCreateAdminUserDTO = {
@@ -139,3 +141,10 @@ export type TInvalidateCacheDTO = {
export type TGetInvalidatingCacheStatus = {
invalidating: boolean;
};
export interface TGetEnvOverrides {
[key: string]: {
name: string;
fields: { key: string; value: string; hasEnvEntry: boolean; description?: string }[];
};
}
@@ -27,5 +27,6 @@ export enum AppConnection {
Render = "render",
Flyio = "flyio",
Gitlab = "gitlab",
Cloudflare = "cloudflare"
Cloudflare = "cloudflare",
Zabbix = "zabbix"
}
@@ -132,6 +132,10 @@ export type TCloudflareConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.Cloudflare;
};
export type TZabbixConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.Zabbix;
};
export type TAppConnectionOption =
| TAwsConnectionOption
| TGitHubConnectionOption
@@ -159,7 +163,8 @@ export type TAppConnectionOption =
| TRenderConnectionOption
| TFlyioConnectionOption
| TGitlabConnectionOption
| TCloudflareConnectionOption;
| TCloudflareConnectionOption
| TZabbixConnectionOption;
export type TAppConnectionOptionMap = {
[AppConnection.AWS]: TAwsConnectionOption;
@@ -191,4 +196,5 @@ export type TAppConnectionOptionMap = {
[AppConnection.Flyio]: TFlyioConnectionOption;
[AppConnection.Gitlab]: TGitlabConnectionOption;
[AppConnection.Cloudflare]: TCloudflareConnectionOption;
[AppConnection.Zabbix]: TZabbixConnectionOption;
};
@@ -29,6 +29,7 @@ import { TTeamCityConnection } from "./teamcity-connection";
import { TTerraformCloudConnection } from "./terraform-cloud-connection";
import { TVercelConnection } from "./vercel-connection";
import { TWindmillConnection } from "./windmill-connection";
import { TZabbixConnection } from "./zabbix-connection";
export * from "./1password-connection";
export * from "./auth0-connection";
@@ -59,6 +60,7 @@ export * from "./teamcity-connection";
export * from "./terraform-cloud-connection";
export * from "./vercel-connection";
export * from "./windmill-connection";
export * from "./zabbix-connection";
export type TAppConnection =
| TAwsConnection
@@ -89,7 +91,8 @@ export type TAppConnection =
| TRenderConnection
| TFlyioConnection
| TGitLabConnection
| TCloudflareConnection;
| TCloudflareConnection
| TZabbixConnection;
export type TAvailableAppConnection = Pick<TAppConnection, "name" | "id">;
@@ -146,4 +149,5 @@ export type TAppConnectionMap = {
[AppConnection.Flyio]: TFlyioConnection;
[AppConnection.Gitlab]: TGitLabConnection;
[AppConnection.Cloudflare]: TCloudflareConnection;
[AppConnection.Zabbix]: TZabbixConnection;
};
@@ -0,0 +1,14 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
export enum ZabbixConnectionMethod {
ApiToken = "api-token"
}
export type TZabbixConnection = TRootAppConnection & { app: AppConnection.Zabbix } & {
method: ZabbixConnectionMethod.ApiToken;
credentials: {
apiToken: string;
instanceUrl: string;
};
};
@@ -0,0 +1,2 @@
export * from "./queries";
export * from "./types";
@@ -0,0 +1,36 @@
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { appConnectionKeys } from "../queries";
import { TZabbixHost } from "./types";
const zabbixConnectionKeys = {
all: [...appConnectionKeys.all, "zabbix"] as const,
listHosts: (connectionId: string) => [...zabbixConnectionKeys.all, "hosts", connectionId] as const
};
export const useZabbixConnectionListHosts = (
connectionId: string,
options?: Omit<
UseQueryOptions<
TZabbixHost[],
unknown,
TZabbixHost[],
ReturnType<typeof zabbixConnectionKeys.listHosts>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: zabbixConnectionKeys.listHosts(connectionId),
queryFn: async () => {
const { data } = await apiRequest.get<TZabbixHost[]>(
`/api/v1/app-connections/zabbix/${connectionId}/hosts`
);
return data;
},
...options
});
};
@@ -0,0 +1,25 @@
export type TZabbixHost = {
host: string;
hostId: string;
};
export enum ZabbixSyncScope {
Host = "host",
Global = "global"
}
export enum ZabbixMacroType {
Text = 0,
Secret = 1
}
export const ZABBIX_SYNC_SCOPES = {
[ZabbixSyncScope.Host]: {
name: "Host",
description: "Sync secrets to a specific host in Zabbix."
},
[ZabbixSyncScope.Global]: {
name: "Global",
description: "Sync secrets to a global scope in Zabbix."
}
};
+2 -2
View File
@@ -30,8 +30,8 @@ export const useGetAuditLogs = (
params: {
...filters,
offset: pageParam,
startDate: filters?.startDate?.toISOString(),
endDate: filters?.endDate?.toISOString(),
startDate: filters.startDate.toISOString(),
endDate: filters.endDate.toISOString(),
...(filters.eventMetadata && Object.keys(filters.eventMetadata).length
? {
eventMetadata: Object.entries(filters.eventMetadata)
+2 -2
View File
@@ -14,8 +14,8 @@ export type TGetAuditLogsFilter = {
actor?: string; // user ID format
secretPath?: string;
secretKey?: string;
startDate?: Date;
endDate?: Date;
startDate: Date;
endDate: Date;
limit: number;
};
+1 -1
View File
@@ -51,7 +51,7 @@ export type IdentityMembershipOrg = {
export type IdentityMembership = {
id: string;
identity: Identity;
project: Pick<Workspace, "id" | "name" | "type">;
project: Pick<Workspace, "id" | "name" | "defaultProduct">;
roles: Array<
{
id: string;
@@ -21,7 +21,7 @@ export const useUpdateOIDCConfig = () => {
clientId,
clientSecret,
isActive,
orgSlug,
organizationId,
manageGroupMemberships,
jwtSignatureAlgorithm
}: {
@@ -36,7 +36,7 @@ export const useUpdateOIDCConfig = () => {
clientSecret?: string;
isActive?: boolean;
configurationType?: string;
orgSlug: string;
organizationId: string;
manageGroupMemberships?: boolean;
jwtSignatureAlgorithm?: OIDCJWTSignatureAlgorithm;
}) => {
@@ -50,7 +50,7 @@ export const useUpdateOIDCConfig = () => {
tokenEndpoint,
userinfoEndpoint,
clientId,
orgSlug,
organizationId,
clientSecret,
isActive,
manageGroupMemberships,
@@ -60,7 +60,7 @@ export const useUpdateOIDCConfig = () => {
return data;
},
onSuccess(_, dto) {
queryClient.invalidateQueries({ queryKey: oidcConfigKeys.getOIDCConfig(dto.orgSlug) });
queryClient.invalidateQueries({ queryKey: oidcConfigKeys.getOIDCConfig(dto.organizationId) });
queryClient.invalidateQueries({ queryKey: organizationKeys.getUserOrganizations });
}
});
@@ -81,7 +81,7 @@ export const useCreateOIDCConfig = () => {
clientId,
clientSecret,
isActive,
orgSlug,
organizationId,
manageGroupMemberships,
jwtSignatureAlgorithm
}: {
@@ -95,7 +95,7 @@ export const useCreateOIDCConfig = () => {
clientId: string;
clientSecret: string;
isActive: boolean;
orgSlug: string;
organizationId: string;
allowedEmailDomains?: string;
manageGroupMemberships?: boolean;
jwtSignatureAlgorithm?: OIDCJWTSignatureAlgorithm;
@@ -112,7 +112,7 @@ export const useCreateOIDCConfig = () => {
clientId,
clientSecret,
isActive,
orgSlug,
organizationId,
manageGroupMemberships,
jwtSignatureAlgorithm
});
@@ -120,7 +120,7 @@ export const useCreateOIDCConfig = () => {
return data;
},
onSuccess(_, dto) {
queryClient.invalidateQueries({ queryKey: oidcConfigKeys.getOIDCConfig(dto.orgSlug) });
queryClient.invalidateQueries({ queryKey: oidcConfigKeys.getOIDCConfig(dto.organizationId) });
}
});
};
@@ -5,18 +5,18 @@ import { apiRequest } from "@app/config/request";
import { OIDCConfigData } from "./types";
export const oidcConfigKeys = {
getOIDCConfig: (orgSlug: string) => [{ orgSlug }, "organization-oidc"] as const,
getOIDCConfig: (orgId: string) => [{ orgId }, "organization-oidc"] as const,
getOIDCManageGroupMembershipsEnabled: (orgId: string) =>
["oidc-manage-group-memberships", orgId] as const
};
export const useGetOIDCConfig = (orgSlug: string) => {
export const useGetOIDCConfig = (orgId: string) => {
return useQuery({
queryKey: oidcConfigKeys.getOIDCConfig(orgSlug),
queryKey: oidcConfigKeys.getOIDCConfig(orgId),
queryFn: async () => {
try {
const { data } = await apiRequest.get<OIDCConfigData>(
`/api/v1/sso/oidc/config?orgSlug=${orgSlug}`
`/api/v1/sso/oidc/config?organizationId=${orgId}`
);
return data;
@@ -6,17 +6,14 @@ import {
TProjectTemplate,
TProjectTemplateResponse
} from "@app/hooks/api/projectTemplates/types";
import { ProjectType } from "@app/hooks/api/workspace/types";
export const projectTemplateKeys = {
all: ["project-template"] as const,
list: (projectType?: ProjectType) =>
[...projectTemplateKeys.all, "list", ...(projectType ? [projectType] : [])] as const,
list: () => [...projectTemplateKeys.all, "list"] as const,
byId: (templateId: string) => [...projectTemplateKeys.all, templateId] as const
};
export const useListProjectTemplates = (
type?: ProjectType,
options?: Omit<
UseQueryOptions<
TProjectTemplate[],
@@ -28,11 +25,9 @@ export const useListProjectTemplates = (
>
) => {
return useQuery({
queryKey: projectTemplateKeys.list(type),
queryKey: projectTemplateKeys.list(),
queryFn: async () => {
const { data } = await apiRequest.get<TListProjectTemplates>("/api/v1/project-templates", {
params: { type }
});
const { data } = await apiRequest.get<TListProjectTemplates>("/api/v1/project-templates");
return data.projectTemplates;
},
@@ -1,10 +1,8 @@
import { TProjectRole } from "@app/hooks/api/roles/types";
import { ProjectType } from "@app/hooks/api/workspace/types";
export type TProjectTemplate = {
id: string;
name: string;
type: ProjectType;
description?: string;
roles: Pick<TProjectRole, "slug" | "name" | "permissions">[];
environments?: { name: string; slug: string; position: number }[] | null;
@@ -16,7 +14,6 @@ export type TListProjectTemplates = { projectTemplates: TProjectTemplate[] };
export type TProjectTemplateResponse = { projectTemplate: TProjectTemplate };
export type TCreateProjectTemplateDTO = {
type: ProjectType;
name: string;
description?: string;
};
@@ -28,9 +28,10 @@ export const secretApprovalRequestKeys = {
] as const,
detail: ({ id }: Omit<TGetSecretApprovalRequestDetails, "decryptKey">) =>
[{ id }, "secret-approval-request-detail"] as const,
count: ({ workspaceId }: TGetSecretApprovalRequestCount) => [
count: ({ workspaceId, policyId }: TGetSecretApprovalRequestCount) => [
{ workspaceId },
"secret-approval-request-count"
"secret-approval-request-count",
...(policyId ? [policyId] : [])
]
};
@@ -125,10 +126,13 @@ export const useGetSecretApprovalRequestDetails = ({
enabled: Boolean(id) && (options?.enabled ?? true)
});
const fetchSecretApprovalRequestCount = async ({ workspaceId }: TGetSecretApprovalRequestCount) => {
const fetchSecretApprovalRequestCount = async ({
workspaceId,
policyId
}: TGetSecretApprovalRequestCount) => {
const { data } = await apiRequest.get<{ approvals: TSecretApprovalRequestCount }>(
"/api/v1/secret-approval-requests/count",
{ params: { workspaceId } }
{ params: { workspaceId, policyId } }
);
return data.approvals;
@@ -136,6 +140,7 @@ const fetchSecretApprovalRequestCount = async ({ workspaceId }: TGetSecretApprov
export const useGetSecretApprovalRequestCount = ({
workspaceId,
policyId,
options = {}
}: TGetSecretApprovalRequestCount & {
options?: Omit<
@@ -149,8 +154,8 @@ export const useGetSecretApprovalRequestCount = ({
>;
}) =>
useQuery({
queryKey: secretApprovalRequestKeys.count({ workspaceId }),
queryKey: secretApprovalRequestKeys.count({ workspaceId, policyId }),
refetchInterval: 15000,
queryFn: () => fetchSecretApprovalRequestCount({ workspaceId }),
queryFn: () => fetchSecretApprovalRequestCount({ workspaceId, policyId }),
enabled: Boolean(workspaceId) && (options?.enabled ?? true)
});
@@ -118,6 +118,7 @@ export type TGetSecretApprovalRequestList = {
export type TGetSecretApprovalRequestCount = {
workspaceId: string;
policyId?: string;
};
export type TGetSecretApprovalRequestDetails = {
+2 -1
View File
@@ -20,7 +20,8 @@ export enum SecretSync {
Render = "render",
Flyio = "flyio",
GitLab = "gitlab",
CloudflarePages = "cloudflare-pages"
CloudflarePages = "cloudflare-pages",
Zabbix = "zabbix"
}
export enum SecretSyncStatus {
@@ -23,6 +23,7 @@ import { TTeamCitySync } from "./teamcity-sync";
import { TTerraformCloudSync } from "./terraform-cloud-sync";
import { TVercelSync } from "./vercel-sync";
import { TWindmillSync } from "./windmill-sync";
import { TZabbixSync } from "./zabbix-sync";
export type TSecretSyncOption = {
name: string;
@@ -53,7 +54,8 @@ export type TSecretSync =
| TRenderSync
| TFlyioSync
| TGitLabSync
| TCloudflarePagesSync;
| TCloudflarePagesSync
| TZabbixSync;
export type TListSecretSyncs = { secretSyncs: TSecretSync[] };
@@ -0,0 +1,25 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
import { ZabbixSyncScope } from "../../appConnections/zabbix";
export type TZabbixSync = TRootSecretSync & {
destination: SecretSync.Zabbix;
destinationConfig:
| {
scope: ZabbixSyncScope.Host;
hostId: string;
hostName: string;
macroType: number;
}
| {
scope: ZabbixSyncScope.Global;
macroType: number;
};
connection: {
app: AppConnection.Zabbix;
name: string;
id: string;
};
};
+15 -16
View File
@@ -154,16 +154,14 @@ export const useGetWorkspaceById = (
export const useGetUserWorkspaces = ({
includeRoles,
type = "all",
options = {}
}: {
includeRoles?: boolean;
type?: ProjectType | "all";
options?: { enabled?: boolean };
} = {}) =>
useQuery({
queryKey: workspaceKeys.getAllUserWorkspace(type),
queryFn: () => fetchUserWorkspaces(includeRoles, type),
queryKey: workspaceKeys.getAllUserWorkspace(),
queryFn: () => fetchUserWorkspaces(includeRoles),
...options
});
@@ -257,17 +255,16 @@ export const useCreateWorkspace = () => {
const queryClient = useQueryClient();
return useMutation<{ data: { project: Workspace } }, object, CreateWorkspaceDTO>({
mutationFn: async ({ projectName, projectDescription, kmsKeyId, template, type }) =>
mutationFn: async ({ projectName, projectDescription, kmsKeyId, template }) =>
createWorkspace({
projectName,
projectDescription,
kmsKeyId,
template,
type
template
}),
onSuccess: (dto) => {
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: workspaceKeys.getAllUserWorkspace(dto.data.project.type)
queryKey: workspaceKeys.getAllUserWorkspace()
});
}
});
@@ -283,7 +280,8 @@ export const useUpdateProject = () => {
newProjectDescription,
newSlug,
secretSharing,
showSnapshotsLegacy
showSnapshotsLegacy,
defaultProduct
}) => {
const { data } = await apiRequest.patch<{ workspace: Workspace }>(
`/api/v1/workspace/${projectID}`,
@@ -291,6 +289,7 @@ export const useUpdateProject = () => {
name: newProjectName,
description: newProjectDescription,
slug: newSlug,
defaultProduct,
secretSharing,
showSnapshotsLegacy
}
@@ -351,8 +350,8 @@ export const useUpdateWorkspaceVersionLimit = () => {
});
return data.workspace;
},
onSuccess: (dto) => {
queryClient.invalidateQueries({ queryKey: workspaceKeys.getAllUserWorkspace(dto.type) });
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: workspaceKeys.getAllUserWorkspace() });
}
});
};
@@ -370,8 +369,8 @@ export const useUpdateWorkspaceAuditLogsRetention = () => {
);
return data.workspace;
},
onSuccess: (dto) => {
queryClient.invalidateQueries({ queryKey: workspaceKeys.getAllUserWorkspace(dto.type) });
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: workspaceKeys.getAllUserWorkspace() });
}
});
};
@@ -384,8 +383,8 @@ export const useDeleteWorkspace = () => {
const { data } = await apiRequest.delete(`/api/v1/workspace/${workspaceID}`);
return data.workspace;
},
onSuccess: (dto) => {
queryClient.invalidateQueries({ queryKey: workspaceKeys.getAllUserWorkspace(dto.type) });
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: workspaceKeys.getAllUserWorkspace() });
queryClient.invalidateQueries({
queryKey: ["org-admin-projects"]
});
@@ -12,8 +12,7 @@ export const workspaceKeys = {
getWorkspaceMemberships: (orgId: string) => [{ orgId }, "workspace-memberships"],
getWorkspaceAuthorization: (workspaceId: string) => [{ workspaceId }, "workspace-authorizations"],
getWorkspaceIntegrations: (workspaceId: string) => [{ workspaceId }, "workspace-integrations"],
getAllUserWorkspace: (type?: string) =>
type ? ["workspaces", { type }] : (["workspaces"] as const),
getAllUserWorkspace: () => ["workspaces"] as const,
getWorkspaceAuditLogs: (workspaceId: string) =>
[{ workspaceId }, "workspace-audit-logs"] as const,
getWorkspaceUsers: (
+2 -3
View File
@@ -24,7 +24,7 @@ export type Workspace = {
__v: number;
id: string;
name: string;
type: ProjectType;
defaultProduct: ProjectType;
description?: string;
orgId: string;
version: ProjectVersion;
@@ -71,7 +71,6 @@ export type CreateWorkspaceDTO = {
projectDescription?: string;
kmsKeyId?: string;
template?: string;
type: ProjectType;
};
export type UpdateProjectDTO = {
@@ -81,6 +80,7 @@ export type UpdateProjectDTO = {
newSlug?: string;
secretSharing?: boolean;
showSnapshotsLegacy?: boolean;
defaultProduct?: ProjectType;
};
export type UpdatePitVersionLimitDTO = { projectSlug: string; pitVersionLimit: number };
@@ -181,7 +181,6 @@ export enum ProjectIdentityOrderBy {
Name = "name"
}
export type TSearchProjectsDTO = {
type?: ProjectType;
name?: string;
limit?: number;
offset?: number;
+2
View File
@@ -1,9 +1,11 @@
export { useDebounce } from "./useDebounce";
export * from "./useGetProjectTypeFromRoute";
export { useLocalStorageState } from "./useLocalStorageState";
export { usePagination } from "./usePagination";
export { usePersistentState } from "./usePersistentState";
export { usePopUp } from "./usePopUp";
export { useResetPageHelper } from "./useResetPageHelper";
export * from "./useResizableHeaderHeight";
export { useSyntaxHighlight } from "./useSyntaxHighlight";
export { useTimedReset } from "./useTimedReset";
export { useToggle } from "./useToggle";
@@ -8,15 +8,10 @@ export const useGetProjectTypeFromRoute = () => {
return useMemo(() => {
const segments = location.pathname.split("/");
const type = segments?.[2];
if (!type) return ProjectType.SecretManager;
let type: ProjectType | undefined;
// location of project type can vary in router path, so we need to check all possible values
segments.forEach((segment) => {
if (Object.values(ProjectType).includes(segment as ProjectType))
type = segment as ProjectType;
});
return type;
// second element would be /projects/$projectId/<type>
return Object.values(ProjectType).find((el) => el === type) || ProjectType.SecretManager;
}, [location]);
};
@@ -0,0 +1,70 @@
import { useCallback, useEffect, useSyncExternalStore } from "react";
type SetStateAction<T> = T | ((prevState: T) => T);
const dispatchStorageEvent = (key: string, newValue: string | null): void => {
window.dispatchEvent(new StorageEvent("storage", { key, newValue }));
};
const setLocalStorageItem = (key: string, value: unknown): void => {
const stringifiedValue = JSON.stringify(value);
window.localStorage.setItem(key, stringifiedValue);
dispatchStorageEvent(key, stringifiedValue);
};
const removeLocalStorageItem = (key: string): void => {
window.localStorage.removeItem(key);
dispatchStorageEvent(key, null);
};
const getLocalStorageItem = (key: string): string | null => {
return window.localStorage.getItem(key);
};
const useLocalStorageSubscribe = (callback: (e: StorageEvent) => void) => {
window.addEventListener("storage", callback);
return () => window.removeEventListener("storage", callback);
};
const getLocalStorageServerSnapshot = (): never => {
throw Error("useLocalStorage is a client-only hook");
};
export const useLocalStorageState = <T>(
key: string,
initialValue: T
): [T, (value: SetStateAction<T>) => void] => {
const getSnapshot = () => getLocalStorageItem(key);
const store = useSyncExternalStore(
useLocalStorageSubscribe,
getSnapshot,
getLocalStorageServerSnapshot
);
const setState = useCallback(
(v: SetStateAction<T>): void => {
try {
const nextState =
typeof v === "function" ? (v as (prevState: T) => T)(JSON.parse(store || "null")) : v;
if (nextState === undefined || nextState === null) {
removeLocalStorageItem(key);
} else {
setLocalStorageItem(key, nextState);
}
} catch (e) {
console.warn(e);
}
},
[key, store]
);
useEffect(() => {
if (getLocalStorageItem(key) === null && typeof initialValue !== "undefined") {
setLocalStorageItem(key, initialValue);
}
}, [key, initialValue]);
return [store ? JSON.parse(store) : initialValue, setState];
};
@@ -0,0 +1,71 @@
import { MouseEvent, useCallback, useEffect, useRef, useState } from "react";
type Params = {
minHeight: number;
maxHeight: number;
initialHeight: number;
};
export const useResizableHeaderHeight = ({ minHeight, maxHeight, initialHeight }: Params) => {
const [headerHeight, setHeaderHeight] = useState(initialHeight);
const [isResizing, setIsResizing] = useState(false);
const startY = useRef(0);
const startHeight = useRef(0);
const handleMouseDown = useCallback(
(e: MouseEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
setIsResizing(true);
startY.current = e.clientY;
startHeight.current = headerHeight;
},
[headerHeight]
);
const handleMouseMove = useCallback(
(e: MouseEvent) => {
if (!isResizing) return;
const deltaY = e.clientY - startY.current;
const newHeight = Math.max(minHeight, Math.min(maxHeight, startHeight.current + deltaY));
setHeaderHeight(newHeight);
},
[isResizing]
);
const handleMouseUp = useCallback(() => {
setIsResizing(false);
}, []);
useEffect(() => {
if (isResizing) {
document.addEventListener(
"mousemove",
// @ts-expect-error native discrepancy
handleMouseMove
);
document.addEventListener("mouseup", handleMouseUp);
document.body.style.cursor = "ns-resize";
document.body.style.userSelect = "none";
}
return () => {
document.removeEventListener(
"mousemove",
// @ts-expect-error native discrepancy
handleMouseMove
);
document.removeEventListener("mouseup", handleMouseUp);
document.body.style.cursor = "";
document.body.style.userSelect = "";
};
}, [isResizing, handleMouseMove, handleMouseUp]);
return {
headerHeight,
handleMouseDown,
isResizing
};
};