Compare commits

...

17 Commits

Author SHA1 Message Date
Sheen Capadngan
0c673f6cca misc: addressed ws vulnerability via package update 2024-07-12 15:36:49 +08:00
Sheen Capadngan
10f4cbf11f Merge pull request #2107 from Infisical/feat/add-share-secret-hide-unhide
feat: add share-secret hide and unhide
2024-07-12 14:57:02 +08:00
BlackMagiq
a6a8c32326 Merge pull request #2106 from Infisical/docs/identity-github-oidc-auth
doc: added docs for connecting github to infisical via oidc auth
2024-07-12 10:26:08 +07:00
Maidul Islam
99a474dba7 Merge pull request #2091 from Infisical/misc/moved-admin-user-deletion-to-pro
misc: moved admin user deletion to pro plan
2024-07-11 13:28:33 -04:00
Maidul Islam
e439f4e5aa Update UserPanel.tsx 2024-07-11 13:25:48 -04:00
Maidul Islam
ae2ecf1540 Merge pull request #2100 from Infisical/misc/add-ttl-max-value-for-identities
misc: add max checks for TTL values of identities
2024-07-11 13:21:53 -04:00
Sheen Capadngan
10214ea5dc misc: renamed button label 2024-07-12 00:45:16 +08:00
Sheen Capadngan
918cd414a8 feat: add share-secret hide and unhide 2024-07-12 00:42:26 +08:00
Sheen Capadngan
52415ea83e misc: removed redundant text' 2024-07-11 23:30:55 +08:00
Sheen Capadngan
c5ca2b6796 doc: added docs for connecting github to infisical via oidc auth 2024-07-11 23:00:45 +08:00
BlackMagiq
ef5bcac925 Merge pull request #2103 from Infisical/move-groups
Consolidate People and Groups Tabs to shared User Tab at Org / Project Level
2024-07-11 18:58:12 +07:00
Tuan Dang
fbe344c0df Fix token auth ref 2024-07-11 14:56:57 +07:00
Tuan Dang
5821f65a63 Fix token auth ref 2024-07-11 14:56:08 +07:00
Tuan Dang
93af7573ac Consolidate people and groups tabs to user / user groups shared tab 2024-07-11 13:35:11 +07:00
Sheen Capadngan
c501c85eb8 misc: renamed to more generic label 2024-07-11 00:14:34 +08:00
Maidul Islam
9832915eba add .? incase adminUserDeletion is empty 2024-07-09 21:09:55 -04:00
Sheen Capadngan
b98c8629e5 misc: moved admin user deletion to pro 2024-07-09 23:51:09 +08:00
35 changed files with 1525 additions and 1263 deletions

View File

