Compare commits

..

4 Commits

Author SHA1 Message Date
e3a356cda9 updated go sdk 2024-10-24 14:47:00 +04:00
7fb3076238 fix: added sdk context support 2024-10-19 08:40:02 +04:00
a0865cda2e fix: enable sdk silent mode 2024-10-19 02:59:29 +04:00
1e7b1ccf22 feat: automatic token refreshing 2024-10-19 01:38:11 +04:00
38 changed files with 358 additions and 600 deletions

View File

@ -95,10 +95,6 @@ RUN mkdir frontend-build
# Production stage
FROM base AS production
RUN apk add --upgrade --no-cache ca-certificates
RUN apk add --no-cache bash curl && curl -1sLf \
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \
&& apk add infisical=0.31.1 && apk add --no-cache git
RUN addgroup --system --gid 1001 nodejs \
&& adduser --system --uid 1001 non-root-user

View File

@ -2,8 +2,6 @@ import { z } from "zod";
import { GitAppOrgSchema, SecretScanningGitRisksSchema } from "@app/db/schemas";
import { SecretScanningRiskStatus } from "@app/ee/services/secret-scanning/secret-scanning-types";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@ -25,13 +23,6 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const appCfg = getConfig();
if (!appCfg.SECRET_SCANNING_ORG_WHITELIST?.includes(req.auth.orgId)) {
throw new BadRequestError({
message: "Secret scanning is temporarily unavailable."
});
}
const session = await server.services.secretScanning.createInstallationSession({
actor: req.permission.type,
actorId: req.permission.id,
@ -39,7 +30,6 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
actorOrgId: req.permission.orgId,
orgId: req.body.organizationId
});
return session;
}
});

View File