@@ -38,7 +38,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
has_used_trial: true,
secretApproval: false,
secretRotation: true,
caCrl: false
caCrl: false,
instanceUserManagement: false
});
export const setupLicenceRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {

View File

@@ -56,6 +56,7 @@ export type TFeatureSet = {
secretApproval: false;
secretRotation: true;
caCrl: false;
instanceUserManagement: false;
};
export type TOrgPlansTableDTO = {

View File

@@ -469,7 +469,8 @@ export const registerRoutes = async (
authService: loginService,
serverCfgDAL: superAdminDAL,
orgService,
keyStore
keyStore,
licenseService
});
const rateLimitService = rateLimitServiceFactory({
rateLimitDAL,

View File

@@ -1,6 +1,7 @@
import bcrypt from "bcrypt";
import { TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
@@ -20,6 +21,7 @@ type TSuperAdminServiceFactoryDep = {
authService: Pick<TAuthLoginFactory, "generateUserTokens">;
orgService: Pick<TOrgServiceFactory, "createOrganization">;
keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry" | "deleteItem">;
licenseService: Pick<TLicenseServiceFactory, "onPremFeatures">;
};
export type TSuperAdminServiceFactory = ReturnType<typeof superAdminServiceFactory>;
@@ -36,7 +38,8 @@ export const superAdminServiceFactory = ({
userDAL,
authService,
orgService,
keyStore
keyStore,
licenseService
}: TSuperAdminServiceFactoryDep) => {
const initServerCfg = async () => {
// TODO(akhilmhdh): bad pattern time less change this later to me itself
@@ -219,6 +222,12 @@ export const superAdminServiceFactory = ({
};
const deleteUser = async (userId: string) => {
if (!licenseService.onPremFeatures?.instanceUserManagement) {
throw new BadRequestError({
message: "Failed to delete user due to plan restriction. Upgrade to Infisical's Pro plan."
});
}
const user = await userDAL.deleteById(userId);
return user;
};

View File

@@ -1,5 +1,5 @@
---
title: OIDC Auth
title: General
description: "Learn how to authenticate with Infisical from any platform or environment using OpenID Connect (OIDC)."
---
@@ -7,7 +7,7 @@ description: "Learn how to authenticate with Infisical from any platform or envi
## Diagram
The following sequence digram illustrates the OIDC Auth workflow for authenticating clients with Infisical.
The following sequence diagram illustrates the OIDC Auth workflow for authenticating clients with Infisical.
```mermaid
sequenceDiagram
@@ -83,7 +83,7 @@ In the following steps, we explore how to create and use identities to access th
<Tip>Restrict access by configuring the Subject, Audiences, and Claims fields</Tip>
Here's some more guidance on each field:
- OIDC Discovery URL: The URL used to retrieve the OpenID Connect configuration information from the identity provider. This will be used to fetch the public key needed for verifying the provided JWT.
- OIDC Discovery URL: The URL used to retrieve the OpenID Connect configuration from the identity provider. This will be used to fetch the public key needed for verifying the provided JWT.
- Issuer: The unique identifier of the identity provider issuing the JWT. This value is used to verify the iss (issuer) claim in the JWT to ensure the token is issued by a trusted provider.
- CA Certificate: The PEM-encoded CA cert for establishing secure communication with the Identity Provider endpoints.
- Subject: The expected principal that is the subject of the JWT. The `sub` (subject) claim in the JWT should match this value.

View File

@@ -0,0 +1,170 @@
---
title: Github
description: "Learn how to authenticate Github workflows with Infisical using OpenID Connect (OIDC)."
---
**OIDC Auth** is a platform-agnostic JWT-based authentication method that can be used to authenticate from any platform or environment using an identity provider with OpenID Connect.
## Diagram
The following sequence diagram illustrates the OIDC Auth workflow for authenticating Github workflows with Infisical.
```mermaid
sequenceDiagram
participant Client as Github Workflow
participant Idp as Identity Provider
participant Infis as Infisical
Client->>Idp: Step 1: Request identity token
Idp-->>Client: Return JWT with verifiable claims
Note over Client,Infis: Step 2: Login Operation
Client->>Infis: Send signed JWT to /api/v1/auth/oidc-auth/login
Note over Infis,Idp: Step 3: Query verification
Infis->>Idp: Request JWT public key using OIDC Discovery
Idp-->>Infis: Return public key
Note over Infis: Step 4: JWT validation
Infis->>Client: Return short-lived access token
Note over Client,Infis: Step 5: Access Infisical API with Token
Client->>Infis: Make authenticated requests using the short-lived access token
```
## Concept
At a high-level, Infisical authenticates a client by verifying the JWT and checking that it meets specific requirements (e.g. it is issued by a trusted identity provider) at the `/api/v1/auth/oidc-auth/login` endpoint. If successful,
then Infisical returns a short-lived access token that can be used to make authenticated requests to the Infisical API.
To be more specific:
1. The Github workflow requests an identity token from Github's identity provider.
2. The fetched identity token is sent to Infisical at the `/api/v1/auth/oidc-auth/login` endpoint.
3. Infisical fetches the public key that was used to sign the identity token from Github's identity provider using OIDC Discovery.
4. Infisical validates the JWT using the public key provided by the identity provider and checks that the subject, audience, and claims of the token matches with the set criteria.
5. If all is well, Infisical returns a short-lived access token that the Github workflow can use to make authenticated requests to the Infisical API.
<Note>
Infisical needs network-level access to Github's identity provider endpoints.
</Note>
## Guide
In the following steps, we explore how to create and use identities to access the Infisical API using the OIDC Auth authentication method.
<Steps>
<Step title="Creating an identity">
To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**.
![identities organization](/images/platform/identities/identities-org.png)
When creating an identity, you specify an organization level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
![identities organization create](/images/platform/identities/identities-org-create.png)
Now input a few details for your new identity. Here's some guidance for each field:
- Name (required): A friendly name for the identity.
- Role (required): A role from the **Organization Roles** tab for the identity to assume. The organization role assigned will determine what organization level resources this identity can have access to.
Once you've created an identity, you'll be redirected to a page where you can manage the identity.
![identities page](/images/platform/identities/identities-page.png)
Since the identity has been configured with Universal Auth by default, you should re-configure it to use OIDC Auth instead. To do this, press to edit the **Authentication** section,
remove the existing Universal Auth configuration, and add a new OIDC Auth configuration onto the identity.
![identities page remove default auth](/images/platform/identities/identities-page-remove-default-auth.png)
![identities create oidc auth method](/images/platform/identities/identities-org-create-oidc-auth-method.png)
<Warning>Restrict access by configuring the Subject, Audiences, and Claims fields</Warning>
Here's some more guidance on each field:
- OIDC Discovery URL: The URL used to retrieve the OpenID Connect configuration from the identity provider. This will be used to fetch the public key needed for verifying the provided JWT. This should be set to `https://token.actions.githubusercontent.com`
- Issuer: The unique identifier of the identity provider issuing the JWT. This value is used to verify the iss (issuer) claim in the JWT to ensure the token is issued by a trusted provider. This should be set to `https://token.actions.githubusercontent.com`
- CA Certificate: The PEM-encoded CA cert for establishing secure communication with the Identity Provider endpoints. For Github workflows, this can be left as blank.
- Subject: The expected principal that is the subject of the JWT. The format of the sub field for GitHub workflow OIDC tokens is as follows: `"repo:<owner>/<repo>:<environment>"`. The environment can be where the GitHub workflow is running, such as `environment`, `ref`, or `job_workflow_ref`. For example, if you have a repository owned by octocat named example-repo, and the GitHub workflow is running on the main branch, the subject field might look like this: `repo:octocat/example-repo:ref:refs/heads/main`
- Audiences: A list of intended recipients. This value is checked against the aud (audience) claim in the token. By default, set this to the URL of the repository owner, such as the organization that owns the repository (e.g. `https://github.com/octo-org`).
- Claims: Additional information or attributes that should be present in the JWT for it to be valid. You can refer to Github's [documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#understanding-the-oidc-token) for the complete list of supported claims.
- Access Token TTL (default is `2592000` equivalent to 30 days): The lifetime for an acccess token in seconds. This value will be referenced at renewal time.
- Access Token Max TTL (default is `2592000` equivalent to 30 days): The maximum lifetime for an acccess token in seconds. This value will be referenced at renewal time.
- Access Token Max Number of Uses (default is `0`): The maximum number of times that an access token can be used; a value of `0` implies infinite number of uses.
- Access Token Trusted IPs: The IPs or CIDR ranges that access tokens can be used from. By default, each token is given the `0.0.0.0/0`, allowing usage from any network address.
<Tip>If you are unsure about what to configure for the subject, audience, and claims fields you can use [github/actions-oidc-debugger](https://github.com/github/actions-oidc-debugger) to get the appropriate values. Alternatively, you can fetch the JWT from the workflow and inspect the fields manually.</Tip>
</Step>
<Step title="Adding an identity to a project">
To enable the identity to access project-level resources such as secrets within a specific project, you should add it to that project.
To do this, head over to the project you want to add the identity to and go to Project Settings > Access Control > Machine Identities and press **Add identity**.
Next, select the identity you want to add to the project and the project level role you want to allow it to assume. The project role assigned will determine what project level resources this identity can have access to.
![identities project](/images/platform/identities/identities-project.png)
![identities project create](/images/platform/identities/identities-project-create.png)
</Step>
<Step title="Accessing the Infisical API with the identity">
As a prerequisite, you will need to set `id-token:write` permissions for the Github workflow. This setting allows the JWT to be requested from Github's OIDC provider.
```yaml
permissions:
id-token: write # This is required for requesting the JWT
...
```
To access the Infisical API as the identity, you need to fetch an identity token from Github's identity provider and make a request to the `/api/v1/auth/oidc-auth/login` endpoint in exchange for an access token.
The identity token can be fetched using either of the following approaches:
- Using environment variables on the runner (`ACTIONS_ID_TOKEN_REQUEST_URL` and `ACTIONS_ID_TOKEN_REQUEST_TOKEN`).
```yaml
steps:
- name: Request OIDC Token
run: |
echo "Requesting OIDC token..."
TOKEN=$(curl -s -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" "$ACTIONS_ID_TOKEN_REQUEST_URL" | jq -r '.value')
echo "TOKEN=$TOKEN" >> $GITHUB_ENV
```
- Using `getIDToken()` from the Github Actions toolkit.
Below is an example of how a Github workflow can be configured to fetch secrets from Infisical using the [Infisical Secrets Action](https://github.com/Infisical/secrets-action) with OIDC Auth.
```yaml
name: Manual workflow
on:
workflow_dispatch:
permissions:
id-token: write # This is required for requesting the JWT
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: Infisical/secrets-action@v1.0.7
with:
method: "oidc"
env-slug: "dev"
project-slug: "ggggg-9-des"
identity-id: "6b579c00-5c85-4b44-aabe-f8a
...
```
Preceding steps can then use the secret values injected onto the workflow's environment.
<Tip>
We recommend using [Infisical Secrets Action](https://github.com/Infisical/secrets-action) to authenticate with Infisical using OIDC Auth as it handles the authentication process including the fetching of identity tokens for you.
</Tip>
<Note>
Each identity access token has a time-to-live (TLL) which you can infer from the response of the login operation;
the default TTL is `7200` seconds which can be adjusted.
If an identity access token expires, it can no longer authenticate with the Infisical API. In this case,
a new access token should be obtained by performing another login operation.
</Note>
</Step>
</Steps>

View File

@@ -168,7 +168,13 @@
"documentation/platform/identities/gcp-auth",
"documentation/platform/identities/azure-auth",
"documentation/platform/identities/aws-auth",
"documentation/platform/identities/oidc-auth",
{
"group": "OIDC Auth",
"pages": [
"documentation/platform/identities/oidc-auth/general",
"documentation/platform/identities/oidc-auth/github"
]
},
"documentation/platform/mfa",
{
"group": "SSO",

View File

@@ -139,7 +139,7 @@
"postcss": "^8.4.14",
"prettier": "^2.8.3",
"prettier-plugin-tailwindcss": "^0.2.2",
"storybook": "^7.5.2",
"storybook": "^7.6.20",
"storybook-dark-mode": "^3.0.0",
"tailwindcss": "3.2",
"typescript": "^4.9.3"
@@ -6200,15 +6200,15 @@
}
},
"node_modules/@storybook/builder-manager": {
"version": "7.6.19",
"resolved": "https://registry.npmjs.org/@storybook/builder-manager/-/builder-manager-7.6.19.tgz",
"integrity": "sha512-Dt5OLh97xeWh4h2mk9uG0SbCxBKHPhIiHLHAKEIDzIZBdwUhuyncVNDPHW2NlXM+S7U0/iKs2tw05waqh2lHvg==",
"version": "7.6.20",
"resolved": "https://registry.npmjs.org/@storybook/builder-manager/-/builder-manager-7.6.20.tgz",
"integrity": "sha512-e2GzpjLaw6CM/XSmc4qJRzBF8GOoOyotyu3JrSPTYOt4RD8kjUsK4QlismQM1DQRu8i39aIexxmRbiJyD74xzQ==",
"dev": true,
"dependencies": {
"@fal-works/esbuild-plugin-global-externals": "^2.1.2",
"@storybook/core-common": "7.6.19",
"@storybook/manager": "7.6.19",
"@storybook/node-logger": "7.6.19",
"@storybook/core-common": "7.6.20",
"@storybook/manager": "7.6.20",
"@storybook/node-logger": "7.6.20",
"@types/ejs": "^3.1.1",
"@types/find-cache-dir": "^3.2.1",
"@yarnpkg/esbuild-plugin-pnp": "^3.0.0-rc.10",
@@ -6228,13 +6228,13 @@
}
},
"node_modules/@storybook/builder-manager/node_modules/@storybook/channels": {
"version": "7.6.19",
"resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.19.tgz",
"integrity": "sha512-2JGh+i95GwjtjqWqhtEh15jM5ifwbRGmXeFqkY7dpdHH50EEWafYHr2mg3opK3heVDwg0rJ/VBptkmshloXuvA==",
"version": "7.6.20",
"resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.20.tgz",
"integrity": "sha512-4hkgPSH6bJclB2OvLnkZOGZW1WptJs09mhQ6j6qLjgBZzL/ZdD6priWSd7iXrmPiN5TzUobkG4P4Dp7FjkiO7A==",
"dev": true,
"dependencies": {
"@storybook/client-logger": "7.6.19",
"@storybook/core-events": "7.6.19",
"@storybook/client-logger": "7.6.20",
"@storybook/core-events": "7.6.20",
"@storybook/global": "^5.0.0",
"qs": "^6.10.0",
"telejson": "^7.2.0",
@@ -6246,9 +6246,9 @@
}
},
"node_modules/@storybook/builder-manager/node_modules/@storybook/client-logger": {
"version": "7.6.19",
"resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.19.tgz",
"integrity": "sha512-oGzOxbmLmciSIfd5gsxDzPmX8DttWhoYdPKxjMuCuWLTO2TWpkCWp1FTUMWO72mm/6V/FswT/aqpJJBBvdZ3RQ==",
"version": "7.6.20",
"resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.20.tgz",
"integrity": "sha512-NwG0VIJQCmKrSaN5GBDFyQgTAHLNishUPLW1NrzqTDNAhfZUoef64rPQlinbopa0H4OXmlB+QxbQIb3ubeXmSQ==",
"dev": true,
"dependencies": {
"@storybook/global": "^5.0.0"
@@ -6259,14 +6259,14 @@
}
},
"node_modules/@storybook/builder-manager/node_modules/@storybook/core-common": {
"version": "7.6.19",
"resolved": "https://registry.npmjs.org/@storybook/core-common/-/core-common-7.6.19.tgz",
"integrity": "sha512-njwpGzFJrfbJr/AFxGP8KMrfPfxN85KOfSlxYnQwRm5Z0H1D/lT33LhEBf5m37gaGawHeG7KryxO6RvaioMt2Q==",
"version": "7.6.20",
"resolved": "https://registry.npmjs.org/@storybook/core-common/-/core-common-7.6.20.tgz",
"integrity": "sha512-8H1zPWPjcmeD4HbDm4FDD0WLsfAKGVr566IZ4hG+h3iWVW57II9JW9MLBtiR2LPSd8u7o0kw64lwRGmtCO1qAw==",
"dev": true,
"dependencies": {
"@storybook/core-events": "7.6.19",
"@storybook/node-logger": "7.6.19",
"@storybook/types": "7.6.19",
"@storybook/core-events": "7.6.20",
"@storybook/node-logger": "7.6.20",
"@storybook/types": "7.6.20",
"@types/find-cache-dir": "^3.2.1",
"@types/node": "^18.0.0",
"@types/node-fetch": "^2.6.4",
@@ -6294,9 +6294,9 @@
}
},
"node_modules/@storybook/builder-manager/node_modules/@storybook/core-events": {
"version": "7.6.19",
"resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.19.tgz",
"integrity": "sha512-K/W6Uvum0ocZSgjbi8hiotpe+wDEHDZlvN+KlPqdh9ae9xDK8aBNBq9IelCoqM+uKO1Zj+dDfSQds7CD781DJg==",
"version": "7.6.20",
"resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.20.tgz",
"integrity": "sha512-tlVDuVbDiNkvPDFAu+0ou3xBBYbx9zUURQz4G9fAq0ScgBOs/bpzcRrFb4mLpemUViBAd47tfZKdH4MAX45KVQ==",
"dev": true,
"dependencies": {
"ts-dedent": "^2.0.0"
@@ -6307,9 +6307,9 @@
}
},
"node_modules/@storybook/builder-manager/node_modules/@storybook/node-logger": {
"version": "7.6.19",
"resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-7.6.19.tgz",
"integrity": "sha512-2g29QC44Zl1jKY37DmQ0/dO7+VSKnGgPI/x0mwVwQffypSapxH3rwLLT5Q5XLHeFyD+fhRu5w9Cj4vTGynJgpA==",
"version": "7.6.20",
"resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-7.6.20.tgz",
"integrity": "sha512-l2i4qF1bscJkOplNffcRTsgQWYR7J51ewmizj5YrTM8BK6rslWT1RntgVJWB1RgPqvx6VsCz1gyP3yW1oKxvYw==",
"dev": true,
"funding": {
"type": "opencollective",
@@ -6317,12 +6317,12 @@
}
},
"node_modules/@storybook/builder-manager/node_modules/@storybook/types": {
"version": "7.6.19",
"resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.19.tgz",
"integrity": "sha512-DeGYrRPRMGTVfT7o2rEZtRzyLT2yKTI2exgpnxbwPWEFAduZCSfzBrcBXZ/nb5B0pjA9tUNWls1YzGkJGlkhpg==",
"version": "7.6.20",
"resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.20.tgz",
"integrity": "sha512-GncdY3x0LpbhmUAAJwXYtJDUQEwfF175gsjH0/fxPkxPoV7Sef9TM41jQLJW/5+6TnZoCZP/+aJZTJtq3ni23Q==",
"dev": true,
"dependencies": {
"@storybook/channels": "7.6.19",
"@storybook/channels": "7.6.20",
"@types/babel__core": "^7.0.0",
"@types/express": "^4.7.0",
"file-system-cache": "2.3.0"
@@ -6438,23 +6438,23 @@
}
},
"node_modules/@storybook/cli": {
"version": "7.6.19",
"resolved": "https://registry.npmjs.org/@storybook/cli/-/cli-7.6.19.tgz",
"integrity": "sha512-7OVy7nPgkLfgivv6/dmvoyU6pKl9EzWFk+g9izyQHiM/jS8jOiEyn6akG8Ebj6k5pWslo5lgiXUSW+cEEZUnqQ==",
"version": "7.6.20",
"resolved": "https://registry.npmjs.org/@storybook/cli/-/cli-7.6.20.tgz",
"integrity": "sha512-ZlP+BJyqg7HlnXf7ypjG2CKMI/KVOn03jFIiClItE/jQfgR6kRFgtjRU7uajh427HHfjv9DRiur8nBzuO7vapA==",
"dev": true,
"dependencies": {
"@babel/core": "^7.23.2",
"@babel/preset-env": "^7.23.2",
"@babel/types": "^7.23.0",
"@ndelangen/get-tarball": "^3.0.7",
"@storybook/codemod": "7.6.19",
"@storybook/core-common": "7.6.19",
"@storybook/core-events": "7.6.19",
"@storybook/core-server": "7.6.19",
"@storybook/csf-tools": "7.6.19",
"@storybook/node-logger": "7.6.19",
"@storybook/telemetry": "7.6.19",
"@storybook/types": "7.6.19",
"@storybook/codemod": "7.6.20",
"@storybook/core-common": "7.6.20",
"@storybook/core-events": "7.6.20",
"@storybook/core-server": "7.6.20",
"@storybook/csf-tools": "7.6.20",
"@storybook/node-logger": "7.6.20",
"@storybook/telemetry": "7.6.20",
"@storybook/types": "7.6.20",
"@types/semver": "^7.3.4",
"@yarnpkg/fslib": "2.10.3",
"@yarnpkg/libzip": "2.3.0",
@@ -6494,13 +6494,13 @@
}
},
"node_modules/@storybook/cli/node_modules/@storybook/channels": {
"version": "7.6.19",
"resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.19.tgz",
"integrity": "sha512-2JGh+i95GwjtjqWqhtEh15jM5ifwbRGmXeFqkY7dpdHH50EEWafYHr2mg3opK3heVDwg0rJ/VBptkmshloXuvA==",
"version": "7.6.20",
"resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.20.tgz",
"integrity": "sha512-4hkgPSH6bJclB2OvLnkZOGZW1WptJs09mhQ6j6qLjgBZzL/ZdD6priWSd7iXrmPiN5TzUobkG4P4Dp7FjkiO7A==",
"dev": true,
"dependencies": {
"@storybook/client-logger": "7.6.19",
"@storybook/core-events": "7.6.19",
"@storybook/client-logger": "7.6.20",
"@storybook/core-events": "7.6.20",
"@storybook/global": "^5.0.0",
"qs": "^6.10.0",
"telejson": "^7.2.0",
@@ -6512,9 +6512,9 @@
}
},
"node_modules/@storybook/cli/node_modules/@storybook/client-logger": {
"version": "7.6.19",
"resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.19.tgz",
"integrity": "sha512-oGzOxbmLmciSIfd5gsxDzPmX8DttWhoYdPKxjMuCuWLTO2TWpkCWp1FTUMWO72mm/6V/FswT/aqpJJBBvdZ3RQ==",
"version": "7.6.20",
"resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.20.tgz",
"integrity": "sha512-NwG0VIJQCmKrSaN5GBDFyQgTAHLNishUPLW1NrzqTDNAhfZUoef64rPQlinbopa0H4OXmlB+QxbQIb3ubeXmSQ==",
"dev": true,
"dependencies": {
"@storybook/global": "^5.0.0"
@@ -6525,14 +6525,14 @@
}
},
"node_modules/@storybook/cli/node_modules/@storybook/core-common": {
"version": "7.6.19",
"resolved": "https://registry.npmjs.org/@storybook/core-common/-/core-common-7.6.19.tgz",
"integrity": "sha512-njwpGzFJrfbJr/AFxGP8KMrfPfxN85KOfSlxYnQwRm5Z0H1D/lT33LhEBf5m37gaGawHeG7KryxO6RvaioMt2Q==",
"version": "7.6.20",
"resolved": "https://registry.npmjs.org/@storybook/core-common/-/core-common-7.6.20.tgz",
"integrity": "sha512-8H1zPWPjcmeD4HbDm4FDD0WLsfAKGVr566IZ4hG+h3iWVW57II9JW9MLBtiR2LPSd8u7o0kw64lwRGmtCO1qAw==",
"dev": true,
"dependencies": {
"@storybook/core-events": "7.6.19",
"@storybook/node-logger": "7.6.19",
"@storybook/types": "7.6.19",
"@storybook/core-events": "7.6.20",
"@storybook/node-logger": "7.6.20",
"@storybook/types": "7.6.20",
"@types/find-cache-dir": "^3.2.1",
"@types/node": "^18.0.0",
"@types/node-fetch": "^2.6.4",
@@ -6560,9 +6560,9 @@
}
},
"node_modules/@storybook/cli/node_modules/@storybook/core-events": {
"version": "7.6.19",
"resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.19.tgz",
"integrity": "sha512-K/W6Uvum0ocZSgjbi8hiotpe+wDEHDZlvN+KlPqdh9ae9xDK8aBNBq9IelCoqM+uKO1Zj+dDfSQds7CD781DJg==",
"version": "7.6.20",
"resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.20.tgz",
"integrity": "sha512-tlVDuVbDiNkvPDFAu+0ou3xBBYbx9zUURQz4G9fAq0ScgBOs/bpzcRrFb4mLpemUViBAd47tfZKdH4MAX45KVQ==",
"dev": true,
"dependencies": {
"ts-dedent": "^2.0.0"
@@ -6573,9 +6573,9 @@
}
},
"node_modules/@storybook/cli/node_modules/@storybook/csf-tools": {
"version": "7.6.19",
"resolved": "https://registry.npmjs.org/@storybook/csf-tools/-/csf-tools-7.6.19.tgz",
"integrity": "sha512-8Vzia3cHhDdGHuS3XKXJReCRxmfRq3vmTm/Te9yKZnPSAsC58CCKcMh8FNEFJ44vxYF9itKTkRutjGs+DprKLQ==",
"version": "7.6.20",
"resolved": "https://registry.npmjs.org/@storybook/csf-tools/-/csf-tools-7.6.20.tgz",
"integrity": "sha512-rwcwzCsAYh/m/WYcxBiEtLpIW5OH1ingxNdF/rK9mtGWhJxXRDV8acPkFrF8rtFWIVKoOCXu5USJYmc3f2gdYQ==",
"dev": true,
"dependencies": {
"@babel/generator": "^7.23.0",
@@ -6583,7 +6583,7 @@
"@babel/traverse": "^7.23.2",
"@babel/types": "^7.23.0",
"@storybook/csf": "^0.1.2",
"@storybook/types": "7.6.19",
"@storybook/types": "7.6.20",
"fs-extra": "^11.1.0",
"recast": "^0.23.1",
"ts-dedent": "^2.0.0"
@@ -6594,9 +6594,9 @@
}
},
"node_modules/@storybook/cli/node_modules/@storybook/node-logger": {
"version": "7.6.19",
"resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-7.6.19.tgz",
"integrity": "sha512-2g29QC44Zl1jKY37DmQ0/dO7+VSKnGgPI/x0mwVwQffypSapxH3rwLLT5Q5XLHeFyD+fhRu5w9Cj4vTGynJgpA==",
"version": "7.6.20",
"resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-7.6.20.tgz",
"integrity": "sha512-l2i4qF1bscJkOplNffcRTsgQWYR7J51ewmizj5YrTM8BK6rslWT1RntgVJWB1RgPqvx6VsCz1gyP3yW1oKxvYw==",
"dev": true,
"funding": {
"type": "opencollective",
@@ -6604,12 +6604,12 @@
}
},
"node_modules/@storybook/cli/node_modules/@storybook/types": {
"version": "7.6.19",
"resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.19.tgz",
"integrity": "sha512-DeGYrRPRMGTVfT7o2rEZtRzyLT2yKTI2exgpnxbwPWEFAduZCSfzBrcBXZ/nb5B0pjA9tUNWls1YzGkJGlkhpg==",
"version": "7.6.20",
"resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.20.tgz",
"integrity": "sha512-GncdY3x0LpbhmUAAJwXYtJDUQEwfF175gsjH0/fxPkxPoV7Sef9TM41jQLJW/5+6TnZoCZP/+aJZTJtq3ni23Q==",
"dev": true,
"dependencies": {
"@storybook/channels": "7.6.19",
"@storybook/channels": "7.6.20",
"@types/babel__core": "^7.0.0",
"@types/express": "^4.7.0",
"file-system-cache": "2.3.0"
@@ -6703,18 +6703,18 @@
}
},
"node_modules/@storybook/codemod": {
"version": "7.6.19",
"resolved": "https://registry.npmjs.org/@storybook/codemod/-/codemod-7.6.19.tgz",
"integrity": "sha512-bmHE0iEEgWZ65dXCmasd+GreChjPiWkXu2FEa0cJmNz/PqY12GsXGls4ke1TkNTj4gdSZnbtJxbclPZZnib2tQ==",
"version": "7.6.20",
"resolved": "https://registry.npmjs.org/@storybook/codemod/-/codemod-7.6.20.tgz",
"integrity": "sha512-8vmSsksO4XukNw0TmqylPmk7PxnfNfE21YsxFa7mnEBmEKQcZCQsNil4ZgWfG0IzdhTfhglAN4r++Ew0WE+PYA==",
"dev": true,
"dependencies": {
"@babel/core": "^7.23.2",
"@babel/preset-env": "^7.23.2",
"@babel/types": "^7.23.0",
"@storybook/csf": "^0.1.2",
"@storybook/csf-tools": "7.6.19",
"@storybook/node-logger": "7.6.19",
"@storybook/types": "7.6.19",
"@storybook/csf-tools": "7.6.20",
"@storybook/node-logger": "7.6.20",
"@storybook/types": "7.6.20",
"@types/cross-spawn": "^6.0.2",
"cross-spawn": "^7.0.3",
"globby": "^11.0.2",
@@ -6729,13 +6729,13 @@
}
},
"node_modules/@storybook/codemod/node_modules/@storybook/channels": {
"version": "7.6.19",
"resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.19.tgz",
"integrity": "sha512-2JGh+i95GwjtjqWqhtEh15jM5ifwbRGmXeFqkY7dpdHH50EEWafYHr2mg3opK3heVDwg0rJ/VBptkmshloXuvA==",
"version": "7.6.20",
"resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.20.tgz",
"integrity": "sha512-4hkgPSH6bJclB2OvLnkZOGZW1WptJs09mhQ6j6qLjgBZzL/ZdD6priWSd7iXrmPiN5TzUobkG4P4Dp7FjkiO7A==",
"dev": true,
"dependencies": {
"@storybook/client-logger": "7.6.19",
"@storybook/core-events": "7.6.19",
"@storybook/client-logger": "7.6.20",
"@storybook/core-events": "7.6.20",
"@storybook/global": "^5.0.0",
"qs": "^6.10.0",
"telejson": "^7.2.0",
@@ -6747,9 +6747,9 @@
}
},
"node_modules/@storybook/codemod/node_modules/@storybook/client-logger": {
"version": "7.6.19",
"resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.19.tgz",
"integrity": "sha512-oGzOxbmLmciSIfd5gsxDzPmX8DttWhoYdPKxjMuCuWLTO2TWpkCWp1FTUMWO72mm/6V/FswT/aqpJJBBvdZ3RQ==",
"version": "7.6.20",
"resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.20.tgz",
"integrity": "sha512-NwG0VIJQCmKrSaN5GBDFyQgTAHLNishUPLW1NrzqTDNAhfZUoef64rPQlinbopa0H4OXmlB+QxbQIb3ubeXmSQ==",
"dev": true,
"dependencies": {
"@storybook/global": "^5.0.0"
@@ -6760,9 +6760,9 @@
}
},
"node_modules/@storybook/codemod/node_modules/@storybook/core-events": {
"version": "7.6.19",
"resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.19.tgz",
"integrity": "sha512-K/W6Uvum0ocZSgjbi8hiotpe+wDEHDZlvN+KlPqdh9ae9xDK8aBNBq9IelCoqM+uKO1Zj+dDfSQds7CD781DJg==",
"version": "7.6.20",
"resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.20.tgz",
"integrity": "sha512-tlVDuVbDiNkvPDFAu+0ou3xBBYbx9zUURQz4G9fAq0ScgBOs/bpzcRrFb4mLpemUViBAd47tfZKdH4MAX45KVQ==",
"dev": true,
"dependencies": {
"ts-dedent": "^2.0.0"
@@ -6773,9 +6773,9 @@
}
},
"node_modules/@storybook/codemod/node_modules/@storybook/csf-tools": {
"version": "7.6.19",
"resolved": "https://registry.npmjs.org/@storybook/csf-tools/-/csf-tools-7.6.19.tgz",
"integrity": "sha512-8Vzia3cHhDdGHuS3XKXJReCRxmfRq3vmTm/Te9yKZnPSAsC58CCKcMh8FNEFJ44vxYF9itKTkRutjGs+DprKLQ==",
"version": "7.6.20",
"resolved": "https://registry.npmjs.org/@storybook/csf-tools/-/csf-tools-7.6.20.tgz",
"integrity": "sha512-rwcwzCsAYh/m/WYcxBiEtLpIW5OH1ingxNdF/rK9mtGWhJxXRDV8acPkFrF8rtFWIVKoOCXu5USJYmc3f2gdYQ==",
"dev": true,
"dependencies": {
"@babel/generator": "^7.23.0",
@@ -6783,7 +6783,7 @@
"@babel/traverse": "^7.23.2",
"@babel/types": "^7.23.0",
"@storybook/csf": "^0.1.2",
"@storybook/types": "7.6.19",
"@storybook/types": "7.6.20",
"fs-extra": "^11.1.0",
"recast": "^0.23.1",
"ts-dedent": "^2.0.0"
@@ -6794,9 +6794,9 @@
}
},
"node_modules/@storybook/codemod/node_modules/@storybook/node-logger": {
"version": "7.6.19",
"resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-7.6.19.tgz",
"integrity": "sha512-2g29QC44Zl1jKY37DmQ0/dO7+VSKnGgPI/x0mwVwQffypSapxH3rwLLT5Q5XLHeFyD+fhRu5w9Cj4vTGynJgpA==",
"version": "7.6.20",
"resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-7.6.20.tgz",
"integrity": "sha512-l2i4qF1bscJkOplNffcRTsgQWYR7J51ewmizj5YrTM8BK6rslWT1RntgVJWB1RgPqvx6VsCz1gyP3yW1oKxvYw==",
"dev": true,
"funding": {
"type": "opencollective",
@@ -6804,12 +6804,12 @@
}
},
"node_modules/@storybook/codemod/node_modules/@storybook/types": {
"version": "7.6.19",
"resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.19.tgz",
"integrity": "sha512-DeGYrRPRMGTVfT7o2rEZtRzyLT2yKTI2exgpnxbwPWEFAduZCSfzBrcBXZ/nb5B0pjA9tUNWls1YzGkJGlkhpg==",
"version": "7.6.20",
"resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.20.tgz",
"integrity": "sha512-GncdY3x0LpbhmUAAJwXYtJDUQEwfF175gsjH0/fxPkxPoV7Sef9TM41jQLJW/5+6TnZoCZP/+aJZTJtq3ni23Q==",
"dev": true,
"dependencies": {
"@storybook/channels": "7.6.19",
"@storybook/channels": "7.6.20",
"@types/babel__core": "^7.0.0",
"@types/express": "^4.7.0",
"file-system-cache": "2.3.0"
@@ -7063,26 +7063,26 @@
}
},
"node_modules/@storybook/core-server": {
"version": "7.6.19",
"resolved": "https://registry.npmjs.org/@storybook/core-server/-/core-server-7.6.19.tgz",
"integrity": "sha512-7mKL73Wv5R2bEl0kJ6QJ9bOu5YY53Idu24QgvTnUdNsQazp2yUONBNwHIrNDnNEXm8SfCi4Mc9o0mmNRMIoiRA==",
"version": "7.6.20",
"resolved": "https://registry.npmjs.org/@storybook/core-server/-/core-server-7.6.20.tgz",
"integrity": "sha512-qC5BdbqqwMLTdCwMKZ1Hbc3+3AaxHYWLiJaXL9e8s8nJw89xV8c8l30QpbJOGvcDmsgY6UTtXYaJ96OsTr7MrA==",
"dev": true,
"dependencies": {
"@aw-web-design/x-default-browser": "1.4.126",
"@discoveryjs/json-ext": "^0.5.3",
"@storybook/builder-manager": "7.6.19",
"@storybook/channels": "7.6.19",
"@storybook/core-common": "7.6.19",
"@storybook/core-events": "7.6.19",
"@storybook/builder-manager": "7.6.20",
"@storybook/channels": "7.6.20",
"@storybook/core-common": "7.6.20",
"@storybook/core-events": "7.6.20",
"@storybook/csf": "^0.1.2",
"@storybook/csf-tools": "7.6.19",
"@storybook/csf-tools": "7.6.20",
"@storybook/docs-mdx": "^0.1.0",
"@storybook/global": "^5.0.0",
"@storybook/manager": "7.6.19",
"@storybook/node-logger": "7.6.19",
"@storybook/preview-api": "7.6.19",
"@storybook/telemetry": "7.6.19",
"@storybook/types": "7.6.19",
"@storybook/manager": "7.6.20",
"@storybook/node-logger": "7.6.20",
"@storybook/preview-api": "7.6.20",
"@storybook/telemetry": "7.6.20",
"@storybook/types": "7.6.20",
"@types/detect-port": "^1.3.0",
"@types/node": "^18.0.0",
"@types/pretty-hrtime": "^1.0.0",
@@ -7095,7 +7095,6 @@
"express": "^4.17.3",
"fs-extra": "^11.1.0",
"globby": "^11.0.2",
"ip": "^2.0.1",
"lodash": "^4.17.21",
"open": "^8.4.0",
"pretty-hrtime": "^1.0.3",
@@ -7116,13 +7115,13 @@
}
},
"node_modules/@storybook/core-server/node_modules/@storybook/channels": {
"version": "7.6.19",
"resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.19.tgz",
"integrity": "sha512-2JGh+i95GwjtjqWqhtEh15jM5ifwbRGmXeFqkY7dpdHH50EEWafYHr2mg3opK3heVDwg0rJ/VBptkmshloXuvA==",
"version": "7.6.20",
"resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.20.tgz",
"integrity": "sha512-4hkgPSH6bJclB2OvLnkZOGZW1WptJs09mhQ6j6qLjgBZzL/ZdD6priWSd7iXrmPiN5TzUobkG4P4Dp7FjkiO7A==",
"dev": true,
"dependencies": {
"@storybook/client-logger": "7.6.19",
"@storybook/core-events": "7.6.19",
"@storybook/client-logger": "7.6.20",
"@storybook/core-events": "7.6.20",
"@storybook/global": "^5.0.0",
"qs": "^6.10.0",
"telejson": "^7.2.0",
@@ -7134,9 +7133,9 @@
}
},
"node_modules/@storybook/core-server/node_modules/@storybook/client-logger": {
"version": "7.6.19",
"resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.19.tgz",
"integrity": "sha512-oGzOxbmLmciSIfd5gsxDzPmX8DttWhoYdPKxjMuCuWLTO2TWpkCWp1FTUMWO72mm/6V/FswT/aqpJJBBvdZ3RQ==",
"version": "7.6.20",
"resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.20.tgz",
"integrity": "sha512-NwG0VIJQCmKrSaN5GBDFyQgTAHLNishUPLW1NrzqTDNAhfZUoef64rPQlinbopa0H4OXmlB+QxbQIb3ubeXmSQ==",
"dev": true,
"dependencies": {
"@storybook/global": "^5.0.0"
@@ -7147,14 +7146,14 @@
}
},
"node_modules/@storybook/core-server/node_modules/@storybook/core-common": {
"version": "7.6.19",
"resolved": "https://registry.npmjs.org/@storybook/core-common/-/core-common-7.6.19.tgz",
"integrity": "sha512-njwpGzFJrfbJr/AFxGP8KMrfPfxN85KOfSlxYnQwRm5Z0H1D/lT33LhEBf5m37gaGawHeG7KryxO6RvaioMt2Q==",
"version": "7.6.20",
"resolved": "https://registry.npmjs.org/@storybook/core-common/-/core-common-7.6.20.tgz",
"integrity": "sha512-8H1zPWPjcmeD4HbDm4FDD0WLsfAKGVr566IZ4hG+h3iWVW57II9JW9MLBtiR2LPSd8u7o0kw64lwRGmtCO1qAw==",
"dev": true,
"dependencies": {
"@storybook/core-events": "7.6.19",
"@storybook/node-logger": "7.6.19",
"@storybook/types": "7.6.19",
"@storybook/core-events": "7.6.20",
"@storybook/node-logger": "7.6.20",
"@storybook/types": "7.6.20",
"@types/find-cache-dir": "^3.2.1",
"@types/node": "^18.0.0",
"@types/node-fetch": "^2.6.4",
@@ -7182,9 +7181,9 @@
}
},
"node_modules/@storybook/core-server/node_modules/@storybook/core-events": {
"version": "7.6.19",
"resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.19.tgz",
"integrity": "sha512-K/W6Uvum0ocZSgjbi8hiotpe+wDEHDZlvN+KlPqdh9ae9xDK8aBNBq9IelCoqM+uKO1Zj+dDfSQds7CD781DJg==",
"version": "7.6.20",
"resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.20.tgz",
"integrity": "sha512-tlVDuVbDiNkvPDFAu+0ou3xBBYbx9zUURQz4G9fAq0ScgBOs/bpzcRrFb4mLpemUViBAd47tfZKdH4MAX45KVQ==",
"dev": true,
"dependencies": {
"ts-dedent": "^2.0.0"
@@ -7195,9 +7194,9 @@
}
},
"node_modules/@storybook/core-server/node_modules/@storybook/csf-tools": {
"version": "7.6.19",
"resolved": "https://registry.npmjs.org/@storybook/csf-tools/-/csf-tools-7.6.19.tgz",
"integrity": "sha512-8Vzia3cHhDdGHuS3XKXJReCRxmfRq3vmTm/Te9yKZnPSAsC58CCKcMh8FNEFJ44vxYF9itKTkRutjGs+DprKLQ==",
"version": "7.6.20",
"resolved": "https://registry.npmjs.org/@storybook/csf-tools/-/csf-tools-7.6.20.tgz",
"integrity": "sha512-rwcwzCsAYh/m/WYcxBiEtLpIW5OH1ingxNdF/rK9mtGWhJxXRDV8acPkFrF8rtFWIVKoOCXu5USJYmc3f2gdYQ==",
"dev": true,
"dependencies": {
"@babel/generator": "^7.23.0",
@@ -7205,7 +7204,7 @@
"@babel/traverse": "^7.23.2",
"@babel/types": "^7.23.0",
"@storybook/csf": "^0.1.2",
"@storybook/types": "7.6.19",
"@storybook/types": "7.6.20",
"fs-extra": "^11.1.0",
"recast": "^0.23.1",
"ts-dedent": "^2.0.0"
@@ -7216,9 +7215,9 @@
}
},
"node_modules/@storybook/core-server/node_modules/@storybook/node-logger": {
"version": "7.6.19",
"resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-7.6.19.tgz",
"integrity": "sha512-2g29QC44Zl1jKY37DmQ0/dO7+VSKnGgPI/x0mwVwQffypSapxH3rwLLT5Q5XLHeFyD+fhRu5w9Cj4vTGynJgpA==",
"version": "7.6.20",
"resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-7.6.20.tgz",
"integrity": "sha512-l2i4qF1bscJkOplNffcRTsgQWYR7J51ewmizj5YrTM8BK6rslWT1RntgVJWB1RgPqvx6VsCz1gyP3yW1oKxvYw==",
"dev": true,
"funding": {
"type": "opencollective",
@@ -7226,17 +7225,17 @@
}
},
"node_modules/@storybook/core-server/node_modules/@storybook/preview-api": {
"version": "7.6.19",
"resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-7.6.19.tgz",
"integrity": "sha512-04hdMSQucroJT4dBjQzRd7ZwH2hij8yx2nm5qd4HYGkd1ORkvlH6GOLph4XewNJl5Um3xfzFQzBhvkqvG0WaCQ==",
"version": "7.6.20",
"resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-7.6.20.tgz",
"integrity": "sha512-3ic2m9LDZEPwZk02wIhNc3n3rNvbi7VDKn52hDXfAxnL5EYm7yDICAkaWcVaTfblru2zn0EDJt7ROpthscTW5w==",
"dev": true,
"dependencies": {
"@storybook/channels": "7.6.19",
"@storybook/client-logger": "7.6.19",
"@storybook/core-events": "7.6.19",
"@storybook/channels": "7.6.20",
"@storybook/client-logger": "7.6.20",
"@storybook/core-events": "7.6.20",
"@storybook/csf": "^0.1.2",
"@storybook/global": "^5.0.0",
"@storybook/types": "7.6.19",
"@storybook/types": "7.6.20",
"@types/qs": "^6.9.5",
"dequal": "^2.0.2",
"lodash": "^4.17.21",
@@ -7252,12 +7251,12 @@
}
},
"node_modules/@storybook/core-server/node_modules/@storybook/types": {
"version": "7.6.19",
"resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.19.tgz",
"integrity": "sha512-DeGYrRPRMGTVfT7o2rEZtRzyLT2yKTI2exgpnxbwPWEFAduZCSfzBrcBXZ/nb5B0pjA9tUNWls1YzGkJGlkhpg==",
"version": "7.6.20",
"resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.20.tgz",
"integrity": "sha512-GncdY3x0LpbhmUAAJwXYtJDUQEwfF175gsjH0/fxPkxPoV7Sef9TM41jQLJW/5+6TnZoCZP/+aJZTJtq3ni23Q==",
"dev": true,
"dependencies": {
"@storybook/channels": "7.6.19",
"@storybook/channels": "7.6.20",
"@types/babel__core": "^7.0.0",
"@types/express": "^4.7.0",
"file-system-cache": "2.3.0"
@@ -7372,9 +7371,9 @@
"dev": true
},
"node_modules/@storybook/manager": {
"version": "7.6.19",
"resolved": "https://registry.npmjs.org/@storybook/manager/-/manager-7.6.19.tgz",
"integrity": "sha512-fZWQcf59x4P0iiBhrL74PZrqKJAPuk9sWjP8BIkGbf8wTZtUunbY5Sv4225fOL4NLJbuX9/RYLUPoxQ3nucGHA==",
"version": "7.6.20",
"resolved": "https://registry.npmjs.org/@storybook/manager/-/manager-7.6.20.tgz",
"integrity": "sha512-0Cf6WN0t7yEG2DR29tN5j+i7H/TH5EfPppg9h9/KiQSoFHk+6KLoy2p5do94acFU+Ro4+zzxvdCGbcYGKuArpg==",
"dev": true,
"funding": {
"type": "opencollective",
@@ -7810,14 +7809,14 @@
}
},
"node_modules/@storybook/telemetry": {
"version": "7.6.19",
"resolved": "https://registry.npmjs.org/@storybook/telemetry/-/telemetry-7.6.19.tgz",
"integrity": "sha512-rA5xum4I36M57iiD3uzmW0MOdpl0vEpHWBSAa5hK0a0ALPeY9TgAsQlI/0dSyNYJ/K7aczEEN6d4qm1NC4u10A==",
"version": "7.6.20",
"resolved": "https://registry.npmjs.org/@storybook/telemetry/-/telemetry-7.6.20.tgz",
"integrity": "sha512-dmAOCWmOscYN6aMbhCMmszQjoycg7tUPRVy2kTaWg6qX10wtMrvEtBV29W4eMvqdsoRj5kcvoNbzRdYcWBUOHQ==",
"dev": true,
"dependencies": {
"@storybook/client-logger": "7.6.19",
"@storybook/core-common": "7.6.19",
"@storybook/csf-tools": "7.6.19",
"@storybook/client-logger": "7.6.20",
"@storybook/core-common": "7.6.20",
"@storybook/csf-tools": "7.6.20",
"chalk": "^4.1.0",
"detect-package-manager": "^2.0.1",
"fetch-retry": "^5.0.2",
@@ -7830,13 +7829,13 @@
}
},
"node_modules/@storybook/telemetry/node_modules/@storybook/channels": {
"version": "7.6.19",
"resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.19.tgz",
"integrity": "sha512-2JGh+i95GwjtjqWqhtEh15jM5ifwbRGmXeFqkY7dpdHH50EEWafYHr2mg3opK3heVDwg0rJ/VBptkmshloXuvA==",
"version": "7.6.20",
"resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.20.tgz",
"integrity": "sha512-4hkgPSH6bJclB2OvLnkZOGZW1WptJs09mhQ6j6qLjgBZzL/ZdD6priWSd7iXrmPiN5TzUobkG4P4Dp7FjkiO7A==",
"dev": true,
"dependencies": {
"@storybook/client-logger": "7.6.19",
"@storybook/core-events": "7.6.19",
"@storybook/client-logger": "7.6.20",
"@storybook/core-events": "7.6.20",
"@storybook/global": "^5.0.0",
"qs": "^6.10.0",
"telejson": "^7.2.0",
@@ -7848,9 +7847,9 @@
}
},
"node_modules/@storybook/telemetry/node_modules/@storybook/client-logger": {
"version": "7.6.19",
"resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.19.tgz",
"integrity": "sha512-oGzOxbmLmciSIfd5gsxDzPmX8DttWhoYdPKxjMuCuWLTO2TWpkCWp1FTUMWO72mm/6V/FswT/aqpJJBBvdZ3RQ==",
"version": "7.6.20",
"resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.20.tgz",
"integrity": "sha512-NwG0VIJQCmKrSaN5GBDFyQgTAHLNishUPLW1NrzqTDNAhfZUoef64rPQlinbopa0H4OXmlB+QxbQIb3ubeXmSQ==",
"dev": true,
"dependencies": {
"@storybook/global": "^5.0.0"
@@ -7861,14 +7860,14 @@
}
},
"node_modules/@storybook/telemetry/node_modules/@storybook/core-common": {
"version": "7.6.19",
"resolved": "https://registry.npmjs.org/@storybook/core-common/-/core-common-7.6.19.tgz",
"integrity": "sha512-njwpGzFJrfbJr/AFxGP8KMrfPfxN85KOfSlxYnQwRm5Z0H1D/lT33LhEBf5m37gaGawHeG7KryxO6RvaioMt2Q==",
"version": "7.6.20",
"resolved": "https://registry.npmjs.org/@storybook/core-common/-/core-common-7.6.20.tgz",
"integrity": "sha512-8H1zPWPjcmeD4HbDm4FDD0WLsfAKGVr566IZ4hG+h3iWVW57II9JW9MLBtiR2LPSd8u7o0kw64lwRGmtCO1qAw==",
"dev": true,
"dependencies": {
"@storybook/core-events": "7.6.19",
"@storybook/node-logger": "7.6.19",
"@storybook/types": "7.6.19",
"@storybook/core-events": "7.6.20",
"@storybook/node-logger": "7.6.20",
"@storybook/types": "7.6.20",
"@types/find-cache-dir": "^3.2.1",
"@types/node": "^18.0.0",
"@types/node-fetch": "^2.6.4",
@@ -7896,9 +7895,9 @@
}
},
"node_modules/@storybook/telemetry/node_modules/@storybook/core-events": {
"version": "7.6.19",
"resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.19.tgz",
"integrity": "sha512-K/W6Uvum0ocZSgjbi8hiotpe+wDEHDZlvN+KlPqdh9ae9xDK8aBNBq9IelCoqM+uKO1Zj+dDfSQds7CD781DJg==",
"version": "7.6.20",
"resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.20.tgz",
"integrity": "sha512-tlVDuVbDiNkvPDFAu+0ou3xBBYbx9zUURQz4G9fAq0ScgBOs/bpzcRrFb4mLpemUViBAd47tfZKdH4MAX45KVQ==",
"dev": true,
"dependencies": {
"ts-dedent": "^2.0.0"
@@ -7909,9 +7908,9 @@
}
},
"node_modules/@storybook/telemetry/node_modules/@storybook/csf-tools": {
"version": "7.6.19",
"resolved": "https://registry.npmjs.org/@storybook/csf-tools/-/csf-tools-7.6.19.tgz",
"integrity": "sha512-8Vzia3cHhDdGHuS3XKXJReCRxmfRq3vmTm/Te9yKZnPSAsC58CCKcMh8FNEFJ44vxYF9itKTkRutjGs+DprKLQ==",
"version": "7.6.20",
"resolved": "https://registry.npmjs.org/@storybook/csf-tools/-/csf-tools-7.6.20.tgz",
"integrity": "sha512-rwcwzCsAYh/m/WYcxBiEtLpIW5OH1ingxNdF/rK9mtGWhJxXRDV8acPkFrF8rtFWIVKoOCXu5USJYmc3f2gdYQ==",
"dev": true,
"dependencies": {
"@babel/generator": "^7.23.0",
@@ -7919,7 +7918,7 @@
"@babel/traverse": "^7.23.2",
"@babel/types": "^7.23.0",
"@storybook/csf": "^0.1.2",
"@storybook/types": "7.6.19",
"@storybook/types": "7.6.20",
"fs-extra": "^11.1.0",
"recast": "^0.23.1",
"ts-dedent": "^2.0.0"
@@ -7930,9 +7929,9 @@
}
},
"node_modules/@storybook/telemetry/node_modules/@storybook/node-logger": {
"version": "7.6.19",
"resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-7.6.19.tgz",
"integrity": "sha512-2g29QC44Zl1jKY37DmQ0/dO7+VSKnGgPI/x0mwVwQffypSapxH3rwLLT5Q5XLHeFyD+fhRu5w9Cj4vTGynJgpA==",
"version": "7.6.20",
"resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-7.6.20.tgz",
"integrity": "sha512-l2i4qF1bscJkOplNffcRTsgQWYR7J51ewmizj5YrTM8BK6rslWT1RntgVJWB1RgPqvx6VsCz1gyP3yW1oKxvYw==",
"dev": true,
"funding": {
"type": "opencollective",
@@ -7940,12 +7939,12 @@
}
},
"node_modules/@storybook/telemetry/node_modules/@storybook/types": {
"version": "7.6.19",
"resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.19.tgz",
"integrity": "sha512-DeGYrRPRMGTVfT7o2rEZtRzyLT2yKTI2exgpnxbwPWEFAduZCSfzBrcBXZ/nb5B0pjA9tUNWls1YzGkJGlkhpg==",
"version": "7.6.20",
"resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.20.tgz",
"integrity": "sha512-GncdY3x0LpbhmUAAJwXYtJDUQEwfF175gsjH0/fxPkxPoV7Sef9TM41jQLJW/5+6TnZoCZP/+aJZTJtq3ni23Q==",
"dev": true,
"dependencies": {
"@storybook/channels": "7.6.19",
"@storybook/channels": "7.6.20",
"@types/babel__core": "^7.0.0",
"@types/express": "^4.7.0",
"file-system-cache": "2.3.0"
@@ -11451,6 +11450,12 @@
"safe-buffer": "~5.1.0"
}
},
"node_modules/confbox": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.7.tgz",
"integrity": "sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==",
"dev": true
},
"node_modules/confusing-browser-globals": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz",
@@ -14389,9 +14394,9 @@
"dev": true
},
"node_modules/flow-parser": {
"version": "0.237.2",
"resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.237.2.tgz",
"integrity": "sha512-mvI/kdfr3l1waaPbThPA8dJa77nHXrfZIun+SWvFwSwDjmeByU7mGJGRmv1+7guU6ccyLV8e1lqZA1lD4iMGnQ==",
"version": "0.239.1",
"resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.239.1.tgz",
"integrity": "sha512-topOrETNxJ6T2gAnQiWqAlzGPj8uI2wtmNOlDIMNB+qyvGJZ6R++STbUOTAYmvPhOMz2gXnXPH0hOvURYmrBow==",
"dev": true,
"engines": {
"node": ">=0.4.0"
@@ -18147,6 +18152,30 @@
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"dev": true
},
"node_modules/mlly": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.1.tgz",
"integrity": "sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==",
"dev": true,
"dependencies": {
"acorn": "^8.11.3",
"pathe": "^1.1.2",
"pkg-types": "^1.1.1",
"ufo": "^1.5.3"
}
},
"node_modules/mlly/node_modules/acorn": {
"version": "8.12.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
"integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
"dev": true,
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/mri": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
@@ -18621,16 +18650,17 @@
}
},
"node_modules/nypm": {
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.3.8.tgz",
"integrity": "sha512-IGWlC6So2xv6V4cIDmoV0SwwWx7zLG086gyqkyumteH2fIgCAM4nDVFB2iDRszDvmdSVW9xb1N+2KjQ6C7d4og==",
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.3.9.tgz",
"integrity": "sha512-BI2SdqqTHg2d4wJh8P9A1W+bslg33vOE9IZDY6eR2QC+Pu1iNBVZUqczrd43rJb+fMzHU7ltAYKsEFY/kHMFcw==",
"dev": true,
"dependencies": {
"citty": "^0.1.6",
"consola": "^3.2.3",
"execa": "^8.0.1",
"pathe": "^1.1.2",
"ufo": "^1.4.0"
"pkg-types": "^1.1.1",
"ufo": "^1.5.3"
},
"bin": {
"nypm": "dist/cli.mjs"
@@ -19405,6 +19435,17 @@
"node": ">=10"
}
},
"node_modules/pkg-types": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.1.3.tgz",
"integrity": "sha512-+JrgthZG6m3ckicaOB74TwQ+tBWsFl3qVQg7mN8ulwSOElJ7gBhKzj2VkCPnZ4NlF6kEquYU+RIYNVAvzd54UA==",
"dev": true,
"dependencies": {
"confbox": "^0.1.7",
"mlly": "^1.7.1",
"pathe": "^1.1.2"
}
},
"node_modules/pnp-webpack-plugin": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/pnp-webpack-plugin/-/pnp-webpack-plugin-1.7.0.tgz",
@@ -20265,9 +20306,9 @@
}
},
"node_modules/puppeteer-core/node_modules/ws": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz",
"integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==",
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz",
"integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==",
"dev": true,
"dependencies": {
"async-limiter": "~1.0.0"
@@ -22420,12 +22461,12 @@
"dev": true
},
"node_modules/storybook": {
"version": "7.6.19",
"resolved": "https://registry.npmjs.org/storybook/-/storybook-7.6.19.tgz",
"integrity": "sha512-xWD1C4vD/4KMffCrBBrUpsLUO/9uNpm8BVW8+Vcb30gkQDfficZ0oziWkmLexpT53VSioa24iazGXMwBqllYjQ==",
"version": "7.6.20",
"resolved": "https://registry.npmjs.org/storybook/-/storybook-7.6.20.tgz",
"integrity": "sha512-Wt04pPTO71pwmRmsgkyZhNo4Bvdb/1pBAMsIFb9nQLykEdzzpXjvingxFFvdOG4nIowzwgxD+CLlyRqVJqnATw==",
"dev": true,
"dependencies": {
"@storybook/cli": "7.6.19"
"@storybook/cli": "7.6.20"
},
"bin": {
"sb": "index.js",
@@ -24661,9 +24702,9 @@
}
},
"node_modules/ws": {
"version": "8.17.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz",
"integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==",
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
"dev": true,
"engines": {
"node": ">=10.0.0"

View File

@@ -147,7 +147,7 @@
"postcss": "^8.4.14",
"prettier": "^2.8.3",
"prettier-plugin-tailwindcss": "^0.2.2",
"storybook": "^7.5.2",
"storybook": "^7.6.20",
"storybook-dark-mode": "^3.0.0",
"tailwindcss": "3.2",
"typescript": "^4.9.3"

View File

@@ -39,4 +39,5 @@ export type SubscriptionPlan = {
trial_end: number | null;
has_used_trial: boolean;
caCrl: boolean;
instanceUserManagement: boolean;
};

View File

@@ -71,6 +71,7 @@ export const IdentityProjectRow = ({
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<Tooltip content="Remove">
<IconButton
colorSchema="danger"
ariaLabel="copy icon"
variant="plain"
className="group relative"

View File

@@ -3,16 +3,10 @@ import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { withPermission } from "@app/hoc";
import {
OrgGroupsTab,
OrgIdentityTab,
OrgMembersTab,
OrgRoleTabSection
} from "./components";
import { OrgIdentityTab, OrgMembersTab, OrgRoleTabSection } from "./components";
enum TabSections {
Member = "members",
Groups = "groups",
Roles = "roles",
Identities = "identities"
}
@@ -25,8 +19,7 @@ export const MembersPage = withPermission(
<p className="mr-4 mb-4 text-3xl font-semibold text-white">Organization Access Control</p>
<Tabs defaultValue={TabSections.Member}>
<TabList>
<Tab value={TabSections.Member}>People</Tab>
<Tab value={TabSections.Groups}>Groups</Tab>
<Tab value={TabSections.Member}>Users</Tab>
<Tab value={TabSections.Identities}>
<div className="flex items-center">
<p>Machine Identities</p>
@@ -37,9 +30,6 @@ export const MembersPage = withPermission(
<TabPanel value={TabSections.Member}>
<OrgMembersTab />
</TabPanel>
<TabPanel value={TabSections.Groups}>
<OrgGroupsTab />
</TabPanel>
<TabPanel value={TabSections.Identities}>
<OrgIdentityTab />
</TabPanel>

View File

@@ -3,16 +3,8 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Button,
DeleteActionModal,
UpgradePlanModal
} from "@app/components/v2";
import {
OrgPermissionActions,
OrgPermissionSubjects,
useSubscription
} from "@app/context";
import { Button, DeleteActionModal, UpgradePlanModal } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context";
import { useDeleteGroup } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
@@ -21,100 +13,88 @@ import { OrgGroupModal } from "./OrgGroupModal";
import { OrgGroupsTable } from "./OrgGroupsTable";
export const OrgGroupsSection = () => {
const { subscription } = useSubscription();
const { mutateAsync: deleteMutateAsync } = useDeleteGroup();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"group",
"groupMembers",
"deleteGroup",
"upgradePlan"
] as const);
const handleAddGroupModal = () => {
if (!subscription?.groups) {
handlePopUpOpen("upgradePlan", {
description: "You can manage users more efficiently with groups if you upgrade your Infisical plan."
});
} else {
handlePopUpOpen("group");
}
const { subscription } = useSubscription();
const { mutateAsync: deleteMutateAsync } = useDeleteGroup();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"group",
"groupMembers",
"deleteGroup",
"upgradePlan"
] as const);
const handleAddGroupModal = () => {
if (!subscription?.groups) {
handlePopUpOpen("upgradePlan", {
description:
"You can manage users more efficiently with groups if you upgrade your Infisical plan."
});
} else {
handlePopUpOpen("group");
}
const onDeleteGroupSubmit = async ({
name,
};
const onDeleteGroupSubmit = async ({ name, slug }: { name: string; slug: string }) => {
try {
await deleteMutateAsync({
slug
}: {
name: string;
slug: string;
}) => {
try {
await deleteMutateAsync({
slug
});
createNotification({
text: `Successfully deleted the group named ${name}`,
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: `Failed to delete the group named ${name}`,
type: "error"
});
}
handlePopUpClose("deleteGroup");
});
createNotification({
text: `Successfully deleted the group named ${name}`,
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: `Failed to delete the group named ${name}`,
type: "error"
});
}
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Groups</p>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Groups}>
{(isAllowed) => (
<Button
colorSchema="primary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handleAddGroupModal()}
isDisabled={!isAllowed}
>
Create Group
</Button>
)}
</OrgPermissionCan>
</div>
<OrgGroupsTable
handlePopUpOpen={handlePopUpOpen}
/>
<OrgGroupModal
popUp={popUp}
handlePopUpClose={handlePopUpClose}
handlePopUpToggle={handlePopUpToggle}
/>
<OrgGroupMembersModal
popUp={popUp}
handlePopUpToggle={handlePopUpToggle}
/>
<DeleteActionModal
isOpen={popUp.deleteGroup.isOpen}
title={`Are you sure want to delete the group named ${
(popUp?.deleteGroup?.data as { name: string })?.name || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteGroup", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
onDeleteGroupSubmit(
(popUp?.deleteGroup?.data as { name: string; slug: string })
)
}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={(popUp.upgradePlan?.data as { description: string })?.description}
/>
</div>
);
}
handlePopUpClose("deleteGroup");
};
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">User Groups</p>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Groups}>
{(isAllowed) => (
<Button
colorSchema="primary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handleAddGroupModal()}
isDisabled={!isAllowed}
>
Create Group
</Button>
)}
</OrgPermissionCan>
</div>
<OrgGroupsTable handlePopUpOpen={handlePopUpOpen} />
<OrgGroupModal
popUp={popUp}
handlePopUpClose={handlePopUpClose}
handlePopUpToggle={handlePopUpToggle}
/>
<OrgGroupMembersModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deleteGroup.isOpen}
title={`Are you sure want to delete the group named ${
(popUp?.deleteGroup?.data as { name: string })?.name || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteGroup", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
onDeleteGroupSubmit(popUp?.deleteGroup?.data as { name: string; slug: string })
}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={(popUp.upgradePlan?.data as { description: string })?.description}
/>
</div>
);
};

View File

@@ -1,12 +1,16 @@
import { useState } from "react";
import { faMagnifyingGlass, faPencil, faUsers, faXmark } from "@fortawesome/free-solid-svg-icons";
import { faEllipsis,faMagnifyingGlass, faUsers } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
EmptyState,
IconButton,
Input,
Select,
SelectItem,
@@ -17,218 +21,200 @@ import {
Td,
Th,
THead,
Tooltip,
Tr
} from "@app/components/v2";
import {
OrgPermissionActions,
OrgPermissionSubjects,
useOrganization} from "@app/context";
import {
useGetOrganizationGroups,
useGetOrgRoles,
useUpdateGroup
} from "@app/hooks/api";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useGetOrganizationGroups, useGetOrgRoles, useUpdateGroup } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
handlePopUpOpen: (
popUpName: keyof UsePopUpState<
["group", "deleteGroup", "groupMembers"]
>,
data?: {
groupId?: string;
name?: string;
slug?: string;
role?: string;
customRole?: {
name: string;
slug: string;
}
}
) => void;
};
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["group", "deleteGroup", "groupMembers"]>,
data?: {
groupId?: string;
name?: string;
slug?: string;
role?: string;
customRole?: {
name: string;
slug: string;
};
}
) => void;
};
export const OrgGroupsTable = ({
handlePopUpOpen
}: Props) => {
const [searchGroupsFilter, setSearchGroupsFilter] = useState("");
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const { isLoading, data: groups } = useGetOrganizationGroups(orgId);
const { mutateAsync: updateMutateAsync } = useUpdateGroup();
const { data: roles } = useGetOrgRoles(orgId);
const handleChangeRole = async ({
export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
const [searchGroupsFilter, setSearchGroupsFilter] = useState("");
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const { isLoading, data: groups } = useGetOrganizationGroups(orgId);
const { mutateAsync: updateMutateAsync } = useUpdateGroup();
const { data: roles } = useGetOrgRoles(orgId);
const handleChangeRole = async ({ currentSlug, role }: { currentSlug: string; role: string }) => {
try {
await updateMutateAsync({
currentSlug,
role
}: {
currentSlug: string;
role: string;
}) => {
try {
await updateMutateAsync({
currentSlug,
role
});
createNotification({
text: "Successfully updated group role",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to update group role",
type: "error"
});
}
});
createNotification({
text: "Successfully updated group role",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to update group role",
type: "error"
});
}
return (
<div>
<Input
value={searchGroupsFilter}
onChange={(e) => setSearchGroupsFilter(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search groups..."
/>
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Slug</Th>
<Th>Role</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={4} innerKey="org-groups" />}
{!isLoading && groups?.map(({ id, name, slug, role, customRole }) => {
return (
<Tr className="h-10" key={`org-group-${id}`}>
<Td>{name}</Td>
<Td>{slug}</Td>
<Td>
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Groups}
>
{(isAllowed) => {
return (
<Select
value={role === "custom" ? (customRole?.slug as string) : role}
isDisabled={!isAllowed}
className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
onValueChange={(selectedRole) =>
handleChangeRole({
currentSlug: slug,
role: selectedRole
})
}
>
{(roles || []).map(({ slug: roleSlug, name: roleName }) => (
<SelectItem value={roleSlug} key={`role-option-${roleSlug}`}>
{roleName}
</SelectItem>
))}
</Select>
);
}}
</OrgPermissionCan>
</Td>
<Td>
<div className="flex items-center justify-end">
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Groups}
>
{(isAllowed) => (
<Tooltip content="Manage group members">
<IconButton
onClick={() => {
handlePopUpOpen("groupMembers", {
slug
});
}}
size="lg"
colorSchema="primary"
variant="plain"
ariaLabel="update"
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faUsers} />
</IconButton>
</Tooltip>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Groups}
>
{(isAllowed) => (
<Tooltip content="Edit group">
<IconButton
onClick={async () => {
handlePopUpOpen("group", {
groupId: id,
name,
slug,
role,
customRole
});
}}
size="lg"
colorSchema="primary"
variant="plain"
ariaLabel="update"
className="ml-4"
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
</Tooltip>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Groups}
>
{(isAllowed) => (
<Tooltip content="Delete group">
<IconButton
onClick={() => {
handlePopUpOpen("deleteGroup", {
slug,
name
});
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="ml-4"
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</Tooltip>
)}
</OrgPermissionCan>
</div>
</Td>
</Tr>
);
})}
</TBody>
</Table>
{groups?.length === 0 && (
<EmptyState title="No groups found" icon={faUsers} />
)}
</TableContainer>
</div>
);
}
};
return (
<div>
<Input
value={searchGroupsFilter}
onChange={(e) => setSearchGroupsFilter(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search groups..."
/>
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Slug</Th>
<Th>Role</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={4} innerKey="org-groups" />}
{!isLoading &&
groups?.map(({ id, name, slug, role, customRole }) => {
return (
<Tr className="h-10" key={`org-group-${id}`}>
<Td>{name}</Td>
<Td>{slug}</Td>
<Td>
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Groups}
>
{(isAllowed) => {
return (
<Select
value={role === "custom" ? (customRole?.slug as string) : role}
isDisabled={!isAllowed}
className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
onValueChange={(selectedRole) =>
handleChangeRole({
currentSlug: slug,
role: selectedRole
})
}
>
{(roles || []).map(({ slug: roleSlug, name: roleName }) => (
<SelectItem value={roleSlug} key={`role-option-${roleSlug}`}>
{roleName}
</SelectItem>
))}
</Select>
);
}}
</OrgPermissionCan>
</Td>
<Td>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Identity}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("groupMembers", {
slug
});
}}
disabled={!isAllowed}
>
Manage Users
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Identity}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("group", {
groupId: id,
name,
slug,
role,
customRole
});
}}
disabled={!isAllowed}
>
Edit Group
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Groups}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("deleteGroup", {
slug,
name
});
}}
disabled={!isAllowed}
>
Delete Group
</DropdownMenuItem>
)}
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Td>
</Tr>
);
})}
</TBody>
</Table>
{groups?.length === 0 && <EmptyState title="No groups found" icon={faUsers} />}
</TableContainer>
</div>
);
};

View File

@@ -1,17 +1,19 @@
import { motion } from "framer-motion";
import { OrgGroupsSection } from "../OrgGroupsTab/components";
import { OrgMembersSection } from "./components";
export const OrgMembersTab = () => {
return (
<motion.div
key="panel-org-members"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
key="panel-org-members"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<OrgMembersSection />
<OrgMembersSection />
<OrgGroupsSection />
</motion.div>
);
}
};

View File

@@ -90,7 +90,7 @@ export const OrgMembersSection = () => {
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Members</p>
<p className="text-xl font-semibold text-mineshaft-100">Users</p>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Member}>
{(isAllowed) => (
<Button

View File

@@ -1,17 +1,9 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { motion } from "framer-motion";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import {
GroupsTab,
IdentityTab,
MemberListTab,
ProjectRoleListTab,
ServiceTokenTab
} from "./components";
import { IdentityTab, MembersTab,ProjectRoleListTab, ServiceTokenTab } from "./components";
enum TabSections {
Member = "members",
@@ -23,17 +15,13 @@ enum TabSections {
export const MembersPage = withProjectPermission(
() => {
const { currentWorkspace } = useWorkspace();
return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl py-6 px-6">
<p className="mr-4 mb-4 text-3xl font-semibold text-white">Project Access Control</p>
<Tabs defaultValue={TabSections.Member}>
<TabList>
<Tab value={TabSections.Member}>People</Tab>
{currentWorkspace?.version && currentWorkspace.version > 1 && (
<Tab value={TabSections.Groups}>Groups</Tab>
)}
<Tab value={TabSections.Member}>Users</Tab>
<Tab value={TabSections.Identities}>
<div className="flex items-center">
<p>Machine Identities</p>
@@ -43,21 +31,8 @@ export const MembersPage = withProjectPermission(
<Tab value={TabSections.Roles}>Project Roles</Tab>
</TabList>
<TabPanel value={TabSections.Member}>
<MemberListTab />
<MembersTab />
</TabPanel>
{currentWorkspace?.version && currentWorkspace.version > 1 && (
<TabPanel value={TabSections.Groups}>
<motion.div
key="panel-groups"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<GroupsTab />
</motion.div>
</TabPanel>
)}
<TabPanel value={TabSections.Identities}>
<IdentityTab />
</TabPanel>

View File

@@ -3,12 +3,13 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal, UpgradePlanModal } from "@app/components/v2";
import {
Button,
DeleteActionModal,
UpgradePlanModal
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useSubscription,useWorkspace } from "@app/context";
ProjectPermissionActions,
ProjectPermissionSub,
useSubscription,
useWorkspace
} from "@app/context";
import { usePopUp } from "@app/hooks";
import { useDeleteGroupFromWorkspace } from "@app/hooks/api";
@@ -16,90 +17,89 @@ import { GroupModal } from "./GroupModal";
import { GroupTable } from "./GroupsTable";
export const GroupsSection = () => {
const { subscription } = useSubscription();
const { currentWorkspace } = useWorkspace();
const { subscription } = useSubscription();
const { currentWorkspace } = useWorkspace();
const { mutateAsync: deleteMutateAsync } = useDeleteGroupFromWorkspace();
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"group",
"deleteGroup",
"upgradePlan"
] as const);
const { mutateAsync: deleteMutateAsync } = useDeleteGroupFromWorkspace();
const handleAddGroupModal = () => {
if (!subscription?.groups) {
handlePopUpOpen("upgradePlan", {
description: "You can manage users more efficiently with groups if you upgrade your Infisical plan."
});
} else {
handlePopUpOpen("group");
}
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"group",
"deleteGroup",
"upgradePlan"
] as const);
const handleAddGroupModal = () => {
if (!subscription?.groups) {
handlePopUpOpen("upgradePlan", {
description:
"You can manage users more efficiently with groups if you upgrade your Infisical plan."
});
} else {
handlePopUpOpen("group");
}
};
const onRemoveGroupSubmit = async (groupSlug: string) => {
try {
await deleteMutateAsync({
groupSlug,
projectSlug: currentWorkspace?.slug || ""
});
createNotification({
text: "Successfully removed identity from project",
type: "success"
});
handlePopUpClose("deleteGroup");
} catch (err) {
console.error(err);
const error = err as any;
const text = error?.response?.data?.message ?? "Failed to remove group from project";
createNotification({
text,
type: "error"
});
const onRemoveGroupSubmit = async (groupSlug: string) => {
try {
await deleteMutateAsync({
groupSlug,
projectSlug: currentWorkspace?.slug || ""
});
createNotification({
text: "Successfully removed identity from project",
type: "success"
});
handlePopUpClose("deleteGroup");
} catch (err) {
console.error(err);
const error = err as any;
const text = error?.response?.data?.message ?? "Failed to remove group from project";
createNotification({
text,
type: "error"
});
}
};
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<p className="text-xl font-semibold text-mineshaft-100">User Groups</p>
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Groups}>
{(isAllowed) => (
<Button
colorSchema="primary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handleAddGroupModal()}
isDisabled={!isAllowed}
>
Add Group
</Button>
)}
</ProjectPermissionCan>
</div>
<GroupModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<GroupTable handlePopUpOpen={handlePopUpOpen} />
<DeleteActionModal
isOpen={popUp.deleteGroup.isOpen}
title={`Are you sure want to remove the group ${
(popUp?.deleteGroup?.data as { name: string })?.name || ""
} from the project?`}
onChange={(isOpen) => handlePopUpToggle("deleteGroup", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
onRemoveGroupSubmit((popUp?.deleteGroup?.data as { slug: string })?.slug)
}
};
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Groups</p>
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Groups}>
{(isAllowed) => (
<Button
colorSchema="primary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handleAddGroupModal()}
isDisabled={!isAllowed}
>
Add Group
</Button>
)}
</ProjectPermissionCan>
</div>
<GroupModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<GroupTable handlePopUpOpen={handlePopUpOpen} />
<DeleteActionModal
isOpen={popUp.deleteGroup.isOpen}
title={`Are you sure want to remove the group ${
(popUp?.deleteGroup?.data as { name: string })?.name || ""
} from the project?`}
onChange={(isOpen) => handlePopUpToggle("deleteGroup", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
onRemoveGroupSubmit(
(popUp?.deleteGroup?.data as { slug: string })?.slug
)
}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={(popUp.upgradePlan?.data as { description: string })?.description}
/>
</div>
);
}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={(popUp.upgradePlan?.data as { description: string })?.description}
/>
</div>
);
};