@ -1,6 +1,6 @@
import { ProbotOctokit } from "probot";
import { OrgMembershipRole, TableName } from "@app/db/schemas";
import { OrgMembershipRole } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
@ -61,7 +61,7 @@ export const secretScanningQueueFactory = ({
const getOrgAdminEmails = async (organizationId: string) => {
// get emails of admins
const adminsOfWork = await orgMemberDAL.findMembership({
[`${TableName.Organization}.id` as string]: organizationId,
orgId: organizationId,
role: OrgMembershipRole.Admin
});
return adminsOfWork.filter((userObject) => userObject.email).map((userObject) => userObject.email as string);

View File

@ -90,7 +90,7 @@ export const secretScanningServiceFactory = ({
const {
data: { repositories }
} = await octokit.apps.listReposAccessibleToInstallation();
if (appCfg.SECRET_SCANNING_ORG_WHITELIST?.includes(actorOrgId)) {
if (!appCfg.DISABLE_SECRET_SCANNING) {
await Promise.all(
repositories.map(({ id, full_name }) =>
secretScanningQueue.startFullRepoScan({
@ -164,7 +164,7 @@ export const secretScanningServiceFactory = ({
});
if (!installationLink) return;
if (appCfg.SECRET_SCANNING_ORG_WHITELIST?.includes(installationLink.orgId)) {
if (!appCfg.DISABLE_SECRET_SCANNING) {
await secretScanningQueue.startPushEventScan({
commits,
pusher: { name: pusher.name, email: pusher.email },

View File

@ -142,7 +142,6 @@ const envSchema = z
SECRET_SCANNING_WEBHOOK_SECRET: zpStr(z.string().optional()),
SECRET_SCANNING_GIT_APP_ID: zpStr(z.string().optional()),
SECRET_SCANNING_PRIVATE_KEY: zpStr(z.string().optional()),
SECRET_SCANNING_ORG_WHITELIST: zpStr(z.string().optional()),
// LICENSE
LICENSE_SERVER_URL: zpStr(z.string().optional().default("https://portal.infisical.com")),
LICENSE_SERVER_KEY: zpStr(z.string().optional()),
@ -178,8 +177,7 @@ const envSchema = z
Boolean(data.SECRET_SCANNING_GIT_APP_ID) &&
Boolean(data.SECRET_SCANNING_PRIVATE_KEY) &&
Boolean(data.SECRET_SCANNING_WEBHOOK_SECRET),
samlDefaultOrgSlug: data.DEFAULT_SAML_ORG_SLUG,
SECRET_SCANNING_ORG_WHITELIST: data.SECRET_SCANNING_ORG_WHITELIST?.split(",")
samlDefaultOrgSlug: data.DEFAULT_SAML_ORG_SLUG
}));
let envCfg: Readonly<z.infer<typeof envSchema>>;

View File

@ -71,13 +71,6 @@ export class BadRequestError extends Error {
}
}
export class RateLimitError extends Error {
constructor({ message }: { message?: string }) {
super(message || "Rate limit exceeded");
this.name = "RateLimitExceeded";
}
}
export class NotFoundError extends Error {
name: string;

View File

@ -2,7 +2,6 @@ import type { RateLimitOptions, RateLimitPluginOptions } from "@fastify/rate-lim
import { Redis } from "ioredis";
import { getConfig } from "@app/lib/config/env";
import { RateLimitError } from "@app/lib/errors";
export const globalRateLimiterCfg = (): RateLimitPluginOptions => {
const appCfg = getConfig();
@ -11,11 +10,6 @@ export const globalRateLimiterCfg = (): RateLimitPluginOptions => {
: null;
return {
errorResponseBuilder: (_, context) => {
throw new RateLimitError({
message: `Rate limit exceeded. Please try again in ${context.after}`
});
},
timeWindow: 60 * 1000,
max: 600,
redis,

View File

@ -10,7 +10,6 @@ import {
GatewayTimeoutError,
InternalServerError,
NotFoundError,
RateLimitError,
ScimRequestError,
UnauthorizedError
} from "@app/lib/errors";
@ -28,8 +27,7 @@ enum HttpStatusCodes {
Forbidden = 403,
// eslint-disable-next-line @typescript-eslint/no-shadow
InternalServerError = 500,
GatewayTimeout = 504,
TooManyRequests = 429
GatewayTimeout = 504
}
export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider) => {
@ -71,12 +69,6 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
message: error.message,
error: error.name
});
} else if (error instanceof RateLimitError) {
void res.status(HttpStatusCodes.TooManyRequests).send({
statusCode: HttpStatusCodes.TooManyRequests,
message: error.message,
error: error.name
});
} else if (error instanceof ScimRequestError) {
void res.status(error.status).send({
schemas: error.schemas,

View File

@ -225,7 +225,9 @@ export const registerRoutes = async (
}: { auditLogDb?: Knex; db: Knex; smtp: TSmtpService; queue: TQueueServiceFactory; keyStore: TKeyStoreFactory }
) => {
const appCfg = getConfig();
await server.register(registerSecretScannerGhApp, { prefix: "/ss-webhook" });
if (!appCfg.DISABLE_SECRET_SCANNING) {
await server.register(registerSecretScannerGhApp, { prefix: "/ss-webhook" });
}
// db layers
const userDAL = userDALFactory(db);

View File

@ -16,10 +16,8 @@ as well as create a new project.
The **Settings** page lets you manage information about your organization including:
- **Name**: The name of your organization.
- **Slug**: The slug of your organization.
- **Default Organization Member Role**: The role assigned to users when joining your organization unless otherwise specified.
- **Incident Contacts**: Emails that should be alerted if anything abnormal is detected within the organization.
- Name: The name of your organization.
- Incident contacts: Emails that should be alerted if anything abnormal is detected within the organization.
![organization settings general](../../images/platform/organization/organization-settings-general.png)

View File

@ -28,13 +28,6 @@ Prerequisites:
![SCIM copy token](/images/platform/scim/scim-copy-token.png)
</Step>
<Step title="Add Users and Groups in Azure">
In Azure, navigate to Enterprise Application > Users and Groups. Add any users and/or groups to your application that you would like
to be provisioned over to Infisical.
![SCIM Azure Users and Groups](/images/platform/scim/azure/scim-azure-add-users-and-groups.png)
</Step>
<Step title="Configure SCIM in Azure">
In Azure, head to your Enterprise Application > Provisioning > Overview and press **Get started**.

View File

@ -1,26 +0,0 @@
---
title: "SCIM Group Mappings"
description: "Learn how to enhance your SCIM implementation using group mappings"
---
<Info>
SCIM provisioning, and by extension group mapping, is a paid feature.
If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical,
then you should contact sales@infisical.com to purchase an enterprise license to use it.
</Info>
## SCIM Group to Organization Role Mapping
By default, when users are provisioned via SCIM, they will be assigned the default organization role configured in [Organization General Settings](/documentation/platform/organization#settings).
For more precise control over membership roles, you can set up SCIM Group to Organization Role Mappings. This enables you to assign specific roles based on the group from which a user is provisioned.
![SCIM Group Mapping](/images/platform/scim/scim-group-mapping.png)
To configure a mapping, simply enter the SCIM group's name and select the role you would like users to be assigned from this group. Be sure
to tap **Update Mappings** once complete.
<Note>
SCIM Group Mappings only apply when users are first provisioned. Previously provisioned users will not be affected, allowing you to customize user roles after they are added.
</Note>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 985 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 523 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

View File

@ -249,8 +249,7 @@
"documentation/platform/scim/overview",
"documentation/platform/scim/okta",
"documentation/platform/scim/azure",
"documentation/platform/scim/jumpcloud",
"documentation/platform/scim/group-mappings"
"documentation/platform/scim/jumpcloud"
]
}
]

View File

@ -11,23 +11,22 @@ If you're working with Go Lang, the official [Infisical Go SDK](https://github.c
- [Package](https://pkg.go.dev/github.com/infisical/go-sdk)
- [Github Repository](https://github.com/infisical/go-sdk)
# Basic Usage
## Basic Usage
```go
package main
import (
"fmt"
"os"
"context"
infisical "github.com/infisical/go-sdk"
"fmt"
"os"
infisical "github.com/infisical/go-sdk"
)
func main() {
client := infisical.NewInfisicalClient(context.Background(), infisical.Config{
client := infisical.NewInfisicalClient(infisical.Config{
SiteUrl: "https://app.infisical.com", // Optional, default is https://app.infisical.com
AutoTokenRefresh: true, // Wether or not to let the SDK handle the access token lifecycle. Defaults to true if not specified.
})
_, err = client.Auth().UniversalAuthLogin("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET")
@ -65,68 +64,32 @@ This example demonstrates how to use the Infisical Go SDK in a simple Go applica
```console
$ go get github.com/infisical/go-sdk
```
# Configuration
Import the SDK and create a client instance.
```go
client := infisical.NewInfisicalClient(context.Background(), infisical.Config{
client := infisical.NewInfisicalClient(infisical.Config{
SiteUrl: "https://app.infisical.com", // Optional, default is https://api.infisical.com
})
```
### Configuration Options
### ClientSettings methods
<ParamField query="options" type="object">
<Expandable title="properties">
<ParamField query="SiteUrl" type="string" optional default="https://app.infisical.com">
The URL of the Infisical API..
<ParamField query="SiteUrl" type="string" optional>
The URL of the Infisical API. Default is `https://api.infisical.com`.
</ParamField>
<ParamField query="UserAgent" type="string">
<ParamField query="UserAgent" type="string" required>
Optionally set the user agent that will be used for HTTP requests. _(Not recommended)_
</ParamField>
<ParamField query="AutoTokenRefresh" type="boolean" default={true} optional>
Whether or not to let the SDK handle the access token lifecycle. Defaults to true if not specified.
</ParamField>
<ParamField query="SilentMode" type="boolean" default={false} optional>
Whether or not to suppress logs such as warnings from the token refreshing process. Defaults to false if not specified.
</ParamField>
</Expandable>
</ParamField>
# Automatic token refreshing
The Infisical Go SDK supports automatic token refreshing. After using one of the auth methods such as Universal Auth, the SDK will automatically renew and re-authenticate when needed.
This behavior is enabled by default, but you can opt-out by setting `AutoTokenRefresh` to `false` in the client settings.
```go
client := infisical.NewInfisicalClient(context.Background(), infisical.Config{
AutoTokenRefresh: false, // <- Disable automatic token refreshing
})
```
When using automatic token refreshing it's important to understand how your application uses the Infiiscal client. If you are instantiating new instances of the client often, it's important to cancel the context when the client is no longer needed to avoid the token refreshing process from running indefinitely.
```go
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // Cancel the context when the client is no longer needed
client := infisical.NewInfisicalClient(ctx, infisical.Config{
AutoTokenRefresh: true,
})
// Use the client
```
This is only necessary if you are creating multiple instances of the client, and those instances are deleted or otherwise removed throughout the application lifecycle.
If you are only creating one instance of the client, and it will be used throughout the lifetime of your application, you don't need to worry about this.
# Authentication
### Authentication
The SDK supports a variety of authentication methods. The most common authentication method is Universal Auth, which uses a client ID and client secret to authenticate.
@ -259,12 +222,9 @@ if err != nil {
}
```
## Working With Secrets
## Working with Secrets
### List Secrets
`client.Secrets().List(options)`
Retrieve all secrets within the Infisical project and environment that client is connected to.
### client.Secrets().List(options)
```go
secrets, err := client.Secrets().List(infisical.ListSecretsOptions{
@ -275,7 +235,9 @@ secrets, err := client.Secrets().List(infisical.ListSecretsOptions{
})
```
### Parameters
Retrieve all secrets within the Infisical project and environment that client is connected to
#### Parameters
<ParamField query="Parameters" type="object">
<Expandable title="properties">
@ -310,11 +272,7 @@ secrets, err := client.Secrets().List(infisical.ListSecretsOptions{
</ParamField>
###
### Retrieve Secret
`client.Secrets().Retrieve(options)`
Retrieve a secret from Infisical. By default `Secrets().Retrieve()` fetches and returns a shared secret.
### client.Secrets().Retrieve(options)
```go
secret, err := client.Secrets().Retrieve(infisical.RetrieveSecretOptions{
@ -324,7 +282,11 @@ secret, err := client.Secrets().Retrieve(infisical.RetrieveSecretOptions{
})
```
### Parameters
Retrieve a secret from Infisical.
By default, `Secrets().Retrieve()` fetches and returns a shared secret.
#### Parameters
<ParamField query="Parameters" type="object" optional>
<Expandable title="properties">
@ -346,11 +308,7 @@ secret, err := client.Secrets().Retrieve(infisical.RetrieveSecretOptions{
</Expandable>
</ParamField>
###
### Create Secret
`client.Secrets().Create(options)`
Create a new secret in Infisical.
### client.Secrets().Create(options)
```go
secret, err := client.Secrets().Create(infisical.CreateSecretOptions{
@ -363,8 +321,9 @@ secret, err := client.Secrets().Create(infisical.CreateSecretOptions{
})
```
Create a new secret in Infisical.
### Parameters
#### Parameters
<ParamField query="Parameters" type="object" optional>
<Expandable title="properties">
@ -392,12 +351,7 @@ secret, err := client.Secrets().Create(infisical.CreateSecretOptions{
</Expandable>
</ParamField>
###
### Update Secret
`client.Secrets().Update(options)`
Update an existing secret in Infisical.
### client.Secrets().Update(options)
```go
secret, err := client.Secrets().Update(infisical.UpdateSecretOptions{
@ -409,7 +363,9 @@ secret, err := client.Secrets().Update(infisical.UpdateSecretOptions{
})
```
### Parameters
Update an existing secret in Infisical.
#### Parameters
<ParamField query="Parameters" type="object" optional>
<Expandable title="properties">
@ -437,11 +393,7 @@ secret, err := client.Secrets().Update(infisical.UpdateSecretOptions{
</Expandable>
</ParamField>
###
### Delete Secret
`client.Secrets().Delete(options)`
Delete a secret in Infisical.
### client.Secrets().Delete(options)
```go
secret, err := client.Secrets().Delete(infisical.DeleteSecretOptions{
@ -451,7 +403,9 @@ secret, err := client.Secrets().Delete(infisical.DeleteSecretOptions{
})
```
### Parameters
Delete a secret in Infisical.
#### Parameters
<ParamField query="Parameters" type="object" optional>
<Expandable title="properties">
@ -473,14 +427,10 @@ secret, err := client.Secrets().Delete(infisical.DeleteSecretOptions{
</Expandable>
</ParamField>
## Working With folders
## Working with folders
###
### List Folders
`client.Folders().List(options)`
Retrieve all within the Infisical project and environment that client is connected to.
### client.Folders().List(options)
```go
folders, err := client.Folders().List(infisical.ListFoldersOptions{
@ -490,7 +440,9 @@ folders, err := client.Folders().List(infisical.ListFoldersOptions{
})
```
### Parameters
Retrieve all within the Infisical project and environment that client is connected to.
#### Parameters
<ParamField query="Parameters" type="object">
<Expandable title="properties">
@ -509,11 +461,7 @@ folders, err := client.Folders().List(infisical.ListFoldersOptions{
</ParamField>
###
### Create Folder
`client.Folders().Create(options)`
Create a new folder in Infisical.
### client.Folders().Create(options)
```go
folder, err := client.Folders().Create(infisical.CreateFolderOptions{
@ -524,7 +472,9 @@ folder, err := client.Folders().Create(infisical.CreateFolderOptions{
})
```
### Parameters
Create a new folder in Infisical.
#### Parameters
<ParamField query="Parameters" type="object" optional>
<Expandable title="properties">
@ -544,11 +494,8 @@ folder, err := client.Folders().Create(infisical.CreateFolderOptions{
</ParamField>
###
### Update Folder
`client.Folders().Update(options)`
Update an existing folder in Infisical.
### client.Folders().Update(options)
```go
folder, err := client.Folders().Update(infisical.UpdateFolderOptions{
@ -560,7 +507,9 @@ folder, err := client.Folders().Update(infisical.UpdateFolderOptions{
})
```
### Parameters
Update an existing folder in Infisical.
#### Parameters
<ParamField query="Parameters" type="object" optional>
<Expandable title="properties">
@ -582,11 +531,7 @@ folder, err := client.Folders().Update(infisical.UpdateFolderOptions{
</Expandable>
</ParamField>
###
### Delete Folder
`client.Folders().Delete(options)`
Delete a folder in Infisical.
### client.Folders().Delete(options)
```go
deletedFolder, err := client.Folders().Delete(infisical.DeleteFolderOptions{
@ -599,7 +544,9 @@ deletedFolder, err := client.Folders().Delete(infisical.DeleteFolderOptions{
})
```
### Parameters
Delete a folder in Infisical.
#### Parameters
<ParamField query="Parameters" type="object" optional>
<Expandable title="properties">
@ -620,6 +567,4 @@ deletedFolder, err := client.Folders().Delete(infisical.DeleteFolderOptions{
The path from where the folder should be deleted.
</ParamField>
</Expandable>
</ParamField>
</ParamField>

View File

@ -115,10 +115,7 @@ export const CreateTagModal = ({ isOpen, onToggle }: Props): JSX.Element => {
formState: { isSubmitting },
handleSubmit
} = useForm<FormData>({
resolver: zodResolver(createTagSchema),
defaultValues: {
color: secretTagsColors[0].hex
}
resolver: zodResolver(createTagSchema)
});
const { currentWorkspace } = useWorkspace();

View File

@ -1,5 +1,5 @@
import { ReactNode } from "react";
import { faCheck, faMinus } from "@fortawesome/free-solid-svg-icons";
import { faCheck } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { twMerge } from "tailwind-merge";
@ -15,7 +15,6 @@ export type CheckboxProps = Omit<
isRequired?: boolean;
checkIndicatorBg?: string | undefined;
isError?: boolean;
isIndeterminate?: boolean;
};
export const Checkbox = ({
@ -27,7 +26,6 @@ export const Checkbox = ({
isRequired,
checkIndicatorBg,
isError,
isIndeterminate,
...props
}: CheckboxProps): JSX.Element => {
return (
@ -47,11 +45,7 @@ export const Checkbox = ({
id={id}
>
<CheckboxPrimitive.Indicator className={`${checkIndicatorBg || "text-bunker-800"}`}>
{isIndeterminate ? (
<FontAwesomeIcon icon={faMinus} size="sm" />
) : (
<FontAwesomeIcon icon={faCheck} size="sm" />
)}
<FontAwesomeIcon icon={faCheck} size="sm" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
<label

View File

@ -1,4 +1,4 @@
import { DetailedHTMLProps, HTMLAttributes, ReactNode, TdHTMLAttributes } from "react";
import { HTMLAttributes, ReactNode, TdHTMLAttributes } from "react";
import { twMerge } from "tailwind-merge";
import { Skeleton } from "../Skeleton";
@ -7,13 +7,12 @@ export type TableContainerProps = {
children: ReactNode;
isRounded?: boolean;
className?: string;
} & DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
};
export const TableContainer = ({
children,
className,
isRounded = true,
...props
isRounded = true
}: TableContainerProps): JSX.Element => (
<div
className={twMerge(
@ -21,7 +20,6 @@ export const TableContainer = ({
isRounded && "rounded-lg",
className
)}
{...props}
>
{children}
</div>

View File

@ -230,7 +230,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
(!orgs?.map((org) => org.id)?.includes(router.query.id as string) &&
!router.asPath.includes("project") &&
!router.asPath.includes("personal") &&
!router.asPath.includes("secret-scanning") &&
!router.asPath.includes("integration")))
) {
router.push(`/org/${currentOrg?.id}/overview`);

View File

@ -72,8 +72,7 @@ const SecretScanning = withPermission(
</div>
{config.isSecretScanningDisabled && (
<NoticeBanner title="Secret scanning is in maintenance" className="mb-4">
We are working on improving the performance of secret scanning due to increased
usage.
We are working on improving the performance of secret scanning due to increased usage.
</NoticeBanner>
)}
<div className="relative mb-6 flex justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6">
@ -117,7 +116,7 @@ const SecretScanning = withPermission(
colorSchema="primary"
onClick={generateNewIntegrationSession}
className="h-min py-2"
isDisabled={!isAllowed}
isDisabled={!isAllowed || config.isSecretScanningDisabled}
>
Integrate with GitHub
</Button>

View File

@ -270,7 +270,7 @@ export const IdentityKubernetesAuthForm = ({
label="Allowed Namespaces"
isError={Boolean(error)}
errorText={error?.message}
tooltipText="A comma-separated list of trusted namespaces that service accounts must belong to authenticate with Infisical."
tooltipText="An optional comma-separated list of trusted service account names that are allowed to authenticate with Infisical. Leave empty to allow any namespaces."
>
<Input {...field} placeholder="namespaceA, namespaceB" type="text" />
</FormControl>

View File

@ -2,33 +2,29 @@ import { createContext, ReactNode, useContext, useEffect, useRef } from "react";
import { useRouter } from "next/router";
import { createStore, StateCreator, StoreApi, useStore } from "zustand";
import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
// akhilmhdh: Don't remove this file if ur thinking why use zustand just for selected selects state
// This is first step and the whole secret crud will be moved to this global page scope state
// this will allow more stuff like undo grouping stuffs etc
type SelectedSecretState = {
selectedSecret: Record<string, SecretV3RawSanitized>;
selectedSecret: Record<string, boolean>;
action: {
toggle: (secret: SecretV3RawSanitized) => void;
toggle: (id: string) => void;
reset: () => void;
set: (secrets: Record<string, SecretV3RawSanitized>) => void;
};
};
const createSelectedSecretStore: StateCreator<SelectedSecretState> = (set) => ({
selectedSecret: {},
action: {
toggle: (secret) =>
toggle: (id) =>
set((state) => {
const isChecked = Boolean(state.selectedSecret?.[secret.id]);
const isChecked = Boolean(state.selectedSecret?.[id]);
const newChecks = { ...state.selectedSecret };
// remove selection if its present else add it
if (isChecked) delete newChecks[secret.id];
else newChecks[secret.id] = secret;
if (isChecked) delete newChecks[id];
else newChecks[id] = true;
return { selectedSecret: newChecks };
}),
reset: () => set({ selectedSecret: {} }),
set: (secrets) => set({ selectedSecret: secrets })
reset: () => set({ selectedSecret: {} })
}
});

View File

@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useRouter } from "next/router";
import { subject } from "@casl/ability";
@ -9,7 +9,7 @@ import { twMerge } from "tailwind-merge";
import NavHeader from "@app/components/navigation/NavHeader";
import { createNotification } from "@app/components/notifications";
import { PermissionDeniedBanner } from "@app/components/permissions";
import { Checkbox, ContentLoader, Pagination, Tooltip } from "@app/components/v2";
import { ContentLoader, Pagination } from "@app/components/v2";
import {
ProjectPermissionActions,
ProjectPermissionSub,
@ -39,11 +39,7 @@ import { PitDrawer } from "./components/PitDrawer";
import { SecretDropzone } from "./components/SecretDropzone";
import { SecretListView } from "./components/SecretListView";
import { SnapshotView } from "./components/SnapshotView";
import {
StoreProvider,
useSelectedSecretActions,
useSelectedSecrets
} from "./SecretMainPage.store";
import { StoreProvider } from "./SecretMainPage.store";
import { Filter, RowType } from "./SecretMainPage.types";
const LOADER_TEXT = [
@ -52,7 +48,7 @@ const LOADER_TEXT = [
"Getting secret import links..."
];
const SecretMainPageContent = () => {
export const SecretMainPage = () => {
const { t } = useTranslation();
const { currentWorkspace, isLoading: isWorkspaceLoading } = useWorkspace();
const router = useRouter();
@ -287,33 +283,6 @@ const SecretMainPageContent = () => {
}
}, [secretPath]);
const selectedSecrets = useSelectedSecrets();
const selectedSecretActions = useSelectedSecretActions();
const allRowsSelectedOnPage = useMemo(() => {
if (secrets?.every((secret) => selectedSecrets[secret.id]))
return { isChecked: true, isIndeterminate: false };
if (secrets?.some((secret) => selectedSecrets[secret.id]))
return { isChecked: true, isIndeterminate: true };
return { isChecked: false, isIndeterminate: false };
}, [selectedSecrets, secrets]);
const toggleSelectAllRows = () => {
const newChecks = { ...selectedSecrets };
secrets?.forEach((secret) => {
if (allRowsSelectedOnPage.isChecked) {
delete newChecks[secret.id];
} else {
newChecks[secret.id] = secret;
}
});
selectedSecretActions.set(newChecks);
};
if (isDetailsLoading) {
return <ContentLoader text={LOADER_TEXT} />;
}
@ -331,192 +300,169 @@ const SecretMainPageContent = () => {
};
return (
<div className="container mx-auto flex flex-col px-6 text-mineshaft-50 dark:[color-scheme:dark]">
<SecretV2MigrationSection />
<div className="relative right-6 -top-2 mb-2 ml-6">
<NavHeader
pageName={t("dashboard.title")}
currentEnv={environment}
userAvailableEnvs={currentWorkspace?.environments}
isFolderMode
secretPath={secretPath}
isProjectRelated
onEnvChange={handleEnvChange}
isProtectedBranch={isProtectedBranch}
protectionPolicyName={boardPolicy?.name}
/>
</div>
{!isRollbackMode ? (
<>
<ActionBar
environment={environment}
workspaceId={workspaceId}
projectSlug={projectSlug}
<StoreProvider>
<div className="container mx-auto flex flex-col px-6 text-mineshaft-50 dark:[color-scheme:dark]">
<SecretV2MigrationSection />
<div className="relative right-6 -top-2 mb-2 ml-6">
<NavHeader
pageName={t("dashboard.title")}
currentEnv={environment}
userAvailableEnvs={currentWorkspace?.environments}
isFolderMode
secretPath={secretPath}
isVisible={isVisible}
filter={filter}
tags={tags}
onVisibilityToggle={handleToggleVisibility}
onSearchChange={handleSearchChange}
onToggleTagFilter={handleTagToggle}
snapshotCount={snapshotCount || 0}
isSnapshotCountLoading={isSnapshotCountLoading}
onToggleRowType={handleToggleRowType}
onClickRollbackMode={() => handlePopUpToggle("snapshots", true)}
isProjectRelated
onEnvChange={handleEnvChange}
isProtectedBranch={isProtectedBranch}
protectionPolicyName={boardPolicy?.name}
/>
<div className="thin-scrollbar mt-3 overflow-y-auto overflow-x-hidden rounded-md rounded-b-none bg-mineshaft-800 text-left text-sm text-bunker-300">
<div className="flex flex-col" id="dashboard">
{isNotEmpty && (
<div
className={twMerge(
"sticky top-0 flex border-b border-mineshaft-600 bg-mineshaft-800 font-medium"
)}
>
<div
className="flex w-80 flex-shrink-0 items-center border-r border-mineshaft-600 px-4 py-2"
role="button"
tabIndex={0}
onClick={handleSortToggle}
onKeyDown={(evt) => {
if (evt.key === "Enter") handleSortToggle();
}}
>
<Tooltip
className="max-w-[20rem] whitespace-nowrap"
content={
totalCount > 0
? `${
!allRowsSelectedOnPage.isChecked ? "Select" : "Unselect"
} all secrets on page`
: ""
}
>
<div className="mr-6 ml-1">
<Checkbox
isDisabled={totalCount === 0}
id="checkbox-select-all-rows"
onClick={(e) => e.stopPropagation()}
isChecked={allRowsSelectedOnPage.isChecked}
isIndeterminate={allRowsSelectedOnPage.isIndeterminate}
onCheckedChange={toggleSelectAllRows}
/>
</div>
</Tooltip>
Key
<FontAwesomeIcon
icon={orderDirection === OrderByDirection.ASC ? faArrowDown : faArrowUp}
className="ml-2"
/>
</div>
<div className="flex-grow px-4 py-2">Value</div>
</div>
)}
{canReadSecret && imports?.length && (
<SecretImportListView
searchTerm={debouncedSearchFilter}
secretImports={imports}
isFetching={isDetailsFetching}
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
importedSecrets={importedSecrets}
/>
)}
{folders?.length && (
<FolderListView
folders={folders}
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
onNavigateToFolder={handleResetFilter}
/>
)}
{canReadSecret && dynamicSecrets?.length && (
<DynamicSecretListView
environment={environment}
projectSlug={projectSlug}
secretPath={secretPath}
dynamicSecrets={dynamicSecrets}
/>
)}
{canReadSecret && secrets?.length && (
<SecretListView
secrets={secrets}
tags={tags}
isVisible={isVisible}
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
isProtectedBranch={isProtectedBranch}
/>
)}
{!canReadSecret && folders?.length === 0 && <PermissionDeniedBanner />}
</div>
</div>
{!isDetailsLoading && totalCount > 0 && (
<Pagination
startAdornment={
<SecretTableResourceCount
dynamicSecretCount={totalDynamicSecretCount}
importCount={totalImportCount}
secretCount={totalSecretCount}
folderCount={totalFolderCount}
/>
}
className="rounded-b-md border-t border-solid border-t-mineshaft-600"
count={totalCount}
page={page}
perPage={perPage}
onChangePage={(newPage) => setPage(newPage)}
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
</div>
{!isRollbackMode ? (
<>
<ActionBar
secrets={secrets}
environment={environment}
workspaceId={workspaceId}
projectSlug={projectSlug}
secretPath={secretPath}
isVisible={isVisible}
filter={filter}
tags={tags}
onVisibilityToggle={handleToggleVisibility}
onSearchChange={handleSearchChange}
onToggleTagFilter={handleTagToggle}
snapshotCount={snapshotCount || 0}
isSnapshotCountLoading={isSnapshotCountLoading}
onToggleRowType={handleToggleRowType}
onClickRollbackMode={() => handlePopUpToggle("snapshots", true)}
/>
)}
<CreateSecretForm
<div className="thin-scrollbar mt-3 overflow-y-auto overflow-x-hidden rounded-md rounded-b-none bg-mineshaft-800 text-left text-sm text-bunker-300">
<div className="flex flex-col" id="dashboard">
{isNotEmpty && (
<div
className={twMerge(
"sticky top-0 flex border-b border-mineshaft-600 bg-mineshaft-800 font-medium"
)}
>
<div style={{ width: "2.8rem" }} className="flex-shrink-0 px-4 py-3" />
<div
className="flex w-80 flex-shrink-0 items-center border-r border-mineshaft-600 px-4 py-2"
role="button"
tabIndex={0}
onClick={handleSortToggle}
onKeyDown={(evt) => {
if (evt.key === "Enter") handleSortToggle();
}}
>
Key
<FontAwesomeIcon
icon={orderDirection === OrderByDirection.ASC ? faArrowDown : faArrowUp}
className="ml-2"
/>
</div>
<div className="flex-grow px-4 py-2">Value</div>
</div>
)}
{canReadSecret && imports?.length && (
<SecretImportListView
searchTerm={debouncedSearchFilter}
secretImports={imports}
isFetching={isDetailsFetching}
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
importedSecrets={importedSecrets}
/>
)}
{folders?.length && (
<FolderListView
folders={folders}
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
onNavigateToFolder={handleResetFilter}
/>
)}
{canReadSecret && dynamicSecrets?.length && (
<DynamicSecretListView
environment={environment}
projectSlug={projectSlug}
secretPath={secretPath}
dynamicSecrets={dynamicSecrets}
/>
)}
{canReadSecret && secrets?.length && (
<SecretListView
secrets={secrets}
tags={tags}
isVisible={isVisible}
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
isProtectedBranch={isProtectedBranch}
/>
)}
{!canReadSecret && folders?.length === 0 && <PermissionDeniedBanner />}
</div>
</div>
{!isDetailsLoading && totalCount > 0 && (
<Pagination
startAdornment={
<SecretTableResourceCount
dynamicSecretCount={totalDynamicSecretCount}
importCount={totalImportCount}
secretCount={totalSecretCount}
folderCount={totalFolderCount}
/>
}
className="rounded-b-md border-t border-solid border-t-mineshaft-600"
count={totalCount}
page={page}
perPage={perPage}
onChangePage={(newPage) => setPage(newPage)}
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
/>
)}
<CreateSecretForm
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
autoCapitalize={currentWorkspace?.autoCapitalization}
isProtectedBranch={isProtectedBranch}
/>
<SecretDropzone
secrets={secrets}
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
isSmaller={isNotEmpty}
environments={currentWorkspace?.environments}
isProtectedBranch={isProtectedBranch}
/>
<PitDrawer
secretSnaphots={snapshotList}
snapshotId={snapshotId}
isDrawerOpen={popUp.snapshots.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("snapshots", isOpen)}
hasNextPage={hasNextSnapshotListPage}
fetchNextPage={fetchNextSnapshotList}
onSelectSnapshot={handleSelectSnapshot}
isFetchingNextPage={isFetchingNextSnapshotList}
/>
</>
) : (
<SnapshotView
snapshotId={snapshotId || ""}
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
autoCapitalize={currentWorkspace?.autoCapitalization}
isProtectedBranch={isProtectedBranch}
/>
<SecretDropzone
secrets={secrets}
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
isSmaller={isNotEmpty}
environments={currentWorkspace?.environments}
isProtectedBranch={isProtectedBranch}
folders={folders}
snapshotCount={snapshotCount}
onGoBack={handleResetSnapshot}
onClickListSnapshot={() => handlePopUpToggle("snapshots", true)}
/>
<PitDrawer
secretSnaphots={snapshotList}
snapshotId={snapshotId}
isDrawerOpen={popUp.snapshots.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("snapshots", isOpen)}
hasNextPage={hasNextSnapshotListPage}
fetchNextPage={fetchNextSnapshotList}
onSelectSnapshot={handleSelectSnapshot}
isFetchingNextPage={isFetchingNextSnapshotList}
/>
</>
) : (
<SnapshotView
snapshotId={snapshotId || ""}
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
secrets={secrets}
folders={folders}
snapshotCount={snapshotCount}
onGoBack={handleResetSnapshot}
onClickListSnapshot={() => handlePopUpToggle("snapshots", true)}
/>
)}
</div>
)}
</div>
</StoreProvider>
);
};
export const SecretMainPage = () => (
<StoreProvider>
<SecretMainPageContent />
</StoreProvider>
);

View File

@ -54,7 +54,7 @@ import {
import { usePopUp } from "@app/hooks";
import { useCreateFolder, useDeleteSecretBatch, useMoveSecrets } from "@app/hooks/api";
import { fetchProjectSecrets } from "@app/hooks/api/secrets/queries";
import { SecretType, WsTag } from "@app/hooks/api/types";
import { SecretType, SecretV3RawSanitized, WsTag } from "@app/hooks/api/types";
import {
PopUpNames,
@ -69,7 +69,8 @@ import { FolderForm } from "./FolderForm";
import { MoveSecretsModal } from "./MoveSecretsModal";
type Props = {
// switch the secrets type as it gets decrypted after api call
secrets?: SecretV3RawSanitized[];
// swtich the secrets type as it gets decrypted after api call
environment: string;
// @depreciated will be moving all these details to zustand
workspaceId: string;
@ -88,6 +89,7 @@ type Props = {
};
export const ActionBar = ({
secrets = [],
environment,
workspaceId,
projectSlug,
@ -200,7 +202,7 @@ export const ActionBar = ({
};
const handleSecretBulkDelete = async () => {
const bulkDeletedSecrets = Object.values(selectedSecrets);
const bulkDeletedSecrets = secrets.filter(({ id }) => Boolean(selectedSecrets?.[id]));
try {
await deleteBatchSecretV3({
secretPath,
@ -233,7 +235,7 @@ export const ActionBar = ({
shouldOverwrite: boolean;
}) => {
try {
const secretsToMove = Object.values(selectedSecrets);
const secretsToMove = secrets.filter(({ id }) => Boolean(selectedSecrets?.[id]));
const { isDestinationUpdated, isSourceUpdated } = await moveSecrets({
projectSlug,
shouldOverwrite,
@ -551,7 +553,7 @@ export const ActionBar = ({
<FontAwesomeIcon icon={faMinusSquare} size="lg" />
</IconButton>
</Tooltip>
<div className="ml-2 flex-grow px-2 text-sm">
<div className="ml-4 flex-grow px-2 text-sm">
{Object.keys(selectedSecrets).length} Selected
</div>
<ProjectPermissionCan

View File

@ -158,17 +158,17 @@ export const SecretImportItem = ({
}
}}
>
<div className="flex w-11 items-center py-2 pl-5 text-green-700">
<div className="flex w-12 items-center px-4 py-2 text-green-700">
<FontAwesomeIcon icon={faFileImport} />
</div>
<div className="flex flex-grow items-center py-2 pl-4 pr-2">
<div className="flex flex-grow items-center px-4 py-2">
<EnvFolderIcon
env={importEnv.slug || ""}
secretPath={secretImport?.importPath || ""}
// isReplication={isReplication}
/>
</div>
<div className="flex items-center space-x-4 py-2 pr-4">
<div className="flex items-center space-x-4 px-4 py-2">
{lastReplicated && (
<Tooltip
position="left"

View File

@ -56,7 +56,7 @@ type Props = {
onDetailViewSecret: (sec: SecretV3RawSanitized) => void;
isVisible?: boolean;
isSelected?: boolean;
onToggleSecretSelect: (secret: SecretV3RawSanitized) => void;
onToggleSecretSelect: (id: string) => void;
tags: WsTag[];
onCreateTag: () => void;
environment: string;
@ -218,7 +218,7 @@ export const SecretItem = memo(
<Checkbox
id={`checkbox-${secret.id}`}
isChecked={isSelected}
onCheckedChange={() => onToggleSecretSelect(secret)}
onCheckedChange={() => onToggleSecretSelect(secret.id)}
className={twMerge("ml-3 hidden group-hover:flex", isSelected && "flex")}
/>
<FontAwesomeSymbol

View File

@ -304,7 +304,7 @@ export const SecretListView = ({
environment={environment}
secretPath={secretPath}
tags={wsTags}
isSelected={Boolean(selectedSecrets?.[secret.id])}
isSelected={selectedSecrets?.[secret.id]}
onToggleSecretSelect={toggleSelectedSecret}
isVisible={isVisible}
secret={secret}

View File

@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import Link from "next/link";
import { useRouter } from "next/router";
@ -25,7 +25,6 @@ import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Button,
Checkbox,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
@ -68,7 +67,7 @@ import { DashboardSecretsOrderBy } from "@app/hooks/api/dashboard/types";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { useUpdateFolderBatch } from "@app/hooks/api/secretFolders/queries";
import { TUpdateFolderBatchDTO } from "@app/hooks/api/secretFolders/types";
import { SecretType, SecretV3RawSanitized, TSecretFolder } from "@app/hooks/api/types";
import { SecretType, TSecretFolder } from "@app/hooks/api/types";
import { ProjectVersion } from "@app/hooks/api/workspace/types";
import { useDynamicSecretOverview, useFolderOverview, useSecretOverview } from "@app/hooks/utils";
import { SecretOverviewDynamicSecretRow } from "@app/views/SecretOverviewPage/components/SecretOverviewDynamicSecretRow";
@ -107,10 +106,18 @@ export const SecretOverviewPage = () => {
const { t } = useTranslation();
const router = useRouter();
const [scrollOffset, setScrollOffset] = useState(0);
const [debouncedScrollOffset] = useDebounce(scrollOffset);
// this is to set expandable table width
// coz when overflow the table goes to the right
const parentTableRef = useRef<HTMLTableElement>(null);
const [expandableTableWidth, setExpandableTableWidth] = useState(0);
const { permission } = useProjectPermission();
useEffect(() => {
if (parentTableRef.current) {
setExpandableTableWidth(parentTableRef.current.clientWidth);
}
}, [parentTableRef.current]);
const { currentWorkspace, isLoading: isWorkspaceLoading } = useWorkspace();
const isProjectV3 = currentWorkspace?.version === ProjectVersion.V3;
const { currentOrg } = useOrganization();
@ -126,9 +133,8 @@ export const SecretOverviewPage = () => {
>(new Map());
const [selectedEntries, setSelectedEntries] = useState<{
// selectedEntries[name/key][envSlug][resource]
[EntryType.FOLDER]: Record<string, Record<string, TSecretFolder>>;
[EntryType.SECRET]: Record<string, Record<string, SecretV3RawSanitized>>;
[EntryType.FOLDER]: Record<string, boolean>;
[EntryType.SECRET]: Record<string, boolean>;
}>({
[EntryType.FOLDER]: {},
[EntryType.SECRET]: {}
@ -146,6 +152,23 @@ export const SecretOverviewPage = () => {
orderBy
} = usePagination<DashboardSecretsOrderBy>(DashboardSecretsOrderBy.Name);
const toggleSelectedEntry = useCallback(
(type: EntryType, key: string) => {
const isChecked = Boolean(selectedEntries[type]?.[key]);
const newChecks = { ...selectedEntries };
// remove selection if its present else add it
if (isChecked) {
delete newChecks[type][key];
} else {
newChecks[type][key] = true;
}
setSelectedEntries(newChecks);
},
[selectedEntries]
);
const resetSelectedEntries = useCallback(() => {
setSelectedEntries({
[EntryType.FOLDER]: {},
@ -154,13 +177,19 @@ export const SecretOverviewPage = () => {
}, []);
useEffect(() => {
const handleParentTableWidthResize = () => {
setExpandableTableWidth(parentTableRef.current?.clientWidth || 0);
};
const onRouteChangeStart = () => {
resetSelectedEntries();
};
router.events.on("routeChangeStart", onRouteChangeStart);
window.addEventListener("resize", handleParentTableWidthResize);
return () => {
window.removeEventListener("resize", handleParentTableWidthResize);
router.events.off("routeChangeStart", onRouteChangeStart);
};
}, []);
@ -522,83 +551,6 @@ export const SecretOverviewPage = () => {
[]
);
const allRowsSelectedOnPage = useMemo(() => {
if (
(!secrets?.length ||
secrets?.every((secret) => selectedEntries[EntryType.SECRET][secret.key])) &&
(!folders?.length ||
folders?.every((folder) => selectedEntries[EntryType.FOLDER][folder.name]))
)
return { isChecked: true, isIndeterminate: false };
if (
secrets?.some((secret) => selectedEntries[EntryType.SECRET][secret.key]) ||
folders?.some((folder) => selectedEntries[EntryType.FOLDER][folder.name])
)
return { isChecked: true, isIndeterminate: true };
return { isChecked: false, isIndeterminate: false };
}, [selectedEntries, secrets, folders]);
const toggleSelectedEntry = useCallback(
(type: EntryType, key: string) => {
const isChecked = Boolean(selectedEntries[type]?.[key]);
const newChecks = { ...selectedEntries };
// remove selection if its present else add it
if (isChecked) {
delete newChecks[type][key];
} else {
newChecks[type][key] = {};
userAvailableEnvs.forEach((env) => {
const resource =
type === EntryType.SECRET
? getSecretByKey(env.slug, key)
: getFolderByNameAndEnv(key, env.slug);
if (resource) newChecks[type][key][env.slug] = resource;
});
}
setSelectedEntries(newChecks);
},
[selectedEntries, getFolderByNameAndEnv, getSecretByKey]
);
const toggleSelectAllRows = () => {
const newChecks = { ...selectedEntries };
userAvailableEnvs.forEach((env) => {
secrets?.forEach((secret) => {
if (allRowsSelectedOnPage.isChecked) {
delete newChecks[EntryType.SECRET][secret.key];
} else {
if (!newChecks[EntryType.SECRET][secret.key])
newChecks[EntryType.SECRET][secret.key] = {};
const resource = getSecretByKey(env.slug, secret.key);
if (resource) newChecks[EntryType.SECRET][secret.key][env.slug] = resource;
}
});
folders?.forEach((folder) => {
if (allRowsSelectedOnPage.isChecked) {
delete newChecks[EntryType.FOLDER][folder.name];
} else {
if (!newChecks[EntryType.FOLDER][folder.name])
newChecks[EntryType.FOLDER][folder.name] = {};
const resource = getFolderByNameAndEnv(folder.name, env.slug);
if (resource) newChecks[EntryType.FOLDER][folder.name][env.slug] = resource;
}
});
});
setSelectedEntries(newChecks);
};
if (isWorkspaceLoading || (isProjectV3 && isOverviewLoading)) {
return (
<div className="container mx-auto flex h-screen w-full items-center justify-center px-8 text-mineshaft-50 dark:[color-scheme:dark]">
@ -847,39 +799,18 @@ export const SecretOverviewPage = () => {
</div>
<SelectionPanel
secretPath={secretPath}
getSecretByKey={getSecretByKey}
getFolderByNameAndEnv={getFolderByNameAndEnv}
selectedEntries={selectedEntries}
resetSelectedEntries={resetSelectedEntries}
/>
<div className="thin-scrollbar mt-4">
<TableContainer
onScroll={(e) => setScrollOffset(e.currentTarget.scrollLeft)}
className="thin-scrollbar"
>
<div className="thin-scrollbar mt-4" ref={parentTableRef}>
<TableContainer className="rounded-b-none">
<Table>
<THead>
<Tr className="sticky top-0 z-20 border-0">
<Th className="sticky left-0 z-20 min-w-[20rem] border-b-0 p-0">
<div className="flex items-center border-b border-r border-mineshaft-600 pr-5 pl-3 pt-3.5 pb-3">
<Tooltip
className="max-w-[20rem] whitespace-nowrap capitalize"
content={
totalCount > 0
? `${
!allRowsSelectedOnPage.isChecked ? "Select" : "Unselect"
} all folders and secrets on page`
: ""
}
>
<div className="mr-4 ml-2">
<Checkbox
isDisabled={totalCount === 0}
id="checkbox-select-all-rows"
isChecked={allRowsSelectedOnPage.isChecked}
isIndeterminate={allRowsSelectedOnPage.isIndeterminate}
onCheckedChange={toggleSelectAllRows}
/>
</div>
</Tooltip>
<div className="flex items-center border-b border-r border-mineshaft-600 px-5 pt-3.5 pb-3">
Name
<IconButton
variant="plain"
@ -1007,7 +938,7 @@ export const SecretOverviewPage = () => {
<SecretOverviewFolderRow
folderName={folderName}
isFolderPresentInEnv={isFolderPresentInEnv}
isSelected={Boolean(selectedEntries.folder[folderName])}
isSelected={selectedEntries.folder[folderName]}
onToggleFolderSelect={() =>
toggleSelectedEntry(EntryType.FOLDER, folderName)
}
@ -1029,7 +960,7 @@ export const SecretOverviewPage = () => {
))}
{secKeys.map((key, index) => (
<SecretOverviewTableRow
isSelected={Boolean(selectedEntries.secret[key])}
isSelected={selectedEntries.secret[key]}
onToggleSecretSelect={() => toggleSelectedEntry(EntryType.SECRET, key)}
secretPath={secretPath}
getImportedSecretByKey={getImportedSecretByKey}
@ -1041,7 +972,7 @@ export const SecretOverviewPage = () => {
environments={visibleEnvs}
secretKey={key}
getSecretByKey={getSecretByKey}
scrollOffset={debouncedScrollOffset}
expandableColWidth={expandableTableWidth}
/>
))}
</>

View File

@ -23,6 +23,7 @@ type Props = {
secretKey: string;
secretPath: string;
environments: { name: string; slug: string }[];
expandableColWidth: number;
isSelected: boolean;
onToggleSecretSelect: (key: string) => void;
getSecretByKey: (slug: string, key: string) => SecretV3RawSanitized | undefined;
@ -40,7 +41,6 @@ type Props = {
env: string,
secretName: string
) => { secret?: SecretV3RawSanitized; environmentInfo?: WorkspaceEnv } | undefined;
scrollOffset: number;
};
export const SecretOverviewTableRow = ({
@ -53,7 +53,9 @@ export const SecretOverviewTableRow = ({
onSecretDelete,
isImportedSecretPresentInEnv,
getImportedSecretByKey,
scrollOffset,
// temporary until below todo is resolved
// eslint-disable-next-line @typescript-eslint/no-unused-vars
expandableColWidth,
onToggleSecretSelect,
isSelected
}: Props) => {
@ -150,11 +152,11 @@ export const SecretOverviewTableRow = ({
}`}
>
<div
className="ml-2 p-2"
style={{
marginLeft: scrollOffset,
width: "calc(100vw - 290px)" // 290px accounts for sidebar and margin
}}
className="ml-2 w-[99%] p-2"
// TODO: scott expandableColWidth sometimes 0 due to parent ref not mounting, opting for relative width until resolved
// style={{
// width: `calc(${expandableColWidth} - 1rem)`
// }}
>
<SecretRenameRow
secretKey={secretKey}

View File

@ -27,23 +27,31 @@ export enum EntryType {
type Props = {
secretPath: string;
getSecretByKey: (slug: string, key: string) => SecretV3RawSanitized | undefined;
getFolderByNameAndEnv: (name: string, env: string) => TSecretFolder | undefined;
resetSelectedEntries: () => void;
selectedEntries: {
[EntryType.FOLDER]: Record<string, Record<string, TSecretFolder>>;
[EntryType.SECRET]: Record<string, Record<string, SecretV3RawSanitized>>;
[EntryType.FOLDER]: Record<string, boolean>;
[EntryType.SECRET]: Record<string, boolean>;
};
};
export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntries }: Props) => {
export const SelectionPanel = ({
getFolderByNameAndEnv,
getSecretByKey,
secretPath,
resetSelectedEntries,
selectedEntries
}: Props) => {
const { permission } = useProjectPermission();
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
"bulkDeleteEntries"
] as const);
const selectedFolderCount = Object.keys(selectedEntries.folder).length;
const selectedKeysCount = Object.keys(selectedEntries.secret).length;
const selectedCount = selectedFolderCount + selectedKeysCount;
const selectedFolderCount = Object.keys(selectedEntries.folder).length
const selectedKeysCount = Object.keys(selectedEntries.secret).length
const selectedCount = selectedFolderCount + selectedKeysCount
const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?.id || "";
@ -69,7 +77,7 @@ export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntri
return "Do you want to delete the selected secrets across environments?";
}
return "Do you want to delete the selected folders across environments?";
};
}
const handleBulkDelete = async () => {
let processedEntries = 0;
@ -86,8 +94,8 @@ export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntri
}
await Promise.all(
Object.values(selectedEntries.folder).map(async (folderRecord) => {
const folder = folderRecord[env.slug];
Object.keys(selectedEntries.folder).map(async (folderName) => {
const folder = getFolderByNameAndEnv(folderName, env.slug);
if (folder) {
processedEntries += 1;
await deleteFolder({
@ -100,9 +108,9 @@ export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntri
})
);
const secretsToDelete = Object.values(selectedEntries.secret).reduce(
(accum: TDeleteSecretBatchDTO["secrets"], secretRecord) => {
const entry = secretRecord[env.slug];
const secretsToDelete = Object.keys(selectedEntries.secret).reduce(
(accum: TDeleteSecretBatchDTO["secrets"], secretName) => {
const entry = getSecretByKey(env.slug, secretName);
if (entry) {
return [
...accum,
@ -165,7 +173,7 @@ export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntri
<FontAwesomeIcon icon={faMinusSquare} size="lg" />
</IconButton>
</Tooltip>
<div className="ml-1 flex-grow px-2 text-sm">{selectedCount} Selected</div>
<div className="ml-4 flex-grow px-2 text-sm">{selectedCount} Selected</div>
{shouldShowDelete && (
<Button
variant="outline_bg"

View File

@ -13,9 +13,9 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: v0.7.2
version: v0.7.3
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "v0.7.2"
appVersion: "v0.7.3"

View File

@ -32,7 +32,7 @@ controllerManager:
- ALL
image:
repository: infisical/kubernetes-operator
tag: v0.7.2
tag: v0.7.3
resources:
limits:
cpu: 500m

View File

@ -39,6 +39,7 @@ type InfisicalSecretReconciler struct {
type ResourceVariables struct {
infisicalClient infisicalSdk.InfisicalClientInterface
cancelCtx context.CancelFunc
authDetails AuthenticationDetails
}
@ -136,11 +137,17 @@ func (r *InfisicalSecretReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&secretsv1alpha1.InfisicalSecret{}, builder.WithPredicates(predicate.Funcs{
UpdateFunc: func(e event.UpdateEvent) bool {
delete(resourceVariablesMap, string(e.ObjectNew.GetUID()))
if rv, ok := resourceVariablesMap[string(e.ObjectNew.GetUID())]; ok {
rv.cancelCtx()
delete(resourceVariablesMap, string(e.ObjectNew.GetUID()))
}
return true
},
DeleteFunc: func(e event.DeleteEvent) bool {
delete(resourceVariablesMap, string(e.Object.GetUID()))
if rv, ok := resourceVariablesMap[string(e.Object.GetUID())]; ok {
rv.cancelCtx()
delete(resourceVariablesMap, string(e.Object.GetUID()))
}
return true
},
})).

View File

@ -293,13 +293,16 @@ func (r *InfisicalSecretReconciler) GetResourceVariables(infisicalSecret v1alpha
if _, ok := resourceVariablesMap[string(infisicalSecret.UID)]; !ok {
client := infisicalSdk.NewInfisicalClient(infisicalSdk.Config{
ctx, cancel := context.WithCancel(context.Background())
client := infisicalSdk.NewInfisicalClient(ctx, infisicalSdk.Config{
SiteUrl: api.API_HOST_URL,
UserAgent: api.USER_AGENT_NAME,
})
resourceVariablesMap[string(infisicalSecret.UID)] = ResourceVariables{
infisicalClient: client,
cancelCtx: cancel,
authDetails: AuthenticationDetails{},
}
@ -321,6 +324,7 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context
resourceVariables := r.GetResourceVariables(infisicalSecret)
infisicalClient := resourceVariables.infisicalClient
cancelCtx := resourceVariables.cancelCtx
authDetails := resourceVariables.authDetails
var err error
@ -335,6 +339,7 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context
r.UpdateResourceVariables(infisicalSecret, ResourceVariables{
infisicalClient: infisicalClient,
cancelCtx: cancelCtx,
authDetails: authDetails,
})
}

View File

@ -3,7 +3,7 @@ module github.com/Infisical/infisical/k8-operator
go 1.21
require (
github.com/infisical/go-sdk v0.3.2
github.com/infisical/go-sdk v0.3.7
github.com/onsi/ginkgo/v2 v2.6.0
github.com/onsi/gomega v1.24.1
k8s.io/apimachinery v0.26.1
@ -12,10 +12,10 @@ require (
)
require (
cloud.google.com/go/auth v0.6.1 // indirect
cloud.google.com/go/auth v0.7.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect
cloud.google.com/go/compute/metadata v0.4.0 // indirect
cloud.google.com/go/iam v1.1.10 // indirect
cloud.google.com/go/iam v1.1.11 // indirect
github.com/aws/aws-sdk-go-v2 v1.30.1 // indirect
github.com/aws/aws-sdk-go-v2/config v1.27.24 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.24 // indirect
@ -41,7 +41,7 @@ require (
go.opentelemetry.io/otel/metric v1.28.0 // indirect
go.opentelemetry.io/otel/trace v1.28.0 // indirect
golang.org/x/sync v0.7.0 // indirect
google.golang.org/api v0.187.0 // indirect
google.golang.org/api v0.188.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240708141625-4ad9e859172b // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240708141625-4ad9e859172b // indirect
google.golang.org/grpc v1.65.0 // indirect

View File

@ -13,8 +13,8 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go/auth v0.6.1 h1:T0Zw1XM5c1GlpN2HYr2s+m3vr1p2wy+8VN+Z1FKxW38=
cloud.google.com/go/auth v0.6.1/go.mod h1:eFHG7zDzbXHKmjJddFG/rBlcGp6t25SwRUiEQSlO4x4=
cloud.google.com/go/auth v0.7.0 h1:kf/x9B3WTbBUHkC+1VS8wwwli9TzhSt0vSTVBmMR8Ts=
cloud.google.com/go/auth v0.7.0/go.mod h1:D+WqdrpcjmiCgWrXmLLxOVq1GACoE36chW6KXoEvuIw=
cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4=
cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
@ -27,8 +27,8 @@ cloud.google.com/go/compute/metadata v0.4.0 h1:vHzJCWaM4g8XIcm8kopr3XmDA4Gy/lblD
cloud.google.com/go/compute/metadata v0.4.0/go.mod h1:SIQh1Kkb4ZJ8zJ874fqVkslA29PRXuleyj6vOzlbK7M=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/iam v1.1.10 h1:ZSAr64oEhQSClwBL670MsJAW5/RLiC6kfw3Bqmd5ZDI=
cloud.google.com/go/iam v1.1.10/go.mod h1:iEgMq62sg8zx446GCaijmA2Miwg5o3UbO+nI47WHJps=
cloud.google.com/go/iam v1.1.11 h1:0mQ8UKSfdHLut6pH9FM3bI55KWR46ketn0PuXleDyxw=
cloud.google.com/go/iam v1.1.11/go.mod h1:biXoiLWYIKntto2joP+62sd9uW5EpkZmKIvfNcTWlnQ=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
@ -217,8 +217,8 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/infisical/go-sdk v0.3.2 h1:BfeQzG7s3qmEGhgXu0d1YNsyaiHucHgI+BaLpx+W8cc=
github.com/infisical/go-sdk v0.3.2/go.mod h1:vHTDVw3k+wfStXab513TGk1n53kaKF2xgLqpw/xvtl4=
github.com/infisical/go-sdk v0.3.7 h1:EE0ALjjdJtNvDzHtxotkBxYZ6L9ZmeruH89u6jh1Bik=
github.com/infisical/go-sdk v0.3.7/go.mod h1:HHW7DgUqoolyQIUw/9HdpkZ3bDLwWyZ0HEtYiVaDKQw=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
@ -601,8 +601,8 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.187.0 h1:Mxs7VATVC2v7CY+7Xwm4ndkX71hpElcvx0D1Ji/p1eo=
google.golang.org/api v0.187.0/go.mod h1:KIHlTc4x7N7gKKuVsdmfBXN13yEEWXWFURWY6SBp2gk=
google.golang.org/api v0.188.0 h1:51y8fJ/b1AaaBRJr4yWm96fPcuxSo0JcegXE3DaHQHw=
google.golang.org/api v0.188.0/go.mod h1:VR0d+2SIiWOYG3r/jdm7adPW9hI2aRv9ETOSCQ9Beag=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=