View File

@@ -1,4 +1,4 @@
import { faServer, faXmark } from "@fortawesome/free-solid-svg-icons";
import { faServer, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
@@ -13,6 +13,7 @@ import {
Td,
Th,
THead,
Tooltip,
Tr
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
@@ -36,68 +37,71 @@ export const GroupTable = ({ handlePopUpOpen }: Props) => {
const { data, isLoading } = useListWorkspaceGroups(currentWorkspace?.slug || "");
return (
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Role</Th>
<Th>Added on</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={4} innerKey="project-groups" />}
{!isLoading &&
data &&
data.length > 0 &&
data.map(({ group: { id, name, slug }, roles, createdAt }) => {
return (
<Tr className="h-10" key={`st-v3-${id}`}>
<Td>{name}</Td>
<Td>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Groups}
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Role</Th>
<Th>Added on</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={4} innerKey="project-groups" />}
{!isLoading &&
data &&
data.length > 0 &&
data.map(({ group: { id, name, slug }, roles, createdAt }) => {
return (
<Tr className="group h-10" key={`st-v3-${id}`}>
<Td>{name}</Td>
<Td>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Groups}
>
{(isAllowed) => (
<GroupRoles roles={roles} disableEdit={!isAllowed} groupSlug={slug} />
)}
</ProjectPermissionCan>
</Td>
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
<Td className="flex justify-end">
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Groups}
>
{(isAllowed) => (
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<Tooltip content="Remove">
<IconButton
onClick={() => {
handlePopUpOpen("deleteGroup", {
slug,
name
});
}}
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="ml-4"
isDisabled={!isAllowed}
>
{(isAllowed) => (
<GroupRoles roles={roles} disableEdit={!isAllowed} groupSlug={slug} />
)}
</ProjectPermissionCan>
</Td>
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
<Td className="flex justify-end">
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Groups}
>
{(isAllowed) => (
<IconButton
onClick={() => {
handlePopUpOpen("deleteGroup", {
slug,
name
});
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="ml-4"
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
)}
</ProjectPermissionCan>
</Td>
</Tr>
);
})}
</TBody>
</Table>
{!isLoading && data?.length === 0 && (
<EmptyState title="No groups have been added to this project" icon={faServer} />
)}
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</Tooltip>
</div>
)}
</ProjectPermissionCan>
</Td>
</Tr>
);
})}
</TBody>
</Table>
{!isLoading && data?.length === 0 && (
<EmptyState title="No groups have been added to this project" icon={faServer} />
)}
</TableContainer>
);
};

View File

@@ -1,505 +0,0 @@
import { useMemo, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import Link from "next/link";
import {
faClock,
faEdit,
faMagnifyingGlass,
faPlus,
faUsers,
faXmark
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Button,
DeleteActionModal,
EmptyState,
FormControl,
HoverCard,
HoverCardContent,
HoverCardTrigger,
IconButton,
Input,
Modal,
ModalContent,
Select,
SelectItem,
Table,
TableContainer,
TableSkeleton,
Tag,
TBody,
Td,
Th,
THead,
Tooltip,
Tr,
UpgradePlanModal
} from "@app/components/v2";
import {
ProjectPermissionActions,
ProjectPermissionSub,
useOrganization,
useUser,
useWorkspace
} from "@app/context";
import { usePopUp } from "@app/hooks";
import {
useAddUserToWsE2EE,
useAddUserToWsNonE2EE,
useDeleteUserFromWorkspace,
useGetOrgUsers,
useGetUserWsKey,
useGetWorkspaceUsers
} from "@app/hooks/api";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { TWorkspaceUser } from "@app/hooks/api/types";
import { ProjectVersion } from "@app/hooks/api/workspace/types";
import { MemberRoleForm } from "./MemberRoleForm";
const addMemberFormSchema = z.object({
orgMembershipId: z.string().trim()
});
type TAddMemberForm = z.infer<typeof addMemberFormSchema>;
const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2;
const formatRoleName = (role: string, customRoleName?: string) => {
if (role === ProjectMembershipRole.Custom) return customRoleName;
if (role === ProjectMembershipRole.Member) return "Developer";
if (role === ProjectMembershipRole.NoAccess) return "No access";
return role;
};
export const MemberListTab = () => {
const { t } = useTranslation();
const { currentOrg } = useOrganization();
const { currentWorkspace } = useWorkspace();
const { user } = useUser();
const userId = user?.id || "";
const orgId = currentOrg?.id || "";
const workspaceId = currentWorkspace?.id || "";
const { data: wsKey } = useGetUserWsKey(workspaceId);
const { data: members, isLoading: isMembersLoading } = useGetWorkspaceUsers(workspaceId);
const { data: orgUsers } = useGetOrgUsers(orgId);
const [searchMemberFilter, setSearchMemberFilter] = useState("");
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"addMember",
"removeMember",
"upgradePlan",
"updateRole"
] as const);
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<TAddMemberForm>({ resolver: zodResolver(addMemberFormSchema) });
const { mutateAsync: addUserToWorkspace } = useAddUserToWsE2EE();
const { mutateAsync: addUserToWorkspaceNonE2EE } = useAddUserToWsNonE2EE();
const { mutateAsync: removeUserFromWorkspace } = useDeleteUserFromWorkspace();
const onAddMember = async ({ orgMembershipId }: TAddMemberForm) => {
if (!currentWorkspace) return;
if (!currentOrg?.id) return;
// TODO(akhilmhdh): Move to memory storage
const userPrivateKey = localStorage.getItem("PRIVATE_KEY");
if (!userPrivateKey || !wsKey) {
createNotification({
text: "Failed to find private key. Try re-login"
});
return;
}
const orgUser = (orgUsers || []).find(({ id }) => id === orgMembershipId);
if (!orgUser) return;
try {
// TODO: update
if (currentWorkspace.version === ProjectVersion.V1) {
await addUserToWorkspace({
workspaceId,
userPrivateKey,
decryptKey: wsKey,
members: [{ orgMembershipId, userPublicKey: orgUser.user.publicKey }]
});
} else if (currentWorkspace.version === ProjectVersion.V2) {
await addUserToWorkspaceNonE2EE({
projectId: workspaceId,
usernames: [orgUser.user.username]
});
} else {
createNotification({
text: "Failed to add user to project, unknown project type",
type: "error"
});
return;
}
createNotification({
text: "Successfully added user to the project",
type: "success"
});
} catch (error) {
console.error(error);
createNotification({
text: "Failed to add user to project",
type: "error"
});
}
handlePopUpClose("addMember");
reset();
};
const handleRemoveUser = async () => {
const username = (popUp?.removeMember?.data as { username: string })?.username;
if (!currentOrg?.id) return;
try {
await removeUserFromWorkspace({ workspaceId, usernames: [username] });
createNotification({
text: "Successfully removed user from project",
type: "success"
});
} catch (error) {
console.error(error);
createNotification({
text: "Failed to remove user from the project",
type: "error"
});
}
handlePopUpClose("removeMember");
};
const filterdUsers = useMemo(
() =>
members?.filter(
({ user: u, inviteEmail }) =>
u?.firstName?.toLowerCase().includes(searchMemberFilter.toLowerCase()) ||
u?.lastName?.toLowerCase().includes(searchMemberFilter.toLowerCase()) ||
u?.username?.toLowerCase().includes(searchMemberFilter.toLowerCase()) ||
u?.email?.toLowerCase().includes(searchMemberFilter.toLowerCase()) ||
inviteEmail?.includes(searchMemberFilter.toLowerCase())
),
[members, searchMemberFilter]
);
const filteredOrgUsers = useMemo(() => {
const wsUserUsernames = new Map();
members?.forEach((member) => {
wsUserUsernames.set(member.user.username, true);
});
return (orgUsers || []).filter(
({ status, user: u }) => status === "accepted" && !wsUserUsernames.has(u.username)
);
}, [orgUsers, members]);
return (
<motion.div
key="user-role-1"
className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<div className="mb-4 flex items-center justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Members</p>
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Member}>
{(isAllowed) => (
<Button
colorSchema="primary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("addMember")}
isDisabled={!isAllowed}
>
Add Member
</Button>
)}
</ProjectPermissionCan>
</div>
<Input
value={searchMemberFilter}
onChange={(e) => setSearchMemberFilter(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search members..."
/>
<div className="mt-4">
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Username</Th>
<Th>Role</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isMembersLoading && <TableSkeleton columns={4} innerKey="project-members" />}
{!isMembersLoading &&
filterdUsers?.map((projectMember, index) => {
const { user: u, inviteEmail, id: membershipId, roles } = projectMember;
const name = u ? `${u.firstName} ${u.lastName}` : "-";
const email = u?.email || inviteEmail;
return (
<Tr key={`membership-${membershipId}`} className="w-full">
<Td>{name}</Td>
<Td>{email}</Td>
<Td>
<div className="flex items-center space-x-2">
{roles
.slice(0, MAX_ROLES_TO_BE_SHOWN_IN_TABLE)
.map(
({
role,
customRoleName,
id,
isTemporary,
temporaryAccessEndTime
}) => {
const isExpired =
new Date() > new Date(temporaryAccessEndTime || ("" as string));
return (
<Tag key={id}>
<div className="flex items-center space-x-2">
<div className="capitalize">
{formatRoleName(role, customRoleName)}
</div>
{isTemporary && (
<div>
<Tooltip
content={
isExpired ? "Timed role expired" : "Timed role access"
}
>
<FontAwesomeIcon
icon={faClock}
className={twMerge(isExpired && "text-red-600")}
/>
</Tooltip>
</div>
)}
</div>
</Tag>
);
}
)}
{roles.length > MAX_ROLES_TO_BE_SHOWN_IN_TABLE && (
<HoverCard>
<HoverCardTrigger>
<Tag>+{roles.length - MAX_ROLES_TO_BE_SHOWN_IN_TABLE}</Tag>
</HoverCardTrigger>
<HoverCardContent className="border border-gray-700 bg-mineshaft-800 p-4">
{roles
.slice(MAX_ROLES_TO_BE_SHOWN_IN_TABLE)
.map(
({
role,
customRoleName,
id,
isTemporary,
temporaryAccessEndTime
}) => {
const isExpired =
new Date() >
new Date(temporaryAccessEndTime || ("" as string));
return (
<Tag key={id} className="capitalize">
<div className="flex items-center space-x-2">
<div>{formatRoleName(role, customRoleName)}</div>
{isTemporary && (
<div>
<Tooltip
content={
isExpired
? "Access expired"
: "Temporary access"
}
>
<FontAwesomeIcon
icon={faClock}
className={twMerge(
new Date() >
new Date(
temporaryAccessEndTime as string
) && "text-red-600"
)}
/>
</Tooltip>
</div>
)}
</div>
</Tag>
);
}
)}
</HoverCardContent>
</HoverCard>
)}
{userId !== u?.id && (
<Tooltip content="Edit permission">
<IconButton
size="sm"
variant="plain"
ariaLabel="update-role"
onClick={() =>
handlePopUpOpen("updateRole", { ...projectMember, index })
}
>
<FontAwesomeIcon icon={faEdit} />
</IconButton>
</Tooltip>
)}
</div>
</Td>
<Td>
{userId !== u?.id && (
<div className="flex items-center space-x-2">
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Member}
>
{(isAllowed) => (
<IconButton
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="ml-4"
isDisabled={userId === u?.id || !isAllowed}
onClick={() =>
handlePopUpOpen("removeMember", { username: u.username })
}
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
)}
</ProjectPermissionCan>
</div>
)}
</Td>
</Tr>
);
})}
</TBody>
</Table>
{!isMembersLoading && filterdUsers?.length === 0 && (
<EmptyState title="No project members found" icon={faUsers} />
)}
</TableContainer>
</div>
<Modal
isOpen={popUp?.addMember?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addMember", isOpen)}
>
<ModalContent
title={t("section.members.add-dialog.add-member-to-project") as string}
subTitle={t("section.members.add-dialog.user-will-email")}
>
{filteredOrgUsers.length ? (
<form onSubmit={handleSubmit(onAddMember)}>
<Controller
control={control}
defaultValue={filteredOrgUsers?.[0]?.user?.username}
name="orgMembershipId"
render={({ field, fieldState: { error } }) => (
<FormControl label="Username" isError={Boolean(error)} errorText={error?.message}>
<Select
position="popper"
className="w-full"
defaultValue={filteredOrgUsers?.[0]?.user?.username}
value={field.value}
onValueChange={field.onChange}
>
{filteredOrgUsers.map(({ id: orgUserId, user: u }) => (
<SelectItem value={orgUserId} key={`org-membership-join-${orgUserId}`}>
{u?.username}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Add Member
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpClose("addMember")}
>
Cancel
</Button>
</div>
</form>
) : (
<div className="flex flex-col space-y-4">
<div>All the users in your organization are already invited.</div>
<Link href={`/org/${currentWorkspace?.orgId}/members`}>
<Button variant="outline_bg">Add users to organization</Button>
</Link>
</div>
)}
</ModalContent>
</Modal>
<Modal
isOpen={popUp.updateRole.isOpen}
onOpenChange={(state) => handlePopUpToggle("updateRole", state)}
>
<ModalContent
className="max-w-4xl"
title={`Manage Access for ${(popUp.updateRole.data as TWorkspaceUser)?.user?.email}`}
subTitle={`
Configure role-based access control by assigning Infisical users a mix of roles and specific privileges. A user will gain access to all actions within the roles assigned to them, not just the actions those roles share in common. You must choose at least one permanent role.
`}
>
<MemberRoleForm
onOpenUpgradeModal={(description) => handlePopUpOpen("upgradePlan", { description })}
projectMember={
filterdUsers?.[
(popUp.updateRole?.data as TWorkspaceUser & { index: number })?.index
] as TWorkspaceUser
}
/>
</ModalContent>
</Modal>
<DeleteActionModal
isOpen={popUp.removeMember.isOpen}
deleteKey="remove"
title="Do you want to remove this user from the project?"
onChange={(isOpen) => handlePopUpToggle("removeMember", isOpen)}
onDeleteApproved={handleRemoveUser}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={(popUp.upgradePlan?.data as { description: string })?.description}
/>
</motion.div>
);
};

View File

@@ -1 +0,0 @@
export { MemberListTab } from "./MemberListTab";

View File

@@ -0,0 +1,22 @@
import { motion } from "framer-motion";
import { useWorkspace } from "@app/context";
import { GroupsSection } from "../GroupsTab/components";
import { MembersSection } from "./components";
export const MembersTab = () => {
const { currentWorkspace } = useWorkspace();
return (
<motion.div
key="panel-project-members"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<MembersSection />
{currentWorkspace?.version && currentWorkspace.version > 1 && <GroupsSection />}
</motion.div>
);
};

View File

@@ -0,0 +1,177 @@
import { useMemo } from "react";
import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import Link from "next/link";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button,FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2";
import { useOrganization, useWorkspace } from "@app/context";
import {
useAddUserToWsE2EE,
useAddUserToWsNonE2EE,
useGetOrgUsers,
useGetUserWsKey,
useGetWorkspaceUsers} from "@app/hooks/api";
import { ProjectVersion } from "@app/hooks/api/workspace/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
const addMemberFormSchema = z.object({
orgMembershipId: z.string().trim()
});
type TAddMemberForm = z.infer<typeof addMemberFormSchema>;
type Props = {
popUp: UsePopUpState<["addMember"]>;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["addMember"]>, state?: boolean) => void;
};
export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
const { t } = useTranslation();
const { currentOrg } = useOrganization();
const { currentWorkspace } = useWorkspace();
const orgId = currentOrg?.id || "";
const workspaceId = currentWorkspace?.id || "";
const { data: wsKey } = useGetUserWsKey(workspaceId);
const { data: members } = useGetWorkspaceUsers(workspaceId);
const { data: orgUsers } = useGetOrgUsers(orgId);
const { mutateAsync: addUserToWorkspace } = useAddUserToWsE2EE();
const { mutateAsync: addUserToWorkspaceNonE2EE } = useAddUserToWsNonE2EE();
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<TAddMemberForm>({ resolver: zodResolver(addMemberFormSchema) });
const onAddMember = async ({ orgMembershipId }: TAddMemberForm) => {
if (!currentWorkspace) return;
if (!currentOrg?.id) return;
// TODO(akhilmhdh): Move to memory storage
const userPrivateKey = localStorage.getItem("PRIVATE_KEY");
if (!userPrivateKey || !wsKey) {
createNotification({
text: "Failed to find private key. Try re-login"
});
return;
}
const orgUser = (orgUsers || []).find(({ id }) => id === orgMembershipId);
if (!orgUser) return;
try {
// TODO: update
if (currentWorkspace.version === ProjectVersion.V1) {
await addUserToWorkspace({
workspaceId,
userPrivateKey,
decryptKey: wsKey,
members: [{ orgMembershipId, userPublicKey: orgUser.user.publicKey }]
});
} else if (currentWorkspace.version === ProjectVersion.V2) {
await addUserToWorkspaceNonE2EE({
projectId: workspaceId,
usernames: [orgUser.user.username]
});
} else {
createNotification({
text: "Failed to add user to project, unknown project type",
type: "error"
});
return;
}
createNotification({
text: "Successfully added user to the project",
type: "success"
});
} catch (error) {
console.error(error);
createNotification({
text: "Failed to add user to project",
type: "error"
});
}
handlePopUpToggle("addMember", false);
reset();
};
const filteredOrgUsers = useMemo(() => {
const wsUserUsernames = new Map();
members?.forEach((member) => {
wsUserUsernames.set(member.user.username, true);
});
return (orgUsers || []).filter(
({ status, user: u }) => status === "accepted" && !wsUserUsernames.has(u.username)
);
}, [orgUsers, members]);
return (
<Modal
isOpen={popUp?.addMember?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addMember", isOpen)}
>
<ModalContent
title={t("section.members.add-dialog.add-member-to-project") as string}
subTitle={t("section.members.add-dialog.user-will-email")}
>
{filteredOrgUsers.length ? (
<form onSubmit={handleSubmit(onAddMember)}>
<Controller
control={control}
defaultValue={filteredOrgUsers?.[0]?.user?.username}
name="orgMembershipId"
render={({ field, fieldState: { error } }) => (
<FormControl label="Username" isError={Boolean(error)} errorText={error?.message}>
<Select
position="popper"
className="w-full"
defaultValue={filteredOrgUsers?.[0]?.user?.username}
value={field.value}
onValueChange={field.onChange}
>
{filteredOrgUsers.map(({ id: orgUserId, user: u }) => (
<SelectItem value={orgUserId} key={`org-membership-join-${orgUserId}`}>
{u?.username}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Add Member
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("addMember", false)}
>
Cancel
</Button>
</div>
</form>
) : (
<div className="flex flex-col space-y-4">
<div>All the users in your organization are already invited.</div>
<Link href={`/org/${currentWorkspace?.orgId}/members`}>
<Button variant="outline_bg">Add users to organization</Button>
</Link>
</div>
)}
</ModalContent>
</Modal>
);
};

View File

@@ -0,0 +1,86 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal, UpgradePlanModal } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub , useOrganization, useWorkspace } from "@app/context";
import { usePopUp } from "@app/hooks";
import { useDeleteUserFromWorkspace } from "@app/hooks/api";
import { AddMemberModal } from "./AddMemberModal";
import { MembersTable } from "./MembersTable";
export const MembersSection = () => {
const { currentOrg } = useOrganization();
const { currentWorkspace } = useWorkspace();
const { mutateAsync: removeUserFromWorkspace } = useDeleteUserFromWorkspace();
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"addMember",
"removeMember",
"upgradePlan",
"updateRole"
] as const);
const handleRemoveUser = async () => {
const username = (popUp?.removeMember?.data as { username: string })?.username;
if (!currentOrg?.id) return;
if (!currentWorkspace?.id) return;
try {
await removeUserFromWorkspace({ workspaceId: currentWorkspace.id, usernames: [username] });
createNotification({
text: "Successfully removed user from project",
type: "success"
});
} catch (error) {
console.error(error);
createNotification({
text: "Failed to remove user from the project",
type: "error"
});
}
handlePopUpClose("removeMember");
};
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Users</p>
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Member}>
{(isAllowed) => (
<Button
colorSchema="primary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("addMember")}
isDisabled={!isAllowed}
>
Add Member
</Button>
)}
</ProjectPermissionCan>
</div>
<MembersTable
popUp={popUp}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
<AddMemberModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.removeMember.isOpen}
deleteKey="remove"
title="Do you want to remove this user from the project?"
onChange={(isOpen) => handlePopUpToggle("removeMember", isOpen)}
onDeleteApproved={handleRemoveUser}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={(popUp.upgradePlan?.data as { description: string })?.description}
/>
</div>
);
};

View File

@@ -0,0 +1,269 @@
import { useMemo,useState } from "react";
import {
faClock,
faEdit,
faMagnifyingGlass,
faTrash,
faUsers} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
EmptyState,
HoverCard,
HoverCardContent,
HoverCardTrigger,
IconButton,
Input,
Modal,
ModalContent,
Table,
TableContainer,
TableSkeleton,
Tag,
TBody,
Td,
Th,
THead,
Tooltip,
Tr} from "@app/components/v2";
import {
ProjectPermissionActions,
ProjectPermissionSub,
useUser,
useWorkspace} from "@app/context";
import { useGetWorkspaceUsers } from "@app/hooks/api";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { TWorkspaceUser } from "@app/hooks/api/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { MemberRoleForm } from "./MemberRoleForm";
const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2;
const formatRoleName = (role: string, customRoleName?: string) => {
if (role === ProjectMembershipRole.Custom) return customRoleName;
if (role === ProjectMembershipRole.Member) return "Developer";
if (role === ProjectMembershipRole.NoAccess) return "No access";
return role;
};
type Props = {
popUp: UsePopUpState<["updateRole"]>;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["removeMember", "updateRole", "upgradePlan"]>,
data?: {}
) => void;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["updateRole"]>, state?: boolean) => void;
};
export const MembersTable = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Props) => {
const [searchMemberFilter, setSearchMemberFilter] = useState("");
const { currentWorkspace } = useWorkspace();
const { user } = useUser();
const userId = user?.id || "";
const workspaceId = currentWorkspace?.id || "";
const { data: members, isLoading: isMembersLoading } = useGetWorkspaceUsers(workspaceId);
const filterdUsers = useMemo(
() =>
members?.filter(
({ user: u, inviteEmail }) =>
u?.firstName?.toLowerCase().includes(searchMemberFilter.toLowerCase()) ||
u?.lastName?.toLowerCase().includes(searchMemberFilter.toLowerCase()) ||
u?.username?.toLowerCase().includes(searchMemberFilter.toLowerCase()) ||
u?.email?.toLowerCase().includes(searchMemberFilter.toLowerCase()) ||
inviteEmail?.includes(searchMemberFilter.toLowerCase())
),
[members, searchMemberFilter]
);
return (
<div>
<Input
value={searchMemberFilter}
onChange={(e) => setSearchMemberFilter(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search members..."
/>
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Username</Th>
<Th>Role</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isMembersLoading && <TableSkeleton columns={4} innerKey="project-members" />}
{!isMembersLoading &&
filterdUsers?.map((projectMember, index) => {
const { user: u, inviteEmail, id: membershipId, roles } = projectMember;
const name = u ? `${u.firstName} ${u.lastName}` : "-";
const email = u?.email || inviteEmail;
return (
<Tr key={`membership-${membershipId}`} className="group w-full">
<Td>{name}</Td>
<Td>{email}</Td>
<Td>
<div className="flex items-center space-x-2">
{roles
.slice(0, MAX_ROLES_TO_BE_SHOWN_IN_TABLE)
.map(
({ role, customRoleName, id, isTemporary, temporaryAccessEndTime }) => {
const isExpired =
new Date() > new Date(temporaryAccessEndTime || ("" as string));
return (
<Tag key={id}>
<div className="flex items-center space-x-2">
<div className="capitalize">
{formatRoleName(role, customRoleName)}
</div>
{isTemporary && (
<div>
<Tooltip
content={
isExpired ? "Timed role expired" : "Timed role access"
}
>
<FontAwesomeIcon
icon={faClock}
className={twMerge(isExpired && "text-red-600")}
/>
</Tooltip>
</div>
)}
</div>
</Tag>
);
}
)}
{roles.length > MAX_ROLES_TO_BE_SHOWN_IN_TABLE && (
<HoverCard>
<HoverCardTrigger>
<Tag>+{roles.length - MAX_ROLES_TO_BE_SHOWN_IN_TABLE}</Tag>
</HoverCardTrigger>
<HoverCardContent className="border border-gray-700 bg-mineshaft-800 p-4">
{roles
.slice(MAX_ROLES_TO_BE_SHOWN_IN_TABLE)
.map(
({
role,
customRoleName,
id,
isTemporary,
temporaryAccessEndTime
}) => {
const isExpired =
new Date() >
new Date(temporaryAccessEndTime || ("" as string));
return (
<Tag key={id} className="capitalize">
<div className="flex items-center space-x-2">
<div>{formatRoleName(role, customRoleName)}</div>
{isTemporary && (
<div>
<Tooltip
content={
isExpired ? "Access expired" : "Temporary access"
}
>
<FontAwesomeIcon
icon={faClock}
className={twMerge(
new Date() >
new Date(temporaryAccessEndTime as string) &&
"text-red-600"
)}
/>
</Tooltip>
</div>
)}
</div>
</Tag>
);
}
)}
</HoverCardContent>
</HoverCard>
)}
{userId !== u?.id && (
<Tooltip content="Edit permission">
<IconButton
size="sm"
variant="plain"
ariaLabel="update-role"
onClick={() =>
handlePopUpOpen("updateRole", { ...projectMember, index })
}
>
<FontAwesomeIcon icon={faEdit} />
</IconButton>
</Tooltip>
)}
</div>
</Td>
<Td>
{userId !== u?.id && (
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Member}
>
{(isAllowed) => (
<IconButton
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="ml-4"
isDisabled={userId === u?.id || !isAllowed}
onClick={() =>
handlePopUpOpen("removeMember", { username: u.username })
}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
)}
</ProjectPermissionCan>
</div>
)}
</Td>
</Tr>
);
})}
</TBody>
</Table>
{!isMembersLoading && filterdUsers?.length === 0 && (
<EmptyState title="No project members found" icon={faUsers} />
)}
</TableContainer>
<Modal
isOpen={popUp.updateRole.isOpen}
onOpenChange={(state) => handlePopUpToggle("updateRole", state)}
>
<ModalContent
className="max-w-4xl"
title={`Manage Access for ${(popUp.updateRole.data as TWorkspaceUser)?.user?.email}`}
subTitle={`
Configure role-based access control by assigning Infisical users a mix of roles and specific privileges. A user will gain access to all actions within the roles assigned to them, not just the actions those roles share in common. You must choose at least one permanent role.
`}
>
<MemberRoleForm
onOpenUpgradeModal={(description) => handlePopUpOpen("upgradePlan", { description })}
projectMember={
filterdUsers?.[
(popUp.updateRole?.data as TWorkspaceUser & { index: number })?.index
] as TWorkspaceUser
}
/>
</ModalContent>
</Modal>
</div>
);
};

View File

@@ -0,0 +1 @@
export { MembersSection } from "./MembersSection";

View File

@@ -0,0 +1 @@
export { MembersTab } from "./MembersTab";

View File

@@ -1,5 +1,5 @@
export { GroupsTab } from "./GroupsTab";
export { IdentityTab } from "./IdentityTab";
export { MemberListTab } from "./MemberListTab";
export { MembersTab } from "./MembersTab";
export { ProjectRoleListTab } from "./ProjectRoleListTab";
export { ServiceTokenTab } from "./ServiceTokenTab";

View File

@@ -1,6 +1,6 @@
import { Modal, ModalContent } from "@app/components/v2";
import { TAccessApprovalPolicy } from "@app/hooks/api/types";
import { SpecificPrivilegeSecretForm } from "@app/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection";
import { SpecificPrivilegeSecretForm } from "@app/views/Project/MembersPage/components/MembersTab/components/MemberRoleForm/SpecificPrivilegeSection";
export const RequestAccessModal = ({
isOpen,

View File

@@ -1,7 +1,8 @@
import { faCheck, faCopy, faKey } from "@fortawesome/free-solid-svg-icons";
import { faCheck, faCopy, faEye, faEyeSlash, faKey } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { EmptyState, IconButton, Td, Tr } from "@app/components/v2";
import { useToggle } from "@app/hooks";
type Props = {
isLoading: boolean;
@@ -10,39 +11,68 @@ type Props = {
copyUrlToClipboard: () => void;
};
const replaceContentWithDot = (str: string) => {
let finalStr = "";
for (let i = 0; i < str.length; i += 1) {
const char = str.at(i);
finalStr += char === "\n" ? "\n" : "*";
}
return finalStr;
};
export const SecretTable = ({
isLoading,
decryptedSecret,
isUrlCopied,
copyUrlToClipboard
}: Props) => (
<div className="flex w-full items-center justify-center rounded-md border border-solid border-mineshaft-700 bg-mineshaft-800 p-2">
{isLoading && <div className="bg-mineshaft-800 text-center text-bunker-400">Loading...</div>}
{!isLoading && !decryptedSecret && (
<Tr>
<Td colSpan={4} className="bg-mineshaft-800 text-center text-bunker-400">
<EmptyState title="Secret has either expired or does not exist!" icon={faKey} />
</Td>
</Tr>
)}
{!isLoading && decryptedSecret && (
<div className="dark relative flex h-full w-full items-center overflow-y-auto border border-mineshaft-700 bg-mineshaft-900 rounded-md p-2 md:p-3 pr-2">
<div className="thin-scrollbar flex h-full max-h-44 w-full flex-1 overflow-y-scroll break-words pr-4 dark:[color-scheme:dark]">
<div className="align-center flex w-full min-w-full whitespace-pre-line">
{decryptedSecret}
}: Props) => {
const [isVisible, setIsVisible] = useToggle(false);
return (
<div className="flex w-full items-center justify-center rounded-md border border-solid border-mineshaft-700 bg-mineshaft-800 p-2">
{isLoading && <div className="bg-mineshaft-800 text-center text-bunker-400">Loading...</div>}
{!isLoading && !decryptedSecret && (
<Tr>
<Td colSpan={4} className="bg-mineshaft-800 text-center text-bunker-400">
<EmptyState title="Secret has either expired or does not exist!" icon={faKey} />
</Td>
</Tr>
)}
{!isLoading && decryptedSecret && (
<div className="dark relative flex h-full w-full items-center overflow-y-auto rounded-md border border-mineshaft-700 bg-mineshaft-900 p-2 pr-2 md:p-3">
<div
className={`thin-scrollbar flex h-full max-h-44 w-full flex-1 overflow-y-scroll ${
isVisible ? "break-words" : "break-all"
} pr-4 dark:[color-scheme:dark]`}
>
<div className="align-center flex w-full min-w-full whitespace-pre-line">
{isVisible ? decryptedSecret : replaceContentWithDot(decryptedSecret)}
</div>
</div>
<div className="absolute top-1 right-0 mx-1 flex max-h-8 sm:top-2 sm:right-5">
<IconButton
variant="outline_bg"
colorSchema="primary"
ariaLabel="copy to clipboard"
onClick={copyUrlToClipboard}
className="mr-1 flex max-h-8 items-center rounded"
size="xs"
>
<FontAwesomeIcon className="pr-2" icon={isUrlCopied ? faCheck : faCopy} /> Copy
</IconButton>
<IconButton
variant="outline_bg"
colorSchema="primary"
ariaLabel="toggle visibility"
onClick={() => setIsVisible.toggle()}
className="flex max-h-8 items-center rounded"
size="xs"
>
<FontAwesomeIcon icon={isVisible ? faEyeSlash : faEye} />
</IconButton>
</div>
</div>
<IconButton
variant="outline_bg"
colorSchema="primary"
ariaLabel="copy to clipboard"
onClick={copyUrlToClipboard}
className="mx-1 flex max-h-8 items-center rounded absolute top-1 sm:top-2 right-0 sm:right-5"
size="xs"
>
<FontAwesomeIcon className="pr-2" icon={isUrlCopied ? faCheck : faCopy} /> Copy
</IconButton>
</div>
)}
</div>
);
)}
</div>
);
};

View File

@@ -16,9 +16,10 @@ import {
Td,
Th,
THead,
Tr
Tr,
UpgradePlanModal
} from "@app/components/v2";
import { useUser } from "@app/context";
import { useSubscription, useUser } from "@app/context";
import { useDebounce, usePopUp } from "@app/hooks";
import { useAdminDeleteUser, useAdminGetUsers } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -27,8 +28,8 @@ const UserPanelTable = ({
handlePopUpOpen
}: {
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["removeUser"]>,
data: {
popUpName: keyof UsePopUpState<["removeUser", "upgradePlan"]>,
data?: {
username: string;
id: string;
}
@@ -38,6 +39,7 @@ const UserPanelTable = ({
const { user } = useUser();
const userId = user?.id || "";
const debounedSearchTerm = useDebounce(searchUserFilter, 500);
const { subscription } = useSubscription();
const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } = useAdminGetUsers({
limit: 20,
@@ -83,7 +85,13 @@ const UserPanelTable = ({
variant="plain"
ariaLabel="update"
isDisabled={userId === id}
onClick={() => handlePopUpOpen("removeUser", { username, id })}
onClick={() => {
if (!subscription?.instanceUserManagement) {
handlePopUpOpen("upgradePlan");
return;
}
handlePopUpOpen("removeUser", { username, id });
}}
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
@@ -117,7 +125,8 @@ const UserPanelTable = ({
export const UserPanel = () => {
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"removeUser"
"removeUser",
"upgradePlan"
] as const);
const { mutateAsync: deleteUser } = useAdminDeleteUser();
@@ -156,6 +165,11 @@ export const UserPanel = () => {
onChange={(isOpen) => handlePopUpToggle("removeUser", isOpen)}
onDeleteApproved={handleRemoveUser}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="Deleting users via Admin UI is only available on Infisical's Pro plan and above."
/>
</div>
);
};