mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-10 07:28:50 +00:00
Merge pull request #793 from akhilmhdh/feat/new-overview-page
Feat/new overview page
This commit is contained in:
@@ -108,7 +108,7 @@ export const getAllSecretsFromImport = async (req: Request, res: Response) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!importSecDoc) {
|
if (!importSecDoc) {
|
||||||
return res.status(200).json({ secrets: {} });
|
return res.status(200).json({ secrets: [] });
|
||||||
}
|
}
|
||||||
|
|
||||||
const secrets = await getAllImportedSecrets(workspaceId, environment, folderId);
|
const secrets = await getAllImportedSecrets(workspaceId, environment, folderId);
|
||||||
|
|||||||
@@ -346,6 +346,7 @@ export const createSecretHelper = async ({
|
|||||||
workspace: new Types.ObjectId(workspaceId),
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
folder: folderId,
|
folder: folderId,
|
||||||
type,
|
type,
|
||||||
|
environment,
|
||||||
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
|
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -362,6 +363,7 @@ export const createSecretHelper = async ({
|
|||||||
secretBlindIndex,
|
secretBlindIndex,
|
||||||
folder: folderId,
|
folder: folderId,
|
||||||
workspace: new Types.ObjectId(workspaceId),
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
|
environment,
|
||||||
type: SECRET_SHARED
|
type: SECRET_SHARED
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Generated
+322
-12
@@ -67,6 +67,7 @@
|
|||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-beautiful-dnd": "^13.1.1",
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
"react-code-input": "^3.10.1",
|
"react-code-input": "^3.10.1",
|
||||||
|
"react-contenteditable": "^3.3.7",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-grid-layout": "^1.3.4",
|
"react-grid-layout": "^1.3.4",
|
||||||
"react-hook-form": "^7.43.0",
|
"react-hook-form": "^7.43.0",
|
||||||
@@ -75,6 +76,7 @@
|
|||||||
"react-markdown": "^8.0.3",
|
"react-markdown": "^8.0.3",
|
||||||
"react-redux": "^8.0.2",
|
"react-redux": "^8.0.2",
|
||||||
"react-table": "^7.8.0",
|
"react-table": "^7.8.0",
|
||||||
|
"sanitize-html": "^2.11.0",
|
||||||
"set-cookie-parser": "^2.5.1",
|
"set-cookie-parser": "^2.5.1",
|
||||||
"sharp": "^0.32.0",
|
"sharp": "^0.32.0",
|
||||||
"styled-components": "^5.3.7",
|
"styled-components": "^5.3.7",
|
||||||
@@ -99,6 +101,7 @@
|
|||||||
"@types/jsrp": "^0.2.4",
|
"@types/jsrp": "^0.2.4",
|
||||||
"@types/node": "18.11.9",
|
"@types/node": "18.11.9",
|
||||||
"@types/react": "^18.0.26",
|
"@types/react": "^18.0.26",
|
||||||
|
"@types/sanitize-html": "^2.9.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
||||||
"@typescript-eslint/parser": "^5.45.0",
|
"@typescript-eslint/parser": "^5.45.0",
|
||||||
"autoprefixer": "^10.4.7",
|
"autoprefixer": "^10.4.7",
|
||||||
@@ -7922,6 +7925,89 @@
|
|||||||
"redux": "^4.0.0"
|
"redux": "^4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/sanitize-html": {
|
||||||
|
"version": "2.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.9.0.tgz",
|
||||||
|
"integrity": "sha512-4fP/kEcKNj2u39IzrxWYuf/FnCCwwQCpif6wwY6ROUS1EPRIfWJjGkY3HIowY1EX/VbX5e86yq8AAE7UPMgATg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"htmlparser2": "^8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/sanitize-html/node_modules/dom-serializer": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.2",
|
||||||
|
"entities": "^4.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/sanitize-html/node_modules/domhandler": {
|
||||||
|
"version": "5.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||||
|
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/sanitize-html/node_modules/domutils": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"dom-serializer": "^2.0.0",
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/sanitize-html/node_modules/entities": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/sanitize-html/node_modules/htmlparser2": {
|
||||||
|
"version": "8.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
||||||
|
"integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/fb55/htmlparser2?sponsor=1",
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3",
|
||||||
|
"domutils": "^3.0.1",
|
||||||
|
"entities": "^4.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/scheduler": {
|
"node_modules/@types/scheduler": {
|
||||||
"version": "0.16.3",
|
"version": "0.16.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz",
|
||||||
@@ -10745,7 +10831,6 @@
|
|||||||
"version": "4.3.1",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||||
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -11203,7 +11288,6 @@
|
|||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||||
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -12690,8 +12774,7 @@
|
|||||||
"node_modules/fast-deep-equal": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/fast-diff": {
|
"node_modules/fast-diff": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
@@ -17860,6 +17943,11 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/parse-srcset": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q=="
|
||||||
|
},
|
||||||
"node_modules/parseurl": {
|
"node_modules/parseurl": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
@@ -18113,7 +18201,6 @@
|
|||||||
"version": "8.4.23",
|
"version": "8.4.23",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz",
|
||||||
"integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==",
|
"integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -18936,6 +19023,18 @@
|
|||||||
"react-dom": ">=16.8.0"
|
"react-dom": ">=16.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-contenteditable": {
|
||||||
|
"version": "3.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-contenteditable/-/react-contenteditable-3.3.7.tgz",
|
||||||
|
"integrity": "sha512-GA9NbC0DkDdpN3iGvib/OMHWTJzDX2cfkgy5Tt98JJAbA3kLnyrNbBIpsSpPpq7T8d3scD39DHP+j8mAM7BIfQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"prop-types": "^15.7.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-docgen": {
|
"node_modules/react-docgen": {
|
||||||
"version": "5.4.3",
|
"version": "5.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-5.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-5.4.3.tgz",
|
||||||
@@ -20017,6 +20116,88 @@
|
|||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/sanitize-html": {
|
||||||
|
"version": "2.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.11.0.tgz",
|
||||||
|
"integrity": "sha512-BG68EDHRaGKqlsNjJ2xUB7gpInPA8gVx/mvjO743hZaeMCZ2DwzW7xvsqZ+KNU4QKwj86HJ3uu2liISf2qBBUA==",
|
||||||
|
"dependencies": {
|
||||||
|
"deepmerge": "^4.2.2",
|
||||||
|
"escape-string-regexp": "^4.0.0",
|
||||||
|
"htmlparser2": "^8.0.0",
|
||||||
|
"is-plain-object": "^5.0.0",
|
||||||
|
"parse-srcset": "^1.0.2",
|
||||||
|
"postcss": "^8.3.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sanitize-html/node_modules/dom-serializer": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.2",
|
||||||
|
"entities": "^4.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sanitize-html/node_modules/domhandler": {
|
||||||
|
"version": "5.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||||
|
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sanitize-html/node_modules/domutils": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
|
||||||
|
"dependencies": {
|
||||||
|
"dom-serializer": "^2.0.0",
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sanitize-html/node_modules/entities": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sanitize-html/node_modules/htmlparser2": {
|
||||||
|
"version": "8.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
||||||
|
"integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/fb55/htmlparser2?sponsor=1",
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3",
|
||||||
|
"domutils": "^3.0.1",
|
||||||
|
"entities": "^4.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/sass-loader": {
|
"node_modules/sass-loader": {
|
||||||
"version": "12.6.0",
|
"version": "12.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz",
|
||||||
@@ -28425,6 +28606,66 @@
|
|||||||
"redux": "^4.0.0"
|
"redux": "^4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/sanitize-html": {
|
||||||
|
"version": "2.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.9.0.tgz",
|
||||||
|
"integrity": "sha512-4fP/kEcKNj2u39IzrxWYuf/FnCCwwQCpif6wwY6ROUS1EPRIfWJjGkY3HIowY1EX/VbX5e86yq8AAE7UPMgATg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"htmlparser2": "^8.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"dom-serializer": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.2",
|
||||||
|
"entities": "^4.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"domhandler": {
|
||||||
|
"version": "5.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||||
|
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"domelementtype": "^2.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"domutils": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"dom-serializer": "^2.0.0",
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"entities": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"htmlparser2": {
|
||||||
|
"version": "8.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
||||||
|
"integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3",
|
||||||
|
"domutils": "^3.0.1",
|
||||||
|
"entities": "^4.4.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/scheduler": {
|
"@types/scheduler": {
|
||||||
"version": "0.16.3",
|
"version": "0.16.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz",
|
||||||
@@ -30633,8 +30874,7 @@
|
|||||||
"deepmerge": {
|
"deepmerge": {
|
||||||
"version": "4.3.1",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||||
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"default-browser": {
|
"default-browser": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
@@ -30963,8 +31203,7 @@
|
|||||||
"domelementtype": {
|
"domelementtype": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||||
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"domhandler": {
|
"domhandler": {
|
||||||
"version": "4.3.1",
|
"version": "4.3.1",
|
||||||
@@ -32126,8 +32365,7 @@
|
|||||||
"fast-deep-equal": {
|
"fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"fast-diff": {
|
"fast-diff": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
@@ -35865,6 +36103,11 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
|
"parse-srcset": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q=="
|
||||||
|
},
|
||||||
"parseurl": {
|
"parseurl": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
@@ -36081,7 +36324,6 @@
|
|||||||
"version": "8.4.23",
|
"version": "8.4.23",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz",
|
||||||
"integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==",
|
"integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"nanoid": "^3.3.6",
|
"nanoid": "^3.3.6",
|
||||||
"picocolors": "^1.0.0",
|
"picocolors": "^1.0.0",
|
||||||
@@ -36653,6 +36895,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
|
"react-contenteditable": {
|
||||||
|
"version": "3.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-contenteditable/-/react-contenteditable-3.3.7.tgz",
|
||||||
|
"integrity": "sha512-GA9NbC0DkDdpN3iGvib/OMHWTJzDX2cfkgy5Tt98JJAbA3kLnyrNbBIpsSpPpq7T8d3scD39DHP+j8mAM7BIfQ==",
|
||||||
|
"requires": {
|
||||||
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"prop-types": "^15.7.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-docgen": {
|
"react-docgen": {
|
||||||
"version": "5.4.3",
|
"version": "5.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-5.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-5.4.3.tgz",
|
||||||
@@ -37421,6 +37672,65 @@
|
|||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"sanitize-html": {
|
||||||
|
"version": "2.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.11.0.tgz",
|
||||||
|
"integrity": "sha512-BG68EDHRaGKqlsNjJ2xUB7gpInPA8gVx/mvjO743hZaeMCZ2DwzW7xvsqZ+KNU4QKwj86HJ3uu2liISf2qBBUA==",
|
||||||
|
"requires": {
|
||||||
|
"deepmerge": "^4.2.2",
|
||||||
|
"escape-string-regexp": "^4.0.0",
|
||||||
|
"htmlparser2": "^8.0.0",
|
||||||
|
"is-plain-object": "^5.0.0",
|
||||||
|
"parse-srcset": "^1.0.2",
|
||||||
|
"postcss": "^8.3.11"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"dom-serializer": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||||
|
"requires": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.2",
|
||||||
|
"entities": "^4.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"domhandler": {
|
||||||
|
"version": "5.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||||
|
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||||
|
"requires": {
|
||||||
|
"domelementtype": "^2.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"domutils": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
|
||||||
|
"requires": {
|
||||||
|
"dom-serializer": "^2.0.0",
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"entities": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="
|
||||||
|
},
|
||||||
|
"htmlparser2": {
|
||||||
|
"version": "8.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
||||||
|
"integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
|
||||||
|
"requires": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3",
|
||||||
|
"domutils": "^3.0.1",
|
||||||
|
"entities": "^4.4.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"sass-loader": {
|
"sass-loader": {
|
||||||
"version": "12.6.0",
|
"version": "12.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz",
|
||||||
|
|||||||
@@ -75,6 +75,7 @@
|
|||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-beautiful-dnd": "^13.1.1",
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
"react-code-input": "^3.10.1",
|
"react-code-input": "^3.10.1",
|
||||||
|
"react-contenteditable": "^3.3.7",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-grid-layout": "^1.3.4",
|
"react-grid-layout": "^1.3.4",
|
||||||
"react-hook-form": "^7.43.0",
|
"react-hook-form": "^7.43.0",
|
||||||
@@ -83,6 +84,7 @@
|
|||||||
"react-markdown": "^8.0.3",
|
"react-markdown": "^8.0.3",
|
||||||
"react-redux": "^8.0.2",
|
"react-redux": "^8.0.2",
|
||||||
"react-table": "^7.8.0",
|
"react-table": "^7.8.0",
|
||||||
|
"sanitize-html": "^2.11.0",
|
||||||
"set-cookie-parser": "^2.5.1",
|
"set-cookie-parser": "^2.5.1",
|
||||||
"sharp": "^0.32.0",
|
"sharp": "^0.32.0",
|
||||||
"styled-components": "^5.3.7",
|
"styled-components": "^5.3.7",
|
||||||
@@ -107,6 +109,7 @@
|
|||||||
"@types/jsrp": "^0.2.4",
|
"@types/jsrp": "^0.2.4",
|
||||||
"@types/node": "18.11.9",
|
"@types/node": "18.11.9",
|
||||||
"@types/react": "^18.0.26",
|
"@types/react": "^18.0.26",
|
||||||
|
"@types/sanitize-html": "^2.9.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
||||||
"@typescript-eslint/parser": "^5.45.0",
|
"@typescript-eslint/parser": "^5.45.0",
|
||||||
"autoprefixer": "^10.4.7",
|
"autoprefixer": "^10.4.7",
|
||||||
|
|||||||
@@ -58,15 +58,13 @@ export default function NavHeader({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row items-center pt-6">
|
<div className="flex flex-row items-center pt-6">
|
||||||
<div className="mr-2 flex h-5 w-5 items-center justify-center rounded-md bg-primary text-black text-sm">
|
<div className="mr-2 flex h-5 w-5 items-center justify-center rounded-md bg-primary text-sm text-black">
|
||||||
{currentOrg?.name?.charAt(0)}
|
{currentOrg?.name?.charAt(0)}
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<Link passHref legacyBehavior href={`/org/${currentOrg?._id}/overview`}>
|
||||||
passHref
|
<a className="pl-0.5 text-sm font-semibold text-primary/80 hover:text-primary">
|
||||||
legacyBehavior
|
{currentOrg?.name}
|
||||||
href={`/org/${currentOrg?._id}/overview`}
|
</a>
|
||||||
>
|
|
||||||
<a className="text-sm font-semibold text-primary/80 hover:text-primary pl-0.5">{currentOrg?.name}</a>
|
|
||||||
</Link>
|
</Link>
|
||||||
{isProjectRelated && (
|
{isProjectRelated && (
|
||||||
<>
|
<>
|
||||||
@@ -85,7 +83,7 @@ export default function NavHeader({
|
|||||||
<Link
|
<Link
|
||||||
passHref
|
passHref
|
||||||
legacyBehavior
|
legacyBehavior
|
||||||
href={{ pathname: "/project/[id]/secrets", query: { id: router.query.id } }}
|
href={{ pathname: "/project/[id]/secrets/overview", query: { id: router.query.id } }}
|
||||||
>
|
>
|
||||||
<a className="text-sm font-semibold text-primary/80 hover:text-primary">{pageName}</a>
|
<a className="text-sm font-semibold text-primary/80 hover:text-primary">{pageName}</a>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -126,7 +124,11 @@ export default function NavHeader({
|
|||||||
{index + 1 === folders?.length ? (
|
{index + 1 === folders?.length ? (
|
||||||
<span className="text-sm font-semibold text-bunker-300">{name}</span>
|
<span className="text-sm font-semibold text-bunker-300">{name}</span>
|
||||||
) : (
|
) : (
|
||||||
<Link passHref legacyBehavior href={{ pathname: "/project/[id]/secrets", query }}>
|
<Link
|
||||||
|
passHref
|
||||||
|
legacyBehavior
|
||||||
|
href={{ pathname: "/project/[id]/secrets/[env]", query }}
|
||||||
|
>
|
||||||
<a className="text-sm font-semibold text-primary/80 hover:text-primary">
|
<a className="text-sm font-semibold text-primary/80 hover:text-primary">
|
||||||
{name === "root" ? selectedEnv?.name : name}
|
{name === "root" ? selectedEnv?.name : name}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
/* eslint-disable react/no-danger */
|
||||||
|
import { HTMLAttributes } from "react";
|
||||||
|
import ContentEditable from "react-contenteditable";
|
||||||
|
import sanitizeHtml from "sanitize-html";
|
||||||
|
|
||||||
|
import { useToggle } from "@app/hooks";
|
||||||
|
|
||||||
|
const REGEX = /\${([^}]+)}/g;
|
||||||
|
const stripSpanTags = (str: string) => str.replace(/<\/?span[^>]*>/g, "");
|
||||||
|
const replaceContentWithDot = (str: string) => {
|
||||||
|
let finalStr = "";
|
||||||
|
let isHtml = false;
|
||||||
|
for (let i = 0; i < str.length; i += 1) {
|
||||||
|
const char = str.at(i);
|
||||||
|
|
||||||
|
if (char === "<" || char === ">") {
|
||||||
|
isHtml = char === "<";
|
||||||
|
finalStr += char;
|
||||||
|
} else if (!isHtml && char !== "\n") {
|
||||||
|
finalStr += "•";
|
||||||
|
} else {
|
||||||
|
finalStr += char;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return finalStr;
|
||||||
|
};
|
||||||
|
|
||||||
|
const syntaxHighlight = (orgContent?: string | null, isVisible?: boolean) => {
|
||||||
|
if (!orgContent) return "";
|
||||||
|
if (!isVisible) return replaceContentWithDot(orgContent);
|
||||||
|
const content = stripSpanTags(orgContent);
|
||||||
|
const newContent = content.replace(
|
||||||
|
REGEX,
|
||||||
|
(_a, b) =>
|
||||||
|
`<span class="ph-no-capture text-yellow">${<span class="ph-no-capture text-yello-200/80">${b}</span>}</span>`
|
||||||
|
);
|
||||||
|
|
||||||
|
return newContent;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sanitizeConf = {
|
||||||
|
allowedTags: ["div", "span", "br", "p"]
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = Omit<HTMLAttributes<HTMLDivElement>, "onChange" | "onBlur"> & {
|
||||||
|
value?: string | null;
|
||||||
|
isVisible?: boolean;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
onChange?: (val: string, html: string) => void;
|
||||||
|
onBlur?: (sanitizedHtml: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SecretInput = ({
|
||||||
|
value,
|
||||||
|
isVisible,
|
||||||
|
onChange,
|
||||||
|
onBlur,
|
||||||
|
isDisabled,
|
||||||
|
...props
|
||||||
|
}: Props) => {
|
||||||
|
const [isSecretFocused, setIsSecretFocused] = useToggle();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="thin-scrollbar relative overflow-y-auto overflow-x-hidden"
|
||||||
|
style={{ maxHeight: `${21 * 7}px` }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: syntaxHighlight(value, isVisible || isSecretFocused)
|
||||||
|
}}
|
||||||
|
className="absolute top-0 left-0 z-0 h-full w-full text-ellipsis whitespace-pre-line break-all"
|
||||||
|
/>
|
||||||
|
<ContentEditable
|
||||||
|
className="relative z-10 h-full w-full text-ellipsis whitespace-pre-line break-all text-transparent caret-white outline-none"
|
||||||
|
role="textbox"
|
||||||
|
onChange={(evt) => {
|
||||||
|
if (onChange) onChange(evt.currentTarget.innerText.trim(), evt.currentTarget.innerHTML);
|
||||||
|
}}
|
||||||
|
onFocus={() => setIsSecretFocused.on()}
|
||||||
|
disabled={isDisabled}
|
||||||
|
spellCheck={false}
|
||||||
|
onBlur={(evt) => {
|
||||||
|
if (onBlur) onBlur(sanitizeHtml(evt.currentTarget.innerHTML || "", sanitizeConf));
|
||||||
|
setIsSecretFocused.off();
|
||||||
|
}}
|
||||||
|
html={isVisible || isSecretFocused ? value || "" : syntaxHighlight(value, false)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { SecretInput } from "./SecretInput";
|
||||||
@@ -52,7 +52,7 @@ export const Loading: Story = {
|
|||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
<TableSkeleton columns={3} key="story-book-table" />
|
<TableSkeleton columns={3} innerKey="story-book-table" />
|
||||||
</TBody>
|
</TBody>
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
|
|||||||
@@ -33,10 +33,7 @@ export type TableProps = {
|
|||||||
|
|
||||||
export const Table = ({ children, className }: TableProps): JSX.Element => (
|
export const Table = ({ children, className }: TableProps): JSX.Element => (
|
||||||
<table
|
<table
|
||||||
className={twMerge(
|
className={twMerge("w-full bg-mineshaft-800 p-2 text-left text-sm text-gray-300", className)}
|
||||||
"w-full bg-mineshaft-800 p-2 text-left text-sm text-gray-300",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</table>
|
</table>
|
||||||
@@ -58,11 +55,24 @@ export const THead = ({ children, className }: THeadProps): JSX.Element => (
|
|||||||
export type TrProps = {
|
export type TrProps = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
isHoverable?: boolean;
|
||||||
|
isSelectable?: boolean;
|
||||||
} & HTMLAttributes<HTMLTableRowElement>;
|
} & HTMLAttributes<HTMLTableRowElement>;
|
||||||
|
|
||||||
export const Tr = ({ children, className, ...props }: TrProps): JSX.Element => (
|
export const Tr = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
isHoverable,
|
||||||
|
isSelectable,
|
||||||
|
...props
|
||||||
|
}: TrProps): JSX.Element => (
|
||||||
<tr
|
<tr
|
||||||
className={twMerge("border border-solid border-mineshaft-700 cursor-default", className)}
|
className={twMerge(
|
||||||
|
"cursor-default border border-solid border-mineshaft-700",
|
||||||
|
isHoverable && "hover:bg-mineshaft-600",
|
||||||
|
isSelectable && "cursor-pointer",
|
||||||
|
className
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -76,7 +86,14 @@ export type ThProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const Th = ({ children, className }: ThProps): JSX.Element => (
|
export const Th = ({ children, className }: ThProps): JSX.Element => (
|
||||||
<th className={twMerge("bg-mineshaft-800 px-5 pt-4 pb-3.5 font-semibold border-b-2 border-mineshaft-600", className)}>{children}</th>
|
<th
|
||||||
|
className={twMerge(
|
||||||
|
"border-b-2 border-mineshaft-600 bg-mineshaft-800 px-5 pt-4 pb-3.5 font-semibold",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</th>
|
||||||
);
|
);
|
||||||
|
|
||||||
// table body
|
// table body
|
||||||
@@ -106,15 +123,15 @@ export type TBodyLoader = {
|
|||||||
columns: number;
|
columns: number;
|
||||||
className?: string;
|
className?: string;
|
||||||
// unique key for mapping
|
// unique key for mapping
|
||||||
key: string;
|
innerKey: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TableSkeleton = ({ rows = 3, columns, key, className }: TBodyLoader): JSX.Element => (
|
export const TableSkeleton = ({ rows = 3, columns, innerKey, className }: TBodyLoader): JSX.Element => (
|
||||||
<>
|
<>
|
||||||
{Array.apply(0, Array(rows)).map((_x, i) => (
|
{Array.apply(0, Array(rows)).map((_x, i) => (
|
||||||
<Tr key={`${key}-skeleton-rows-${i + 1}`}>
|
<Tr key={`${innerKey}-skeleton-rows-${i + 1}`}>
|
||||||
{Array.apply(0, Array(columns)).map((_y, j) => (
|
{Array.apply(0, Array(columns)).map((_y, j) => (
|
||||||
<Td key={`${key}-skeleton-rows-${i + 1}-column-${j + 1}`}>
|
<Td key={`${innerKey}-skeleton-rows-${i + 1}-column-${j + 1}`}>
|
||||||
<Skeleton className={className} />
|
<Skeleton className={className} />
|
||||||
</Td>
|
</Td>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export * from "./Checkbox";
|
|||||||
export * from "./DeleteActionModal";
|
export * from "./DeleteActionModal";
|
||||||
export * from "./Drawer";
|
export * from "./Drawer";
|
||||||
export * from "./Dropdown";
|
export * from "./Dropdown";
|
||||||
export * from "./EmailServiceSetupModal"
|
export * from "./EmailServiceSetupModal";
|
||||||
export * from "./EmptyState";
|
export * from "./EmptyState";
|
||||||
export * from "./FormControl";
|
export * from "./FormControl";
|
||||||
export * from "./HoverCardv2";
|
export * from "./HoverCardv2";
|
||||||
@@ -13,6 +13,7 @@ export * from "./Input";
|
|||||||
export * from "./Menu";
|
export * from "./Menu";
|
||||||
export * from "./Modal";
|
export * from "./Modal";
|
||||||
export * from "./Popoverv2";
|
export * from "./Popoverv2";
|
||||||
|
export * from "./SecretInput";
|
||||||
export * from "./Select";
|
export * from "./Select";
|
||||||
export * from "./Skeleton";
|
export * from "./Skeleton";
|
||||||
export * from "./Spinner";
|
export * from "./Spinner";
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export {
|
export {
|
||||||
useCreateFolder,
|
useCreateFolder,
|
||||||
useDeleteFolder,
|
useDeleteFolder,
|
||||||
|
useGetFoldersByEnv,
|
||||||
useGetProjectFolders,
|
useGetProjectFolders,
|
||||||
useGetProjectFoldersBatch,
|
useGetProjectFoldersBatch,
|
||||||
useUpdateFolder
|
useUpdateFolder
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
DeleteFolderDTO,
|
DeleteFolderDTO,
|
||||||
GetProjectFoldersBatchDTO,
|
GetProjectFoldersBatchDTO,
|
||||||
GetProjectFoldersDTO,
|
GetProjectFoldersDTO,
|
||||||
|
TGetFoldersByEnvDTO,
|
||||||
TSecretFolder,
|
TSecretFolder,
|
||||||
UpdateFolderDTO
|
UpdateFolderDTO
|
||||||
} from "./types";
|
} from "./types";
|
||||||
@@ -62,6 +63,48 @@ export const useGetProjectFolders = ({
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const useGetFoldersByEnv = ({
|
||||||
|
parentFolderPath,
|
||||||
|
workspaceId,
|
||||||
|
environments,
|
||||||
|
parentFolderId
|
||||||
|
}: TGetFoldersByEnvDTO) => {
|
||||||
|
const folders = useQueries({
|
||||||
|
queries: environments.map((env) => ({
|
||||||
|
queryKey: queryKeys.getSecretFolders(workspaceId, env, parentFolderPath || parentFolderId),
|
||||||
|
queryFn: async () => fetchProjectFolders(workspaceId, env, parentFolderId, parentFolderPath),
|
||||||
|
enabled: Boolean(workspaceId) && Boolean(env)
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
|
||||||
|
const folderNames = useMemo(() => {
|
||||||
|
const names = new Set<string>();
|
||||||
|
folders?.forEach(({ data }) => {
|
||||||
|
data?.folders.forEach(({ name }) => {
|
||||||
|
names.add(name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return [...names];
|
||||||
|
}, [(folders || []).map((folder) => folder.data)]);
|
||||||
|
|
||||||
|
const isFolderPresentInEnv = useCallback(
|
||||||
|
(name: string, env: string) => {
|
||||||
|
const selectedEnvIndex = environments.indexOf(env);
|
||||||
|
if (selectedEnvIndex !== -1) {
|
||||||
|
return Boolean(
|
||||||
|
folders?.[selectedEnvIndex]?.data?.folders?.find(
|
||||||
|
({ name: folderName }) => folderName === name
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
[(folders || []).map((folder) => folder.data)]
|
||||||
|
);
|
||||||
|
|
||||||
|
return { folders, folderNames, isFolderPresentInEnv };
|
||||||
|
};
|
||||||
|
|
||||||
export const useGetProjectFoldersBatch = ({
|
export const useGetProjectFoldersBatch = ({
|
||||||
folders = [],
|
folders = [],
|
||||||
isPaused,
|
isPaused,
|
||||||
|
|||||||
@@ -17,6 +17,13 @@ export type GetProjectFoldersBatchDTO = {
|
|||||||
parentFolderPath?: string;
|
parentFolderPath?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TGetFoldersByEnvDTO = {
|
||||||
|
environments: string[];
|
||||||
|
workspaceId: string;
|
||||||
|
parentFolderPath?: string;
|
||||||
|
parentFolderId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type CreateFolderDTO = {
|
export type CreateFolderDTO = {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
export { useCreateSecretV3, useDeleteSecretV3, useUpdateSecretV3 } from "./mutations";
|
||||||
export {
|
export {
|
||||||
useBatchSecretsOp,
|
useBatchSecretsOp,
|
||||||
useGetProjectSecrets,
|
useGetProjectSecrets,
|
||||||
useGetProjectSecretsByKey,
|
useGetProjectSecretsAllEnv,
|
||||||
useGetSecretVersion
|
useGetSecretVersion
|
||||||
} from "./queries";
|
} from "./queries";
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
import crypto from "crypto";
|
||||||
|
|
||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import {
|
||||||
|
decryptAssymmetric,
|
||||||
|
encryptSymmetric
|
||||||
|
} from "@app/components/utilities/cryptography/crypto";
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
import { secretKeys } from "./queries";
|
||||||
|
import { TCreateSecretsV3DTO, TDeleteSecretsV3DTO, TUpdateSecretsV3DTO } from "./types";
|
||||||
|
|
||||||
|
const encryptSecret = (randomBytes: string, key: string, value?: string, comment?: string) => {
|
||||||
|
// encrypt key
|
||||||
|
const {
|
||||||
|
ciphertext: secretKeyCiphertext,
|
||||||
|
iv: secretKeyIV,
|
||||||
|
tag: secretKeyTag
|
||||||
|
} = encryptSymmetric({
|
||||||
|
plaintext: key,
|
||||||
|
key: randomBytes
|
||||||
|
});
|
||||||
|
|
||||||
|
// encrypt value
|
||||||
|
const {
|
||||||
|
ciphertext: secretValueCiphertext,
|
||||||
|
iv: secretValueIV,
|
||||||
|
tag: secretValueTag
|
||||||
|
} = encryptSymmetric({
|
||||||
|
plaintext: value ?? "",
|
||||||
|
key: randomBytes
|
||||||
|
});
|
||||||
|
|
||||||
|
// encrypt comment
|
||||||
|
const {
|
||||||
|
ciphertext: secretCommentCiphertext,
|
||||||
|
iv: secretCommentIV,
|
||||||
|
tag: secretCommentTag
|
||||||
|
} = encryptSymmetric({
|
||||||
|
plaintext: comment ?? "",
|
||||||
|
key: randomBytes
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
secretCommentCiphertext,
|
||||||
|
secretCommentIV,
|
||||||
|
secretCommentTag
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCreateSecretV3 = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<{}, {}, TCreateSecretsV3DTO>({
|
||||||
|
mutationFn: async ({
|
||||||
|
secretPath = "/",
|
||||||
|
type,
|
||||||
|
environment,
|
||||||
|
workspaceId,
|
||||||
|
secretName,
|
||||||
|
secretValue,
|
||||||
|
latestFileKey,
|
||||||
|
secretComment
|
||||||
|
}) => {
|
||||||
|
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
||||||
|
|
||||||
|
const randomBytes = latestFileKey
|
||||||
|
? decryptAssymmetric({
|
||||||
|
ciphertext: latestFileKey.encryptedKey,
|
||||||
|
nonce: latestFileKey.nonce,
|
||||||
|
publicKey: latestFileKey.sender.publicKey,
|
||||||
|
privateKey: PRIVATE_KEY
|
||||||
|
})
|
||||||
|
: crypto.randomBytes(16).toString("hex");
|
||||||
|
|
||||||
|
const reqBody = {
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
type,
|
||||||
|
secretPath,
|
||||||
|
...encryptSecret(randomBytes, secretName, secretValue, secretComment)
|
||||||
|
};
|
||||||
|
const { data } = await apiRequest.post(`/api/v3/secrets/${secretName}`, reqBody);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { workspaceId, environment, secretPath }) => {
|
||||||
|
queryClient.invalidateQueries(
|
||||||
|
secretKeys.getProjectSecret(workspaceId, environment, secretPath)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateSecretV3 = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<{}, {}, TUpdateSecretsV3DTO>({
|
||||||
|
mutationFn: async ({
|
||||||
|
secretPath = "/",
|
||||||
|
type,
|
||||||
|
environment,
|
||||||
|
workspaceId,
|
||||||
|
secretName,
|
||||||
|
secretValue,
|
||||||
|
latestFileKey
|
||||||
|
}) => {
|
||||||
|
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
||||||
|
|
||||||
|
const randomBytes = latestFileKey
|
||||||
|
? decryptAssymmetric({
|
||||||
|
ciphertext: latestFileKey.encryptedKey,
|
||||||
|
nonce: latestFileKey.nonce,
|
||||||
|
publicKey: latestFileKey.sender.publicKey,
|
||||||
|
privateKey: PRIVATE_KEY
|
||||||
|
})
|
||||||
|
: crypto.randomBytes(16).toString("hex");
|
||||||
|
const { secretValueIV, secretValueTag, secretValueCiphertext } = encryptSecret(
|
||||||
|
randomBytes,
|
||||||
|
secretName,
|
||||||
|
secretValue,
|
||||||
|
""
|
||||||
|
);
|
||||||
|
|
||||||
|
const reqBody = {
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
type,
|
||||||
|
secretPath,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
secretValueCiphertext
|
||||||
|
};
|
||||||
|
const { data } = await apiRequest.patch(`/api/v3/secrets/${secretName}`, reqBody);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { workspaceId, environment, secretPath }) => {
|
||||||
|
queryClient.invalidateQueries(
|
||||||
|
secretKeys.getProjectSecret(workspaceId, environment, secretPath)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteSecretV3 = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<{}, {}, TDeleteSecretsV3DTO>({
|
||||||
|
mutationFn: async ({ secretPath = "/", type, environment, workspaceId, secretName }) => {
|
||||||
|
const reqBody = {
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
type,
|
||||||
|
secretPath
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data } = await apiRequest.delete(`/api/v3/secrets/${secretName}`, {
|
||||||
|
data: reqBody
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { workspaceId, environment, secretPath }) => {
|
||||||
|
queryClient.invalidateQueries(
|
||||||
|
secretKeys.getProjectSecret(workspaceId, environment, secretPath)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable no-param-reassign */
|
/* eslint-disable no-param-reassign */
|
||||||
import { useCallback } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
decryptAssymmetric,
|
decryptAssymmetric,
|
||||||
@@ -15,7 +15,8 @@ import {
|
|||||||
EncryptedSecret,
|
EncryptedSecret,
|
||||||
EncryptedSecretVersion,
|
EncryptedSecretVersion,
|
||||||
GetProjectSecretsDTO,
|
GetProjectSecretsDTO,
|
||||||
GetSecretVersionsDTO
|
GetSecretVersionsDTO,
|
||||||
|
TGetProjectSecretsAllEnvDTO
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export const secretKeys = {
|
export const secretKeys = {
|
||||||
@@ -37,40 +38,15 @@ const fetchProjectEncryptedSecrets = async (
|
|||||||
folderId?: string,
|
folderId?: string,
|
||||||
secretPath?: string
|
secretPath?: string
|
||||||
) => {
|
) => {
|
||||||
if (typeof env === "string") {
|
const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>("/api/v2/secrets", {
|
||||||
const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>("/api/v2/secrets", {
|
params: {
|
||||||
params: {
|
environment: env,
|
||||||
environment: env,
|
workspaceId,
|
||||||
workspaceId,
|
folderId: folderId || undefined,
|
||||||
folderId: folderId || undefined,
|
secretPath
|
||||||
secretPath
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return data.secrets;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof env === "object") {
|
|
||||||
let allEnvData: any = [];
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
|
||||||
for (const envPoint of env) {
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>("/api/v2/secrets", {
|
|
||||||
params: {
|
|
||||||
environment: envPoint,
|
|
||||||
workspaceId,
|
|
||||||
folderId,
|
|
||||||
secretPath
|
|
||||||
}
|
|
||||||
});
|
|
||||||
allEnvData = allEnvData.concat(data.secrets);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
return allEnvData;
|
return data.secrets;
|
||||||
// eslint-disable-next-line no-else-return
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetProjectSecrets = ({
|
export const useGetProjectSecrets = ({
|
||||||
@@ -160,22 +136,19 @@ export const useGetProjectSecrets = ({
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useGetProjectSecretsByKey = ({
|
export const useGetProjectSecretsAllEnv = ({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
env,
|
envs,
|
||||||
decryptFileKey,
|
decryptFileKey,
|
||||||
isPaused,
|
|
||||||
folderId,
|
folderId,
|
||||||
secretPath
|
secretPath
|
||||||
}: GetProjectSecretsDTO) =>
|
}: TGetProjectSecretsAllEnvDTO) => {
|
||||||
useQuery({
|
const secrets = useQueries({
|
||||||
// wait for all values to be available
|
queries: envs.map((env) => ({
|
||||||
enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused,
|
queryKey: secretKeys.getProjectSecret(workspaceId, env, secretPath || folderId),
|
||||||
// right now secretpath is passed as folderid as only this is used in overview
|
enabled: Boolean(decryptFileKey && workspaceId && env),
|
||||||
queryKey: secretKeys.getProjectSecret(workspaceId, env, secretPath),
|
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId, secretPath),
|
||||||
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId, secretPath),
|
select: (data: EncryptedSecret[]) => {
|
||||||
select: useCallback(
|
|
||||||
(data: EncryptedSecret[]) => {
|
|
||||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
||||||
const latestKey = decryptFileKey;
|
const latestKey = decryptFileKey;
|
||||||
const key = decryptAssymmetric({
|
const key = decryptAssymmetric({
|
||||||
@@ -185,12 +158,11 @@ export const useGetProjectSecretsByKey = ({
|
|||||||
privateKey: PRIVATE_KEY
|
privateKey: PRIVATE_KEY
|
||||||
});
|
});
|
||||||
|
|
||||||
const sharedSecrets: Record<string, DecryptedSecret[]> = {};
|
const sharedSecrets: Record<string, DecryptedSecret> = {};
|
||||||
const personalSecrets: Record<string, { id: string; value: string }> = {};
|
const personalSecrets: Record<string, { id: string; value: string }> = {};
|
||||||
// this used for add-only mode in dashboard
|
// this used for add-only mode in dashboard
|
||||||
// type won't be there thus only one key is shown
|
// type won't be there thus only one key is shown
|
||||||
const duplicateSecretKey: Record<string, boolean> = {};
|
const duplicateSecretKey: Record<string, boolean> = {};
|
||||||
const uniqSecKeys: Record<string, boolean> = {};
|
|
||||||
data.forEach((encSecret: EncryptedSecret) => {
|
data.forEach((encSecret: EncryptedSecret) => {
|
||||||
const secretKey = decryptSymmetric({
|
const secretKey = decryptSymmetric({
|
||||||
ciphertext: encSecret.secretKeyCiphertext,
|
ciphertext: encSecret.secretKeyCiphertext,
|
||||||
@@ -198,7 +170,6 @@ export const useGetProjectSecretsByKey = ({
|
|||||||
tag: encSecret.secretKeyTag,
|
tag: encSecret.secretKeyTag,
|
||||||
key
|
key
|
||||||
});
|
});
|
||||||
if (!uniqSecKeys?.[secretKey]) uniqSecKeys[secretKey] = true;
|
|
||||||
|
|
||||||
const secretValue = decryptSymmetric({
|
const secretValue = decryptSymmetric({
|
||||||
ciphertext: encSecret.secretValueCiphertext,
|
ciphertext: encSecret.secretValueCiphertext,
|
||||||
@@ -226,35 +197,65 @@ export const useGetProjectSecretsByKey = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (encSecret.type === "personal") {
|
if (encSecret.type === "personal") {
|
||||||
personalSecrets[`${decryptedSecret.key}-${decryptedSecret.env}`] = {
|
personalSecrets[decryptedSecret.key] = {
|
||||||
id: encSecret._id,
|
id: encSecret._id,
|
||||||
value: secretValue
|
value: secretValue
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
if (!duplicateSecretKey?.[`${decryptedSecret.key}-${decryptedSecret.env}`]) {
|
if (!duplicateSecretKey?.[decryptedSecret.key]) {
|
||||||
if (!sharedSecrets?.[secretKey]) sharedSecrets[secretKey] = [];
|
sharedSecrets[decryptedSecret.key] = decryptedSecret;
|
||||||
sharedSecrets[secretKey].push(decryptedSecret);
|
|
||||||
}
|
}
|
||||||
duplicateSecretKey[`${decryptedSecret.key}-${decryptedSecret.env}`] = true;
|
duplicateSecretKey[decryptedSecret.key] = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Object.keys(sharedSecrets).forEach((secName) => {
|
|
||||||
sharedSecrets[secName].forEach((val) => {
|
|
||||||
const dupKey = `${val.key}-${val.env}`;
|
|
||||||
if (personalSecrets?.[dupKey]) {
|
|
||||||
val.idOverride = personalSecrets[dupKey].id;
|
|
||||||
val.valueOverride = personalSecrets[dupKey].value;
|
|
||||||
val.overrideAction = "modified";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return { secrets: sharedSecrets, uniqueSecCount: Object.keys(uniqSecKeys).length };
|
Object.keys(sharedSecrets).forEach((val) => {
|
||||||
},
|
if (personalSecrets?.[val]) {
|
||||||
[decryptFileKey]
|
sharedSecrets[val].idOverride = personalSecrets[val].id;
|
||||||
)
|
sharedSecrets[val].valueOverride = personalSecrets[val].value;
|
||||||
|
sharedSecrets[val].overrideAction = "modified";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return sharedSecrets;
|
||||||
|
}
|
||||||
|
}))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const secKeys = useMemo(() => {
|
||||||
|
const keys = new Set<string>();
|
||||||
|
secrets?.forEach(({ data }) => {
|
||||||
|
// TODO(akhilmhdh): find out why this is unknown
|
||||||
|
Object.keys(data || {}).forEach((key) => keys.add(key));
|
||||||
|
});
|
||||||
|
return [...keys];
|
||||||
|
}, [(secrets || []).map((sec) => sec.data)]);
|
||||||
|
|
||||||
|
const getEnvSecretKeyCount = useCallback(
|
||||||
|
(env: string) => {
|
||||||
|
const selectedEnvIndex = envs.indexOf(env);
|
||||||
|
if (selectedEnvIndex !== -1) {
|
||||||
|
return Object.keys(secrets[selectedEnvIndex]?.data || {}).length;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
[(secrets || []).map((sec) => sec.data)]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getSecretByKey = useCallback(
|
||||||
|
(env: string, key: string) => {
|
||||||
|
const selectedEnvIndex = envs.indexOf(env);
|
||||||
|
if (selectedEnvIndex !== -1) {
|
||||||
|
const sec = secrets[selectedEnvIndex]?.data?.[key];
|
||||||
|
return sec;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
[(secrets || []).map((sec) => sec.data)]
|
||||||
|
);
|
||||||
|
|
||||||
|
return { data: secrets, secKeys, getSecretByKey, getEnvSecretKeyCount };
|
||||||
|
};
|
||||||
|
|
||||||
const fetchEncryptedSecretVersion = async (secretId: string, offset: number, limit: number) => {
|
const fetchEncryptedSecretVersion = async (secretId: string, offset: number, limit: number) => {
|
||||||
const { data } = await apiRequest.get<{ secretVersions: EncryptedSecretVersion[] }>(
|
const { data } = await apiRequest.get<{ secretVersions: EncryptedSecretVersion[] }>(
|
||||||
`/api/v1/secret/${secretId}/secret-versions`,
|
`/api/v1/secret/${secretId}/secret-versions`,
|
||||||
|
|||||||
@@ -103,9 +103,47 @@ export type GetProjectSecretsDTO = {
|
|||||||
onSuccess?: (data: DecryptedSecret[]) => void;
|
onSuccess?: (data: DecryptedSecret[]) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TGetProjectSecretsAllEnvDTO = {
|
||||||
|
workspaceId: string;
|
||||||
|
envs: string[];
|
||||||
|
decryptFileKey: UserWsKeyPair;
|
||||||
|
folderId?: string;
|
||||||
|
secretPath?: string;
|
||||||
|
isPaused?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export type GetSecretVersionsDTO = {
|
export type GetSecretVersionsDTO = {
|
||||||
secretId: string;
|
secretId: string;
|
||||||
limit: number;
|
limit: number;
|
||||||
offset: number;
|
offset: number;
|
||||||
decryptFileKey: UserWsKeyPair;
|
decryptFileKey: UserWsKeyPair;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TCreateSecretsV3DTO = {
|
||||||
|
latestFileKey: UserWsKeyPair;
|
||||||
|
secretName: string;
|
||||||
|
secretValue: string;
|
||||||
|
secretComment: string;
|
||||||
|
secretPath: string;
|
||||||
|
workspaceId: string;
|
||||||
|
environment: string;
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TUpdateSecretsV3DTO = {
|
||||||
|
latestFileKey: UserWsKeyPair;
|
||||||
|
workspaceId: string;
|
||||||
|
environment: string;
|
||||||
|
type: string;
|
||||||
|
secretPath: string;
|
||||||
|
secretName: string;
|
||||||
|
secretValue: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDeleteSecretsV3DTO = {
|
||||||
|
workspaceId: string;
|
||||||
|
environment: string;
|
||||||
|
type: string;
|
||||||
|
secretPath: string;
|
||||||
|
secretName: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -13,7 +13,18 @@ import { useTranslation } from "react-i18next";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons";
|
import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons";
|
||||||
import { faAngleDown, faArrowLeft, faArrowUpRightFromSquare, faBook, faCheck, faEnvelope, faInfinity, faMobile, faPlus, faQuestion } from "@fortawesome/free-solid-svg-icons";
|
import {
|
||||||
|
faAngleDown,
|
||||||
|
faArrowLeft,
|
||||||
|
faArrowUpRightFromSquare,
|
||||||
|
faBook,
|
||||||
|
faCheck,
|
||||||
|
faEnvelope,
|
||||||
|
faInfinity,
|
||||||
|
faMobile,
|
||||||
|
faPlus,
|
||||||
|
faQuestion
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
|
import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
|
||||||
@@ -41,7 +52,14 @@ import {
|
|||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useOrganization, useSubscription, useUser, useWorkspace } from "@app/context";
|
import { useOrganization, useSubscription, useUser, useWorkspace } from "@app/context";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import { fetchOrgUsers, useAddUserToWs, useCreateWorkspace, useGetOrgTrialUrl, useLogoutUser, useUploadWsKey } from "@app/hooks/api";
|
import {
|
||||||
|
fetchOrgUsers,
|
||||||
|
useAddUserToWs,
|
||||||
|
useCreateWorkspace,
|
||||||
|
useGetOrgTrialUrl,
|
||||||
|
useLogoutUser,
|
||||||
|
useUploadWsKey
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -89,7 +107,9 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
// const [ isLearningNoteOpen, setIsLearningNoteOpen ] = useState(true);
|
// const [ isLearningNoteOpen, setIsLearningNoteOpen ] = useState(true);
|
||||||
|
|
||||||
const isAddingProjectsAllowed = subscription?.workspaceLimit ? (subscription.workspacesUsed < subscription.workspaceLimit) : true;
|
const isAddingProjectsAllowed = subscription?.workspaceLimit
|
||||||
|
? subscription.workspacesUsed < subscription.workspaceLimit
|
||||||
|
: true;
|
||||||
|
|
||||||
const createWs = useCreateWorkspace();
|
const createWs = useCreateWorkspace();
|
||||||
const uploadWsKey = useUploadWsKey();
|
const uploadWsKey = useUploadWsKey();
|
||||||
@@ -110,22 +130,22 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleRouteChange = () => {
|
const handleRouteChange = () => {
|
||||||
(window).Intercom("update");
|
window.Intercom("update");
|
||||||
};
|
};
|
||||||
|
|
||||||
router.events.on("routeChangeComplete", handleRouteChange);
|
router.events.on("routeChangeComplete", handleRouteChange);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
router.events.off("routeChangeComplete", handleRouteChange);
|
router.events.off("routeChangeComplete", handleRouteChange);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const logout = useLogoutUser();
|
const logout = useLogoutUser();
|
||||||
const logOutUser = async () => {
|
const logOutUser = async () => {
|
||||||
try {
|
try {
|
||||||
console.log("Logging out...")
|
console.log("Logging out...");
|
||||||
await logout.mutateAsync();
|
await logout.mutateAsync();
|
||||||
localStorage.removeItem("protectedKey");
|
localStorage.removeItem("protectedKey");
|
||||||
localStorage.removeItem("protectedKeyIV");
|
localStorage.removeItem("protectedKeyIV");
|
||||||
@@ -145,27 +165,30 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
|
|
||||||
const changeOrg = async (orgId) => {
|
const changeOrg = async (orgId) => {
|
||||||
localStorage.setItem("orgData.id", orgId);
|
localStorage.setItem("orgData.id", orgId);
|
||||||
router.push(`/org/${orgId}/overview`)
|
router.push(`/org/${orgId}/overview`);
|
||||||
}
|
};
|
||||||
|
|
||||||
// TODO(akhilmhdh): This entire logic will be rechecked and will try to avoid
|
// TODO(akhilmhdh): This entire logic will be rechecked and will try to avoid
|
||||||
// Placing the localstorage as much as possible
|
// Placing the localstorage as much as possible
|
||||||
// Wait till tony integrates the azure and its launched
|
// Wait till tony integrates the azure and its launched
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
||||||
// Put a user in an org if they're not in one yet
|
// Put a user in an org if they're not in one yet
|
||||||
const putUserInOrg = async () => {
|
const putUserInOrg = async () => {
|
||||||
if (tempLocalStorage("orgData.id") === "") {
|
if (tempLocalStorage("orgData.id") === "") {
|
||||||
localStorage.setItem("orgData.id", orgs[0]?._id);
|
localStorage.setItem("orgData.id", orgs[0]?._id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentOrg && (
|
if (
|
||||||
(workspaces?.length === 0 && router.asPath.includes("project"))
|
currentOrg &&
|
||||||
|| router.asPath.includes("/project/undefined")
|
((workspaces?.length === 0 && router.asPath.includes("project")) ||
|
||||||
|| (!orgs?.map(org => org._id)?.includes(router.query.id) && !router.asPath.includes("project") && !router.asPath.includes("personal") && !router.asPath.includes("integration"))
|
router.asPath.includes("/project/undefined") ||
|
||||||
)) {
|
(!orgs?.map((org) => org._id)?.includes(router.query.id) &&
|
||||||
|
!router.asPath.includes("project") &&
|
||||||
|
!router.asPath.includes("personal") &&
|
||||||
|
!router.asPath.includes("integration")))
|
||||||
|
) {
|
||||||
router.push(`/org/${currentOrg?._id}/overview`);
|
router.push(`/org/${currentOrg?._id}/overview`);
|
||||||
}
|
}
|
||||||
// else if (!router.asPath.includes("org") && !router.asPath.includes("project") && !router.asPath.includes("integrations") && !router.asPath.includes("personal-settings")) {
|
// else if (!router.asPath.includes("org") && !router.asPath.includes("project") && !router.asPath.includes("integrations") && !router.asPath.includes("personal-settings")) {
|
||||||
|
|
||||||
// const pathSegments = router.asPath.split("/").filter((segment) => segment.length > 0);
|
// const pathSegments = router.asPath.split("/").filter((segment) => segment.length > 0);
|
||||||
@@ -233,7 +256,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
}
|
}
|
||||||
createNotification({ text: "Workspace created", type: "success" });
|
createNotification({ text: "Workspace created", type: "success" });
|
||||||
handlePopUpClose("addNewWs");
|
handlePopUpClose("addNewWs");
|
||||||
router.push(`/project/${newWorkspaceId}/secrets`);
|
router.push(`/project/${newWorkspaceId}/secrets/overview`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
createNotification({ text: "Failed to create workspace", type: "error" });
|
createNotification({ text: "Failed to create workspace", type: "error" });
|
||||||
@@ -244,190 +267,235 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
<>
|
<>
|
||||||
<div className="dark hidden h-screen w-full flex-col overflow-x-hidden md:flex">
|
<div className="dark hidden h-screen w-full flex-col overflow-x-hidden md:flex">
|
||||||
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
|
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
|
||||||
<aside className="w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60 dark">
|
<aside className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60">
|
||||||
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]">
|
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]">
|
||||||
<div>
|
<div>
|
||||||
{!router.asPath.includes("personal") && <div className="h-12 px-3 flex items-center pt-6 cursor-default">
|
{!router.asPath.includes("personal") && (
|
||||||
{(router.asPath.includes("project") || router.asPath.includes("integrations")) && <Link href={`/org/${currentOrg?._id}/overview`}><div className="pl-1 pr-2 text-mineshaft-400 hover:text-mineshaft-100 duration-200">
|
<div className="flex h-12 cursor-default items-center px-3 pt-6">
|
||||||
<FontAwesomeIcon icon={faArrowLeft} />
|
{(router.asPath.includes("project") ||
|
||||||
</div></Link>}
|
router.asPath.includes("integrations")) && (
|
||||||
<DropdownMenu>
|
<Link href={`/org/${currentOrg?._id}/overview`}>
|
||||||
<DropdownMenuTrigger asChild className="data-[state=open]:bg-mineshaft-600">
|
<div className="pl-1 pr-2 text-mineshaft-400 duration-200 hover:text-mineshaft-100">
|
||||||
<div className="mr-auto flex items-center hover:bg-mineshaft-600 py-1.5 pl-1.5 pr-2 rounded-md">
|
<FontAwesomeIcon icon={faArrowLeft} />
|
||||||
<div className="w-5 h-5 rounded-md bg-primary flex justify-center items-center text-sm">{currentOrg?.name.charAt(0)}</div>
|
</div>
|
||||||
<div className="pl-3 text-mineshaft-100 text-sm">{currentOrg?.name} <FontAwesomeIcon icon={faAngleDown} className="text-xs pl-1 pt-1 text-mineshaft-300" /></div>
|
</Link>
|
||||||
</div>
|
)}
|
||||||
</DropdownMenuTrigger>
|
<DropdownMenu>
|
||||||
<DropdownMenuContent align="start" className="p-1">
|
<DropdownMenuTrigger asChild className="data-[state=open]:bg-mineshaft-600">
|
||||||
<div className="text-xs text-mineshaft-400 px-2 py-1">{user?.email}</div>
|
<div className="mr-auto flex items-center rounded-md py-1.5 pl-1.5 pr-2 hover:bg-mineshaft-600">
|
||||||
{orgs?.map(org => <DropdownMenuItem key={org._id}>
|
<div className="flex h-5 w-5 items-center justify-center rounded-md bg-primary text-sm">
|
||||||
<Button
|
{currentOrg?.name.charAt(0)}
|
||||||
onClick={() => changeOrg(org?._id)}
|
</div>
|
||||||
variant="plain"
|
<div className="pl-3 text-sm text-mineshaft-100">
|
||||||
colorSchema="secondary"
|
{currentOrg?.name}{" "}
|
||||||
size="xs"
|
<FontAwesomeIcon
|
||||||
className="w-full flex items-center justify-start p-0 font-normal"
|
icon={faAngleDown}
|
||||||
leftIcon={currentOrg._id === org._id && <FontAwesomeIcon icon={faCheck} className="mr-3 text-primary"/>}
|
className="pl-1 pt-1 text-xs text-mineshaft-300"
|
||||||
>
|
/>
|
||||||
<div className="w-full flex justify-between items-center">{org.name}</div>
|
</div>
|
||||||
</Button>
|
</div>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuTrigger>
|
||||||
)}
|
<DropdownMenuContent align="start" className="p-1">
|
||||||
<div className="h-1 mt-1 border-t border-mineshaft-600"/>
|
<div className="px-2 py-1 text-xs text-mineshaft-400">{user?.email}</div>
|
||||||
<button
|
{orgs?.map((org) => (
|
||||||
type="button"
|
<DropdownMenuItem key={org._id}>
|
||||||
onClick={logOutUser}
|
<Button
|
||||||
className="w-full"
|
onClick={() => changeOrg(org?._id)}
|
||||||
>
|
variant="plain"
|
||||||
<DropdownMenuItem>Log Out</DropdownMenuItem>
|
colorSchema="secondary"
|
||||||
</button>
|
size="xs"
|
||||||
</DropdownMenuContent>
|
className="flex w-full items-center justify-start p-0 font-normal"
|
||||||
</DropdownMenu>
|
leftIcon={
|
||||||
<DropdownMenu>
|
currentOrg._id === org._id && (
|
||||||
<DropdownMenuTrigger asChild className="hover:bg-primary-400 hover:text-black data-[state=open]:text-black data-[state=open]:bg-primary-400 p-1">
|
<FontAwesomeIcon icon={faCheck} className="mr-3 text-primary" />
|
||||||
<div className="child w-6 h-6 rounded-full bg-mineshaft hover:bg-mineshaft-500 pr-1 text-xs text-mineshaft-300 flex justify-center items-center">
|
)
|
||||||
{user?.firstName?.charAt(0)}{user?.lastName && user?.lastName?.charAt(0)}
|
}
|
||||||
</div>
|
>
|
||||||
</DropdownMenuTrigger>
|
<div className="flex w-full items-center justify-between">
|
||||||
<DropdownMenuContent align="start" className="p-1">
|
{org.name}
|
||||||
<div className="text-xs text-mineshaft-400 px-2 py-1">{user?.email}</div>
|
</div>
|
||||||
<Link href="/personal-settings"><DropdownMenuItem>Personal Settings</DropdownMenuItem></Link>
|
</Button>
|
||||||
<a
|
</DropdownMenuItem>
|
||||||
href="https://infisical.com/docs/documentation/getting-started/introduction"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="w-full mt-3 text-sm text-mineshaft-300 font-normal leading-[1.2rem] hover:text-mineshaft-100"
|
|
||||||
>
|
|
||||||
<DropdownMenuItem>Documentation<FontAwesomeIcon icon={faArrowUpRightFromSquare} className="pl-1.5 text-xxs mb-[0.06rem]" /></DropdownMenuItem>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="https://infisical.com/slack"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="w-full mt-3 text-sm text-mineshaft-300 font-normal leading-[1.2rem] hover:text-mineshaft-100"
|
|
||||||
>
|
|
||||||
<DropdownMenuItem>Join Slack Community<FontAwesomeIcon icon={faArrowUpRightFromSquare} className="pl-1.5 text-xxs mb-[0.06rem]" /></DropdownMenuItem>
|
|
||||||
</a>
|
|
||||||
<div className="h-1 mt-1 border-t border-mineshaft-600"/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={logOutUser}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
<DropdownMenuItem>Log Out</DropdownMenuItem>
|
|
||||||
</button>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>}
|
|
||||||
{!router.asPath.includes("org") && (!router.asPath.includes("personal") && currentWorkspace ? (
|
|
||||||
<div className="mt-5 mb-4 w-full p-3">
|
|
||||||
<p className="ml-1.5 mb-1 text-xs font-semibold uppercase text-gray-400">
|
|
||||||
Project
|
|
||||||
</p>
|
|
||||||
<Select
|
|
||||||
defaultValue={currentWorkspace?._id}
|
|
||||||
value={currentWorkspace?._id}
|
|
||||||
className="w-full truncate bg-mineshaft-600 py-2.5 font-medium"
|
|
||||||
onValueChange={(value) => {
|
|
||||||
router.push(`/project/${value}/secrets`);
|
|
||||||
localStorage.setItem("projectData.id", value);
|
|
||||||
}}
|
|
||||||
position="popper"
|
|
||||||
dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 z-50 max-h-96 border-gray-700"
|
|
||||||
>
|
|
||||||
<div className='h-full no-scrollbar no-scrollbar::-webkit-scrollbar'>
|
|
||||||
{workspaces
|
|
||||||
.filter((ws) => ws.organization === currentOrg?._id)
|
|
||||||
.map(({ _id, name }) => (
|
|
||||||
<SelectItem
|
|
||||||
key={`ws-layout-list-${_id}`}
|
|
||||||
value={_id}
|
|
||||||
className={`${currentWorkspace?._id === _id && "bg-mineshaft-600"}`}
|
|
||||||
>
|
|
||||||
{name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
<div className="mt-1 h-1 border-t border-mineshaft-600" />
|
||||||
<hr className="mt-1 mb-1 h-px border-0 bg-gray-700" />
|
<button type="button" onClick={logOutUser} className="w-full">
|
||||||
<div className="w-full">
|
<DropdownMenuItem>Log Out</DropdownMenuItem>
|
||||||
<Button
|
</button>
|
||||||
className="w-full bg-mineshaft-700 py-2 text-bunker-200"
|
</DropdownMenuContent>
|
||||||
colorSchema="primary"
|
</DropdownMenu>
|
||||||
variant="outline_bg"
|
<DropdownMenu>
|
||||||
size="sm"
|
<DropdownMenuTrigger
|
||||||
onClick={() => {
|
asChild
|
||||||
if (isAddingProjectsAllowed) {
|
className="p-1 hover:bg-primary-400 hover:text-black data-[state=open]:bg-primary-400 data-[state=open]:text-black"
|
||||||
handlePopUpOpen("addNewWs")
|
>
|
||||||
} else {
|
<div className="child flex h-6 w-6 items-center justify-center rounded-full bg-mineshaft pr-1 text-xs text-mineshaft-300 hover:bg-mineshaft-500">
|
||||||
handlePopUpOpen("upgradePlan");
|
{user?.firstName?.charAt(0)}
|
||||||
}
|
{user?.lastName && user?.lastName?.charAt(0)}
|
||||||
}}
|
</div>
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start" className="p-1">
|
||||||
|
<div className="px-2 py-1 text-xs text-mineshaft-400">{user?.email}</div>
|
||||||
|
<Link href="/personal-settings">
|
||||||
|
<DropdownMenuItem>Personal Settings</DropdownMenuItem>
|
||||||
|
</Link>
|
||||||
|
<a
|
||||||
|
href="https://infisical.com/docs/documentation/getting-started/introduction"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="mt-3 w-full text-sm font-normal leading-[1.2rem] text-mineshaft-300 hover:text-mineshaft-100"
|
||||||
>
|
>
|
||||||
Add Project
|
<DropdownMenuItem>
|
||||||
</Button>
|
Documentation
|
||||||
</div>
|
<FontAwesomeIcon
|
||||||
</Select>
|
icon={faArrowUpRightFromSquare}
|
||||||
|
className="mb-[0.06rem] pl-1.5 text-xxs"
|
||||||
|
/>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://infisical.com/slack"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="mt-3 w-full text-sm font-normal leading-[1.2rem] text-mineshaft-300 hover:text-mineshaft-100"
|
||||||
|
>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
Join Slack Community
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowUpRightFromSquare}
|
||||||
|
className="mb-[0.06rem] pl-1.5 text-xxs"
|
||||||
|
/>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</a>
|
||||||
|
<div className="mt-1 h-1 border-t border-mineshaft-600" />
|
||||||
|
<button type="button" onClick={logOutUser} className="w-full">
|
||||||
|
<DropdownMenuItem>Log Out</DropdownMenuItem>
|
||||||
|
</button>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
) : <Link href={`/org/${currentOrg?._id}/overview`}><div className="pr-2 my-6 flex justify-center items-center text-mineshaft-300 hover:text-mineshaft-100 cursor-default text-sm">
|
)}
|
||||||
<FontAwesomeIcon icon={faArrowLeft} className="pr-3"/>
|
{!router.asPath.includes("org") &&
|
||||||
Back to organization
|
(!router.asPath.includes("personal") && currentWorkspace ? (
|
||||||
</div></Link>)}
|
<div className="mt-5 mb-4 w-full p-3">
|
||||||
|
<p className="ml-1.5 mb-1 text-xs font-semibold uppercase text-gray-400">
|
||||||
|
Project
|
||||||
|
</p>
|
||||||
|
<Select
|
||||||
|
defaultValue={currentWorkspace?._id}
|
||||||
|
value={currentWorkspace?._id}
|
||||||
|
className="w-full truncate bg-mineshaft-600 py-2.5 font-medium"
|
||||||
|
onValueChange={(value) => {
|
||||||
|
router.push(`/project/${value}/secrets/overview`);
|
||||||
|
localStorage.setItem("projectData.id", value);
|
||||||
|
}}
|
||||||
|
position="popper"
|
||||||
|
dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 z-50 max-h-96 border-gray-700"
|
||||||
|
>
|
||||||
|
<div className="no-scrollbar::-webkit-scrollbar h-full no-scrollbar">
|
||||||
|
{workspaces
|
||||||
|
.filter((ws) => ws.organization === currentOrg?._id)
|
||||||
|
.map(({ _id, name }) => (
|
||||||
|
<SelectItem
|
||||||
|
key={`ws-layout-list-${_id}`}
|
||||||
|
value={_id}
|
||||||
|
className={`${currentWorkspace?._id === _id && "bg-mineshaft-600"}`}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<hr className="mt-1 mb-1 h-px border-0 bg-gray-700" />
|
||||||
|
<div className="w-full">
|
||||||
|
<Button
|
||||||
|
className="w-full bg-mineshaft-700 py-2 text-bunker-200"
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
if (isAddingProjectsAllowed) {
|
||||||
|
handlePopUpOpen("addNewWs");
|
||||||
|
} else {
|
||||||
|
handlePopUpOpen("upgradePlan");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
>
|
||||||
|
Add Project
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Link href={`/org/${currentOrg?._id}/overview`}>
|
||||||
|
<div className="my-6 flex cursor-default items-center justify-center pr-2 text-sm text-mineshaft-300 hover:text-mineshaft-100">
|
||||||
|
<FontAwesomeIcon icon={faArrowLeft} className="pr-3" />
|
||||||
|
Back to organization
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
<div className={`px-1 ${!router.asPath.includes("personal") ? "block" : "hidden"}`}>
|
<div className={`px-1 ${!router.asPath.includes("personal") ? "block" : "hidden"}`}>
|
||||||
{((router.asPath.includes("project") || router.asPath.includes("integrations")) && currentWorkspace) ? <Menu>
|
{(router.asPath.includes("project") || router.asPath.includes("integrations")) &&
|
||||||
<Link href={`/project/${currentWorkspace?._id}/secrets`} passHref>
|
currentWorkspace ? (
|
||||||
<a>
|
<Menu>
|
||||||
<MenuItem
|
<Link href={`/project/${currentWorkspace?._id}/secrets/overview`} passHref>
|
||||||
isSelected={router.asPath.includes(`/project/${currentWorkspace?._id}/secrets`)}
|
<a>
|
||||||
icon="system-outline-90-lock-closed"
|
<MenuItem
|
||||||
>
|
isSelected={router.asPath.includes(
|
||||||
{t("nav.menu.secrets")}
|
`/project/${currentWorkspace?._id}/secrets/overview`
|
||||||
</MenuItem>
|
)}
|
||||||
</a>
|
icon="system-outline-90-lock-closed"
|
||||||
</Link>
|
>
|
||||||
<Link href={`/project/${currentWorkspace?._id}/members`} passHref>
|
{t("nav.menu.secrets")}
|
||||||
<a>
|
</MenuItem>
|
||||||
<MenuItem
|
</a>
|
||||||
isSelected={router.asPath === `/project/${currentWorkspace?._id}/members`}
|
</Link>
|
||||||
icon="system-outline-96-groups"
|
<Link href={`/project/${currentWorkspace?._id}/members`} passHref>
|
||||||
>
|
<a>
|
||||||
{t("nav.menu.members")}
|
<MenuItem
|
||||||
</MenuItem>
|
isSelected={
|
||||||
</a>
|
router.asPath === `/project/${currentWorkspace?._id}/members`
|
||||||
</Link>
|
}
|
||||||
<Link href={`/integrations/${currentWorkspace?._id}`} passHref>
|
icon="system-outline-96-groups"
|
||||||
<a>
|
>
|
||||||
<MenuItem
|
{t("nav.menu.members")}
|
||||||
isSelected={router.asPath === `/integrations/${currentWorkspace?._id}`}
|
</MenuItem>
|
||||||
icon="system-outline-82-extension"
|
</a>
|
||||||
>
|
</Link>
|
||||||
{t("nav.menu.integrations")}
|
<Link href={`/integrations/${currentWorkspace?._id}`} passHref>
|
||||||
</MenuItem>
|
<a>
|
||||||
</a>
|
<MenuItem
|
||||||
</Link>
|
isSelected={router.asPath === `/integrations/${currentWorkspace?._id}`}
|
||||||
<Link href={`/project/${currentWorkspace?._id}/allowlist`} passHref>
|
icon="system-outline-82-extension"
|
||||||
<a>
|
>
|
||||||
<MenuItem
|
{t("nav.menu.integrations")}
|
||||||
isSelected={
|
</MenuItem>
|
||||||
router.asPath === `/project/${currentWorkspace?._id}/allowlist`
|
</a>
|
||||||
}
|
</Link>
|
||||||
icon="system-outline-126-verified"
|
<Link href={`/project/${currentWorkspace?._id}/allowlist`} passHref>
|
||||||
>
|
<a>
|
||||||
IP Allowlist
|
<MenuItem
|
||||||
</MenuItem>
|
isSelected={
|
||||||
</a>
|
router.asPath === `/project/${currentWorkspace?._id}/allowlist`
|
||||||
</Link>
|
}
|
||||||
<Link href={`/project/${currentWorkspace?._id}/audit-logs`} passHref>
|
icon="system-outline-126-verified"
|
||||||
<a>
|
>
|
||||||
<MenuItem
|
IP Allowlist
|
||||||
isSelected={router.asPath === `/project/${currentWorkspace?._id}/audit-logs`}
|
</MenuItem>
|
||||||
icon="system-outline-168-view-headline"
|
</a>
|
||||||
>
|
</Link>
|
||||||
Audit Logs
|
<Link href={`/project/${currentWorkspace?._id}/audit-logs`} passHref>
|
||||||
</MenuItem>
|
<a>
|
||||||
</a>
|
<MenuItem
|
||||||
</Link>
|
isSelected={
|
||||||
{/* <Link href={`/project/${currentWorkspace?._id}/secret-scanning`} passHref>
|
router.asPath === `/project/${currentWorkspace?._id}/audit-logs`
|
||||||
|
}
|
||||||
|
icon="system-outline-168-view-headline"
|
||||||
|
>
|
||||||
|
Audit Logs
|
||||||
|
</MenuItem>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
{/* <Link href={`/project/${currentWorkspace?._id}/secret-scanning`} passHref>
|
||||||
<a>
|
<a>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
isSelected={router.asPath === `/project/${currentWorkspace?._id}/secret-scanning`}
|
isSelected={router.asPath === `/project/${currentWorkspace?._id}/secret-scanning`}
|
||||||
@@ -438,20 +506,21 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
</a>
|
</a>
|
||||||
</Link> */}
|
</Link> */}
|
||||||
<Link href={`/project/${currentWorkspace?._id}/settings`} passHref>
|
<Link href={`/project/${currentWorkspace?._id}/settings`} passHref>
|
||||||
<a>
|
<a>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
isSelected={
|
isSelected={
|
||||||
router.asPath === `/project/${currentWorkspace?._id}/settings`
|
router.asPath === `/project/${currentWorkspace?._id}/settings`
|
||||||
}
|
}
|
||||||
icon="system-outline-109-slider-toggle-settings"
|
icon="system-outline-109-slider-toggle-settings"
|
||||||
>
|
>
|
||||||
{t("nav.menu.project-settings")}
|
{t("nav.menu.project-settings")}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
</Menu>
|
</Menu>
|
||||||
: <Menu className="mt-4">
|
) : (
|
||||||
|
<Menu className="mt-4">
|
||||||
<Link href={`/org/${currentOrg?._id}/overview`} passHref>
|
<Link href={`/org/${currentOrg?._id}/overview`} passHref>
|
||||||
<a>
|
<a>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
@@ -462,7 +531,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
{/* {workspaces.map(project => <Link key={project._id} href={`/project/${project?._id}/secrets`} passHref>
|
{/* {workspaces.map(project => <Link key={project._id} href={`/project/${project?._id}/secrets/overview`} passHref>
|
||||||
<a>
|
<a>
|
||||||
<SubMenuItem
|
<SubMenuItem
|
||||||
isSelected={false}
|
isSelected={false}
|
||||||
@@ -508,20 +577,25 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
<Link href={`/org/${currentOrg?._id}/settings`} passHref>
|
<Link href={`/org/${currentOrg?._id}/settings`} passHref>
|
||||||
<a>
|
<a>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
isSelected={
|
isSelected={router.asPath === `/org/${currentOrg?._id}/settings`}
|
||||||
router.asPath === `/org/${currentOrg?._id}/settings`
|
|
||||||
}
|
|
||||||
icon="system-outline-109-slider-toggle-settings"
|
icon="system-outline-109-slider-toggle-settings"
|
||||||
>
|
>
|
||||||
Organization Settings
|
Organization Settings
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
</Menu>}
|
</Menu>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={`relative mt-10 ${subscription && subscription.slug === "starter" && !subscription.has_used_trial ? "mb-2" : "mb-4"} w-full px-3 text-mineshaft-400 cursor-default text-sm flex flex-col items-center`}>
|
<div
|
||||||
{/* <div className={`${isLearningNoteOpen ? "block" : "hidden"} z-0 absolute h-60 w-[9.9rem] ${router.asPath.includes("org") ? "bottom-[8.4rem]" : "bottom-[5.4rem]"} bg-mineshaft-900 border border-mineshaft-600 mb-4 rounded-md opacity-30`}/>
|
className={`relative mt-10 ${
|
||||||
|
subscription && subscription.slug === "starter" && !subscription.has_used_trial
|
||||||
|
? "mb-2"
|
||||||
|
: "mb-4"
|
||||||
|
} flex w-full cursor-default flex-col items-center px-3 text-sm text-mineshaft-400`}
|
||||||
|
>
|
||||||
|
{/* <div className={`${isLearningNoteOpen ? "block" : "hidden"} z-0 absolute h-60 w-[9.9rem] ${router.asPath.includes("org") ? "bottom-[8.4rem]" : "bottom-[5.4rem]"} bg-mineshaft-900 border border-mineshaft-600 mb-4 rounded-md opacity-30`}/>
|
||||||
<div className={`${isLearningNoteOpen ? "block" : "hidden"} z-0 absolute h-60 w-[10.7rem] ${router.asPath.includes("org") ? "bottom-[8.15rem]" : "bottom-[5.15rem]"} bg-mineshaft-900 border border-mineshaft-600 mb-4 rounded-md opacity-50`}/>
|
<div className={`${isLearningNoteOpen ? "block" : "hidden"} z-0 absolute h-60 w-[10.7rem] ${router.asPath.includes("org") ? "bottom-[8.15rem]" : "bottom-[5.15rem]"} bg-mineshaft-900 border border-mineshaft-600 mb-4 rounded-md opacity-50`}/>
|
||||||
<div className={`${isLearningNoteOpen ? "block" : "hidden"} z-0 absolute h-60 w-[11.5rem] ${router.asPath.includes("org") ? "bottom-[7.9rem]" : "bottom-[4.9rem]"} bg-mineshaft-900 border border-mineshaft-600 mb-4 rounded-md opacity-70`}/>
|
<div className={`${isLearningNoteOpen ? "block" : "hidden"} z-0 absolute h-60 w-[11.5rem] ${router.asPath.includes("org") ? "bottom-[7.9rem]" : "bottom-[4.9rem]"} bg-mineshaft-900 border border-mineshaft-600 mb-4 rounded-md opacity-70`}/>
|
||||||
<div className={`${isLearningNoteOpen ? "block" : "hidden"} z-0 absolute h-60 w-[12.3rem] ${router.asPath.includes("org") ? "bottom-[7.65rem]" : "bottom-[4.65rem]"} bg-mineshaft-900 border border-mineshaft-600 mb-4 rounded-md opacity-90`}/>
|
<div className={`${isLearningNoteOpen ? "block" : "hidden"} z-0 absolute h-60 w-[12.3rem] ${router.asPath.includes("org") ? "bottom-[7.65rem]" : "bottom-[4.65rem]"} bg-mineshaft-900 border border-mineshaft-600 mb-4 rounded-md opacity-90`}/>
|
||||||
@@ -549,22 +623,24 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div> */}
|
</div> */}
|
||||||
{router.asPath.includes("org") && <div
|
{router.asPath.includes("org") && (
|
||||||
onKeyDown={() => null}
|
<div
|
||||||
role="button"
|
onKeyDown={() => null}
|
||||||
tabIndex={0}
|
role="button"
|
||||||
onClick={() => router.push(`/org/${router.query.id}/members?action=invite`)}
|
tabIndex={0}
|
||||||
className="w-full"
|
onClick={() => router.push(`/org/${router.query.id}/members?action=invite`)}
|
||||||
>
|
className="w-full"
|
||||||
<div className="hover:text-mineshaft-200 duration-200 mb-3 pl-5 w-full">
|
>
|
||||||
<FontAwesomeIcon icon={faPlus} className="mr-3"/>
|
<div className="mb-3 w-full pl-5 duration-200 hover:text-mineshaft-200">
|
||||||
Invite people
|
<FontAwesomeIcon icon={faPlus} className="mr-3" />
|
||||||
|
Invite people
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>}
|
)}
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<div className="hover:text-mineshaft-200 duration-200 mb-2 pl-5 w-full">
|
<div className="mb-2 w-full pl-5 duration-200 hover:text-mineshaft-200">
|
||||||
<FontAwesomeIcon icon={faQuestion} className="px-[0.1rem] mr-3"/>
|
<FontAwesomeIcon icon={faQuestion} className="mr-3 px-[0.1rem]" />
|
||||||
Help & Support
|
Help & Support
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
@@ -586,28 +662,33 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
))}
|
))}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
{subscription && subscription.slug === "starter" && !subscription.has_used_trial && (
|
{subscription &&
|
||||||
<button
|
subscription.slug === "starter" &&
|
||||||
type="button"
|
!subscription.has_used_trial && (
|
||||||
onClick={async () => {
|
<button
|
||||||
if (!subscription || !currentOrg) return;
|
type="button"
|
||||||
|
onClick={async () => {
|
||||||
// direct user to start pro trial
|
if (!subscription || !currentOrg) return;
|
||||||
const url = await mutateAsync({
|
|
||||||
orgId: currentOrg._id,
|
// direct user to start pro trial
|
||||||
success_url: window.location.href
|
const url = await mutateAsync({
|
||||||
});
|
orgId: currentOrg._id,
|
||||||
|
success_url: window.location.href
|
||||||
window.location.href = url;
|
});
|
||||||
}}
|
|
||||||
className="w-full mt-1.5"
|
window.location.href = url;
|
||||||
>
|
}}
|
||||||
<div className="hover:text-primary-400 text-mineshaft-300 duration-200 flex justify-left items-center py-1 bg-mineshaft-600 rounded-md hover:bg-mineshaft-500 mb-1.5 mt-1.5 pl-4 w-full">
|
className="mt-1.5 w-full"
|
||||||
<FontAwesomeIcon icon={faInfinity} className="mr-3 ml-0.5 py-2 text-primary"/>
|
>
|
||||||
Start Free Pro Trial
|
<div className="justify-left mb-1.5 mt-1.5 flex w-full items-center rounded-md bg-mineshaft-600 py-1 pl-4 text-mineshaft-300 duration-200 hover:bg-mineshaft-500 hover:text-primary-400">
|
||||||
</div>
|
<FontAwesomeIcon
|
||||||
</button>
|
icon={faInfinity}
|
||||||
)}
|
className="mr-3 ml-0.5 py-2 text-primary"
|
||||||
|
/>
|
||||||
|
Start Free Pro Trial
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
@@ -9,14 +8,31 @@ import { useRouter } from "next/router";
|
|||||||
import { IconProp } from "@fortawesome/fontawesome-svg-core";
|
import { IconProp } from "@fortawesome/fontawesome-svg-core";
|
||||||
import { faSlack } from "@fortawesome/free-brands-svg-icons";
|
import { faSlack } from "@fortawesome/free-brands-svg-icons";
|
||||||
import { faFolderOpen } from "@fortawesome/free-regular-svg-icons";
|
import { faFolderOpen } from "@fortawesome/free-regular-svg-icons";
|
||||||
import { faArrowRight, faCheckCircle, faHandPeace, faMagnifyingGlass, faNetworkWired, faPlug, faPlus, faUserPlus } from "@fortawesome/free-solid-svg-icons";
|
import {
|
||||||
|
faArrowRight,
|
||||||
|
faCheckCircle,
|
||||||
|
faHandPeace,
|
||||||
|
faMagnifyingGlass,
|
||||||
|
faNetworkWired,
|
||||||
|
faPlug,
|
||||||
|
faPlus,
|
||||||
|
faUserPlus
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
|
|
||||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck";
|
import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck";
|
||||||
import { Button, Checkbox, FormControl, Input, Modal, ModalContent, UpgradePlanModal } from "@app/components/v2";
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
UpgradePlanModal
|
||||||
|
} from "@app/components/v2";
|
||||||
import { TabsObject } from "@app/components/v2/Tabs";
|
import { TabsObject } from "@app/components/v2/Tabs";
|
||||||
import { useSubscription, useUser, useWorkspace } from "@app/context";
|
import { useSubscription, useUser, useWorkspace } from "@app/context";
|
||||||
import { fetchOrgUsers, useAddUserToWs, useCreateWorkspace, useUploadWsKey } from "@app/hooks/api";
|
import { fetchOrgUsers, useAddUserToWs, useCreateWorkspace, useUploadWsKey } from "@app/hooks/api";
|
||||||
@@ -25,11 +41,14 @@ import { usePopUp } from "@app/hooks/usePopUp";
|
|||||||
import { encryptAssymmetric } from "../../../../components/utilities/cryptography/crypto";
|
import { encryptAssymmetric } from "../../../../components/utilities/cryptography/crypto";
|
||||||
import registerUserAction from "../../../api/userActions/registerUserAction";
|
import registerUserAction from "../../../api/userActions/registerUserAction";
|
||||||
|
|
||||||
const features = [{
|
const features = [
|
||||||
"_id": 0,
|
{
|
||||||
"name": "Kubernetes Operator",
|
_id: 0,
|
||||||
"description": "Pull secrets into your Kubernetes containers and automatically redeploy upon secret changes."
|
name: "Kubernetes Operator",
|
||||||
}]
|
description:
|
||||||
|
"Pull secrets into your Kubernetes containers and automatically redeploy upon secret changes."
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
type ItemProps = {
|
type ItemProps = {
|
||||||
text: string;
|
text: string;
|
||||||
@@ -58,7 +77,11 @@ const learningItem = ({
|
|||||||
className={`w-full ${complete && "opacity-30 duration-200 hover:opacity-100"}`}
|
className={`w-full ${complete && "opacity-30 duration-200 hover:opacity-100"}`}
|
||||||
href={link}
|
href={link}
|
||||||
>
|
>
|
||||||
<div className={`${complete ? "bg-gradient-to-r from-primary-500/70 p-[0.07rem]" : ""} mb-3 rounded-md`}>
|
<div
|
||||||
|
className={`${
|
||||||
|
complete ? "bg-gradient-to-r from-primary-500/70 p-[0.07rem]" : ""
|
||||||
|
} mb-3 rounded-md`}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
onKeyDown={() => null}
|
onKeyDown={() => null}
|
||||||
role="button"
|
role="button"
|
||||||
@@ -70,7 +93,11 @@ const learningItem = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={`group relative flex h-[5.5rem] w-full items-center justify-between overflow-hidden rounded-md border ${complete? "bg-gradient-to-r from-[#0e1f01] to-mineshaft-700 border-mineshaft-900 cursor-default" : "bg-mineshaft-800 hover:bg-mineshaft-700 border-mineshaft-600 shadow-xl cursor-pointer"} duration-200 text-mineshaft-100`}
|
className={`group relative flex h-[5.5rem] w-full items-center justify-between overflow-hidden rounded-md border ${
|
||||||
|
complete
|
||||||
|
? "cursor-default border-mineshaft-900 bg-gradient-to-r from-[#0e1f01] to-mineshaft-700"
|
||||||
|
: "cursor-pointer border-mineshaft-600 bg-mineshaft-800 shadow-xl hover:bg-mineshaft-700"
|
||||||
|
} text-mineshaft-100 duration-200`}
|
||||||
>
|
>
|
||||||
<div className="mr-4 flex flex-row items-center">
|
<div className="mr-4 flex flex-row items-center">
|
||||||
<FontAwesomeIcon icon={icon} className="mx-2 w-16 text-4xl" />
|
<FontAwesomeIcon icon={icon} className="mx-2 w-16 text-4xl" />
|
||||||
@@ -148,7 +175,11 @@ const learningItemSquare = ({
|
|||||||
className={`w-full ${complete && "opacity-30 duration-200 hover:opacity-100"}`}
|
className={`w-full ${complete && "opacity-30 duration-200 hover:opacity-100"}`}
|
||||||
href={link}
|
href={link}
|
||||||
>
|
>
|
||||||
<div className={`${complete ? "bg-gradient-to-r from-primary-500/70 p-[0.07rem]" : ""} rounded-md w-full`}>
|
<div
|
||||||
|
className={`${
|
||||||
|
complete ? "bg-gradient-to-r from-primary-500/70 p-[0.07rem]" : ""
|
||||||
|
} w-full rounded-md`}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
onKeyDown={() => null}
|
onKeyDown={() => null}
|
||||||
role="button"
|
role="button"
|
||||||
@@ -160,23 +191,32 @@ const learningItemSquare = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={`group relative flex w-full items-center justify-between overflow-hidden rounded-md border ${complete? "bg-gradient-to-r from-[#0e1f01] to-mineshaft-700 border-mineshaft-900 cursor-default" : "bg-mineshaft-800 hover:bg-mineshaft-700 border-mineshaft-600 shadow-xl cursor-pointer"} duration-200 text-mineshaft-100`}
|
className={`group relative flex w-full items-center justify-between overflow-hidden rounded-md border ${
|
||||||
|
complete
|
||||||
|
? "cursor-default border-mineshaft-900 bg-gradient-to-r from-[#0e1f01] to-mineshaft-700"
|
||||||
|
: "cursor-pointer border-mineshaft-600 bg-mineshaft-800 shadow-xl hover:bg-mineshaft-700"
|
||||||
|
} text-mineshaft-100 duration-200`}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col items-center w-full px-6 py-4">
|
<div className="flex w-full flex-col items-center px-6 py-4">
|
||||||
<div className="flex flex-row items-start justify-between w-full">
|
<div className="flex w-full flex-row items-start justify-between">
|
||||||
<FontAwesomeIcon icon={icon} className="w-16 text-5xl text-mineshaft-200 group-hover:text-mineshaft-100 duration-100 pt-2" />
|
<FontAwesomeIcon
|
||||||
|
icon={icon}
|
||||||
|
className="w-16 pt-2 text-5xl text-mineshaft-200 duration-100 group-hover:text-mineshaft-100"
|
||||||
|
/>
|
||||||
{complete && (
|
{complete && (
|
||||||
<div className="absolute left-14 top-12 flex h-7 w-7 items-center justify-center rounded-full bg-bunker-500 p-2 group-hover:bg-mineshaft-700">
|
<div className="absolute left-14 top-12 flex h-7 w-7 items-center justify-center rounded-full bg-bunker-500 p-2 group-hover:bg-mineshaft-700">
|
||||||
<FontAwesomeIcon icon={faCheckCircle} className="h-5 w-5 text-4xl text-primary" />
|
<FontAwesomeIcon icon={faCheckCircle} className="h-5 w-5 text-4xl text-primary" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className={`text-right text-sm font-normal text-mineshaft-300 ${complete ? "text-primary font-semibold" : ""}`}
|
className={`text-right text-sm font-normal text-mineshaft-300 ${
|
||||||
|
complete ? "font-semibold text-primary" : ""
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{complete ? "Complete!" : `About ${time}`}
|
{complete ? "Complete!" : `About ${time}`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-start justify-start w-full pt-4">
|
<div className="flex w-full flex-col items-start justify-start pt-4">
|
||||||
<div className="mt-0.5 text-lg font-medium">{text}</div>
|
<div className="mt-0.5 text-lg font-medium">{text}</div>
|
||||||
<div className="text-sm font-normal text-mineshaft-300">{subText}</div>
|
<div className="text-sm font-normal text-mineshaft-300">{subText}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -202,11 +242,14 @@ export default function Organization() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { workspaces } = useWorkspace();
|
const { workspaces } = useWorkspace();
|
||||||
const orgWorkspaces = workspaces?.filter(workspace => workspace.organization === localStorage.getItem("orgData.id")) || []
|
const orgWorkspaces =
|
||||||
|
workspaces?.filter(
|
||||||
|
(workspace) => workspace.organization === localStorage.getItem("orgData.id")
|
||||||
|
) || [];
|
||||||
const currentOrg = String(router.query.id);
|
const currentOrg = String(router.query.id);
|
||||||
const { createNotification } = useNotificationContext();
|
const { createNotification } = useNotificationContext();
|
||||||
const addWsUser = useAddUserToWs();
|
const addWsUser = useAddUserToWs();
|
||||||
|
|
||||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
"addNewWs",
|
"addNewWs",
|
||||||
"upgradePlan"
|
"upgradePlan"
|
||||||
@@ -269,7 +312,7 @@ export default function Organization() {
|
|||||||
}
|
}
|
||||||
createNotification({ text: "Workspace created", type: "success" });
|
createNotification({ text: "Workspace created", type: "success" });
|
||||||
handlePopUpClose("addNewWs");
|
handlePopUpClose("addNewWs");
|
||||||
router.push(`/project/${newWorkspaceId}/secrets`);
|
router.push(`/project/${newWorkspaceId}/secrets/overview`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
createNotification({ text: "Failed to create workspace", type: "error" });
|
createNotification({ text: "Failed to create workspace", type: "error" });
|
||||||
@@ -278,7 +321,9 @@ export default function Organization() {
|
|||||||
|
|
||||||
const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
|
|
||||||
const isAddingProjectsAllowed = subscription?.workspaceLimit ? (subscription.workspacesUsed < subscription.workspaceLimit) : true;
|
const isAddingProjectsAllowed = subscription?.workspaceLimit
|
||||||
|
? subscription.workspacesUsed < subscription.workspaceLimit
|
||||||
|
: true;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onboardingCheck({
|
onboardingCheck({
|
||||||
@@ -290,16 +335,16 @@ export default function Organization() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex max-w-7xl mx-auto flex-col justify-start bg-bunker-800 md:h-screen">
|
<div className="mx-auto flex max-w-7xl flex-col justify-start bg-bunker-800 md:h-screen">
|
||||||
<Head>
|
<Head>
|
||||||
<title>{t("common.head-title", { title: t("settings.members.title") })}</title>
|
<title>{t("common.head-title", { title: t("settings.members.title") })}</title>
|
||||||
<link rel="icon" href="/infisical.ico" />
|
<link rel="icon" href="/infisical.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<div className="flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl mb-4">
|
<div className="mb-4 flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl">
|
||||||
<p className="mr-4 font-semibold text-white">Projects</p>
|
<p className="mr-4 font-semibold text-white">Projects</p>
|
||||||
<div className="w-full flex flex-row mt-6">
|
<div className="mt-6 flex w-full flex-row">
|
||||||
<Input
|
<Input
|
||||||
className="h-[2.3rem] text-sm bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
|
className="h-[2.3rem] bg-mineshaft-800 text-sm placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
|
||||||
placeholder="Search by project name..."
|
placeholder="Search by project name..."
|
||||||
value={searchFilter}
|
value={searchFilter}
|
||||||
onChange={(e) => setSearchFilter(e.target.value)}
|
onChange={(e) => setSearchFilter(e.target.value)}
|
||||||
@@ -310,7 +355,7 @@ export default function Organization() {
|
|||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isAddingProjectsAllowed) {
|
if (isAddingProjectsAllowed) {
|
||||||
handlePopUpOpen("addNewWs")
|
handlePopUpOpen("addNewWs");
|
||||||
} else {
|
} else {
|
||||||
handlePopUpOpen("upgradePlan");
|
handlePopUpOpen("upgradePlan");
|
||||||
}
|
}
|
||||||
@@ -320,120 +365,169 @@ export default function Organization() {
|
|||||||
Add New Project
|
Add New Project
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 w-full grid gap-4 grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
<div className="mt-4 grid w-full grid-cols-1 gap-4 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||||||
{orgWorkspaces.filter(ws => ws?.name?.toLowerCase().includes(searchFilter.toLowerCase())).map(workspace => <div key={workspace._id} className="h-40 min-w-72 rounded-md bg-mineshaft-800 border border-mineshaft-600 p-4 flex flex-col justify-between">
|
{orgWorkspaces
|
||||||
<div className="text-lg text-mineshaft-100 mt-0">{workspace.name}</div>
|
.filter((ws) => ws?.name?.toLowerCase().includes(searchFilter.toLowerCase()))
|
||||||
<div className="text-sm text-mineshaft-300 mt-0 pb-6">{(workspace.environments?.length || 0)} environments</div>
|
.map((workspace) => (
|
||||||
<button type="button" onClick={() => {
|
<div
|
||||||
router.push(`/project/${workspace._id}/secrets`);
|
key={workspace._id}
|
||||||
localStorage.setItem("projectData.id", workspace._id);
|
className="min-w-72 flex h-40 flex-col justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4"
|
||||||
}}>
|
>
|
||||||
<div className="group cursor-default ml-auto hover:bg-primary-800/20 text-sm text-mineshaft-300 hover:text-mineshaft-200 bg-mineshaft-900 py-2 px-4 rounded-full w-max border border-mineshaft-600 hover:border-primary-500/80">Explore <FontAwesomeIcon icon={faArrowRight} className="pl-1.5 pr-0.5 group-hover:pl-2 group-hover:pr-0 duration-200" /></div>
|
<div className="mt-0 text-lg text-mineshaft-100">{workspace.name}</div>
|
||||||
</button>
|
<div className="mt-0 pb-6 text-sm text-mineshaft-300">
|
||||||
</div>)}
|
{workspace.environments?.length || 0} environments
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
router.push(`/project/${workspace._id}/secrets/overview`);
|
||||||
|
localStorage.setItem("projectData.id", workspace._id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="group ml-auto w-max cursor-default rounded-full border border-mineshaft-600 bg-mineshaft-900 py-2 px-4 text-sm text-mineshaft-300 hover:border-primary-500/80 hover:bg-primary-800/20 hover:text-mineshaft-200">
|
||||||
|
Explore{" "}
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowRight}
|
||||||
|
className="pl-1.5 pr-0.5 duration-200 group-hover:pl-2 group-hover:pr-0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
{orgWorkspaces.length === 0 && (
|
{orgWorkspaces.length === 0 && (
|
||||||
<div className="w-full rounded-md bg-mineshaft-800 border border-mineshaft-700 px-4 py-6 text-mineshaft-300 text-base">
|
<div className="w-full rounded-md border border-mineshaft-700 bg-mineshaft-800 px-4 py-6 text-base text-mineshaft-300">
|
||||||
<FontAwesomeIcon icon={faFolderOpen} className="w-full text-center text-5xl mb-4 mt-2 text-mineshaft-400" />
|
<FontAwesomeIcon
|
||||||
|
icon={faFolderOpen}
|
||||||
|
className="mb-4 mt-2 w-full text-center text-5xl text-mineshaft-400"
|
||||||
|
/>
|
||||||
<div className="text-center font-light">
|
<div className="text-center font-light">
|
||||||
You are not part of any projects in this organization yet. When you are, they will appear
|
You are not part of any projects in this organization yet. When you are, they will
|
||||||
here.
|
appear here.
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-0.5 text-center font-light">
|
<div className="mt-0.5 text-center font-light">
|
||||||
Create a new project, or ask other organization members to give you necessary permissions.
|
Create a new project, or ask other organization members to give you necessary
|
||||||
|
permissions.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{((new Date()).getTime() - (new Date(user?.createdAt)).getTime()) < 30 * 24 * 60 * 60 * 1000 && <div className="flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl mb-4">
|
{new Date().getTime() - new Date(user?.createdAt).getTime() < 30 * 24 * 60 * 60 * 1000 && (
|
||||||
<p className="mr-4 font-semibold text-white mb-4">Onboarding Guide</p>
|
<div className="mb-4 flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl">
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-3 w-full mb-3">
|
<p className="mr-4 mb-4 font-semibold text-white">Onboarding Guide</p>
|
||||||
{learningItemSquare({
|
<div className="mb-3 grid w-full grid-cols-1 gap-3 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||||||
text: "Watch Infisical demo",
|
{learningItemSquare({
|
||||||
subText: "Set up Infisical in 3 min.",
|
text: "Watch Infisical demo",
|
||||||
complete: hasUserClickedIntro,
|
subText: "Set up Infisical in 3 min.",
|
||||||
icon: faHandPeace,
|
complete: hasUserClickedIntro,
|
||||||
time: "3 min",
|
icon: faHandPeace,
|
||||||
userAction: "intro_cta_clicked",
|
time: "3 min",
|
||||||
link: "https://www.youtube.com/watch?v=PK23097-25I"
|
userAction: "intro_cta_clicked",
|
||||||
})}
|
link: "https://www.youtube.com/watch?v=PK23097-25I"
|
||||||
{orgWorkspaces.length !== 0 && learningItemSquare({
|
})}
|
||||||
text: "Add your secrets",
|
{orgWorkspaces.length !== 0 &&
|
||||||
subText: "Drop a .env file or type your secrets.",
|
learningItemSquare({
|
||||||
complete: hasUserPushedSecrets,
|
text: "Add your secrets",
|
||||||
icon: faPlus,
|
subText: "Drop a .env file or type your secrets.",
|
||||||
time: "1 min",
|
complete: hasUserPushedSecrets,
|
||||||
userAction: "first_time_secrets_pushed",
|
icon: faPlus,
|
||||||
link: `/project/${orgWorkspaces[0]?._id}/secrets`
|
time: "1 min",
|
||||||
})}
|
userAction: "first_time_secrets_pushed",
|
||||||
{learningItemSquare({
|
link: `/project/${orgWorkspaces[0]?._id}/secrets`
|
||||||
text: "Invite your teammates",
|
})}
|
||||||
subText: "Infisical is better used as a team.",
|
{learningItemSquare({
|
||||||
complete: usersInOrg,
|
text: "Invite your teammates",
|
||||||
icon: faUserPlus,
|
subText: "Infisical is better used as a team.",
|
||||||
time: "2 min",
|
complete: usersInOrg,
|
||||||
link: `/org/${router.query.id}/members?action=invite`
|
icon: faUserPlus,
|
||||||
})}
|
time: "2 min",
|
||||||
<div className="block xl:hidden 2xl:block">{learningItemSquare({
|
link: `/org/${router.query.id}/members?action=invite`
|
||||||
text: "Join Infisical Slack",
|
})}
|
||||||
subText: "Have any questions? Ask us!",
|
<div className="block xl:hidden 2xl:block">
|
||||||
complete: hasUserClickedSlack,
|
{learningItemSquare({
|
||||||
icon: faSlack,
|
text: "Join Infisical Slack",
|
||||||
time: "1 min",
|
subText: "Have any questions? Ask us!",
|
||||||
userAction: "slack_cta_clicked",
|
complete: hasUserClickedSlack,
|
||||||
link: "https://infisical.com/slack"
|
icon: faSlack,
|
||||||
})}</div>
|
time: "1 min",
|
||||||
</div>
|
userAction: "slack_cta_clicked",
|
||||||
{orgWorkspaces.length !== 0 && <div className="group text-mineshaft-100 relative mb-3 flex h-full w-full cursor-default flex-col items-center justify-between overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-800 pl-2 pr-2 pt-4 pb-2 shadow-xl duration-200">
|
link: "https://infisical.com/slack"
|
||||||
<div className="mb-4 flex w-full flex-row items-center pr-4">
|
})}
|
||||||
<div className="mr-4 flex w-full flex-row items-center">
|
|
||||||
<FontAwesomeIcon icon={faNetworkWired} className="mx-2 w-16 text-4xl" />
|
|
||||||
{false && (
|
|
||||||
<div className="absolute left-12 top-10 flex h-7 w-7 items-center justify-center rounded-full bg-bunker-500 p-2 group-hover:bg-mineshaft-700">
|
|
||||||
<FontAwesomeIcon icon={faCheckCircle} className="h-5 w-5 text-4xl text-green" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex flex-col items-start pl-0.5">
|
|
||||||
<div className="mt-0.5 text-xl font-semibold">Inject secrets locally</div>
|
|
||||||
<div className="text-sm font-normal">
|
|
||||||
Replace .env files with a more secure and efficient alternative.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={`w-28 pr-4 text-right text-sm font-semibold ${false && "text-green"}`}>
|
|
||||||
About 2 min
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<TabsObject />
|
{orgWorkspaces.length !== 0 && (
|
||||||
{false && <div className="absolute bottom-0 left-0 h-1 w-full bg-green" />}
|
<div className="group relative mb-3 flex h-full w-full cursor-default flex-col items-center justify-between overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-800 pl-2 pr-2 pt-4 pb-2 text-mineshaft-100 shadow-xl duration-200">
|
||||||
</div>}
|
<div className="mb-4 flex w-full flex-row items-center pr-4">
|
||||||
{orgWorkspaces.length !== 0 && learningItem({
|
<div className="mr-4 flex w-full flex-row items-center">
|
||||||
text: "Integrate Infisical with your infrastructure",
|
<FontAwesomeIcon icon={faNetworkWired} className="mx-2 w-16 text-4xl" />
|
||||||
subText: "Connect Infisical to various 3rd party services and platforms.",
|
{false && (
|
||||||
complete: false,
|
<div className="absolute left-12 top-10 flex h-7 w-7 items-center justify-center rounded-full bg-bunker-500 p-2 group-hover:bg-mineshaft-700">
|
||||||
icon: faPlug,
|
<FontAwesomeIcon
|
||||||
time: "15 min",
|
icon={faCheckCircle}
|
||||||
link: "https://infisical.com/docs/integrations/overview"
|
className="h-5 w-5 text-4xl text-green"
|
||||||
})}
|
/>
|
||||||
</div>}
|
</div>
|
||||||
<div className="flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl mb-4 pb-6">
|
)}
|
||||||
<p className="mr-4 font-semibold text-white">Explore More</p>
|
<div className="flex flex-col items-start pl-0.5">
|
||||||
<div className="mt-4 w-full grid grid-flow-dense gap-4" style={{ gridTemplateColumns: "repeat(auto-fill, minmax(256px, 4fr))" }}>
|
<div className="mt-0.5 text-xl font-semibold">Inject secrets locally</div>
|
||||||
{features.map(feature => <div key={feature._id} className="h-44 w-96 rounded-md bg-mineshaft-800 border border-mineshaft-600 p-4 flex flex-col justify-between">
|
<div className="text-sm font-normal">
|
||||||
<div className="text-lg text-mineshaft-100 mt-0">{feature.name}</div>
|
Replace .env files with a more secure and efficient alternative.
|
||||||
<div className="text-[15px] font-light text-mineshaft-300 mb-4 mt-2">{feature.description}</div>
|
</div>
|
||||||
<div className="w-full flex items-center">
|
</div>
|
||||||
<div className="text-mineshaft-300 text-[15px] font-light">Setup time: 20 min</div>
|
</div>
|
||||||
<a
|
<div
|
||||||
target="_blank"
|
className={`w-28 pr-4 text-right text-sm font-semibold ${false && "text-green"}`}
|
||||||
rel="noopener noreferrer"
|
>
|
||||||
className="group cursor-default ml-auto hover:bg-primary-800/20 text-sm text-mineshaft-300 hover:text-mineshaft-200 bg-mineshaft-900 py-2 px-4 rounded-full w-max border border-mineshaft-600 hover:border-primary-500/80"
|
About 2 min
|
||||||
href="https://infisical.com/docs/documentation/getting-started/kubernetes"
|
</div>
|
||||||
>
|
</div>
|
||||||
Learn more <FontAwesomeIcon icon={faArrowRight} className="pl-1.5 pr-0.5 group-hover:pl-2 group-hover:pr-0 duration-200"/>
|
<TabsObject />
|
||||||
</a>
|
{false && <div className="absolute bottom-0 left-0 h-1 w-full bg-green" />}
|
||||||
</div>
|
</div>
|
||||||
</div>)}
|
)}
|
||||||
|
{orgWorkspaces.length !== 0 &&
|
||||||
|
learningItem({
|
||||||
|
text: "Integrate Infisical with your infrastructure",
|
||||||
|
subText: "Connect Infisical to various 3rd party services and platforms.",
|
||||||
|
complete: false,
|
||||||
|
icon: faPlug,
|
||||||
|
time: "15 min",
|
||||||
|
link: "https://infisical.com/docs/integrations/overview"
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="mb-4 flex flex-col items-start justify-start px-6 py-6 pb-0 pb-6 text-3xl">
|
||||||
|
<p className="mr-4 font-semibold text-white">Explore More</p>
|
||||||
|
<div
|
||||||
|
className="mt-4 grid w-full grid-flow-dense gap-4"
|
||||||
|
style={{ gridTemplateColumns: "repeat(auto-fill, minmax(256px, 4fr))" }}
|
||||||
|
>
|
||||||
|
{features.map((feature) => (
|
||||||
|
<div
|
||||||
|
key={feature._id}
|
||||||
|
className="flex h-44 w-96 flex-col justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4"
|
||||||
|
>
|
||||||
|
<div className="mt-0 text-lg text-mineshaft-100">{feature.name}</div>
|
||||||
|
<div className="mb-4 mt-2 text-[15px] font-light text-mineshaft-300">
|
||||||
|
{feature.description}
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full items-center">
|
||||||
|
<div className="text-[15px] font-light text-mineshaft-300">Setup time: 20 min</div>
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="group ml-auto w-max cursor-default rounded-full border border-mineshaft-600 bg-mineshaft-900 py-2 px-4 text-sm text-mineshaft-300 hover:border-primary-500/80 hover:bg-primary-800/20 hover:text-mineshaft-200"
|
||||||
|
href="https://infisical.com/docs/documentation/getting-started/kubernetes"
|
||||||
|
>
|
||||||
|
Learn more{" "}
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowRight}
|
||||||
|
className="pl-1.5 pr-0.5 duration-200 group-hover:pl-2 group-hover:pr-0"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
+1
-7
@@ -1,16 +1,10 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useRouter } from "next/router";
|
|
||||||
|
|
||||||
import { DashboardPage } from "@app/views/DashboardPage";
|
import { DashboardPage } from "@app/views/DashboardPage";
|
||||||
import { DashboardEnvOverview } from "@app/views/DashboardPage/DashboardEnvOverview";
|
|
||||||
|
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const queryEnv = router.query.env as string;
|
|
||||||
const isOverviewMode = !queryEnv;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -22,7 +16,7 @@ const Dashboard = () => {
|
|||||||
<meta name="og:description" content={String(t("dashboard.og-description"))} />
|
<meta name="og:description" content={String(t("dashboard.og-description"))} />
|
||||||
</Head>
|
</Head>
|
||||||
<div className="h-full">
|
<div className="h-full">
|
||||||
{isOverviewMode ? <DashboardEnvOverview /> : <DashboardPage envFromTop={queryEnv} />}
|
<DashboardPage />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import Head from "next/head";
|
||||||
|
|
||||||
|
import { SecretOverviewPage } from "@app/views/SecretOverviewPage";
|
||||||
|
|
||||||
|
const Dashboard = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>{t("common.head-title", { title: t("dashboard.title") })}</title>
|
||||||
|
<link rel="icon" href="/infisical.ico" />
|
||||||
|
<meta property="og:image" content="/images/message.png" />
|
||||||
|
<meta property="og:title" content={String(t("dashboard.og-title"))} />
|
||||||
|
<meta name="og:description" content={String(t("dashboard.og-description"))} />
|
||||||
|
</Head>
|
||||||
|
<div className="h-full">
|
||||||
|
<SecretOverviewPage />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Dashboard;
|
||||||
|
|
||||||
|
Dashboard.requireAuth = true;
|
||||||
@@ -13,6 +13,11 @@
|
|||||||
.flex-3 {
|
.flex-3 {
|
||||||
flex-grow: 3;
|
flex-grow: 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.min-table-row {
|
||||||
|
width: 1%;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
@@ -40,7 +45,7 @@
|
|||||||
|
|
||||||
.breadcrumb::after,
|
.breadcrumb::after,
|
||||||
.breadcrumb::before {
|
.breadcrumb::before {
|
||||||
content: '';
|
content: "";
|
||||||
height: 60%;
|
height: 60%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
@@ -58,18 +63,32 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.breadcrumb::after {
|
.breadcrumb::after {
|
||||||
left: 5px;
|
left: 4px;
|
||||||
bottom: -3px;
|
bottom: -2.5px;
|
||||||
transform: skew(-30deg);
|
transform: skew(-30deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.breadcrumb::before {
|
.breadcrumb::before {
|
||||||
left: 5px;
|
left: 4px;
|
||||||
top: -3px;
|
top: -2.5px;
|
||||||
transform: skew(30deg);
|
transform: skew(30deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.thin-scrollbar::-webkit-scrollbar {
|
||||||
|
width: 0.25rem;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thin-scrollbar::-webkit-scrollbar-thumb {
|
||||||
|
background-color: gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thin-scrollbar {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: gray transparent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@import '@fontsource/inter/400.css';
|
@import "@fontsource/inter/400.css";
|
||||||
@import '@fontsource/inter/500.css';
|
@import "@fontsource/inter/500.css";
|
||||||
@import '@fontsource/inter/700.css';
|
@import "@fontsource/inter/700.css";
|
||||||
|
|||||||
@@ -1,326 +0,0 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { faFolderOpen, faKey, faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
import NavHeader from "@app/components/navigation/NavHeader";
|
|
||||||
import { Button, Input, TableContainer, Tooltip } from "@app/components/v2";
|
|
||||||
import { useOrganization, useWorkspace } from "@app/context";
|
|
||||||
import {
|
|
||||||
useGetProjectFoldersBatch,
|
|
||||||
useGetProjectSecretsByKey,
|
|
||||||
useGetUserWsEnvironments,
|
|
||||||
useGetUserWsKey
|
|
||||||
} from "@app/hooks/api";
|
|
||||||
|
|
||||||
import { EnvComparisonRow } from "./components/EnvComparisonRow";
|
|
||||||
import { FolderComparisonRow } from "./components/EnvComparisonRow/FolderComparisonRow";
|
|
||||||
|
|
||||||
export const DashboardEnvOverview = () => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const { currentWorkspace, isLoading } = useWorkspace();
|
|
||||||
const { currentOrg } = useOrganization();
|
|
||||||
const workspaceId = currentWorkspace?._id as string;
|
|
||||||
const { data: latestFileKey } = useGetUserWsKey(workspaceId);
|
|
||||||
const [searchFilter, setSearchFilter] = useState("");
|
|
||||||
const secretPath = router.query?.secretPath as string;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isLoading && !workspaceId && router.isReady) {
|
|
||||||
router.push(`/org/${currentOrg?._id}/overview`);
|
|
||||||
}
|
|
||||||
}, [isLoading, workspaceId, router.isReady]);
|
|
||||||
|
|
||||||
const { data: wsEnv, isLoading: isEnvListLoading } = useGetUserWsEnvironments({
|
|
||||||
workspaceId
|
|
||||||
});
|
|
||||||
|
|
||||||
const userAvailableEnvs = wsEnv?.filter(({ isReadDenied }) => !isReadDenied);
|
|
||||||
|
|
||||||
const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecretsByKey({
|
|
||||||
workspaceId,
|
|
||||||
env: userAvailableEnvs?.map((env) => env.slug) ?? [],
|
|
||||||
decryptFileKey: latestFileKey!,
|
|
||||||
isPaused: false,
|
|
||||||
secretPath
|
|
||||||
});
|
|
||||||
|
|
||||||
const folders = useGetProjectFoldersBatch({
|
|
||||||
folders:
|
|
||||||
userAvailableEnvs?.map((env) => ({
|
|
||||||
environment: env.slug,
|
|
||||||
workspaceId
|
|
||||||
})) ?? [],
|
|
||||||
parentFolderPath: secretPath
|
|
||||||
});
|
|
||||||
|
|
||||||
const foldersGroupedByEnv = useMemo(() => {
|
|
||||||
const res: Record<string, Record<string, boolean>> = {};
|
|
||||||
folders.forEach(({ data }) => {
|
|
||||||
data?.folders
|
|
||||||
?.filter(({ name }) => name.toLowerCase().includes(searchFilter))
|
|
||||||
?.forEach((folder) => {
|
|
||||||
if (!res?.[folder.name]) res[folder.name] = {};
|
|
||||||
res[folder.name][data.environment] = true;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return res;
|
|
||||||
}, [folders, userAvailableEnvs, searchFilter]);
|
|
||||||
|
|
||||||
const numSecretsMissingPerEnv = useMemo(() => {
|
|
||||||
// first get all sec in the env then subtract with total to get missing ones
|
|
||||||
const secPerEnvMissing: Record<string, number> = Object.fromEntries(
|
|
||||||
(userAvailableEnvs || [])?.map(({ slug }) => [slug, 0])
|
|
||||||
);
|
|
||||||
Object.keys(secrets?.secrets || {}).forEach((key) =>
|
|
||||||
secrets?.secrets?.[key].forEach((val) => {
|
|
||||||
secPerEnvMissing[val.env] += 1;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
Object.keys(secPerEnvMissing).forEach((k) => {
|
|
||||||
secPerEnvMissing[k] = (secrets?.uniqueSecCount || 0) - secPerEnvMissing[k];
|
|
||||||
});
|
|
||||||
return secPerEnvMissing;
|
|
||||||
}, [secrets, userAvailableEnvs]);
|
|
||||||
|
|
||||||
const onExploreEnv = (slug: string) => {
|
|
||||||
const query: Record<string, string> = { ...router.query, env: slug };
|
|
||||||
delete query.secretPath;
|
|
||||||
// the dir return will have the present directory folder id
|
|
||||||
// use that when clicking on explore to redirect user to there
|
|
||||||
const envFolder = folders.find(({ data }) => slug === data?.environment);
|
|
||||||
const dir = envFolder?.data?.dir?.pop();
|
|
||||||
if (dir) {
|
|
||||||
query.folderId = dir.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
router.push({
|
|
||||||
pathname: router.pathname,
|
|
||||||
query
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onFolderClick = (path: string) => {
|
|
||||||
router.push({
|
|
||||||
pathname: router.pathname,
|
|
||||||
query: {
|
|
||||||
...router.query,
|
|
||||||
secretPath: `${router.query?.secretPath || ""}/${path}`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onFolderCrumbClick = (index: number) => {
|
|
||||||
const newSecPath = secretPath.split("/").filter(Boolean).slice(0, index).join("/");
|
|
||||||
const query = { ...router.query, secretPath: `/${newSecPath}` } as Record<string, string>;
|
|
||||||
// root condition
|
|
||||||
if (index === 0) delete query.secretPath;
|
|
||||||
router.push({
|
|
||||||
pathname: router.pathname,
|
|
||||||
query
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isSecretsLoading || isEnvListLoading) {
|
|
||||||
return (
|
|
||||||
<div className="container mx-auto flex h-screen w-full items-center justify-center px-8 text-mineshaft-50 dark:[color-scheme:dark]">
|
|
||||||
<img src="/images/loading/loading.gif" height={70} width={120} alt="loading animation" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const filteredSecrets = Object.keys(secrets?.secrets || {})?.filter((secret: any) =>
|
|
||||||
secret.toUpperCase().includes(searchFilter.toUpperCase())
|
|
||||||
);
|
|
||||||
// when secrets is not loading and secrets list is empty
|
|
||||||
const isDashboardSecretEmpty = !isSecretsLoading && !filteredSecrets?.length;
|
|
||||||
const isFoldersEmtpy =
|
|
||||||
!folders.some(({ isLoading: isFolderLoading }) => isFolderLoading) &&
|
|
||||||
!Object.keys(foldersGroupedByEnv).length;
|
|
||||||
const isDashboardEmpty = isFoldersEmtpy && isDashboardSecretEmpty;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container mx-auto max-w-full px-6 text-mineshaft-50 dark:[color-scheme:dark]">
|
|
||||||
<div className="relative right-5 ml-4">
|
|
||||||
<NavHeader pageName={t("dashboard.title")} isProjectRelated />
|
|
||||||
</div>
|
|
||||||
<div className="mt-6">
|
|
||||||
<p className="text-3xl font-semibold text-bunker-100">Secrets Overview</p>
|
|
||||||
<p className="text-md text-bunker-300">
|
|
||||||
Inject your secrets using
|
|
||||||
<a
|
|
||||||
className="mx-1 text-primary/80 hover:text-primary"
|
|
||||||
href="https://infisical.com/docs/cli/overview"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Infisical CLI
|
|
||||||
</a>
|
|
||||||
or
|
|
||||||
<a
|
|
||||||
className="mx-1 text-primary/80 hover:text-primary"
|
|
||||||
href="https://infisical.com/docs/sdks/overview"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Infisical SDKs
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="mt-8 flex items-center justify-between">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<div
|
|
||||||
className="breadcrumb relative z-20 border-solid border-mineshaft-600 bg-mineshaft-800 hover:bg-mineshaft-600 py-1 pl-5 pr-2 text-sm"
|
|
||||||
onClick={() => onFolderCrumbClick(0)}
|
|
||||||
onKeyDown={() => null}
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faFolderOpen} className="text-primary" />
|
|
||||||
</div>
|
|
||||||
{(secretPath || "")
|
|
||||||
.split("/")
|
|
||||||
.filter(Boolean)
|
|
||||||
.map((path, index, arr) => (
|
|
||||||
<div
|
|
||||||
key={`secret-path-${index + 1}`}
|
|
||||||
className={`breadcrumb relative z-20 ${
|
|
||||||
index + 1 === arr.length ? "cursor-default" : "cursor-pointer"
|
|
||||||
} border-solid border-mineshaft-600 py-1 pl-5 pr-2 text-sm text-mineshaft-200`}
|
|
||||||
onClick={() => onFolderCrumbClick(index + 1)}
|
|
||||||
onKeyDown={() => null}
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
>
|
|
||||||
{path}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="w-80">
|
|
||||||
<Input
|
|
||||||
className="h-[2.3rem] bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
|
|
||||||
placeholder="Search by secret/folder name..."
|
|
||||||
value={searchFilter}
|
|
||||||
onChange={(e) => setSearchFilter(e.target.value)}
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="overflow-y-auto">
|
|
||||||
<div className="sticky top-0 mt-3 flex h-10 min-w-[60.3rem] flex-row rounded-md border border-mineshaft-600 bg-mineshaft-800">
|
|
||||||
<div className="sticky top-0 flex w-10 items-center justify-center border-none px-4">
|
|
||||||
<div className="w-10 text-center text-xs text-transparent">{0}</div>
|
|
||||||
</div>
|
|
||||||
<div className="sticky top-0 border-none">
|
|
||||||
<div className="relative flex h-full w-full min-w-[200px] items-center justify-start lg:min-w-[220px] xl:min-w-[250px]">
|
|
||||||
<div className="text-sm font-medium ">Secret</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{numSecretsMissingPerEnv &&
|
|
||||||
userAvailableEnvs?.map((env) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={`header-${env.slug}`}
|
|
||||||
className="flex w-full min-w-[11rem] flex-row items-center rounded-md border-none bg-mineshaft-800"
|
|
||||||
>
|
|
||||||
<div className="flex w-full flex-row justify-center text-center text-sm font-medium text-bunker-200/[.99]">
|
|
||||||
{env.name}
|
|
||||||
{numSecretsMissingPerEnv[env.slug] > 0 && (
|
|
||||||
<div className="mt-0.5 ml-2.5 flex h-[1.1rem] w-[1.1rem] cursor-default items-center justify-center rounded-sm border border-red-400 bg-red text-xs text-bunker-100">
|
|
||||||
<Tooltip
|
|
||||||
content={`${
|
|
||||||
numSecretsMissingPerEnv[env.slug]
|
|
||||||
} secrets missing compared to other environments`}
|
|
||||||
>
|
|
||||||
<span className="text-bunker-100">
|
|
||||||
{numSecretsMissingPerEnv[env.slug]}
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={`${
|
|
||||||
isDashboardEmpty ? "" : ""
|
|
||||||
} no-scrollbar::-webkit-scrollbar mt-3 flex h-full max-h-[calc(100vh-370px)] w-full min-w-[60.3rem] flex-grow flex-row items-start justify-center overflow-x-hidden rounded-md border border-mineshaft-600 no-scrollbar`}
|
|
||||||
>
|
|
||||||
{!isDashboardEmpty && (
|
|
||||||
<TableContainer className="border-none">
|
|
||||||
<table className="secret-table relative w-full bg-mineshaft-900">
|
|
||||||
<tbody className="max-h-screen overflow-y-auto">
|
|
||||||
{Object.keys(foldersGroupedByEnv || {}).map((folderName, index) => (
|
|
||||||
<FolderComparisonRow
|
|
||||||
key={`${folderName}-${index + 1}`}
|
|
||||||
folderName={folderName}
|
|
||||||
userAvailableEnvs={userAvailableEnvs}
|
|
||||||
folderInEnv={foldersGroupedByEnv[folderName]}
|
|
||||||
onClick={onFolderClick}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{Object.keys(secrets?.secrets || {})
|
|
||||||
?.filter((secret: any) =>
|
|
||||||
secret.toUpperCase().includes(searchFilter.toUpperCase())
|
|
||||||
)
|
|
||||||
.map((key) => (
|
|
||||||
<EnvComparisonRow
|
|
||||||
key={`row-${key}`}
|
|
||||||
secrets={secrets?.secrets?.[key]}
|
|
||||||
isReadOnly
|
|
||||||
isSecretValueHidden
|
|
||||||
userAvailableEnvs={userAvailableEnvs}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</TableContainer>
|
|
||||||
)}
|
|
||||||
{isDashboardEmpty && (
|
|
||||||
<div className="flex h-40 w-full flex-row rounded-md">
|
|
||||||
<div className="flex w-full min-w-[11rem] flex-col items-center justify-center rounded-md border-none bg-mineshaft-800 text-bunker-300">
|
|
||||||
<FontAwesomeIcon icon={faKey} className="mb-4 text-4xl" />
|
|
||||||
<span className="mb-1">No secrets/folders found.</span>
|
|
||||||
<span>To add more secrets you can explore any environment.</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="group mt-4 flex min-w-[60.3rem] flex-row items-center">
|
|
||||||
<div className="flex h-10 w-10 items-center justify-center border-none px-4">
|
|
||||||
<div className="w-10 text-center text-xs text-transparent">0</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex min-w-[200px] flex-row items-center justify-between lg:min-w-[220px] xl:min-w-[250px]">
|
|
||||||
<span className="text-transparent">0</span>
|
|
||||||
<button type="button" className="mr-2 text-transparent">
|
|
||||||
1
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{userAvailableEnvs?.map((env) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={`button-${env.slug}`}
|
|
||||||
className="mx-2 mb-1 flex h-10 w-full min-w-[11rem] flex-row items-center justify-center border-none"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
onClick={() => onExploreEnv(env.slug)}
|
|
||||||
variant="outline_bg"
|
|
||||||
colorSchema="primary"
|
|
||||||
isFullWidth
|
|
||||||
className="h-10"
|
|
||||||
>
|
|
||||||
Explore {env.name}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { FormProvider, useFieldArray, useForm } from "react-hook-form";
|
import { FormProvider, useFieldArray, useForm } from "react-hook-form";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@@ -30,7 +30,11 @@ import {
|
|||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuTrigger
|
||||||
|
} from "@radix-ui/react-dropdown-menu";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
@@ -119,12 +123,13 @@ type TDeleteSecretImport = { environment: string; secretPath: string };
|
|||||||
* Instead when user delete we raise a flag so if user decides to go back to toggle personal before saving
|
* Instead when user delete we raise a flag so if user decides to go back to toggle personal before saving
|
||||||
* They will get it back
|
* They will get it back
|
||||||
*/
|
*/
|
||||||
export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
export const DashboardPage = () => {
|
||||||
const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { createNotification } = useNotificationContext();
|
const { createNotification } = useNotificationContext();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const envQuery = router.query.env as string;
|
||||||
|
|
||||||
const secretContainer = useRef<HTMLDivElement | null>(null);
|
const secretContainer = useRef<HTMLDivElement | null>(null);
|
||||||
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
|
||||||
@@ -172,8 +177,8 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
// get an env with one of the access available
|
// get an env with one of the access available
|
||||||
const env = data.find(({ isReadDenied, isWriteDenied }) => !isWriteDenied || !isReadDenied);
|
const env = data.find(({ isReadDenied, isWriteDenied }) => !isWriteDenied || !isReadDenied);
|
||||||
if (env && data?.map((wsenv) => wsenv.slug).includes(envFromTop)) {
|
if (env && data?.map((wsenv) => wsenv.slug).includes(envQuery)) {
|
||||||
setSelectedEnv(data?.filter((dp) => dp.slug === envFromTop)[0]);
|
setSelectedEnv(data?.filter((dp) => dp.slug === envQuery)[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -293,11 +298,12 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
register,
|
||||||
control,
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
getValues,
|
getValues,
|
||||||
setValue,
|
setValue,
|
||||||
formState: { isSubmitting, isDirty },
|
formState: { isSubmitting, isDirty, errors },
|
||||||
reset
|
reset
|
||||||
} = method;
|
} = method;
|
||||||
const { fields, prepend, append, remove } = useFieldArray({ control, name: "secrets" });
|
const { fields, prepend, append, remove } = useFieldArray({ control, name: "secrets" });
|
||||||
@@ -461,9 +467,9 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDrawerOpen = (dto: TSecretDetailsOpen) => {
|
const onDrawerOpen = useCallback((id: string | undefined, index: number) => {
|
||||||
handlePopUpOpen("secretDetails", dto);
|
handlePopUpOpen("secretDetails", { id, index } as TSecretDetailsOpen);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const onEnvChange = (slug: string) => {
|
const onEnvChange = (slug: string) => {
|
||||||
if (hasUnsavedChanges) {
|
if (hasUnsavedChanges) {
|
||||||
@@ -480,17 +486,27 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDownloadSecret = () => {
|
||||||
|
const secretsFromImport: { key: string; value: string; comment: string }[] = [];
|
||||||
|
importedSecrets?.forEach(({ secrets: impSec }) => {
|
||||||
|
impSec.forEach((el) => {
|
||||||
|
secretsFromImport.push({ key: el.key, value: el.value, comment: el.comment });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
downloadSecret(getValues("secrets"), secretsFromImport, selectedEnv?.slug);
|
||||||
|
};
|
||||||
|
|
||||||
// record all deleted ids
|
// record all deleted ids
|
||||||
// This will make final deletion easier
|
// This will make final deletion easier
|
||||||
const onSecretDelete = (index: number, id?: string, overrideId?: string) => {
|
const onSecretDelete = useCallback((index: number, id?: string, overrideId?: string) => {
|
||||||
if (id) deletedSecretIds.current.push(id);
|
if (id) deletedSecretIds.current.push(id);
|
||||||
if (overrideId) deletedSecretIds.current.push(overrideId);
|
if (overrideId) deletedSecretIds.current.push(overrideId);
|
||||||
remove(index);
|
remove(index);
|
||||||
// just the case if this is called from drawer
|
// just the case if this is called from drawer
|
||||||
handlePopUpClose("secretDetails");
|
handlePopUpClose("secretDetails");
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const onCreateWsTag = async (tagName: string) => {
|
const onCreateWsTag = useCallback(async (tagName: string) => {
|
||||||
try {
|
try {
|
||||||
await createWsTag({
|
await createWsTag({
|
||||||
workspaceID: workspaceId,
|
workspaceID: workspaceId,
|
||||||
@@ -509,10 +525,11 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
type: "error"
|
type: "error"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const handleFolderOpen = (id: string) => {
|
const handleFolderOpen = useCallback((id: string) => {
|
||||||
setSearchFilter("");
|
setSearchFilter("");
|
||||||
|
console.log(router.query);
|
||||||
router.push({
|
router.push({
|
||||||
pathname: router.pathname,
|
pathname: router.pathname,
|
||||||
query: {
|
query: {
|
||||||
@@ -520,10 +537,11 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
folderId: id
|
folderId: id
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const isEditFolder = Boolean(popUp?.folderForm?.data);
|
const isEditFolder = Boolean(popUp?.folderForm?.data);
|
||||||
|
|
||||||
|
// FOLDER SECTION
|
||||||
const handleFolderCreate = async (name: string) => {
|
const handleFolderCreate = async (name: string) => {
|
||||||
try {
|
try {
|
||||||
await createFolder({
|
await createFolder({
|
||||||
@@ -546,7 +564,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFolderUpdate = async (name: string) => {
|
const handleFolderUpdate = useCallback(async (name: string) => {
|
||||||
const { id } = popUp?.folderForm?.data as TDeleteFolderForm;
|
const { id } = popUp?.folderForm?.data as TDeleteFolderForm;
|
||||||
try {
|
try {
|
||||||
await updateFolder({
|
await updateFolder({
|
||||||
@@ -567,9 +585,9 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
type: "error"
|
type: "error"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const handleFolderDelete = async () => {
|
const handleFolderDelete = useCallback(async () => {
|
||||||
const { id } = popUp?.deleteFolder?.data as TDeleteFolderForm;
|
const { id } = popUp?.deleteFolder?.data as TDeleteFolderForm;
|
||||||
try {
|
try {
|
||||||
deleteFolder({
|
deleteFolder({
|
||||||
@@ -589,8 +607,9 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
type: "error"
|
type: "error"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
|
// SECRET IMPORT SECTION
|
||||||
const handleSecretImportCreate = async (env: string, secretPath: string) => {
|
const handleSecretImportCreate = async (env: string, secretPath: string) => {
|
||||||
try {
|
try {
|
||||||
await createSecretImport({
|
await createSecretImport({
|
||||||
@@ -664,6 +683,25 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// OPTIMIZATION HOOKS PURELY FOR PERFORMANCE AND TO AVOID RE-RENDERING
|
||||||
|
const handleCreateTagModalOpen = useCallback(() => handlePopUpOpen("addTag"), []);
|
||||||
|
const handleFolderCreatePopUpOpen = useCallback(
|
||||||
|
(id: string, name: string) => handlePopUpOpen("folderForm", { id, name }),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const handleFolderDeletePopUpOpen = useCallback(
|
||||||
|
(id: string, name: string) => handlePopUpOpen("deleteFolder", { id, name }),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const handleSecretImportDelPopUpOpen = useCallback(
|
||||||
|
(impSecEnv: string, impSecPath: string) =>
|
||||||
|
handlePopUpOpen("deleteSecretImport", {
|
||||||
|
environment: impSecEnv,
|
||||||
|
secretPath: impSecPath
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
// when secrets is not loading and secrets list is empty
|
// when secrets is not loading and secrets list is empty
|
||||||
const isDashboardSecretEmpty = !isSecretsLoading && !fields?.length;
|
const isDashboardSecretEmpty = !isSecretsLoading && !fields?.length;
|
||||||
|
|
||||||
@@ -693,259 +731,259 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto h-full px-6 text-mineshaft-50 dark:[color-scheme:dark]">
|
<div className="container mx-auto h-full px-6 text-mineshaft-50 dark:[color-scheme:dark]">
|
||||||
<FormProvider {...method}>
|
<form autoComplete="off" className="h-full">
|
||||||
<form autoComplete="off" className="h-full">
|
{/* breadcrumb row */}
|
||||||
{/* breadcrumb row */}
|
<div className="relative right-6 -top-2 mb-2 ml-6">
|
||||||
<div className="relative right-6 -top-2 mb-2 ml-6">
|
<NavHeader
|
||||||
<NavHeader
|
pageName={t("dashboard.title")}
|
||||||
pageName={t("dashboard.title")}
|
currentEnv={userAvailableEnvs?.filter((envir) => envir.slug === envQuery)[0].name || ""}
|
||||||
currentEnv={
|
isFolderMode
|
||||||
userAvailableEnvs?.filter((envir) => envir.slug === envFromTop)[0].name || ""
|
folders={folderData?.dir}
|
||||||
}
|
isProjectRelated
|
||||||
isFolderMode
|
userAvailableEnvs={userAvailableEnvs}
|
||||||
folders={folderData?.dir}
|
onEnvChange={onEnvChange}
|
||||||
isProjectRelated
|
/>
|
||||||
userAvailableEnvs={userAvailableEnvs}
|
</div>
|
||||||
onEnvChange={onEnvChange}
|
<div className="mb-4">
|
||||||
|
<h6 className="text-2xl">{isRollbackMode ? "Secret Snapshot" : ""}</h6>
|
||||||
|
{isRollbackMode && Boolean(snapshotSecret) && (
|
||||||
|
<Tag colorSchema="green">
|
||||||
|
{new Date(snapshotSecret?.createdAt || "").toLocaleString()}
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* Environment, search and other action row */}
|
||||||
|
<div className="flex items-center justify-between space-x-2">
|
||||||
|
<div className="flex max-w-lg flex-grow space-x-2">
|
||||||
|
<Input
|
||||||
|
className="h-[2.3rem] bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
|
||||||
|
placeholder="Search by folder name, key name, comment..."
|
||||||
|
value={searchFilter}
|
||||||
|
onChange={(e) => setSearchFilter(e.target.value)}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-4">
|
<div className="flex items-center space-x-2">
|
||||||
<h6 className="text-2xl">{isRollbackMode ? "Secret Snapshot" : ""}</h6>
|
<div>
|
||||||
{isRollbackMode && Boolean(snapshotSecret) && (
|
<Popover>
|
||||||
<Tag colorSchema="green">
|
<PopoverTrigger asChild>
|
||||||
{new Date(snapshotSecret?.createdAt || "").toLocaleString()}
|
<IconButton ariaLabel="download" variant="outline_bg">
|
||||||
</Tag>
|
<FontAwesomeIcon icon={faDownload} />
|
||||||
)}
|
</IconButton>
|
||||||
</div>
|
</PopoverTrigger>
|
||||||
{/* Environment, search and other action row */}
|
<PopoverContent
|
||||||
<div className="flex items-center justify-between space-x-2">
|
className="w-auto border border-mineshaft-600 bg-mineshaft-800 p-1"
|
||||||
<div className="flex max-w-lg flex-grow space-x-2">
|
hideCloseBtn
|
||||||
<Input
|
>
|
||||||
className="h-[2.3rem] bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
|
<div className="flex flex-col space-y-2">
|
||||||
placeholder="Search by folder name, key name, comment..."
|
<Button
|
||||||
value={searchFilter}
|
onClick={handleDownloadSecret}
|
||||||
onChange={(e) => setSearchFilter(e.target.value)}
|
colorSchema="primary"
|
||||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
variant="outline_bg"
|
||||||
/>
|
className="h-8 bg-bunker-700"
|
||||||
|
>
|
||||||
|
Download as .env
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div>
|
||||||
<div>
|
<Tooltip content={isSecretValueHidden ? "Reveal Secrets" : "Hide secrets"}>
|
||||||
<Popover>
|
<IconButton
|
||||||
<PopoverTrigger asChild>
|
ariaLabel="reveal"
|
||||||
<IconButton ariaLabel="download" variant="outline_bg">
|
|
||||||
<FontAwesomeIcon icon={faDownload} />
|
|
||||||
</IconButton>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent
|
|
||||||
className="w-auto border border-mineshaft-600 bg-mineshaft-800 p-1"
|
|
||||||
hideCloseBtn
|
|
||||||
>
|
|
||||||
<div className="flex flex-col space-y-2">
|
|
||||||
<Button
|
|
||||||
onClick={() => downloadSecret(getValues("secrets"), selectedEnv?.slug)}
|
|
||||||
colorSchema="primary"
|
|
||||||
variant="outline_bg"
|
|
||||||
className="h-8 bg-bunker-700"
|
|
||||||
>
|
|
||||||
Download as .env
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Tooltip content={isSecretValueHidden ? "Reveal Secrets" : "Hide secrets"}>
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="reveal"
|
|
||||||
variant="outline_bg"
|
|
||||||
onClick={() => setIsSecretValueHidden.toggle()}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={isSecretValueHidden ? faEye : faEyeSlash} />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
<div className="block xl:hidden">
|
|
||||||
<Tooltip content="Point-in-time Recovery">
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="recovery"
|
|
||||||
variant="outline_bg"
|
|
||||||
onClick={() => handlePopUpOpen("secretSnapshots")}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faCodeCommit} />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
<div className="hidden xl:block">
|
|
||||||
<Button
|
|
||||||
variant="outline_bg"
|
variant="outline_bg"
|
||||||
onClick={() => {
|
onClick={() => setIsSecretValueHidden.toggle()}
|
||||||
if (subscription && subscription.pitRecovery) {
|
|
||||||
handlePopUpOpen("secretSnapshots");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
|
||||||
}}
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faCodeCommit} />}
|
|
||||||
isLoading={isLoadingSnapshotCount}
|
|
||||||
isDisabled={!canDoRollback}
|
|
||||||
className="h-10"
|
|
||||||
>
|
>
|
||||||
{snapshotCount} Commits
|
<FontAwesomeIcon icon={isSecretValueHidden ? faEye : faEyeSlash} />
|
||||||
</Button>
|
</IconButton>
|
||||||
</div>
|
</Tooltip>
|
||||||
{!isReadOnly && !isRollbackMode && (
|
</div>
|
||||||
<div className="flex flex-row items-center justify-center">
|
<div className="block xl:hidden">
|
||||||
<button
|
<Tooltip content="Point-in-time Recovery">
|
||||||
type="button"
|
<IconButton
|
||||||
onClick={() => {
|
ariaLabel="recovery"
|
||||||
if (!(isReadOnly || isRollbackMode)) {
|
variant="outline_bg"
|
||||||
if (secretContainer.current) {
|
onClick={() => handlePopUpOpen("secretSnapshots")}
|
||||||
secretContainer.current.scroll({
|
|
||||||
top: 0,
|
|
||||||
behavior: "smooth"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
prepend(DEFAULT_SECRET_VALUE, { shouldFocus: false });
|
|
||||||
setSearchFilter("");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="font-semibold bg-mineshaft-600 border border-mineshaft-500 p-2 rounded-l-md text-sm text-mineshaft-300 cursor-pointer hover:bg-primary/[0.1] hover:border-primary/40 pr-4 duration-200"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faPlus} className="px-2"/>Add Secret
|
|
||||||
</button>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild className="data-[state=open]:bg-mineshaft-600">
|
|
||||||
<div className="bg-mineshaft-600 border border-mineshaft-500 p-2 rounded-r-md text-sm text-mineshaft-300 cursor-pointer hover:bg-primary/[0.1] hover:border-primary/40 duration-200">
|
|
||||||
<FontAwesomeIcon icon={faAngleDown} className="pr-2 pl-1.5"/>
|
|
||||||
</div>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end" className="mt-1 z-[60] left-20 w-[10.8rem]">
|
|
||||||
<div className="bg-mineshaft-800 p-1 border border-mineshaft-600 rounded-md">
|
|
||||||
<div className="w-full pb-1">
|
|
||||||
<Button
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faFolderPlus} />}
|
|
||||||
onClick={() => handlePopUpOpen("folderForm")}
|
|
||||||
isDisabled={isReadOnly || isRollbackMode}
|
|
||||||
variant="outline_bg"
|
|
||||||
className="h-10"
|
|
||||||
isFullWidth
|
|
||||||
>
|
|
||||||
Add Folder
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="w-full">
|
|
||||||
<Button
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faFileImport} />}
|
|
||||||
onClick={() => handlePopUpOpen("addSecretImport")}
|
|
||||||
isDisabled={isReadOnly || isRollbackMode}
|
|
||||||
variant="outline_bg"
|
|
||||||
className="h-10"
|
|
||||||
isFullWidth
|
|
||||||
>
|
|
||||||
Add Import
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{isRollbackMode && (
|
|
||||||
<Button
|
|
||||||
variant="star"
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faArrowLeft} />}
|
|
||||||
onClick={() => {
|
|
||||||
setSnaphotId(null);
|
|
||||||
reset({ ...secrets, isSnapshotMode: false });
|
|
||||||
}}
|
|
||||||
className="h-10"
|
|
||||||
>
|
>
|
||||||
Go back
|
<FontAwesomeIcon icon={faCodeCommit} />
|
||||||
</Button>
|
</IconButton>
|
||||||
)}
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div className="hidden xl:block">
|
||||||
<Button
|
<Button
|
||||||
isDisabled={isSubmitDisabled}
|
variant="outline_bg"
|
||||||
isLoading={isSubmitting}
|
onClick={() => {
|
||||||
leftIcon={<FontAwesomeIcon icon={isRollbackMode ? faClockRotateLeft : faCheck} />}
|
if (subscription && subscription.pitRecovery) {
|
||||||
onClick={handleSubmit(onSaveSecret)}
|
handlePopUpOpen("secretSnapshots");
|
||||||
className="h-10 text-black"
|
return;
|
||||||
color="primary"
|
}
|
||||||
variant="solid"
|
|
||||||
|
handlePopUpOpen("upgradePlan");
|
||||||
|
}}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faCodeCommit} />}
|
||||||
|
isLoading={isLoadingSnapshotCount}
|
||||||
|
isDisabled={!canDoRollback}
|
||||||
|
className="h-10"
|
||||||
>
|
>
|
||||||
{isRollbackMode ? "Rollback" : "Save Changes"}
|
{snapshotCount} Commits
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{!isReadOnly && !isRollbackMode && (
|
||||||
<div
|
<div className="flex flex-row items-center justify-center">
|
||||||
className={`${
|
<button
|
||||||
isEmptyPage ? "flex flex-col items-center justify-center" : ""
|
type="button"
|
||||||
} no-scrollbar::-webkit-scrollbar mt-3 h-3/4 overflow-x-hidden overflow-y-scroll no-scrollbar`}
|
onClick={() => {
|
||||||
ref={secretContainer}
|
if (!(isReadOnly || isRollbackMode)) {
|
||||||
>
|
if (secretContainer.current) {
|
||||||
{!isEmptyPage && (
|
secretContainer.current.scroll({
|
||||||
<DndContext
|
top: 0,
|
||||||
onDragEnd={handleDragEnd}
|
behavior: "smooth"
|
||||||
sensors={sensors}
|
});
|
||||||
collisionDetection={closestCenter}
|
}
|
||||||
modifiers={[restrictToVerticalAxis]}
|
prepend(DEFAULT_SECRET_VALUE, { shouldFocus: false });
|
||||||
>
|
setSearchFilter("");
|
||||||
<TableContainer className="no-scrollbar::-webkit-scrollbar max-h-[calc(100%-120px)] no-scrollbar">
|
}
|
||||||
<table className="secret-table relative">
|
}}
|
||||||
<SecretTableHeader sortDir={sortDir} onSort={onSortSecrets} />
|
className="cursor-pointer rounded-l-md border border-mineshaft-500 bg-mineshaft-600 p-2 pr-4 text-sm font-semibold text-mineshaft-300 duration-200 hover:border-primary/40 hover:bg-primary/[0.1]"
|
||||||
<tbody className="max-h-96 overflow-y-auto">
|
>
|
||||||
<SecretImportSection
|
<FontAwesomeIcon icon={faPlus} className="px-2" />
|
||||||
onSecretImportDelete={(impSecEnv, impSecPath) =>
|
Add Secret
|
||||||
handlePopUpOpen("deleteSecretImport", {
|
</button>
|
||||||
environment: impSecEnv,
|
<DropdownMenu>
|
||||||
secretPath: impSecPath
|
<DropdownMenuTrigger asChild className="data-[state=open]:bg-mineshaft-600">
|
||||||
})
|
<div className="cursor-pointer rounded-r-md border border-mineshaft-500 bg-mineshaft-600 p-2 text-sm text-mineshaft-300 duration-200 hover:border-primary/40 hover:bg-primary/[0.1]">
|
||||||
}
|
<FontAwesomeIcon icon={faAngleDown} className="pr-2 pl-1.5" />
|
||||||
secrets={secrets?.secrets}
|
</div>
|
||||||
importedSecrets={importedSecrets}
|
</DropdownMenuTrigger>
|
||||||
items={items}
|
<DropdownMenuContent align="end" className="left-20 z-[60] mt-1 w-[10.8rem]">
|
||||||
/>
|
<div className="rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1">
|
||||||
<FolderSection
|
<div className="w-full pb-1">
|
||||||
onFolderOpen={handleFolderOpen}
|
<Button
|
||||||
onFolderUpdate={(id, name) => handlePopUpOpen("folderForm", { id, name })}
|
leftIcon={<FontAwesomeIcon icon={faFolderPlus} />}
|
||||||
onFolderDelete={(id, name) => handlePopUpOpen("deleteFolder", { id, name })}
|
onClick={() => handlePopUpOpen("folderForm")}
|
||||||
folders={folderList}
|
isDisabled={isReadOnly || isRollbackMode}
|
||||||
search={searchFilter}
|
variant="outline_bg"
|
||||||
/>
|
className="h-10"
|
||||||
{fields.map(({ id, _id }, index) => (
|
isFullWidth
|
||||||
<SecretInputRow
|
>
|
||||||
key={id}
|
Add Folder
|
||||||
isReadOnly={isReadOnly}
|
</Button>
|
||||||
isRollbackMode={isRollbackMode}
|
</div>
|
||||||
isAddOnly={isAddOnly}
|
<div className="w-full">
|
||||||
index={index}
|
<Button
|
||||||
searchTerm={searchFilter}
|
leftIcon={<FontAwesomeIcon icon={faFileImport} />}
|
||||||
onSecretDelete={onSecretDelete}
|
onClick={() => handlePopUpOpen("addSecretImport")}
|
||||||
onRowExpand={() => onDrawerOpen({ id: _id as string, index })}
|
isDisabled={isReadOnly || isRollbackMode}
|
||||||
isSecretValueHidden={isSecretValueHidden}
|
variant="outline_bg"
|
||||||
wsTags={wsTags}
|
className="h-10"
|
||||||
onCreateTagOpen={() => handlePopUpOpen("addTag")}
|
isFullWidth
|
||||||
/>
|
>
|
||||||
))}
|
Add Import
|
||||||
{!isReadOnly && !isRollbackMode && (
|
</Button>
|
||||||
<tr>
|
</div>
|
||||||
<td colSpan={3} className="hover:bg-mineshaft-700">
|
</div>
|
||||||
<button
|
</DropdownMenuContent>
|
||||||
type="button"
|
</DropdownMenu>
|
||||||
className="flex h-8 w-full cursor-default items-center justify-start pl-12 font-normal text-bunker-300"
|
</div>
|
||||||
onClick={onAppendSecret}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faPlus} />
|
|
||||||
<span className="ml-2 w-20">Add Secret</span>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</TableContainer>
|
|
||||||
</DndContext>
|
|
||||||
)}
|
)}
|
||||||
|
{isRollbackMode && (
|
||||||
|
<Button
|
||||||
|
variant="star"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faArrowLeft} />}
|
||||||
|
onClick={() => {
|
||||||
|
setSnaphotId(null);
|
||||||
|
reset({ ...secrets, isSnapshotMode: false });
|
||||||
|
}}
|
||||||
|
className="h-10"
|
||||||
|
>
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
isDisabled={isSubmitDisabled}
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={isRollbackMode ? faClockRotateLeft : faCheck} />}
|
||||||
|
onClick={handleSubmit(onSaveSecret)}
|
||||||
|
className="h-10 text-black"
|
||||||
|
color="primary"
|
||||||
|
variant="solid"
|
||||||
|
>
|
||||||
|
{isRollbackMode ? "Rollback" : "Save Changes"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`${
|
||||||
|
isEmptyPage ? "flex flex-col items-center justify-center" : ""
|
||||||
|
} no-scrollbar::-webkit-scrollbar mt-3 h-3/4 overflow-x-hidden overflow-y-scroll no-scrollbar`}
|
||||||
|
ref={secretContainer}
|
||||||
|
>
|
||||||
|
{!isEmptyPage && (
|
||||||
|
<DndContext
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
sensors={sensors}
|
||||||
|
collisionDetection={closestCenter}
|
||||||
|
modifiers={[restrictToVerticalAxis]}
|
||||||
|
>
|
||||||
|
<TableContainer className="no-scrollbar::-webkit-scrollbar max-h-[calc(100%-120px)] no-scrollbar">
|
||||||
|
<table className="secret-table relative">
|
||||||
|
<SecretTableHeader sortDir={sortDir} onSort={onSortSecrets} />
|
||||||
|
<tbody className="max-h-96 overflow-y-auto">
|
||||||
|
<SecretImportSection
|
||||||
|
onSecretImportDelete={handleSecretImportDelPopUpOpen}
|
||||||
|
secrets={secrets?.secrets}
|
||||||
|
importedSecrets={importedSecrets}
|
||||||
|
items={items}
|
||||||
|
/>
|
||||||
|
<FolderSection
|
||||||
|
onFolderOpen={handleFolderOpen}
|
||||||
|
onFolderUpdate={handleFolderCreatePopUpOpen}
|
||||||
|
onFolderDelete={handleFolderDeletePopUpOpen}
|
||||||
|
folders={folderList}
|
||||||
|
search={searchFilter}
|
||||||
|
/>
|
||||||
|
{fields.map(({ id, _id }, index) => (
|
||||||
|
<SecretInputRow
|
||||||
|
key={id}
|
||||||
|
secUniqId={_id}
|
||||||
|
isReadOnly={isReadOnly}
|
||||||
|
isRollbackMode={isRollbackMode}
|
||||||
|
isAddOnly={isAddOnly}
|
||||||
|
index={index}
|
||||||
|
searchTerm={searchFilter}
|
||||||
|
onSecretDelete={onSecretDelete}
|
||||||
|
isKeyError={Boolean(errors?.secrets?.[index]?.key?.message)}
|
||||||
|
keyError={errors?.secrets?.[index]?.key?.message}
|
||||||
|
onRowExpand={onDrawerOpen}
|
||||||
|
isSecretValueHidden={isSecretValueHidden}
|
||||||
|
wsTags={wsTags}
|
||||||
|
onCreateTagOpen={handleCreateTagModalOpen}
|
||||||
|
register={register}
|
||||||
|
control={control}
|
||||||
|
setValue={setValue}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{!isReadOnly && !isRollbackMode && (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={3} className="hover:bg-mineshaft-700">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex h-8 w-full cursor-default items-center justify-start pl-12 font-normal text-bunker-300"
|
||||||
|
onClick={onAppendSecret}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faPlus} />
|
||||||
|
<span className="ml-2 w-20">Add Secret</span>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</TableContainer>
|
||||||
|
</DndContext>
|
||||||
|
)}
|
||||||
|
<FormProvider {...method}>
|
||||||
<PitDrawer
|
<PitDrawer
|
||||||
isDrawerOpen={popUp?.secretSnapshots?.isOpen}
|
isDrawerOpen={popUp?.secretSnapshots?.isOpen}
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("secretSnapshots", isOpen)}
|
onOpenChange={(isOpen) => handlePopUpToggle("secretSnapshots", isOpen)}
|
||||||
@@ -966,120 +1004,121 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
index={(popUp?.secretDetails?.data as TSecretDetailsOpen)?.index}
|
index={(popUp?.secretDetails?.data as TSecretDetailsOpen)?.index}
|
||||||
onEnvCompare={(key) => handlePopUpOpen("compareSecrets", key)}
|
onEnvCompare={(key) => handlePopUpOpen("compareSecrets", key)}
|
||||||
/>
|
/>
|
||||||
<SecretDropzone
|
</FormProvider>
|
||||||
isSmaller={!isEmptyPage}
|
|
||||||
onParsedEnv={handleUploadedEnv}
|
<SecretDropzone
|
||||||
onAddNewSecret={onAppendSecret}
|
isSmaller={!isEmptyPage}
|
||||||
/>
|
onParsedEnv={handleUploadedEnv}
|
||||||
</div>
|
onAddNewSecret={onAppendSecret}
|
||||||
{/* secrets table and drawers, modals */}
|
/>
|
||||||
</form>
|
</div>
|
||||||
{/* Create a new tag modal */}
|
{/* secrets table and drawers, modals */}
|
||||||
<Modal
|
</form>
|
||||||
isOpen={popUp?.addTag?.isOpen}
|
{/* Create a new tag modal */}
|
||||||
onOpenChange={(open) => {
|
<Modal
|
||||||
handlePopUpToggle("addTag", open);
|
isOpen={popUp?.addTag?.isOpen}
|
||||||
}}
|
onOpenChange={(open) => {
|
||||||
|
handlePopUpToggle("addTag", open);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
title="Create tag"
|
||||||
|
subTitle="Specify your tag name, and the slug will be created automatically."
|
||||||
>
|
>
|
||||||
<ModalContent
|
<CreateTagModal onCreateTag={onCreateWsTag} />
|
||||||
title="Create tag"
|
</ModalContent>
|
||||||
subTitle="Specify your tag name, and the slug will be created automatically."
|
</Modal>
|
||||||
>
|
{/* Uploaded env override or not confirmation modal */}
|
||||||
<CreateTagModal onCreateTag={onCreateWsTag} />
|
<Modal
|
||||||
</ModalContent>
|
isOpen={popUp?.uploadedSecOpts?.isOpen}
|
||||||
</Modal>
|
onOpenChange={(open) => handlePopUpToggle("uploadedSecOpts", open)}
|
||||||
{/* Uploaded env override or not confirmation modal */}
|
>
|
||||||
<Modal
|
<ModalContent
|
||||||
isOpen={popUp?.uploadedSecOpts?.isOpen}
|
title="Duplicate Secrets"
|
||||||
onOpenChange={(open) => handlePopUpToggle("uploadedSecOpts", open)}
|
footerContent={[
|
||||||
|
<Button
|
||||||
|
key="keep-old-btn"
|
||||||
|
className="mr-4"
|
||||||
|
onClick={() => handlePopUpClose("uploadedSecOpts")}
|
||||||
|
>
|
||||||
|
Keep old
|
||||||
|
</Button>,
|
||||||
|
<Button colorSchema="danger" key="overwrite-btn" onClick={onOverwriteSecrets}>
|
||||||
|
Overwrite
|
||||||
|
</Button>
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<ModalContent
|
<div className="flex flex-col space-y-2 text-gray-300">
|
||||||
title="Duplicate Secrets"
|
<div>Your file contains following duplicate secrets</div>
|
||||||
footerContent={[
|
<div className="text-sm text-gray-400">
|
||||||
<Button
|
{Object.keys((popUp?.uploadedSecOpts?.data as TSecOverwriteOpt)?.secrets || {})
|
||||||
key="keep-old-btn"
|
?.map((key) => key)
|
||||||
className="mr-4"
|
.join(", ")}
|
||||||
onClick={() => handlePopUpClose("uploadedSecOpts")}
|
|
||||||
>
|
|
||||||
Keep old
|
|
||||||
</Button>,
|
|
||||||
<Button colorSchema="danger" key="overwrite-btn" onClick={onOverwriteSecrets}>
|
|
||||||
Overwrite
|
|
||||||
</Button>
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col space-y-2 text-gray-300">
|
|
||||||
<div>Your file contains following duplicate secrets</div>
|
|
||||||
<div className="text-sm text-gray-400">
|
|
||||||
{Object.keys((popUp?.uploadedSecOpts?.data as TSecOverwriteOpt)?.secrets || {})
|
|
||||||
?.map((key) => key)
|
|
||||||
.join(", ")}
|
|
||||||
</div>
|
|
||||||
<div>Are you sure you want to overwrite these secrets?</div>
|
|
||||||
</div>
|
</div>
|
||||||
</ModalContent>
|
<div>Are you sure you want to overwrite these secrets?</div>
|
||||||
</Modal>
|
</div>
|
||||||
<Modal
|
</ModalContent>
|
||||||
isOpen={popUp?.folderForm?.isOpen}
|
</Modal>
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("folderForm", isOpen)}
|
<Modal
|
||||||
|
isOpen={popUp?.folderForm?.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("folderForm", isOpen)}
|
||||||
|
>
|
||||||
|
<ModalContent title={isEditFolder ? "Edit Folder" : "Create Folder"}>
|
||||||
|
<FolderForm
|
||||||
|
isEdit={isEditFolder}
|
||||||
|
onUpdateFolder={handleFolderUpdate}
|
||||||
|
onCreateFolder={handleFolderCreate}
|
||||||
|
defaultFolderName={(popUp?.folderForm?.data as TEditFolderForm)?.name}
|
||||||
|
/>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp?.addSecretImport?.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("addSecretImport", isOpen)}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
title="Add Secret Link"
|
||||||
|
subTitle="To inherit secrets from another environment or folder"
|
||||||
>
|
>
|
||||||
<ModalContent title={isEditFolder ? "Edit Folder" : "Create Folder"}>
|
<SecretImportForm
|
||||||
<FolderForm
|
environments={currentWorkspace?.environments}
|
||||||
isEdit={isEditFolder}
|
onCreate={handleSecretImportCreate}
|
||||||
onUpdateFolder={handleFolderUpdate}
|
/>
|
||||||
onCreateFolder={handleFolderCreate}
|
</ModalContent>
|
||||||
defaultFolderName={(popUp?.folderForm?.data as TEditFolderForm)?.name}
|
</Modal>
|
||||||
/>
|
<DeleteActionModal
|
||||||
</ModalContent>
|
isOpen={popUp.deleteFolder.isOpen}
|
||||||
</Modal>
|
deleteKey={(popUp.deleteFolder?.data as TDeleteFolderForm)?.name}
|
||||||
<Modal
|
title="Do you want to delete this folder?"
|
||||||
isOpen={popUp?.addSecretImport?.isOpen}
|
onChange={(isOpen) => handlePopUpToggle("deleteFolder", isOpen)}
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("addSecretImport", isOpen)}
|
onDeleteApproved={handleFolderDelete}
|
||||||
|
/>
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.deleteSecretImport.isOpen}
|
||||||
|
deleteKey="unlink"
|
||||||
|
title="Do you want to remove this secret import?"
|
||||||
|
subTitle={`This will unlink secrets from environment ${
|
||||||
|
(popUp.deleteSecretImport?.data as TDeleteSecretImport)?.environment
|
||||||
|
} of path ${(popUp.deleteSecretImport?.data as TDeleteSecretImport)?.secretPath}?`}
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("deleteSecretImport", isOpen)}
|
||||||
|
onDeleteApproved={handleSecretImportDelete}
|
||||||
|
/>
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp?.compareSecrets?.isOpen}
|
||||||
|
onOpenChange={(open) => handlePopUpToggle("compareSecrets", open)}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
title={popUp?.compareSecrets?.data as string}
|
||||||
|
subTitle="Below is the comparison of secret values across available environments"
|
||||||
|
overlayClassName="z-[90]"
|
||||||
>
|
>
|
||||||
<ModalContent
|
<CompareSecret
|
||||||
title="Add Secret Link"
|
workspaceId={workspaceId}
|
||||||
subTitle="To inherit secrets from another environment or folder"
|
envs={userAvailableEnvs || []}
|
||||||
>
|
secretKey={popUp?.compareSecrets?.data as string}
|
||||||
<SecretImportForm
|
/>
|
||||||
environments={currentWorkspace?.environments}
|
</ModalContent>
|
||||||
onCreate={handleSecretImportCreate}
|
</Modal>
|
||||||
/>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
<DeleteActionModal
|
|
||||||
isOpen={popUp.deleteFolder.isOpen}
|
|
||||||
deleteKey={(popUp.deleteFolder?.data as TDeleteFolderForm)?.name}
|
|
||||||
title="Do you want to delete this folder?"
|
|
||||||
onChange={(isOpen) => handlePopUpToggle("deleteFolder", isOpen)}
|
|
||||||
onDeleteApproved={handleFolderDelete}
|
|
||||||
/>
|
|
||||||
<DeleteActionModal
|
|
||||||
isOpen={popUp.deleteSecretImport.isOpen}
|
|
||||||
deleteKey="unlink"
|
|
||||||
title="Do you want to remove this secret import?"
|
|
||||||
subTitle={`This will unlink secrets from environment ${
|
|
||||||
(popUp.deleteSecretImport?.data as TDeleteSecretImport)?.environment
|
|
||||||
} of path ${(popUp.deleteSecretImport?.data as TDeleteSecretImport)?.secretPath}?`}
|
|
||||||
onChange={(isOpen) => handlePopUpToggle("deleteSecretImport", isOpen)}
|
|
||||||
onDeleteApproved={handleSecretImportDelete}
|
|
||||||
/>
|
|
||||||
<Modal
|
|
||||||
isOpen={popUp?.compareSecrets?.isOpen}
|
|
||||||
onOpenChange={(open) => handlePopUpToggle("compareSecrets", open)}
|
|
||||||
>
|
|
||||||
<ModalContent
|
|
||||||
title={popUp?.compareSecrets?.data as string}
|
|
||||||
subTitle="Below is the comparison of secret values across available environments"
|
|
||||||
overlayClassName="z-[90]"
|
|
||||||
>
|
|
||||||
<CompareSecret
|
|
||||||
workspaceId={workspaceId}
|
|
||||||
envs={userAvailableEnvs || []}
|
|
||||||
secretKey={popUp?.compareSecrets?.data as string}
|
|
||||||
/>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
</FormProvider>
|
|
||||||
{subscription && (
|
{subscription && (
|
||||||
<UpgradePlanModal
|
<UpgradePlanModal
|
||||||
isOpen={popUp.upgradePlan.isOpen}
|
isOpen={popUp.upgradePlan.isOpen}
|
||||||
|
|||||||
@@ -75,12 +75,29 @@ export type FormData = yup.InferType<typeof schema>;
|
|||||||
export type TSecretDetailsOpen = { index: number; id: string };
|
export type TSecretDetailsOpen = { index: number; id: string };
|
||||||
export type TSecOverwriteOpt = { secrets: Record<string, { comments: string[]; value: string }> };
|
export type TSecOverwriteOpt = { secrets: Record<string, { comments: string[]; value: string }> };
|
||||||
|
|
||||||
export const downloadSecret = (secrets: FormData["secrets"] = [], env: string = "unknown") => {
|
export const downloadSecret = (
|
||||||
const finalSecret = secrets.map(({ key, value, valueOverride, overrideAction, comment }) => ({
|
secrets: FormData["secrets"] = [],
|
||||||
key,
|
importedSecrets: { key: string; value?: string; comment?: string }[] = [],
|
||||||
value: overrideAction && overrideAction !== SecretActionType.Deleted ? valueOverride : value,
|
env: string = "unknown"
|
||||||
comment
|
) => {
|
||||||
}));
|
const importSecPos: Record<string, number> = {};
|
||||||
|
importedSecrets.forEach((el, index) => {
|
||||||
|
importSecPos[el.key] = index;
|
||||||
|
});
|
||||||
|
const finalSecret = [...importedSecrets];
|
||||||
|
secrets.forEach(({ key, value, valueOverride, overrideAction, comment }) => {
|
||||||
|
const newValue = {
|
||||||
|
key,
|
||||||
|
value: overrideAction && overrideAction !== SecretActionType.Deleted ? valueOverride : value,
|
||||||
|
comment
|
||||||
|
};
|
||||||
|
// can also be zero thus failing
|
||||||
|
if (typeof importSecPos?.[key] === "undefined") {
|
||||||
|
finalSecret.push(newValue);
|
||||||
|
} else {
|
||||||
|
finalSecret[importSecPos[key]] = newValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let file = "";
|
let file = "";
|
||||||
finalSecret.forEach(({ key, value, comment }) => {
|
finalSecret.forEach(({ key, value, comment }) => {
|
||||||
|
|||||||
@@ -1,135 +0,0 @@
|
|||||||
/* eslint-disable react/jsx-no-useless-fragment */
|
|
||||||
import { useCallback, useRef, useState } from "react";
|
|
||||||
import { faEye, faEyeSlash, faKey, faMinus } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
import { useSyntaxHighlight } from "@app/hooks";
|
|
||||||
import { useToggle } from "@app/hooks/useToggle";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
secrets: any[] | undefined;
|
|
||||||
// permission and external state's that decided to hide or show
|
|
||||||
isReadOnly?: boolean;
|
|
||||||
isSecretValueHidden: boolean;
|
|
||||||
userAvailableEnvs?: any[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const SEC_VAL_LINE_HEIGHT = 21;
|
|
||||||
const MAX_MULTI_LINE = 6;
|
|
||||||
|
|
||||||
const DashboardInput = ({
|
|
||||||
isOverridden,
|
|
||||||
isSecretValueHidden,
|
|
||||||
secret,
|
|
||||||
isReadOnly = true
|
|
||||||
}: {
|
|
||||||
isOverridden: boolean;
|
|
||||||
isSecretValueHidden: boolean;
|
|
||||||
isReadOnly?: boolean;
|
|
||||||
secret?: any;
|
|
||||||
}): JSX.Element => {
|
|
||||||
const ref = useRef<HTMLElement | null>(null);
|
|
||||||
const [isFocused, setIsFocused] = useToggle();
|
|
||||||
const syntaxHighlight = useSyntaxHighlight();
|
|
||||||
|
|
||||||
const value = isOverridden ? secret.valueOverride : secret?.value;
|
|
||||||
const multilineExpandUnit = ((value?.match(/\n/g)?.length || 0) + 1) * SEC_VAL_LINE_HEIGHT;
|
|
||||||
const maxMultilineHeight = Math.min(multilineExpandUnit, 21 * MAX_MULTI_LINE);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<td
|
|
||||||
key={`row-${secret?.key || ""}--`}
|
|
||||||
className={`flex w-full cursor-default flex-row ${
|
|
||||||
!(secret?.value || secret?.value === "") ? "bg-red-800/10" : "bg-mineshaft-900/30"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="group relative flex w-full flex-col whitespace-pre px-1.5 pt-1.5">
|
|
||||||
<textarea
|
|
||||||
readOnly={isReadOnly}
|
|
||||||
value={value}
|
|
||||||
className="ph-no-capture min-w-16 duration-50 peer z-20 w-full resize-none overflow-auto text-ellipsis bg-transparent px-2 font-mono text-sm text-transparent caret-white outline-none no-scrollbar"
|
|
||||||
style={{ height: `${maxMultilineHeight}px` }}
|
|
||||||
spellCheck="false"
|
|
||||||
onBlur={() => setIsFocused.off()}
|
|
||||||
onFocus={() => setIsFocused.on()}
|
|
||||||
onInput={(el) => {
|
|
||||||
if (ref.current) {
|
|
||||||
ref.current.scrollTop = el.currentTarget.scrollTop;
|
|
||||||
ref.current.scrollLeft = el.currentTarget.scrollLeft;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onScroll={(el) => {
|
|
||||||
if (ref.current) {
|
|
||||||
ref.current.scrollTop = el.currentTarget.scrollTop;
|
|
||||||
ref.current.scrollLeft = el.currentTarget.scrollLeft;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<pre className="whitespace-pre-wrap break-words">
|
|
||||||
<code
|
|
||||||
ref={ref}
|
|
||||||
className={`absolute top-1.5 left-3.5 z-10 overflow-auto font-mono text-sm transition-all no-scrollbar ${
|
|
||||||
isOverridden && "text-primary-300"
|
|
||||||
} ${
|
|
||||||
(value || "") === "" && "text-mineshaft-400"
|
|
||||||
}`}
|
|
||||||
style={{ height: `${maxMultilineHeight}px`, width: "calc(100% - 12px)" }}
|
|
||||||
>
|
|
||||||
{value === undefined ? (
|
|
||||||
<span className="cursor-default font-sans text-xs italic text-red-500/80">
|
|
||||||
<FontAwesomeIcon icon={faMinus} className="mt-1" />
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
syntaxHighlight(value || "", isSecretValueHidden ? !isFocused : isSecretValueHidden)
|
|
||||||
)}
|
|
||||||
</code>
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EnvComparisonRow = ({
|
|
||||||
secrets,
|
|
||||||
isSecretValueHidden,
|
|
||||||
isReadOnly,
|
|
||||||
userAvailableEnvs
|
|
||||||
}: Props): JSX.Element => {
|
|
||||||
const [areValuesHiddenThisRow, setAreValuesHiddenThisRow] = useState(true);
|
|
||||||
|
|
||||||
const getSecretByEnv = useCallback(
|
|
||||||
(secEnv: string, secs?: any[]) => secs?.find(({ env }) => env === secEnv),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr className="group flex min-w-full flex-row hover:bg-mineshaft-800">
|
|
||||||
<td className="flex w-10 justify-center border-none px-4">
|
|
||||||
<div className="flex h-8 w-10 items-center justify-center text-center text-xs text-bunker-400">
|
|
||||||
<FontAwesomeIcon icon={faKey} />
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="flex min-w-[200px] flex-row justify-between lg:min-w-[220px] xl:min-w-[250px]">
|
|
||||||
<div className="flex h-8 cursor-default flex-row items-center justify-center truncate">
|
|
||||||
{secrets![0].key || ""}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="invisible mr-1 ml-2 text-bunker-400 hover:text-bunker-300 group-hover:visible"
|
|
||||||
onClick={() => setAreValuesHiddenThisRow(!areValuesHiddenThisRow)}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={areValuesHiddenThisRow ? faEye : faEyeSlash} />
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
{userAvailableEnvs?.map(({ slug }) => (
|
|
||||||
<DashboardInput
|
|
||||||
isReadOnly={isReadOnly}
|
|
||||||
key={`row-${secrets![0].key || ""}-${slug}`}
|
|
||||||
isOverridden={false}
|
|
||||||
secret={getSecretByEnv(slug, secrets)}
|
|
||||||
isSecretValueHidden={areValuesHiddenThisRow && isSecretValueHidden}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import { faCheck, faFolder, faXmark } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
folderInEnv: Record<string, boolean>;
|
|
||||||
userAvailableEnvs?: Array<{ slug: string; name: string }>;
|
|
||||||
folderName: string;
|
|
||||||
onClick: (folderName: string) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FolderComparisonRow = ({
|
|
||||||
folderInEnv = {},
|
|
||||||
userAvailableEnvs = [],
|
|
||||||
folderName,
|
|
||||||
onClick
|
|
||||||
}: Props) => (
|
|
||||||
<tr
|
|
||||||
className="group flex min-w-full cursor-pointer flex-row items-center hover:bg-mineshaft-800"
|
|
||||||
onClick={() => onClick(folderName)}
|
|
||||||
>
|
|
||||||
<td className="flex h-10 w-10 items-center justify-center border-none px-4">
|
|
||||||
<div className="w-10 text-center text-xs text-bunker-400">
|
|
||||||
<FontAwesomeIcon icon={faFolder} className="text-primary-700" />
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="flex h-full min-w-[200px] flex-row items-center justify-between lg:min-w-[200px] xl:min-w-[250px]">
|
|
||||||
<div className="flex h-8 flex-row items-center truncate">{folderName}</div>
|
|
||||||
</td>
|
|
||||||
{userAvailableEnvs?.map(({ slug }) => (
|
|
||||||
<td
|
|
||||||
className={`flex h-10 w-full cursor-default flex-row items-center justify-center ${
|
|
||||||
folderInEnv[slug]
|
|
||||||
? "bg-mineshaft-900/30 text-green-500/80"
|
|
||||||
: "bg-red-800/10 text-red-500/80"
|
|
||||||
}`}
|
|
||||||
key={`${folderName}-${slug}`}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={folderInEnv[slug] ? faCheck : faXmark} />
|
|
||||||
</td>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { EnvComparisonRow } from "./EnvComparisonRow";
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { memo } from "react";
|
||||||
import { faEdit, faFolder, faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faEdit, faFolder, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
@@ -11,70 +12,74 @@ type Props = {
|
|||||||
onFolderOpen: (folderId: string) => void;
|
onFolderOpen: (folderId: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FolderSection = ({
|
export const FolderSection = memo(
|
||||||
onFolderUpdate: handleFolderUpdate,
|
({
|
||||||
onFolderDelete: handleFolderDelete,
|
onFolderUpdate: handleFolderUpdate,
|
||||||
onFolderOpen: handleFolderOpen,
|
onFolderDelete: handleFolderDelete,
|
||||||
search = "",
|
onFolderOpen: handleFolderOpen,
|
||||||
folders = []
|
search = "",
|
||||||
}: Props) => {
|
folders = []
|
||||||
return (
|
}: Props) => {
|
||||||
<>
|
return (
|
||||||
{folders
|
<>
|
||||||
.filter(({ name }) => name.toLowerCase().includes(search.toLowerCase()))
|
{folders
|
||||||
.map(({ id, name }) => (
|
.filter(({ name }) => name.toLowerCase().includes(search.toLowerCase()))
|
||||||
<tr
|
.map(({ id, name }) => (
|
||||||
key={id}
|
<tr
|
||||||
className="group flex cursor-default flex-row items-center hover:bg-mineshaft-700"
|
key={id}
|
||||||
>
|
className="group flex cursor-default flex-row items-center hover:bg-mineshaft-700"
|
||||||
<td className="ml-0.5 flex h-10 w-10 items-center justify-center border-none px-4">
|
|
||||||
<FontAwesomeIcon icon={faFolder} className="text-primary-700" />
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
colSpan={2}
|
|
||||||
className="relative flex w-full min-w-[220px] items-center justify-between overflow-hidden text-ellipsis lg:min-w-[240px] xl:min-w-[280px]"
|
|
||||||
style={{ paddingTop: "0", paddingBottom: "0" }}
|
|
||||||
>
|
>
|
||||||
<div
|
<td className="ml-0.5 flex h-10 w-10 items-center justify-center border-none px-4">
|
||||||
className="flex-grow cursor-default p-2"
|
<FontAwesomeIcon icon={faFolder} className="text-primary-700" />
|
||||||
onKeyDown={() => null}
|
</td>
|
||||||
tabIndex={0}
|
<td
|
||||||
role="button"
|
colSpan={2}
|
||||||
onClick={() => handleFolderOpen(id)}
|
className="relative flex w-full min-w-[220px] items-center justify-between overflow-hidden text-ellipsis lg:min-w-[240px] xl:min-w-[280px]"
|
||||||
|
style={{ paddingTop: "0", paddingBottom: "0" }}
|
||||||
>
|
>
|
||||||
{name}
|
<div
|
||||||
</div>
|
className="flex-grow cursor-default p-2"
|
||||||
<div className="duration-0 flex h-10 w-16 items-center justify-end space-x-2.5 overflow-hidden border-l border-mineshaft-600 transition-all">
|
onKeyDown={() => null}
|
||||||
<div className="opacity-0 group-hover:opacity-100">
|
tabIndex={0}
|
||||||
<Tooltip content="Settings" className="capitalize">
|
role="button"
|
||||||
<IconButton
|
onClick={() => handleFolderOpen(id)}
|
||||||
size="md"
|
>
|
||||||
colorSchema="primary"
|
{name}
|
||||||
variant="plain"
|
|
||||||
onClick={() => handleFolderUpdate(id, name)}
|
|
||||||
ariaLabel="expand"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faEdit} />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="opacity-0 group-hover:opacity-100">
|
<div className="duration-0 flex h-10 w-16 items-center justify-end space-x-2.5 overflow-hidden border-l border-mineshaft-600 transition-all">
|
||||||
<Tooltip content="Delete" className="capitalize">
|
<div className="opacity-0 group-hover:opacity-100">
|
||||||
<IconButton
|
<Tooltip content="Settings" className="capitalize">
|
||||||
size="md"
|
<IconButton
|
||||||
variant="plain"
|
size="md"
|
||||||
colorSchema="danger"
|
colorSchema="primary"
|
||||||
ariaLabel="delete"
|
variant="plain"
|
||||||
onClick={() => handleFolderDelete(id, name)}
|
onClick={() => handleFolderUpdate(id, name)}
|
||||||
>
|
ariaLabel="expand"
|
||||||
<FontAwesomeIcon icon={faXmark} size="lg" />
|
>
|
||||||
</IconButton>
|
<FontAwesomeIcon icon={faEdit} />
|
||||||
</Tooltip>
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div className="opacity-0 group-hover:opacity-100">
|
||||||
|
<Tooltip content="Delete" className="capitalize">
|
||||||
|
<IconButton
|
||||||
|
size="md"
|
||||||
|
variant="plain"
|
||||||
|
colorSchema="danger"
|
||||||
|
ariaLabel="delete"
|
||||||
|
onClick={() => handleFolderDelete(id, name)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faXmark} size="lg" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</td>
|
||||||
</td>
|
</tr>
|
||||||
</tr>
|
))}
|
||||||
))}
|
</>
|
||||||
</>
|
);
|
||||||
);
|
}
|
||||||
};
|
);
|
||||||
|
|
||||||
|
FolderSection.displayName = "FolderSection";
|
||||||
|
|||||||
+2
-2
@@ -9,7 +9,7 @@ import {
|
|||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import { EmptyState, IconButton, TableContainer, Tooltip } from "@app/components/v2";
|
import { EmptyState, IconButton, SecretInput, TableContainer, Tooltip } from "@app/components/v2";
|
||||||
import { useWorkspace } from "@app/context";
|
import { useWorkspace } from "@app/context";
|
||||||
import { useToggle } from "@app/hooks/useToggle";
|
import { useToggle } from "@app/hooks/useToggle";
|
||||||
|
|
||||||
@@ -138,7 +138,7 @@ export const SecretImportItem = ({
|
|||||||
{key}
|
{key}
|
||||||
</td>
|
</td>
|
||||||
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
|
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
|
||||||
{value}
|
<SecretInput value={value} isDisabled isVisible />
|
||||||
</td>
|
</td>
|
||||||
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
|
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
|
||||||
<EnvFolderIcon env={overriden?.env} secretPath={overriden?.secretPath} />
|
<EnvFolderIcon env={overriden?.env} secretPath={overriden?.secretPath} />
|
||||||
|
|||||||
+28
-28
@@ -1,3 +1,4 @@
|
|||||||
|
import { memo } from "react";
|
||||||
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||||
|
|
||||||
import { useWorkspace } from "@app/context";
|
import { useWorkspace } from "@app/context";
|
||||||
@@ -62,32 +63,31 @@ type Props = {
|
|||||||
items: { id: string; environment: string; secretPath: string }[];
|
items: { id: string; environment: string; secretPath: string }[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SecretImportSection = ({
|
export const SecretImportSection = memo(
|
||||||
secrets = [],
|
({ secrets = [], importedSecrets = [], onSecretImportDelete, items = [] }: Props) => {
|
||||||
importedSecrets = [],
|
const { currentWorkspace } = useWorkspace();
|
||||||
onSecretImportDelete,
|
const environments = currentWorkspace?.environments || [];
|
||||||
items = []
|
|
||||||
}: Props) => {
|
|
||||||
const { currentWorkspace } = useWorkspace();
|
|
||||||
const environments = currentWorkspace?.environments || [];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SortableContext items={items} strategy={verticalListSortingStrategy}>
|
<SortableContext items={items} strategy={verticalListSortingStrategy}>
|
||||||
{items.map(({ secretPath: impSecPath, environment: importSecEnv, id }) => (
|
{items.map(({ secretPath: impSecPath, environment: importSecEnv, id }) => (
|
||||||
<SecretImportItem
|
<SecretImportItem
|
||||||
key={id}
|
key={id}
|
||||||
importedEnv={importSecEnv}
|
importedEnv={importSecEnv}
|
||||||
importedSecrets={computeImportedSecretRows(
|
importedSecrets={computeImportedSecretRows(
|
||||||
importSecEnv,
|
importSecEnv,
|
||||||
impSecPath,
|
impSecPath,
|
||||||
importedSecrets,
|
importedSecrets,
|
||||||
secrets,
|
secrets,
|
||||||
environments
|
environments
|
||||||
)}
|
)}
|
||||||
onDelete={onSecretImportDelete}
|
onDelete={onSecretImportDelete}
|
||||||
importedSecPath={impSecPath}
|
importedSecPath={impSecPath}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
SecretImportSection.displayName = "SecretImportSection";
|
||||||
|
|||||||
@@ -1,107 +0,0 @@
|
|||||||
import { useRef } from "react";
|
|
||||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
|
||||||
|
|
||||||
import { useSyntaxHighlight, useToggle } from "@app/hooks";
|
|
||||||
|
|
||||||
import { FormData } from "../../DashboardPage.utils";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
isReadOnly?: boolean;
|
|
||||||
isSecretValueHidden?: boolean;
|
|
||||||
isOverridden?: boolean;
|
|
||||||
index: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const SEC_VAL_LINE_HEIGHT = 21;
|
|
||||||
const MAX_MULTI_LINE = 6;
|
|
||||||
|
|
||||||
export const MaskedInput = ({ isReadOnly, isSecretValueHidden, index, isOverridden }: Props) => {
|
|
||||||
const { control } = useFormContext<FormData>();
|
|
||||||
const ref = useRef<HTMLElement | null>(null);
|
|
||||||
const [isFocused, setIsFocused] = useToggle();
|
|
||||||
const syntaxHighlight = useSyntaxHighlight();
|
|
||||||
|
|
||||||
const secretValue = useWatch({ control, name: `secrets.${index}.value` });
|
|
||||||
const secretValueOverride = useWatch({ control, name: `secrets.${index}.valueOverride` });
|
|
||||||
const value = isOverridden ? secretValueOverride : secretValue;
|
|
||||||
|
|
||||||
const multilineExpandUnit = ((value?.match(/\n/g)?.length || 0) + 1) * SEC_VAL_LINE_HEIGHT;
|
|
||||||
const maxMultilineHeight = Math.min(multilineExpandUnit, 21 * MAX_MULTI_LINE);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="group relative flex w-full flex-col whitespace-pre px-1.5 pt-1.5">
|
|
||||||
{isOverridden ? (
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name={`secrets.${index}.valueOverride`}
|
|
||||||
render={({ field }) => (
|
|
||||||
<textarea
|
|
||||||
key={`secrets.${index}.valueOverride`}
|
|
||||||
{...field}
|
|
||||||
readOnly={isReadOnly}
|
|
||||||
className="ph-no-capture min-w-16 duration-50 peer z-20 w-full resize-none overflow-auto text-ellipsis bg-transparent px-2 font-mono text-sm text-transparent caret-white outline-none no-scrollbar"
|
|
||||||
style={{ height: `${maxMultilineHeight}px` }}
|
|
||||||
spellCheck="false"
|
|
||||||
onBlur={() => setIsFocused.off()}
|
|
||||||
onFocus={() => setIsFocused.on()}
|
|
||||||
onInput={(el) => {
|
|
||||||
if (ref.current) {
|
|
||||||
ref.current.scrollTop = el.currentTarget.scrollTop;
|
|
||||||
ref.current.scrollLeft = el.currentTarget.scrollLeft;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onScroll={(el) => {
|
|
||||||
if (ref.current) {
|
|
||||||
ref.current.scrollTop = el.currentTarget.scrollTop;
|
|
||||||
ref.current.scrollLeft = el.currentTarget.scrollLeft;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name={`secrets.${index}.value`}
|
|
||||||
key={`secrets.${index}.value`}
|
|
||||||
render={({ field }) => (
|
|
||||||
<textarea
|
|
||||||
{...field}
|
|
||||||
readOnly={isReadOnly}
|
|
||||||
className="ph-no-capture min-w-16 duration-50 peer z-20 w-full resize-none overflow-auto text-ellipsis bg-transparent px-2 font-mono text-sm text-transparent caret-white outline-none no-scrollbar"
|
|
||||||
style={{ height: `${maxMultilineHeight}px` }}
|
|
||||||
spellCheck="false"
|
|
||||||
onBlur={() => setIsFocused.off()}
|
|
||||||
onFocus={() => setIsFocused.on()}
|
|
||||||
onInput={(el) => {
|
|
||||||
if (ref.current) {
|
|
||||||
ref.current.scrollTop = el.currentTarget.scrollTop;
|
|
||||||
ref.current.scrollLeft = el.currentTarget.scrollLeft;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onScroll={(el) => {
|
|
||||||
if (ref.current) {
|
|
||||||
ref.current.scrollTop = el.currentTarget.scrollTop;
|
|
||||||
ref.current.scrollLeft = el.currentTarget.scrollLeft;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<pre className="whitespace-pre-wrap break-words">
|
|
||||||
<code
|
|
||||||
ref={ref}
|
|
||||||
className={`absolute top-1.5 left-3.5 z-10 w-full overflow-auto font-mono text-sm transition-all no-scrollbar ${
|
|
||||||
isOverridden && "text-primary-300"
|
|
||||||
} ${
|
|
||||||
(value || "") === "" && "text-mineshaft-400"
|
|
||||||
}`}
|
|
||||||
style={{ height: `${maxMultilineHeight}px`, width: "calc(100% - 12px)" }}
|
|
||||||
>
|
|
||||||
{syntaxHighlight(value || "", isSecretValueHidden ? !isFocused : isSecretValueHidden)}
|
|
||||||
</code>
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,13 @@
|
|||||||
/* eslint-disable react/jsx-no-useless-fragment */
|
/* eslint-disable react/jsx-no-useless-fragment */
|
||||||
import { memo, useEffect, useRef } from "react";
|
import { memo, useEffect, useRef, useState } from "react";
|
||||||
import { Controller, useFieldArray, useFormContext, useWatch } from "react-hook-form";
|
import {
|
||||||
|
Control,
|
||||||
|
Controller,
|
||||||
|
useFieldArray,
|
||||||
|
UseFormRegister,
|
||||||
|
UseFormSetValue,
|
||||||
|
useWatch
|
||||||
|
} from "react-hook-form";
|
||||||
import {
|
import {
|
||||||
faCheck,
|
faCheck,
|
||||||
faCodeBranch,
|
faCodeBranch,
|
||||||
@@ -28,6 +35,7 @@ import {
|
|||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
|
SecretInput,
|
||||||
Tag,
|
Tag,
|
||||||
TextArea,
|
TextArea,
|
||||||
Tooltip
|
Tooltip
|
||||||
@@ -36,24 +44,6 @@ import { useToggle } from "@app/hooks";
|
|||||||
import { WsTag } from "@app/hooks/api/types";
|
import { WsTag } from "@app/hooks/api/types";
|
||||||
|
|
||||||
import { FormData, SecretActionType } from "../../DashboardPage.utils";
|
import { FormData, SecretActionType } from "../../DashboardPage.utils";
|
||||||
import { MaskedInput } from "./MaskedInput";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
index: number;
|
|
||||||
// permission and external state's that decided to hide or show
|
|
||||||
isReadOnly?: boolean;
|
|
||||||
isAddOnly?: boolean;
|
|
||||||
isRollbackMode?: boolean;
|
|
||||||
isSecretValueHidden: boolean;
|
|
||||||
searchTerm: string;
|
|
||||||
// to record the ids of deleted ones
|
|
||||||
onSecretDelete: (index: number, id?: string, overrideId?: string) => void;
|
|
||||||
// sidebar control props
|
|
||||||
onRowExpand: () => void;
|
|
||||||
// tag props
|
|
||||||
wsTags?: WsTag[];
|
|
||||||
onCreateTagOpen: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const tagColors = [
|
const tagColors = [
|
||||||
{ bg: "bg-[#f1c40f]/40", text: "text-[#fcf0c3]/70" },
|
{ bg: "bg-[#f1c40f]/40", text: "text-[#fcf0c3]/70" },
|
||||||
@@ -66,6 +56,31 @@ const tagColors = [
|
|||||||
{ bg: "bg-[#332FD0]/40", text: "text-[#DFF6FF]/70" }
|
{ bg: "bg-[#332FD0]/40", text: "text-[#DFF6FF]/70" }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
index: number;
|
||||||
|
// backend generated unique id
|
||||||
|
secUniqId?: string;
|
||||||
|
// permission and external state's that decided to hide or show
|
||||||
|
isReadOnly?: boolean;
|
||||||
|
isAddOnly?: boolean;
|
||||||
|
isRollbackMode?: boolean;
|
||||||
|
isSecretValueHidden: boolean;
|
||||||
|
searchTerm: string;
|
||||||
|
// to record the ids of deleted ones
|
||||||
|
onSecretDelete: (index: number, id?: string, overrideId?: string) => void;
|
||||||
|
// sidebar control props
|
||||||
|
onRowExpand: (secId: string | undefined, index: number) => void;
|
||||||
|
// tag props
|
||||||
|
wsTags?: WsTag[];
|
||||||
|
onCreateTagOpen: () => void;
|
||||||
|
// rhf specific functions, dont put this using useFormContext. This is passed as props to avoid re-rendering
|
||||||
|
control: Control<FormData>;
|
||||||
|
register: UseFormRegister<FormData>;
|
||||||
|
setValue: UseFormSetValue<FormData>;
|
||||||
|
isKeyError?: boolean;
|
||||||
|
keyError?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export const SecretInputRow = memo(
|
export const SecretInputRow = memo(
|
||||||
({
|
({
|
||||||
index,
|
index,
|
||||||
@@ -77,10 +92,15 @@ export const SecretInputRow = memo(
|
|||||||
wsTags,
|
wsTags,
|
||||||
onCreateTagOpen,
|
onCreateTagOpen,
|
||||||
onSecretDelete,
|
onSecretDelete,
|
||||||
searchTerm
|
searchTerm,
|
||||||
|
control,
|
||||||
|
register,
|
||||||
|
setValue,
|
||||||
|
isKeyError,
|
||||||
|
keyError,
|
||||||
|
secUniqId
|
||||||
}: Props): JSX.Element => {
|
}: Props): JSX.Element => {
|
||||||
const isKeySubDisabled = useRef<boolean>(false);
|
const isKeySubDisabled = useRef<boolean>(false);
|
||||||
const { register, setValue, control } = useFormContext<FormData>();
|
|
||||||
// comment management in a row
|
// comment management in a row
|
||||||
const {
|
const {
|
||||||
fields: secretTags,
|
fields: secretTags,
|
||||||
@@ -89,28 +109,40 @@ export const SecretInputRow = memo(
|
|||||||
} = useFieldArray({ control, name: `secrets.${index}.tags` });
|
} = useFieldArray({ control, name: `secrets.${index}.tags` });
|
||||||
|
|
||||||
// to get details on a secret
|
// to get details on a secret
|
||||||
const overrideAction = useWatch({ control, name: `secrets.${index}.overrideAction` });
|
const overrideAction = useWatch({
|
||||||
const idOverride = useWatch({ control, name: `secrets.${index}.idOverride` });
|
control,
|
||||||
const secComment = useWatch({ control, name: `secrets.${index}.comment` });
|
name: `secrets.${index}.overrideAction`,
|
||||||
|
exact: true
|
||||||
|
});
|
||||||
|
const idOverride = useWatch({ control, name: `secrets.${index}.idOverride`, exact: true });
|
||||||
|
const secComment = useWatch({ control, name: `secrets.${index}.comment`, exact: true });
|
||||||
const hasComment = Boolean(secComment);
|
const hasComment = Boolean(secComment);
|
||||||
const secKey = useWatch({
|
const secKey = useWatch({
|
||||||
control,
|
control,
|
||||||
name: `secrets.${index}.key`,
|
name: `secrets.${index}.key`,
|
||||||
disabled: isKeySubDisabled.current
|
disabled: isKeySubDisabled.current,
|
||||||
|
exact: true
|
||||||
});
|
});
|
||||||
const secValue = useWatch({
|
const secValue = useWatch({
|
||||||
control,
|
control,
|
||||||
name: `secrets.${index}.value`,
|
name: `secrets.${index}.value`,
|
||||||
disabled: isKeySubDisabled.current
|
disabled: isKeySubDisabled.current,
|
||||||
|
exact: true
|
||||||
});
|
});
|
||||||
const secValueOverride = useWatch({
|
const secValueOverride = useWatch({
|
||||||
control,
|
control,
|
||||||
name: `secrets.${index}.valueOverride`,
|
name: `secrets.${index}.valueOverride`,
|
||||||
disabled: isKeySubDisabled.current
|
disabled: isKeySubDisabled.current,
|
||||||
})
|
exact: true
|
||||||
const secId = useWatch({ control, name: `secrets.${index}._id` });
|
});
|
||||||
|
// when secret is override by personal values
|
||||||
|
const isOverridden =
|
||||||
|
overrideAction === SecretActionType.Created || overrideAction === SecretActionType.Modified;
|
||||||
|
const [editorRef, setEditorRef] = useState(isOverridden ? secValueOverride : secValue);
|
||||||
|
|
||||||
const tags = useWatch({ control, name: `secrets.${index}.tags`, defaultValue: [] }) || [];
|
const secId = useWatch({ control, name: `secrets.${index}._id`, exact: true });
|
||||||
|
const tags =
|
||||||
|
useWatch({ control, name: `secrets.${index}.tags`, exact: true, defaultValue: [] }) || [];
|
||||||
const selectedTagIds = tags.reduce<Record<string, boolean>>(
|
const selectedTagIds = tags.reduce<Record<string, boolean>>(
|
||||||
(prev, curr) => ({ ...prev, [curr.slug]: true }),
|
(prev, curr) => ({ ...prev, [curr.slug]: true }),
|
||||||
{}
|
{}
|
||||||
@@ -126,15 +158,15 @@ export const SecretInputRow = memo(
|
|||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [isInviteLinkCopied]);
|
}, [isInviteLinkCopied]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setEditorRef(isOverridden ? secValueOverride : secValue);
|
||||||
|
}, [isOverridden]);
|
||||||
|
|
||||||
const copyTokenToClipboard = () => {
|
const copyTokenToClipboard = () => {
|
||||||
navigator.clipboard.writeText((secValueOverride || secValue) as string);
|
navigator.clipboard.writeText((secValueOverride || secValue) as string);
|
||||||
setInviteLinkCopied.on();
|
setInviteLinkCopied.on();
|
||||||
};
|
};
|
||||||
|
|
||||||
// when secret is override by personal values
|
|
||||||
const isOverridden =
|
|
||||||
overrideAction === SecretActionType.Created || overrideAction === SecretActionType.Modified;
|
|
||||||
|
|
||||||
const onSecretOverride = () => {
|
const onSecretOverride = () => {
|
||||||
if (isOverridden) {
|
if (isOverridden) {
|
||||||
// when user created a new override but then removes
|
// when user created a new override but then removes
|
||||||
@@ -193,10 +225,10 @@ export const SecretInputRow = memo(
|
|||||||
control={control}
|
control={control}
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
name={`secrets.${index}.key`}
|
name={`secrets.${index}.key`}
|
||||||
render={({ fieldState: { error }, field }) => (
|
render={({ field }) => (
|
||||||
<HoverCard openDelay={0} open={error?.message ? undefined : false}>
|
<HoverCard openDelay={0} open={isKeyError ? undefined : false}>
|
||||||
<HoverCardTrigger asChild>
|
<HoverCardTrigger asChild>
|
||||||
<td className={cx(error?.message ? "rounded ring ring-red/50" : null)}>
|
<td className={cx(isKeyError ? "rounded ring ring-red/50" : null)}>
|
||||||
<div className="relative flex w-full min-w-[220px] items-center justify-end lg:min-w-[240px] xl:min-w-[280px]">
|
<div className="relative flex w-full min-w-[220px] items-center justify-end lg:min-w-[240px] xl:min-w-[280px]">
|
||||||
<Input
|
<Input
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
@@ -220,21 +252,70 @@ export const SecretInputRow = memo(
|
|||||||
<div>
|
<div>
|
||||||
<FontAwesomeIcon icon={faInfoCircle} className="text-red" />
|
<FontAwesomeIcon icon={faInfoCircle} className="text-red" />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm">{error?.message}</div>
|
<div className="text-sm">{keyError}</div>
|
||||||
</div>
|
</div>
|
||||||
</HoverCardContent>
|
</HoverCardContent>
|
||||||
</HoverCard>
|
</HoverCard>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<td className="flex w-full flex-grow flex-row border-r border-none border-red">
|
<td
|
||||||
<MaskedInput
|
className="flex w-full flex-grow flex-row border-r border-none border-red"
|
||||||
isReadOnly={
|
style={{ padding: "0.5rem 0 0.5rem 1rem" }}
|
||||||
isReadOnly || isRollbackMode || (isOverridden ? isAddOnly : shouldBeBlockedInAddOnly)
|
>
|
||||||
}
|
<div className="w-full">
|
||||||
isOverridden={isOverridden}
|
{isOverridden ? (
|
||||||
isSecretValueHidden={isSecretValueHidden}
|
<Controller
|
||||||
index={index}
|
control={control}
|
||||||
/>
|
name={`secrets.${index}.valueOverride`}
|
||||||
|
render={({ field: { onChange, onBlur } }) => (
|
||||||
|
<SecretInput
|
||||||
|
key={`secrets.${index}.valueOverride`}
|
||||||
|
isDisabled={
|
||||||
|
isReadOnly ||
|
||||||
|
isRollbackMode ||
|
||||||
|
(isOverridden ? isAddOnly : shouldBeBlockedInAddOnly)
|
||||||
|
}
|
||||||
|
value={editorRef}
|
||||||
|
isVisible={!isSecretValueHidden}
|
||||||
|
onChange={(val, html) => {
|
||||||
|
console.log(val);
|
||||||
|
onChange(val);
|
||||||
|
setEditorRef(html);
|
||||||
|
}}
|
||||||
|
onBlur={(html) => {
|
||||||
|
setEditorRef(html);
|
||||||
|
onBlur();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`secrets.${index}.value`}
|
||||||
|
render={({ field: { onBlur, onChange } }) => (
|
||||||
|
<SecretInput
|
||||||
|
key={`secrets.${index}.value`}
|
||||||
|
isVisible={!isSecretValueHidden}
|
||||||
|
isDisabled={
|
||||||
|
isReadOnly ||
|
||||||
|
isRollbackMode ||
|
||||||
|
(isOverridden ? isAddOnly : shouldBeBlockedInAddOnly)
|
||||||
|
}
|
||||||
|
onChange={(val, html) => {
|
||||||
|
onChange(val);
|
||||||
|
setEditorRef(html);
|
||||||
|
}}
|
||||||
|
value={editorRef}
|
||||||
|
onBlur={(html) => {
|
||||||
|
setEditorRef(html);
|
||||||
|
onBlur();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="min-w-sm flex">
|
<td className="min-w-sm flex">
|
||||||
<div className="flex h-8 items-center pl-2">
|
<div className="flex h-8 items-center pl-2">
|
||||||
@@ -251,7 +332,7 @@ export const SecretInputRow = memo(
|
|||||||
{slug}
|
{slug}
|
||||||
</Tag>
|
</Tag>
|
||||||
))}
|
))}
|
||||||
<div className="w-0 group-hover:w-6 overflow-hidden">
|
<div className="w-0 overflow-hidden group-hover:w-6">
|
||||||
<Tooltip content="Copy value">
|
<Tooltip content="Copy value">
|
||||||
<IconButton
|
<IconButton
|
||||||
variant="plain"
|
variant="plain"
|
||||||
@@ -396,7 +477,7 @@ export const SecretInputRow = memo(
|
|||||||
size="lg"
|
size="lg"
|
||||||
colorSchema="primary"
|
colorSchema="primary"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
onClick={onRowExpand}
|
onClick={() => onRowExpand(secUniqId, index)}
|
||||||
ariaLabel="expand"
|
ariaLabel="expand"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faEllipsis} />
|
<FontAwesomeIcon icon={faEllipsis} />
|
||||||
|
|||||||
+3
-3
@@ -53,7 +53,7 @@ export const OrgIncidentContactsTable = ({
|
|||||||
isLoading
|
isLoading
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [searchContact, setSearchContact] = useState("");
|
const [searchContact, setSearchContact] = useState("");
|
||||||
const {data: serverDetails } = useFetchServerStatus()
|
const { data: serverDetails } = useFetchServerStatus();
|
||||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
"addContact",
|
"addContact",
|
||||||
"removeContact",
|
"removeContact",
|
||||||
@@ -98,7 +98,7 @@ export const OrgIncidentContactsTable = ({
|
|||||||
<Button
|
<Button
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (serverDetails?.emailConfigured){
|
if (serverDetails?.emailConfigured) {
|
||||||
handlePopUpOpen("addContact");
|
handlePopUpOpen("addContact");
|
||||||
} else {
|
} else {
|
||||||
handlePopUpOpen("setUpEmail");
|
handlePopUpOpen("setUpEmail");
|
||||||
@@ -119,7 +119,7 @@ export const OrgIncidentContactsTable = ({
|
|||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
{isLoading && <TableSkeleton columns={2} key="incident-contact" />}
|
{isLoading && <TableSkeleton columns={2} innerKey="incident-contact" />}
|
||||||
{filteredContacts?.map(({ email }) => (
|
{filteredContacts?.map(({ email }) => (
|
||||||
<Tr key={email}>
|
<Tr key={email}>
|
||||||
<Td className="w-full">{email}</Td>
|
<Td className="w-full">{email}</Td>
|
||||||
|
|||||||
+120
-85
@@ -1,7 +1,14 @@
|
|||||||
import { Dispatch, SetStateAction, useEffect, useMemo, useState } from "react";
|
import { Dispatch, SetStateAction, useEffect, useMemo, useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { faCheck, faCopy, faMagnifyingGlass, faPlus, faTrash, faUsers } from "@fortawesome/free-solid-svg-icons";
|
import {
|
||||||
|
faCheck,
|
||||||
|
faCopy,
|
||||||
|
faMagnifyingGlass,
|
||||||
|
faPlus,
|
||||||
|
faTrash,
|
||||||
|
faUsers
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
@@ -10,7 +17,8 @@ import { useNotificationContext } from "@app/components/context/Notifications/No
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
DeleteActionModal,
|
DeleteActionModal,
|
||||||
EmailServiceSetupModal, EmptyState,
|
EmailServiceSetupModal,
|
||||||
|
EmptyState,
|
||||||
FormControl,
|
FormControl,
|
||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
@@ -29,7 +37,7 @@ import {
|
|||||||
Tr,
|
Tr,
|
||||||
UpgradePlanModal
|
UpgradePlanModal
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useOrganization , useWorkspace } from "@app/context";
|
import { useOrganization, useWorkspace } from "@app/context";
|
||||||
import { usePopUp, useToggle } from "@app/hooks";
|
import { usePopUp, useToggle } from "@app/hooks";
|
||||||
import { useGetSSOConfig } from "@app/hooks/api";
|
import { useGetSSOConfig } from "@app/hooks/api";
|
||||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||||
@@ -47,8 +55,8 @@ type Props = {
|
|||||||
onGrantAccess: (userId: string, publicKey: string) => Promise<void>;
|
onGrantAccess: (userId: string, publicKey: string) => Promise<void>;
|
||||||
// the current user id to block remove org button
|
// the current user id to block remove org button
|
||||||
userId: string;
|
userId: string;
|
||||||
completeInviteLink: string | undefined,
|
completeInviteLink: string | undefined;
|
||||||
setCompleteInviteLink: Dispatch<SetStateAction<string | undefined>>
|
setCompleteInviteLink: Dispatch<SetStateAction<string | undefined>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const addMemberFormSchema = yup.object({
|
const addMemberFormSchema = yup.object({
|
||||||
@@ -76,7 +84,7 @@ export const OrgMembersTable = ({
|
|||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const { data: ssoConfig, isLoading: isLoadingSSOConfig } = useGetSSOConfig(currentOrg?._id ?? "");
|
const { data: ssoConfig, isLoading: isLoadingSSOConfig } = useGetSSOConfig(currentOrg?._id ?? "");
|
||||||
const [searchMemberFilter, setSearchMemberFilter] = useState("");
|
const [searchMemberFilter, setSearchMemberFilter] = useState("");
|
||||||
const {data: serverDetails } = useFetchServerStatus()
|
const { data: serverDetails } = useFetchServerStatus();
|
||||||
const { workspaces } = useWorkspace();
|
const { workspaces } = useWorkspace();
|
||||||
const [isInviteLinkCopied, setInviteLinkCopied] = useToggle(false);
|
const [isInviteLinkCopied, setInviteLinkCopied] = useToggle(false);
|
||||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
@@ -85,7 +93,7 @@ export const OrgMembersTable = ({
|
|||||||
"upgradePlan",
|
"upgradePlan",
|
||||||
"setUpEmail"
|
"setUpEmail"
|
||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (router.query.action === "invite") {
|
if (router.query.action === "invite") {
|
||||||
handlePopUpOpen("addMember");
|
handlePopUpOpen("addMember");
|
||||||
@@ -101,11 +109,11 @@ export const OrgMembersTable = ({
|
|||||||
|
|
||||||
const onAddMember = async ({ email }: TAddMemberForm) => {
|
const onAddMember = async ({ email }: TAddMemberForm) => {
|
||||||
await onInviteMember(email);
|
await onInviteMember(email);
|
||||||
if (serverDetails?.emailConfigured){
|
if (serverDetails?.emailConfigured) {
|
||||||
handlePopUpClose("addMember");
|
handlePopUpClose("addMember");
|
||||||
}
|
}
|
||||||
|
|
||||||
reset();
|
reset();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onRemoveOrgMemberApproved = async () => {
|
const onRemoveOrgMemberApproved = async () => {
|
||||||
@@ -118,7 +126,7 @@ export const OrgMembersTable = ({
|
|||||||
() => members.find(({ user }) => userId === user?._id)?.role === "owner",
|
() => members.find(({ user }) => userId === user?._id)?.role === "owner",
|
||||||
[userId, members]
|
[userId, members]
|
||||||
);
|
);
|
||||||
|
|
||||||
const filterdUser = useMemo(
|
const filterdUser = useMemo(
|
||||||
() =>
|
() =>
|
||||||
members.filter(
|
members.filter(
|
||||||
@@ -163,10 +171,10 @@ export const OrgMembersTable = ({
|
|||||||
text: "You cannot invite users when SAML SSO is configured for your organization",
|
text: "You cannot invite users when SAML SSO is configured for your organization",
|
||||||
type: "error"
|
type: "error"
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isMoreUserNotAllowed) {
|
if (isMoreUserNotAllowed) {
|
||||||
handlePopUpOpen("upgradePlan");
|
handlePopUpOpen("upgradePlan");
|
||||||
} else {
|
} else {
|
||||||
@@ -190,7 +198,7 @@ export const OrgMembersTable = ({
|
|||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
{isLoading && <TableSkeleton columns={5} key="org-members" />}
|
{isLoading && <TableSkeleton columns={5} innerKey="org-members" />}
|
||||||
{!isLoading &&
|
{!isLoading &&
|
||||||
filterdUser.map(({ user, inviteEmail, role, _id: orgMembershipId, status }) => {
|
filterdUser.map(({ user, inviteEmail, role, _id: orgMembershipId, status }) => {
|
||||||
const name = user ? `${user.firstName} ${user.lastName}` : "-";
|
const name = user ? `${user.firstName} ${user.lastName}` : "-";
|
||||||
@@ -219,11 +227,17 @@ export const OrgMembersTable = ({
|
|||||||
<SelectItem value="member">member</SelectItem>
|
<SelectItem value="member">member</SelectItem>
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
{((status === "invited" || status === "verified") && serverDetails?.emailConfigured) && (
|
{(status === "invited" || status === "verified") &&
|
||||||
<Button className='w-40' colorSchema="primary" variant="outline_bg" onClick={() => onInviteMember(email)}>
|
serverDetails?.emailConfigured && (
|
||||||
Resend Invite
|
<Button
|
||||||
</Button>
|
className="w-40"
|
||||||
)}
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
onClick={() => onInviteMember(email)}
|
||||||
|
>
|
||||||
|
Resend Invite
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{status === "completed" && (
|
{status === "completed" && (
|
||||||
<Button
|
<Button
|
||||||
colorSchema="secondary"
|
colorSchema="secondary"
|
||||||
@@ -241,18 +255,33 @@ export const OrgMembersTable = ({
|
|||||||
</Tag>
|
</Tag>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className='flex flex-row'>
|
<div className="flex flex-row">
|
||||||
{((status === "invited" || status === "verified") && serverDetails?.emailConfigured)
|
{(status === "invited" || status === "verified") &&
|
||||||
? <Tag colorSchema="red">This user hasn't accepted the invite yet</Tag>
|
serverDetails?.emailConfigured ? (
|
||||||
: <Tag colorSchema="red">This user isn't part of any projects yet</Tag>}
|
<Tag colorSchema="red">
|
||||||
{router.query.id !== "undefined" && !((status === "invited" || status === "verified") && serverDetails?.emailConfigured) && <button
|
This user hasn't accepted the invite yet
|
||||||
type="button"
|
</Tag>
|
||||||
onClick={() => router.push(`/project/${workspaces[0]?._id}/members`)}
|
) : (
|
||||||
className='text-sm bg-mineshaft w-max px-1.5 py-0.5 hover:bg-primary duration-200 hover:text-black cursor-pointer rounded-sm'
|
<Tag colorSchema="red">
|
||||||
>
|
This user isn't part of any projects yet
|
||||||
<FontAwesomeIcon icon={faPlus} className="mr-1" />
|
</Tag>
|
||||||
Add to projects
|
)}
|
||||||
</button>}
|
{router.query.id !== "undefined" &&
|
||||||
|
!(
|
||||||
|
(status === "invited" || status === "verified") &&
|
||||||
|
serverDetails?.emailConfigured
|
||||||
|
) && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
router.push(`/project/${workspaces[0]?._id}/members`)
|
||||||
|
}
|
||||||
|
className="w-max cursor-pointer rounded-sm bg-mineshaft px-1.5 py-0.5 text-sm duration-200 hover:bg-primary hover:text-black"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faPlus} className="mr-1" />
|
||||||
|
Add to projects
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Td>
|
</Td>
|
||||||
@@ -282,67 +311,73 @@ export const OrgMembersTable = ({
|
|||||||
isOpen={popUp?.addMember?.isOpen}
|
isOpen={popUp?.addMember?.isOpen}
|
||||||
onOpenChange={(isOpen) => {
|
onOpenChange={(isOpen) => {
|
||||||
handlePopUpToggle("addMember", isOpen);
|
handlePopUpToggle("addMember", isOpen);
|
||||||
setCompleteInviteLink(undefined)
|
setCompleteInviteLink(undefined);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title={`Invite others to ${orgName}`}
|
title={`Invite others to ${orgName}`}
|
||||||
subTitle={
|
subTitle={
|
||||||
<div>
|
<div>
|
||||||
{!completeInviteLink && <div>
|
{!completeInviteLink && (
|
||||||
An invite is specific to an email address and expires after 1 day.
|
<div>
|
||||||
<br />
|
An invite is specific to an email address and expires after 1 day.
|
||||||
For security reasons, you will need to separately add members to projects.
|
<br />
|
||||||
</div>}
|
For security reasons, you will need to separately add members to projects.
|
||||||
{completeInviteLink && "This Infisical instance does not have a email provider setup. Please share this invite link with the invitee manually"}
|
</div>
|
||||||
|
)}
|
||||||
|
{completeInviteLink &&
|
||||||
|
"This Infisical instance does not have a email provider setup. Please share this invite link with the invitee manually"}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{!completeInviteLink && <form onSubmit={handleSubmit(onAddMember)} >
|
{!completeInviteLink && (
|
||||||
<Controller
|
<form onSubmit={handleSubmit(onAddMember)}>
|
||||||
control={control}
|
<Controller
|
||||||
defaultValue=""
|
control={control}
|
||||||
name="email"
|
defaultValue=""
|
||||||
render={({ field, fieldState: { error } }) => (
|
name="email"
|
||||||
<FormControl label="Email" isError={Boolean(error)} errorText={error?.message}>
|
render={({ field, fieldState: { error } }) => (
|
||||||
<Input {...field} />
|
<FormControl label="Email" isError={Boolean(error)} errorText={error?.message}>
|
||||||
</FormControl>
|
<Input {...field} />
|
||||||
)}
|
</FormControl>
|
||||||
/>
|
)}
|
||||||
<div className="mt-8 flex items-center">
|
/>
|
||||||
<Button
|
<div className="mt-8 flex items-center">
|
||||||
className="mr-4"
|
<Button
|
||||||
size="sm"
|
className="mr-4"
|
||||||
type="submit"
|
size="sm"
|
||||||
isLoading={isSubmitting}
|
type="submit"
|
||||||
isDisabled={isSubmitting}
|
isLoading={isSubmitting}
|
||||||
>
|
isDisabled={isSubmitting}
|
||||||
Add Member
|
>
|
||||||
</Button>
|
Add Member
|
||||||
<Button
|
</Button>
|
||||||
colorSchema="secondary"
|
<Button
|
||||||
variant="plain"
|
colorSchema="secondary"
|
||||||
onClick={() => handlePopUpClose("addMember")}
|
variant="plain"
|
||||||
>
|
onClick={() => handlePopUpClose("addMember")}
|
||||||
Cancel
|
>
|
||||||
</Button>
|
Cancel
|
||||||
</div>
|
</Button>
|
||||||
</form>}
|
</div>
|
||||||
{
|
</form>
|
||||||
completeInviteLink &&
|
)}
|
||||||
|
{completeInviteLink && (
|
||||||
<div className="mt-2 mb-3 mr-2 flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
<div className="mt-2 mb-3 mr-2 flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||||
<p className="mr-4 break-all">{completeInviteLink}</p>
|
<p className="mr-4 break-all">{completeInviteLink}</p>
|
||||||
<IconButton
|
<IconButton
|
||||||
ariaLabel="copy icon"
|
ariaLabel="copy icon"
|
||||||
colorSchema="secondary"
|
colorSchema="secondary"
|
||||||
className="group relative"
|
className="group relative"
|
||||||
onClick={copyTokenToClipboard}
|
onClick={copyTokenToClipboard}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={isInviteLinkCopied ? faCheck : faCopy} />
|
<FontAwesomeIcon icon={isInviteLinkCopied ? faCheck : faCopy} />
|
||||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">click to copy</span>
|
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||||
</IconButton>
|
click to copy
|
||||||
</div>
|
</span>
|
||||||
}
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
@@ -363,4 +398,4 @@ export const OrgMembersTable = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+313
-313
@@ -1,14 +1,15 @@
|
|||||||
import { useEffect, useMemo,useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { Controller,useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import {
|
import {
|
||||||
faCheck,
|
faCheck,
|
||||||
faCopy,
|
faCopy,
|
||||||
faMagnifyingGlass,
|
faMagnifyingGlass,
|
||||||
faPencil,
|
faPencil,
|
||||||
faPlus,
|
faPlus,
|
||||||
faServer,
|
faServer,
|
||||||
faTrash} from "@fortawesome/free-solid-svg-icons";
|
faTrash
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
@@ -37,9 +38,10 @@ import {
|
|||||||
import { useOrganization, useWorkspace } from "@app/context";
|
import { useOrganization, useWorkspace } from "@app/context";
|
||||||
import { usePopUp, useToggle } from "@app/hooks";
|
import { usePopUp, useToggle } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
useCreateServiceAccount,
|
useCreateServiceAccount,
|
||||||
useDeleteServiceAccount,
|
useDeleteServiceAccount,
|
||||||
useGetServiceAccounts} from "@app/hooks/api";
|
useGetServiceAccounts
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
|
||||||
const serviceAccountExpiration = [
|
const serviceAccountExpiration = [
|
||||||
{ label: "1 Day", value: 86400 },
|
{ label: "1 Day", value: 86400 },
|
||||||
@@ -51,317 +53,315 @@ const serviceAccountExpiration = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const addServiceAccountFormSchema = yup.object({
|
const addServiceAccountFormSchema = yup.object({
|
||||||
name: yup.string().required().label("Name").trim(),
|
name: yup.string().required().label("Name").trim(),
|
||||||
expiresIn: yup.string().required().label("Service Account Expiration")
|
expiresIn: yup.string().required().label("Service Account Expiration")
|
||||||
});
|
});
|
||||||
|
|
||||||
type TAddServiceAccountForm = yup.InferType<typeof addServiceAccountFormSchema>;
|
type TAddServiceAccountForm = yup.InferType<typeof addServiceAccountFormSchema>;
|
||||||
|
|
||||||
export const OrgServiceAccountsTable = () => {
|
export const OrgServiceAccountsTable = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
const orgId = currentOrg?._id || "";
|
|
||||||
const [step, setStep] = useState(0);
|
|
||||||
const [isAccessKeyCopied, setIsAccessKeyCopied] = useToggle(false);
|
|
||||||
const [isPublicKeyCopied, setIsPublicKeyCopied] = useToggle(false);
|
|
||||||
const [isPrivateKeyCopied, setIsPrivateKeyCopied] = useToggle(false);
|
|
||||||
const [accessKey, setAccessKey] = useState("");
|
|
||||||
const [publicKey, setPublicKey] = useState("");
|
|
||||||
const [privateKey, setPrivateKey] = useState("");
|
|
||||||
const [searchServiceAccountFilter, setSearchServiceAccountFilter] = useState("");
|
|
||||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
|
||||||
"addServiceAccount",
|
|
||||||
"removeServiceAccount",
|
|
||||||
] as const);
|
|
||||||
|
|
||||||
const { data: serviceAccounts = [], isLoading: isServiceAccountsLoading } = useGetServiceAccounts(orgId);
|
const orgId = currentOrg?._id || "";
|
||||||
|
const [step, setStep] = useState(0);
|
||||||
const createServiceAccount = useCreateServiceAccount();
|
const [isAccessKeyCopied, setIsAccessKeyCopied] = useToggle(false);
|
||||||
const removeServiceAccount = useDeleteServiceAccount();
|
const [isPublicKeyCopied, setIsPublicKeyCopied] = useToggle(false);
|
||||||
|
const [isPrivateKeyCopied, setIsPrivateKeyCopied] = useToggle(false);
|
||||||
useEffect(() => {
|
const [accessKey, setAccessKey] = useState("");
|
||||||
let timer: NodeJS.Timeout;
|
const [publicKey, setPublicKey] = useState("");
|
||||||
if (isAccessKeyCopied) {
|
const [privateKey, setPrivateKey] = useState("");
|
||||||
timer = setTimeout(() => setIsAccessKeyCopied.off(), 2000);
|
const [searchServiceAccountFilter, setSearchServiceAccountFilter] = useState("");
|
||||||
}
|
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
|
"addServiceAccount",
|
||||||
|
"removeServiceAccount"
|
||||||
|
] as const);
|
||||||
|
|
||||||
if (isPublicKeyCopied) {
|
const { data: serviceAccounts = [], isLoading: isServiceAccountsLoading } =
|
||||||
timer = setTimeout(() => setIsPublicKeyCopied.off(), 2000);
|
useGetServiceAccounts(orgId);
|
||||||
}
|
|
||||||
|
|
||||||
if (isPrivateKeyCopied) {
|
const createServiceAccount = useCreateServiceAccount();
|
||||||
timer = setTimeout(() => setIsPrivateKeyCopied.off(), 2000);
|
const removeServiceAccount = useDeleteServiceAccount();
|
||||||
}
|
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
useEffect(() => {
|
||||||
}, [isAccessKeyCopied, isPublicKeyCopied, isPrivateKeyCopied]);
|
let timer: NodeJS.Timeout;
|
||||||
|
if (isAccessKeyCopied) {
|
||||||
const {
|
timer = setTimeout(() => setIsAccessKeyCopied.off(), 2000);
|
||||||
control,
|
|
||||||
handleSubmit,
|
|
||||||
reset,
|
|
||||||
formState: { isSubmitting }
|
|
||||||
} = useForm<TAddServiceAccountForm>({ resolver: yupResolver(addServiceAccountFormSchema) });
|
|
||||||
|
|
||||||
const onAddServiceAccount = async ({ name, expiresIn }: TAddServiceAccountForm) => {
|
|
||||||
if (!currentOrg?._id) return;
|
|
||||||
|
|
||||||
const keyPair = generateKeyPair();
|
|
||||||
setPublicKey(keyPair.publicKey);
|
|
||||||
setPrivateKey(keyPair.privateKey);
|
|
||||||
|
|
||||||
const serviceAccountDetails = await createServiceAccount.mutateAsync({
|
|
||||||
name,
|
|
||||||
organizationId: currentOrg?._id,
|
|
||||||
publicKey: keyPair.publicKey,
|
|
||||||
expiresIn: Number(expiresIn)
|
|
||||||
});
|
|
||||||
|
|
||||||
setAccessKey(serviceAccountDetails.serviceAccountAccessKey);
|
|
||||||
|
|
||||||
setStep(1);
|
|
||||||
reset();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onRemoveServiceAccount = async () => {
|
if (isPublicKeyCopied) {
|
||||||
const serviceAccountId = (popUp?.removeServiceAccount?.data as { _id: string })?._id;
|
timer = setTimeout(() => setIsPublicKeyCopied.off(), 2000);
|
||||||
await removeServiceAccount.mutateAsync(serviceAccountId);
|
|
||||||
handlePopUpClose("removeServiceAccount");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredServiceAccounts = useMemo(
|
if (isPrivateKeyCopied) {
|
||||||
() =>
|
timer = setTimeout(() => setIsPrivateKeyCopied.off(), 2000);
|
||||||
serviceAccounts.filter(
|
|
||||||
({ name }) =>
|
|
||||||
name.toLowerCase().includes(searchServiceAccountFilter)
|
|
||||||
),
|
|
||||||
[serviceAccounts, searchServiceAccountFilter]
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderStep = (stepToRender: number) => {
|
|
||||||
switch (stepToRender) {
|
|
||||||
case 0:
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit(onAddServiceAccount)}>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
defaultValue=""
|
|
||||||
name="name"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl label="Name" isError={Boolean(error)} errorText={error?.message}>
|
|
||||||
<Input {...field} />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="expiresIn"
|
|
||||||
defaultValue={String(serviceAccountExpiration?.[0]?.value)}
|
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
|
|
||||||
return (
|
|
||||||
<FormControl
|
|
||||||
label="Expiration"
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
defaultValue={field.value}
|
|
||||||
{...field}
|
|
||||||
onValueChange={(e) => onChange(e)}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
{serviceAccountExpiration.map(({ label, value }) => (
|
|
||||||
<SelectItem value={String(value)} key={label}>
|
|
||||||
{label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="mt-8 flex items-center">
|
|
||||||
<Button
|
|
||||||
className="mr-4"
|
|
||||||
size="sm"
|
|
||||||
type="submit"
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
isDisabled={isSubmitting}
|
|
||||||
>
|
|
||||||
Create Service Account
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
colorSchema="secondary"
|
|
||||||
variant="plain"
|
|
||||||
onClick={() => handlePopUpClose("addServiceAccount")}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
case 1:
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<p>Access Key</p>
|
|
||||||
<div className="flex items-center justify-end rounded-md p-2 text-base text-gray-400 bg-white/[0.07]">
|
|
||||||
<p className="mr-4 break-all">{accessKey}</p>
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="copy icon"
|
|
||||||
colorSchema="secondary"
|
|
||||||
className="group relative"
|
|
||||||
onClick={() => {
|
|
||||||
navigator.clipboard.writeText(accessKey);
|
|
||||||
setIsAccessKeyCopied.on();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={isAccessKeyCopied ? faCheck : faCopy} />
|
|
||||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
|
||||||
Copy
|
|
||||||
</span>
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
<p className="mt-4">Public Key</p>
|
|
||||||
<div className="flex items-center justify-end rounded-md p-2 text-base text-gray-400 bg-white/[0.07]">
|
|
||||||
<p className="mr-4 break-all">{publicKey}</p>
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="copy icon"
|
|
||||||
colorSchema="secondary"
|
|
||||||
className="group relative"
|
|
||||||
onClick={() => {
|
|
||||||
navigator.clipboard.writeText(publicKey);
|
|
||||||
setIsPublicKeyCopied.on();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={isPublicKeyCopied ? faCheck : faCopy} />
|
|
||||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
|
||||||
Copy
|
|
||||||
</span>
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
<p className="mt-4">Private Key</p>
|
|
||||||
<div className="flex items-center justify-end rounded-md p-2 text-base text-gray-400 bg-white/[0.07]">
|
|
||||||
<p className="mr-4 break-all">{privateKey}</p>
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="copy icon"
|
|
||||||
colorSchema="secondary"
|
|
||||||
className="group relative"
|
|
||||||
onClick={() => {
|
|
||||||
navigator.clipboard.writeText(privateKey);
|
|
||||||
setIsPrivateKeyCopied.on();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={isPrivateKeyCopied ? faCheck : faCopy} />
|
|
||||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
|
||||||
Copy
|
|
||||||
</span>
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return <div />
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return () => clearTimeout(timer);
|
||||||
<div className="w-full">
|
}, [isAccessKeyCopied, isPublicKeyCopied, isPrivateKeyCopied]);
|
||||||
<div className="mb-4 flex">
|
|
||||||
<div className="mr-4 flex-1">
|
const {
|
||||||
<Input
|
control,
|
||||||
value={searchServiceAccountFilter}
|
handleSubmit,
|
||||||
onChange={(e) => setSearchServiceAccountFilter(e.target.value)}
|
reset,
|
||||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
formState: { isSubmitting }
|
||||||
placeholder="Search service accounts..."
|
} = useForm<TAddServiceAccountForm>({ resolver: yupResolver(addServiceAccountFormSchema) });
|
||||||
/>
|
|
||||||
</div>
|
const onAddServiceAccount = async ({ name, expiresIn }: TAddServiceAccountForm) => {
|
||||||
<Button
|
if (!currentOrg?._id) return;
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
onClick={() => {
|
const keyPair = generateKeyPair();
|
||||||
setStep(0);
|
setPublicKey(keyPair.publicKey);
|
||||||
reset();
|
setPrivateKey(keyPair.privateKey);
|
||||||
handlePopUpOpen("addServiceAccount");
|
|
||||||
}}
|
const serviceAccountDetails = await createServiceAccount.mutateAsync({
|
||||||
>
|
name,
|
||||||
Add Service Account
|
organizationId: currentOrg?._id,
|
||||||
</Button>
|
publicKey: keyPair.publicKey,
|
||||||
</div>
|
expiresIn: Number(expiresIn)
|
||||||
<TableContainer>
|
});
|
||||||
<Table>
|
|
||||||
<THead>
|
setAccessKey(serviceAccountDetails.serviceAccountAccessKey);
|
||||||
<Th>Name</Th>
|
|
||||||
<Th className="w-full">Valid Until</Th>
|
setStep(1);
|
||||||
<Th aria-label="actions" />
|
reset();
|
||||||
</THead>
|
};
|
||||||
<TBody>
|
|
||||||
{isServiceAccountsLoading && <TableSkeleton columns={5} key="org-service-accounts" />}
|
const onRemoveServiceAccount = async () => {
|
||||||
{!isServiceAccountsLoading && (
|
const serviceAccountId = (popUp?.removeServiceAccount?.data as { _id: string })?._id;
|
||||||
filteredServiceAccounts.map(({
|
await removeServiceAccount.mutateAsync(serviceAccountId);
|
||||||
name,
|
handlePopUpClose("removeServiceAccount");
|
||||||
expiresAt,
|
};
|
||||||
_id: serviceAccountId
|
|
||||||
}) => {
|
const filteredServiceAccounts = useMemo(
|
||||||
return (
|
() =>
|
||||||
<Tr key={`org-service-account-${serviceAccountId}`}>
|
serviceAccounts.filter(({ name }) => name.toLowerCase().includes(searchServiceAccountFilter)),
|
||||||
<Td>{name}</Td>
|
[serviceAccounts, searchServiceAccountFilter]
|
||||||
<Td>{new Date(expiresAt).toUTCString()}</Td>
|
);
|
||||||
<Td>
|
|
||||||
<div className="flex">
|
const renderStep = (stepToRender: number) => {
|
||||||
<IconButton
|
switch (stepToRender) {
|
||||||
ariaLabel="edit"
|
case 0:
|
||||||
colorSchema="secondary"
|
return (
|
||||||
onClick={() => {
|
<form onSubmit={handleSubmit(onAddServiceAccount)}>
|
||||||
if (currentWorkspace?._id) {
|
<Controller
|
||||||
router.push(`/settings/org/${currentWorkspace._id}/service-accounts/${serviceAccountId}`);
|
control={control}
|
||||||
}
|
defaultValue=""
|
||||||
}}
|
name="name"
|
||||||
className="mr-2"
|
render={({ field, fieldState: { error } }) => (
|
||||||
>
|
<FormControl label="Name" isError={Boolean(error)} errorText={error?.message}>
|
||||||
<FontAwesomeIcon icon={faPencil} />
|
<Input {...field} />
|
||||||
</IconButton>
|
</FormControl>
|
||||||
<IconButton
|
)}
|
||||||
ariaLabel="delete"
|
|
||||||
colorSchema="danger"
|
|
||||||
onClick={() => handlePopUpOpen("removeServiceAccount", { _id: serviceAccountId })}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faTrash} />
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
</TBody>
|
|
||||||
</Table>
|
|
||||||
{!isServiceAccountsLoading && filteredServiceAccounts?.length === 0 && (
|
|
||||||
<EmptyState title="No service accounts found" icon={faServer} />
|
|
||||||
)}
|
|
||||||
</TableContainer>
|
|
||||||
<Modal
|
|
||||||
isOpen={popUp?.addServiceAccount?.isOpen}
|
|
||||||
onOpenChange={(isOpen) => {
|
|
||||||
handlePopUpToggle("addServiceAccount", isOpen);
|
|
||||||
reset();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ModalContent
|
|
||||||
title="Add Service Account"
|
|
||||||
subTitle="A service account represents a machine identity such as a VM or application client."
|
|
||||||
>
|
|
||||||
{renderStep(step)}
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
<DeleteActionModal
|
|
||||||
isOpen={popUp.removeServiceAccount.isOpen}
|
|
||||||
deleteKey="remove"
|
|
||||||
title="Do you want to remove this service account from the org?"
|
|
||||||
onChange={(isOpen) => handlePopUpToggle("removeServiceAccount", isOpen)}
|
|
||||||
onDeleteApproved={onRemoveServiceAccount}
|
|
||||||
/>
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="expiresIn"
|
||||||
|
defaultValue={String(serviceAccountExpiration?.[0]?.value)}
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
|
||||||
|
return (
|
||||||
|
<FormControl
|
||||||
|
label="Expiration"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{serviceAccountExpiration.map(({ label, value }) => (
|
||||||
|
<SelectItem value={String(value)} key={label}>
|
||||||
|
{label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="mt-8 flex items-center">
|
||||||
|
<Button
|
||||||
|
className="mr-4"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Create Service Account
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
variant="plain"
|
||||||
|
onClick={() => handlePopUpClose("addServiceAccount")}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
case 1:
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p>Access Key</p>
|
||||||
|
<div className="flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||||
|
<p className="mr-4 break-all">{accessKey}</p>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="copy icon"
|
||||||
|
colorSchema="secondary"
|
||||||
|
className="group relative"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(accessKey);
|
||||||
|
setIsAccessKeyCopied.on();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={isAccessKeyCopied ? faCheck : faCopy} />
|
||||||
|
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||||
|
Copy
|
||||||
|
</span>
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
<p className="mt-4">Public Key</p>
|
||||||
|
<div className="flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||||
|
<p className="mr-4 break-all">{publicKey}</p>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="copy icon"
|
||||||
|
colorSchema="secondary"
|
||||||
|
className="group relative"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(publicKey);
|
||||||
|
setIsPublicKeyCopied.on();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={isPublicKeyCopied ? faCheck : faCopy} />
|
||||||
|
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||||
|
Copy
|
||||||
|
</span>
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
<p className="mt-4">Private Key</p>
|
||||||
|
<div className="flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||||
|
<p className="mr-4 break-all">{privateKey}</p>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="copy icon"
|
||||||
|
colorSchema="secondary"
|
||||||
|
className="group relative"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(privateKey);
|
||||||
|
setIsPrivateKeyCopied.on();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={isPrivateKeyCopied ? faCheck : faCopy} />
|
||||||
|
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||||
|
Copy
|
||||||
|
</span>
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return <div />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="mb-4 flex">
|
||||||
|
<div className="mr-4 flex-1">
|
||||||
|
<Input
|
||||||
|
value={searchServiceAccountFilter}
|
||||||
|
onChange={(e) => setSearchServiceAccountFilter(e.target.value)}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
|
placeholder="Search service accounts..."
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
<Button
|
||||||
}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
onClick={() => {
|
||||||
|
setStep(0);
|
||||||
|
reset();
|
||||||
|
handlePopUpOpen("addServiceAccount");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add Service Account
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Th>Name</Th>
|
||||||
|
<Th className="w-full">Valid Until</Th>
|
||||||
|
<Th aria-label="actions" />
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isServiceAccountsLoading && (
|
||||||
|
<TableSkeleton columns={5} innerKey="org-service-accounts" />
|
||||||
|
)}
|
||||||
|
{!isServiceAccountsLoading &&
|
||||||
|
filteredServiceAccounts.map(({ name, expiresAt, _id: serviceAccountId }) => {
|
||||||
|
return (
|
||||||
|
<Tr key={`org-service-account-${serviceAccountId}`}>
|
||||||
|
<Td>{name}</Td>
|
||||||
|
<Td>{new Date(expiresAt).toUTCString()}</Td>
|
||||||
|
<Td>
|
||||||
|
<div className="flex">
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="edit"
|
||||||
|
colorSchema="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
if (currentWorkspace?._id) {
|
||||||
|
router.push(
|
||||||
|
`/settings/org/${currentWorkspace._id}/service-accounts/${serviceAccountId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="mr-2"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faPencil} />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="delete"
|
||||||
|
colorSchema="danger"
|
||||||
|
onClick={() =>
|
||||||
|
handlePopUpOpen("removeServiceAccount", { _id: serviceAccountId })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
{!isServiceAccountsLoading && filteredServiceAccounts?.length === 0 && (
|
||||||
|
<EmptyState title="No service accounts found" icon={faServer} />
|
||||||
|
)}
|
||||||
|
</TableContainer>
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp?.addServiceAccount?.isOpen}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
handlePopUpToggle("addServiceAccount", isOpen);
|
||||||
|
reset();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
title="Add Service Account"
|
||||||
|
subTitle="A service account represents a machine identity such as a VM or application client."
|
||||||
|
>
|
||||||
|
{renderStep(step)}
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.removeServiceAccount.isOpen}
|
||||||
|
deleteKey="remove"
|
||||||
|
title="Do you want to remove this service account from the org?"
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("removeServiceAccount", isOpen)}
|
||||||
|
onDeleteApproved={onRemoveServiceAccount}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,348 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
|
import NavHeader from "@app/components/navigation/NavHeader";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TableSkeleton,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tooltip,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useOrganization, useWorkspace } from "@app/context";
|
||||||
|
import {
|
||||||
|
useCreateSecretV3,
|
||||||
|
useDeleteSecretV3,
|
||||||
|
useGetFoldersByEnv,
|
||||||
|
useGetProjectSecretsAllEnv,
|
||||||
|
useGetUserWsEnvironments,
|
||||||
|
useGetUserWsKey,
|
||||||
|
useUpdateSecretV3
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
|
||||||
|
import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs";
|
||||||
|
import { SecretOverviewFolderRow } from "./components/SecretOverviewFolderRow";
|
||||||
|
import { SecretOverviewTableRow } from "./components/SecretOverviewTableRow";
|
||||||
|
|
||||||
|
export const SecretOverviewPage = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { createNotification } = useNotificationContext();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// this is to set expandable table width
|
||||||
|
// coz when overflow the table goes to the right
|
||||||
|
const parentTableRef = useRef<HTMLTableElement>(null);
|
||||||
|
const [expandableTableWidth, setExpandableTableWidth] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleParentTableWidthResize = () => {
|
||||||
|
setExpandableTableWidth(parentTableRef.current?.clientWidth || 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("resize", handleParentTableWidthResize);
|
||||||
|
return () => window.removeEventListener("resize", handleParentTableWidthResize);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (parentTableRef.current) {
|
||||||
|
setExpandableTableWidth(parentTableRef.current.clientWidth);
|
||||||
|
}
|
||||||
|
}, [parentTableRef.current]);
|
||||||
|
|
||||||
|
const { currentWorkspace, isLoading: isWorkspaceLoading } = useWorkspace();
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
const workspaceId = currentWorkspace?._id as string;
|
||||||
|
const { data: latestFileKey } = useGetUserWsKey(workspaceId);
|
||||||
|
const [searchFilter, setSearchFilter] = useState("");
|
||||||
|
const secretPath = router.query?.secretPath as string;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isWorkspaceLoading && !workspaceId && router.isReady) {
|
||||||
|
router.push(`/org/${currentOrg?._id}/overview`);
|
||||||
|
}
|
||||||
|
}, [isWorkspaceLoading, workspaceId, router.isReady]);
|
||||||
|
|
||||||
|
const { data: wsEnv, isLoading: isEnvListLoading } = useGetUserWsEnvironments({
|
||||||
|
workspaceId
|
||||||
|
});
|
||||||
|
|
||||||
|
const userAvailableEnvs = wsEnv?.filter(({ isReadDenied }) => !isReadDenied) || [];
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: secrets,
|
||||||
|
getSecretByKey,
|
||||||
|
secKeys,
|
||||||
|
getEnvSecretKeyCount
|
||||||
|
} = useGetProjectSecretsAllEnv({
|
||||||
|
workspaceId,
|
||||||
|
envs: userAvailableEnvs.map(({ slug }) => slug),
|
||||||
|
secretPath,
|
||||||
|
decryptFileKey: latestFileKey!
|
||||||
|
});
|
||||||
|
const { folders, folderNames, isFolderPresentInEnv } = useGetFoldersByEnv({
|
||||||
|
workspaceId,
|
||||||
|
environments: userAvailableEnvs.map(({ slug }) => slug),
|
||||||
|
parentFolderPath: secretPath
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
|
||||||
|
const { mutateAsync: updateSecretV3 } = useUpdateSecretV3();
|
||||||
|
const { mutateAsync: deleteSecretV3 } = useDeleteSecretV3();
|
||||||
|
|
||||||
|
const handleSecretCreate = async (env: string, key: string, value: string) => {
|
||||||
|
try {
|
||||||
|
await createSecretV3({
|
||||||
|
environment: env,
|
||||||
|
workspaceId,
|
||||||
|
secretPath,
|
||||||
|
secretName: key,
|
||||||
|
secretValue: value,
|
||||||
|
secretComment: "",
|
||||||
|
type: "shared",
|
||||||
|
latestFileKey: latestFileKey!
|
||||||
|
});
|
||||||
|
createNotification({
|
||||||
|
type: "success",
|
||||||
|
text: "Successfully created secret"
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to create secret"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSecretUpdate = async (env: string, key: string, value: string) => {
|
||||||
|
try {
|
||||||
|
await updateSecretV3({
|
||||||
|
environment: env,
|
||||||
|
workspaceId,
|
||||||
|
secretPath,
|
||||||
|
secretName: key,
|
||||||
|
secretValue: value,
|
||||||
|
type: "shared",
|
||||||
|
latestFileKey: latestFileKey!
|
||||||
|
});
|
||||||
|
createNotification({
|
||||||
|
type: "success",
|
||||||
|
text: "Successfully updated secret"
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to update secret"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSecretDelete = async (env: string, key: string) => {
|
||||||
|
try {
|
||||||
|
await deleteSecretV3({
|
||||||
|
environment: env,
|
||||||
|
workspaceId,
|
||||||
|
secretPath,
|
||||||
|
secretName: key,
|
||||||
|
type: "shared"
|
||||||
|
});
|
||||||
|
createNotification({
|
||||||
|
type: "success",
|
||||||
|
text: "Successfully deleted secret"
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to delete secret"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResetSearch = () => setSearchFilter("");
|
||||||
|
|
||||||
|
const handleFolderClick = (path: string) => {
|
||||||
|
router.push({
|
||||||
|
pathname: router.pathname,
|
||||||
|
query: {
|
||||||
|
...router.query,
|
||||||
|
secretPath: `${router.query?.secretPath || ""}/${path}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExploreEnvClick = (slug: string) => {
|
||||||
|
const query: Record<string, string> = { ...router.query, env: slug };
|
||||||
|
delete query.secretPath;
|
||||||
|
// the dir return will have the present directory folder id
|
||||||
|
// use that when clicking on explore to redirect user to there
|
||||||
|
const envIndex = userAvailableEnvs.findIndex((el) => slug === el.slug);
|
||||||
|
if (envIndex !== -1) {
|
||||||
|
const envFolder = folders?.[envIndex];
|
||||||
|
const dir = envFolder?.data?.dir?.pop();
|
||||||
|
if (dir) {
|
||||||
|
query.folderId = dir.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
router.push({
|
||||||
|
pathname: "/project/[id]/secrets/[env]",
|
||||||
|
query
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isEnvListLoading) {
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto flex h-screen w-full items-center justify-center px-8 text-mineshaft-50 dark:[color-scheme:dark]">
|
||||||
|
<img src="/images/loading/loading.gif" height={70} width={120} alt="loading animation" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isTableLoading =
|
||||||
|
folders?.some(({ isLoading }) => isLoading) && secrets?.some(({ isLoading }) => isLoading);
|
||||||
|
|
||||||
|
const filteredSecretNames = secKeys?.filter((name) =>
|
||||||
|
name.toUpperCase().includes(searchFilter.toUpperCase())
|
||||||
|
);
|
||||||
|
const filteredFolderNames = folderNames?.filter((name) =>
|
||||||
|
name.toLowerCase().includes(searchFilter.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto px-6 text-mineshaft-50 dark:[color-scheme:dark]">
|
||||||
|
<div className="relative right-5 ml-4">
|
||||||
|
<NavHeader pageName={t("dashboard.title")} isProjectRelated />
|
||||||
|
</div>
|
||||||
|
<div className="mt-6">
|
||||||
|
<p className="text-3xl font-semibold text-bunker-100">Secrets Overview</p>
|
||||||
|
<p className="text-md text-bunker-300">
|
||||||
|
Inject your secrets using
|
||||||
|
<a
|
||||||
|
className="mx-1 text-primary/80 hover:text-primary"
|
||||||
|
href="https://infisical.com/docs/cli/overview"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Infisical CLI
|
||||||
|
</a>
|
||||||
|
or
|
||||||
|
<a
|
||||||
|
className="mx-1 text-primary/80 hover:text-primary"
|
||||||
|
href="https://infisical.com/docs/sdks/overview"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Infisical SDKs
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-8 flex items-center justify-between">
|
||||||
|
<FolderBreadCrumbs secretPath={secretPath} onResetSearch={handleResetSearch} />
|
||||||
|
<div className="w-80">
|
||||||
|
<Input
|
||||||
|
className="h-[2.3rem] bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
|
||||||
|
placeholder="Search by secret/folder name..."
|
||||||
|
value={searchFilter}
|
||||||
|
onChange={(e) => setSearchFilter(e.target.value)}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="thin-scrollbar mt-4 max-h-[calc(100vh-250px)] overflow-y-auto" ref={parentTableRef}>
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead className="sticky top-0">
|
||||||
|
<Tr>
|
||||||
|
<Th className="sticky left-0 z-10 min-w-[20rem] bg-clip-padding">Name</Th>
|
||||||
|
{userAvailableEnvs?.map(({ name, slug }, index) => {
|
||||||
|
const envSecKeyCount = getEnvSecretKeyCount(slug);
|
||||||
|
const missingKeyCount = secKeys.length - envSecKeyCount;
|
||||||
|
return (
|
||||||
|
<Th
|
||||||
|
className="min-table-row min-w-[11rem] text-center"
|
||||||
|
key={`secret-overview-${name}-${index + 1}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
{name}
|
||||||
|
{missingKeyCount > 0 && (
|
||||||
|
<Tooltip
|
||||||
|
className="max-w-none lowercase"
|
||||||
|
content={`${missingKeyCount} secrets missing\n compared to other environments`}
|
||||||
|
>
|
||||||
|
<div className="ml-2 h-[1.1rem] font-medium flex cursor-default items-center justify-center rounded-sm bg-red-600 border border-red-400 p-1 text-xs text-bunker-100">
|
||||||
|
<span className="text-bunker-100">{missingKeyCount}</span>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Th>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isTableLoading && (
|
||||||
|
<TableSkeleton
|
||||||
|
columns={userAvailableEnvs.length + 1}
|
||||||
|
innerKey="secret-overview-loading"
|
||||||
|
rows={5}
|
||||||
|
className="bg-mineshaft-700"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{filteredFolderNames.map((folderName, index) => (
|
||||||
|
<SecretOverviewFolderRow
|
||||||
|
folderName={folderName}
|
||||||
|
isFolderPresentInEnv={isFolderPresentInEnv}
|
||||||
|
environments={userAvailableEnvs}
|
||||||
|
key={`overview-${folderName}-${index + 1}`}
|
||||||
|
onClick={handleFolderClick}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{filteredSecretNames.map((key, index) => (
|
||||||
|
<SecretOverviewTableRow
|
||||||
|
onSecretCreate={handleSecretCreate}
|
||||||
|
onSecretDelete={handleSecretDelete}
|
||||||
|
onSecretUpdate={handleSecretUpdate}
|
||||||
|
key={`overview-${key}-${index + 1}`}
|
||||||
|
environments={userAvailableEnvs}
|
||||||
|
secretKey={key}
|
||||||
|
getSecretByKey={getSecretByKey}
|
||||||
|
expandableColWidth={expandableTableWidth}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<Tr>
|
||||||
|
<Td className="fixed left-0 z-10 border-x border-mineshaft-700 bg-mineshaft-800 bg-clip-padding" />
|
||||||
|
{userAvailableEnvs.map(({ name, slug }) => (
|
||||||
|
<Td key={`explore-${name}-btn`} className=" border-x border-mineshaft-700">
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="outline_bg"
|
||||||
|
isFullWidth
|
||||||
|
onClick={() => handleExploreEnvClick(slug)}
|
||||||
|
>
|
||||||
|
Explore
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
))}
|
||||||
|
</Tr>
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { faFolderOpen } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
secretPath: string;
|
||||||
|
onResetSearch: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FolderBreadCrumbs = ({ secretPath = "/", onResetSearch }: Props) => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const onFolderCrumbClick = (index: number) => {
|
||||||
|
const newSecPath = secretPath.split("/").filter(Boolean).slice(0, index).join("/");
|
||||||
|
if (secretPath === `/${newSecPath}`) return;
|
||||||
|
const query = { ...router.query, secretPath: `/${newSecPath}` } as Record<string, string>;
|
||||||
|
// root condition
|
||||||
|
if (index === 0) delete query.secretPath;
|
||||||
|
router
|
||||||
|
.push({
|
||||||
|
pathname: router.pathname,
|
||||||
|
query
|
||||||
|
})
|
||||||
|
.then(() => onResetSearch());
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div
|
||||||
|
className="breadcrumb relative z-20 border-solid border-mineshaft-600 bg-mineshaft-800 py-1 pl-5 pr-2 text-sm hover:bg-mineshaft-600"
|
||||||
|
onClick={() => onFolderCrumbClick(0)}
|
||||||
|
onKeyDown={() => null}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faFolderOpen} className="text-primary" />
|
||||||
|
</div>
|
||||||
|
{(secretPath || "")
|
||||||
|
.split("/")
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((path, index, arr) => (
|
||||||
|
<div
|
||||||
|
key={`secret-path-${index + 1}`}
|
||||||
|
className={`breadcrumb relative z-20 ${
|
||||||
|
index + 1 === arr.length ? "cursor-default" : "cursor-pointer"
|
||||||
|
} border-solid border-mineshaft-600 py-1 pl-5 pr-2 text-sm text-mineshaft-200`}
|
||||||
|
onClick={() => onFolderCrumbClick(index + 1)}
|
||||||
|
onKeyDown={() => null}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
{path}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { FolderBreadCrumbs } from "./FolderBreadCrumbs";
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
import { faCheck, faFolder, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import { Td, Tr } from "@app/components/v2";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
folderName: string;
|
||||||
|
environments: { name: string; slug: string }[];
|
||||||
|
isFolderPresentInEnv: (name: string, env: string) => boolean;
|
||||||
|
onClick: (path: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SecretOverviewFolderRow = ({
|
||||||
|
folderName,
|
||||||
|
environments = [],
|
||||||
|
isFolderPresentInEnv,
|
||||||
|
onClick
|
||||||
|
}: Props) => {
|
||||||
|
return (
|
||||||
|
<Tr isHoverable isSelectable className="group" onClick={() => onClick(folderName)}>
|
||||||
|
<Td className="sticky left-0 z-10 border-x border-mineshaft-700 bg-mineshaft-800 bg-clip-padding py-2.5 group-hover:bg-mineshaft-700">
|
||||||
|
<div className="flex items-center space-x-5">
|
||||||
|
<div className="text-yellow-700">
|
||||||
|
<FontAwesomeIcon icon={faFolder} />
|
||||||
|
</div>
|
||||||
|
<div>{folderName}</div>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
{environments.map(({ slug }, i) => {
|
||||||
|
const isPresent = isFolderPresentInEnv(folderName, slug);
|
||||||
|
return (
|
||||||
|
<Td
|
||||||
|
key={`sec-overview-${slug}-${i + 1}-folder`}
|
||||||
|
className={twMerge(
|
||||||
|
"border-x border-mineshaft-700 py-3 group-hover:bg-mineshaft-700",
|
||||||
|
isPresent ? "text-green-600" : "text-red-600"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<FontAwesomeIcon icon={isPresent ? faCheck : faXmark} />
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { SecretOverviewFolderRow } from "./SecretOverviewFolderRow";
|
||||||
+171
@@ -0,0 +1,171 @@
|
|||||||
|
import { useRef } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { faCheck, faCopy, faTrash, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
|
import { IconButton, SecretInput, Tooltip } from "@app/components/v2";
|
||||||
|
import { useToggle } from "@app/hooks";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
defaultValue?: string | null;
|
||||||
|
secretName: string;
|
||||||
|
isCreatable?: boolean;
|
||||||
|
isVisible?: boolean;
|
||||||
|
environment: string;
|
||||||
|
onSecretCreate: (env: string, key: string, value: string) => Promise<void>;
|
||||||
|
onSecretUpdate: (env: string, key: string, value: string) => Promise<void>;
|
||||||
|
onSecretDelete: (env: string, key: string) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SecretEditRow = ({
|
||||||
|
defaultValue,
|
||||||
|
isCreatable,
|
||||||
|
onSecretUpdate,
|
||||||
|
secretName,
|
||||||
|
onSecretCreate,
|
||||||
|
onSecretDelete,
|
||||||
|
environment,
|
||||||
|
isVisible
|
||||||
|
}: Props) => {
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
control,
|
||||||
|
reset,
|
||||||
|
getValues,
|
||||||
|
formState: { isDirty, isSubmitting }
|
||||||
|
} = useForm({
|
||||||
|
values: {
|
||||||
|
value: defaultValue
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const editorRef = useRef(defaultValue);
|
||||||
|
const [isDeleting, setIsDeleting] = useToggle();
|
||||||
|
const { createNotification } = useNotificationContext();
|
||||||
|
|
||||||
|
const handleFormReset = () => {
|
||||||
|
reset();
|
||||||
|
const val = getValues();
|
||||||
|
editorRef.current = val.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopySecretToClipboard = async () => {
|
||||||
|
const { value } = getValues();
|
||||||
|
if (value) {
|
||||||
|
try {
|
||||||
|
await window.navigator.clipboard.writeText(value);
|
||||||
|
createNotification({ type: "success", text: "Copied secret to clipboard" });
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
createNotification({ type: "error", text: "Failed to copy secret to clipboard" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFormSubmit = async ({ value }: { value?: string | null }) => {
|
||||||
|
if (value && secretName) {
|
||||||
|
if (isCreatable) {
|
||||||
|
await onSecretCreate(environment, secretName, value);
|
||||||
|
} else {
|
||||||
|
await onSecretUpdate(environment, secretName, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reset({ value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteSecret = async () => {
|
||||||
|
setIsDeleting.on();
|
||||||
|
try {
|
||||||
|
await onSecretDelete(environment, secretName);
|
||||||
|
reset({ value: undefined });
|
||||||
|
editorRef.current = undefined;
|
||||||
|
} finally {
|
||||||
|
setIsDeleting.off();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="group flex w-full cursor-text items-start space-x-2 items-center">
|
||||||
|
<div className="flex-grow border-r border-r-mineshaft-600 pr-2 pl-1">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="value"
|
||||||
|
render={({ field: { onChange, onBlur } }) => (
|
||||||
|
<SecretInput
|
||||||
|
value={editorRef.current}
|
||||||
|
onChange={(val, html) => {
|
||||||
|
onChange(val);
|
||||||
|
editorRef.current = html;
|
||||||
|
}}
|
||||||
|
onBlur={(html) => {
|
||||||
|
editorRef.current = html;
|
||||||
|
onBlur();
|
||||||
|
}}
|
||||||
|
isVisible={isVisible}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-16 justify-center space-x-3 pl-2 transition-all">
|
||||||
|
{isDirty ? (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<Tooltip content="save">
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="submit-value"
|
||||||
|
className="h-full"
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
onClick={handleSubmit(handleFormSubmit)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faCheck} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Tooltip content="cancel">
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="reset-value"
|
||||||
|
className="h-full"
|
||||||
|
onClick={handleFormReset}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="opacity-0 group-hover:opacity-100">
|
||||||
|
<Tooltip content="Copy Secret">
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="copy-value"
|
||||||
|
onClick={handleCopySecretToClipboard}
|
||||||
|
variant="plain"
|
||||||
|
className="h-full"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faCopy} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div className="opacity-0 group-hover:opacity-100">
|
||||||
|
<Tooltip content="Delete">
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="delete-value"
|
||||||
|
className="h-full"
|
||||||
|
onClick={handleDeleteSecret}
|
||||||
|
isDisabled={isDeleting}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+140
@@ -0,0 +1,140 @@
|
|||||||
|
import { faCircle } from "@fortawesome/free-regular-svg-icons";
|
||||||
|
import { faCheck, faEye, faEyeSlash, faKey, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import { Button, TableContainer, Td, Tr } from "@app/components/v2";
|
||||||
|
import { useToggle } from "@app/hooks";
|
||||||
|
import { DecryptedSecret } from "@app/hooks/api/secrets/types";
|
||||||
|
|
||||||
|
import { SecretEditRow } from "./SecretEditRow";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
secretKey: string;
|
||||||
|
environments: { name: string; slug: string }[];
|
||||||
|
expandableColWidth: number;
|
||||||
|
getSecretByKey: (slug: string, key: string) => DecryptedSecret | undefined;
|
||||||
|
onSecretCreate: (env: string, key: string, value: string) => Promise<void>;
|
||||||
|
onSecretUpdate: (env: string, key: string, value: string) => Promise<void>;
|
||||||
|
onSecretDelete: (env: string, key: string) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SecretOverviewTableRow = ({
|
||||||
|
secretKey,
|
||||||
|
environments = [],
|
||||||
|
getSecretByKey,
|
||||||
|
onSecretUpdate,
|
||||||
|
onSecretCreate,
|
||||||
|
onSecretDelete,
|
||||||
|
expandableColWidth
|
||||||
|
}: Props) => {
|
||||||
|
const [isFormExpanded, setIsFormExpanded] = useToggle();
|
||||||
|
const totalCols = environments.length + 1; // secret key row
|
||||||
|
const [isSecretVisible, setIsSecretVisible] = useToggle();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tr isHoverable isSelectable onClick={() => setIsFormExpanded.toggle()} className="group">
|
||||||
|
<Td className="sticky left-0 z-10 border-x border-mineshaft-700 bg-mineshaft-800 bg-clip-padding py-2.5 group-hover:bg-mineshaft-700">
|
||||||
|
<div className="flex items-center space-x-5">
|
||||||
|
<div className="text-blue-300/70">
|
||||||
|
<FontAwesomeIcon icon={faKey} />
|
||||||
|
</div>
|
||||||
|
<div>{secretKey}</div>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
{environments.map(({ slug }, i) => {
|
||||||
|
const secret = getSecretByKey(slug, secretKey);
|
||||||
|
const isSecretPresent = Boolean(secret);
|
||||||
|
const isSecretEmpty = secret?.value === "";
|
||||||
|
return (
|
||||||
|
<Td
|
||||||
|
key={`sec-overview-${slug}-${i + 1}-value`}
|
||||||
|
className={twMerge(
|
||||||
|
"border-x border-mineshaft-600 py-3 group-hover:bg-mineshaft-700",
|
||||||
|
isSecretPresent && !isSecretEmpty ? "text-green-600" : "",
|
||||||
|
isSecretPresent && isSecretEmpty ? "text-yellow" : "",
|
||||||
|
!isSecretPresent && !isSecretEmpty ? "text-red-600" : ""
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex justify-center">
|
||||||
|
{!isSecretEmpty && <FontAwesomeIcon icon={!isSecretPresent ? faCheck : faXmark} />}
|
||||||
|
{isSecretEmpty && <FontAwesomeIcon icon={faCircle} />}
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Tr>
|
||||||
|
{isFormExpanded && (
|
||||||
|
<Tr>
|
||||||
|
<Td colSpan={totalCols}>
|
||||||
|
<div
|
||||||
|
className="rounded-md bg-bunker-700 p-2"
|
||||||
|
style={{
|
||||||
|
width: `calc(${expandableColWidth}px - 2rem)`,
|
||||||
|
position: "sticky",
|
||||||
|
left: "1.25rem",
|
||||||
|
right: "1.25rem"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TableContainer>
|
||||||
|
<table className="secret-table">
|
||||||
|
<thead>
|
||||||
|
<tr className="h-10 border-b-2 border-mineshaft-600">
|
||||||
|
<th
|
||||||
|
style={{ padding: "0.5rem 1rem" }}
|
||||||
|
className="min-table-row min-w-[11rem]"
|
||||||
|
>
|
||||||
|
Environment
|
||||||
|
</th>
|
||||||
|
<th style={{ padding: "0.5rem 1rem" }} className="border-none">Value</th>
|
||||||
|
<div className="absolute top-0 right-0 w-min ml-auto mt-1 mr-1">
|
||||||
|
<Button
|
||||||
|
variant="outline_bg"
|
||||||
|
className="p-1"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={isSecretVisible ? faEyeSlash : faEye} />}
|
||||||
|
onClick={() => setIsSecretVisible.toggle()}
|
||||||
|
>
|
||||||
|
{isSecretVisible ? "Hide Values" : "Reveal Values"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="border-t-2 border-mineshaft-600">
|
||||||
|
{environments.map(({ name, slug }) => {
|
||||||
|
const secret = getSecretByKey(slug, secretKey);
|
||||||
|
const isCreatable = !secret;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
key={`secret-expanded-${slug}-${secretKey}`}
|
||||||
|
className="hover:bg-mineshaft-700"
|
||||||
|
>
|
||||||
|
<td className="flex" style={{ padding: "0.25rem 1rem" }}>
|
||||||
|
<div className="flex h-8 items-center">{name}</div>
|
||||||
|
</td>
|
||||||
|
<td className="h-8 col-span-2 w-full">
|
||||||
|
<SecretEditRow
|
||||||
|
isVisible={isSecretVisible}
|
||||||
|
secretName={secretKey}
|
||||||
|
defaultValue={secret?.value}
|
||||||
|
isCreatable={isCreatable}
|
||||||
|
onSecretDelete={onSecretDelete}
|
||||||
|
onSecretCreate={onSecretCreate}
|
||||||
|
onSecretUpdate={onSecretUpdate}
|
||||||
|
environment={slug}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</TableContainer>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { SecretOverviewTableRow } from "./SecretOverviewTableRow";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { SecretOverviewPage } from "./SecretOverviewPage";
|
||||||
@@ -10,90 +10,94 @@ import {
|
|||||||
Td,
|
Td,
|
||||||
Th,
|
Th,
|
||||||
THead,
|
THead,
|
||||||
Tr} from "@app/components/v2";
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
import timeSince from "@app/ee/utilities/timeSince";
|
import timeSince from "@app/ee/utilities/timeSince";
|
||||||
import getRisksByOrganization, { GitRisks } from "@app/pages/api/secret-scanning/getRisksByOrganization";
|
import getRisksByOrganization, {
|
||||||
|
GitRisks
|
||||||
|
} from "@app/pages/api/secret-scanning/getRisksByOrganization";
|
||||||
|
|
||||||
import { RiskStatusSelection } from "./RiskStatusSelection";
|
import { RiskStatusSelection } from "./RiskStatusSelection";
|
||||||
|
|
||||||
export const SecretScanningLogsTable = () => {
|
export const SecretScanningLogsTable = () => {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [gitRisks, setGitRisks] = useState<GitRisks[]>([]);
|
const [gitRisks, setGitRisks] = useState<GitRisks[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchRisks = async () => {
|
const fetchRisks = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const risks = await getRisksByOrganization(String(localStorage.getItem("orgData.id")))
|
const risks = await getRisksByOrganization(String(localStorage.getItem("orgData.id")));
|
||||||
setGitRisks(risks);
|
setGitRisks(risks);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
};
|
||||||
|
|
||||||
fetchRisks();
|
fetchRisks();
|
||||||
},[])
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
return (
|
<TableContainer className="mt-8">
|
||||||
<TableContainer className="mt-8">
|
<Table>
|
||||||
<Table>
|
<THead>
|
||||||
<THead>
|
<Tr>
|
||||||
<Tr>
|
<Th className="flex-1">Date</Th>
|
||||||
<Th className="flex-1">Date</Th>
|
<Th className="flex-1">Secret Type</Th>
|
||||||
<Th className="flex-1">Secret Type</Th>
|
<Th className="flex-1">View Risk</Th>
|
||||||
<Th className="flex-1">View Risk</Th>
|
<Th className="flex-1">Info</Th>
|
||||||
<Th className="flex-1">Info</Th>
|
<Th className="flex-1">Status</Th>
|
||||||
<Th className="flex-1">Status</Th>
|
<Th className="flex-1">Action</Th>
|
||||||
<Th className="flex-1">Action</Th>
|
<Th className="w-5" />
|
||||||
<Th className="w-5" />
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{!isLoading &&
|
||||||
|
gitRisks &&
|
||||||
|
gitRisks?.map((risk) => {
|
||||||
|
return (
|
||||||
|
<Tr key={risk.ruleID} className="h-10">
|
||||||
|
<Td>{timeSince(new Date(risk.createdAt))}</Td>
|
||||||
|
<Td>{risk.ruleID}</Td>
|
||||||
|
<Td>
|
||||||
|
<a
|
||||||
|
href={`https://github.com/${risk.repositoryFullName}/blob/${risk.commit}/${risk.file}#L${risk.startLine}-L${risk.endLine}`}
|
||||||
|
target="_blank"
|
||||||
|
className="text-red-500"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
View Exposed Secret
|
||||||
|
</a>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<div className="font-bold">
|
||||||
|
<a href={`https://github.com/${risk.repositoryFullName}`}>
|
||||||
|
{risk.repositoryFullName}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs">
|
||||||
|
<span>{risk.file}</span>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<span className="font-bold">{risk.author}</span>
|
||||||
|
<br />
|
||||||
|
<span>{risk.email}</span>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td>{risk.isResolved ? "Resolved" : "Needs Attention"}</Td>
|
||||||
|
<Td>
|
||||||
|
<RiskStatusSelection riskId={risk._id} currentSelection={risk.status} />
|
||||||
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
);
|
||||||
<TBody>
|
})}
|
||||||
{!isLoading && gitRisks && gitRisks?.map((risk) => {
|
{isLoading && <TableSkeleton columns={7} innerKey="gitRisks" />}
|
||||||
return (
|
{!isLoading && gitRisks && gitRisks?.length === 0 && (
|
||||||
<Tr key={risk.ruleID} className="h-10">
|
<Tr>
|
||||||
<Td>{timeSince(new Date(risk.createdAt))}</Td>
|
<Td colSpan={7}>
|
||||||
<Td>{risk.ruleID}</Td>
|
<EmptyState title="No risks detected." icon={faCheck} />
|
||||||
<Td>
|
</Td>
|
||||||
<a
|
</Tr>
|
||||||
href={`https://github.com/${risk.repositoryFullName}/blob/${risk.commit}/${risk.file}#L${risk.startLine}-L${risk.endLine}`}
|
)}
|
||||||
target="_blank"
|
</TBody>
|
||||||
className="text-red-500" rel="noreferrer"
|
</Table>
|
||||||
>
|
</TableContainer>
|
||||||
View Exposed Secret
|
);
|
||||||
</a>
|
};
|
||||||
</Td>
|
|
||||||
<Td>
|
|
||||||
<div className="font-bold">
|
|
||||||
<a href={`https://github.com/${risk.repositoryFullName}`}>
|
|
||||||
{risk.repositoryFullName}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div className="text-xs">
|
|
||||||
<span>{risk.file}</span><br/>
|
|
||||||
<br/>
|
|
||||||
<span className="font-bold">{risk.author}</span><br/>
|
|
||||||
<span>{risk.email}</span>
|
|
||||||
</div>
|
|
||||||
</Td>
|
|
||||||
<Td>{risk.isResolved ? "Resolved" : "Needs Attention"}</Td>
|
|
||||||
<Td>
|
|
||||||
<RiskStatusSelection riskId={risk._id} currentSelection={risk.status}/>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{isLoading && <TableSkeleton columns={7} key="gitRisks" />}
|
|
||||||
{!isLoading && gitRisks && gitRisks?.length === 0 && (
|
|
||||||
<Tr>
|
|
||||||
<Td colSpan={7}>
|
|
||||||
<EmptyState
|
|
||||||
title="No risks detected."
|
|
||||||
icon={faCheck}
|
|
||||||
/>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
)}
|
|
||||||
</TBody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
+55
-70
@@ -1,4 +1,4 @@
|
|||||||
import { faCircleCheck, faCircleXmark,faFileInvoice } from "@fortawesome/free-solid-svg-icons";
|
import { faCircleCheck, faCircleXmark, faFileInvoice } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -10,78 +10,63 @@ import {
|
|||||||
Td,
|
Td,
|
||||||
Th,
|
Th,
|
||||||
THead,
|
THead,
|
||||||
Tr} from "@app/components/v2";
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
import { useOrganization } from "@app/context";
|
import { useOrganization } from "@app/context";
|
||||||
import {
|
import { useGetOrgPlanTable } from "@app/hooks/api";
|
||||||
useGetOrgPlanTable
|
|
||||||
} from "@app/hooks/api";
|
|
||||||
|
|
||||||
export const CurrentPlanSection = () => {
|
export const CurrentPlanSection = () => {
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const { data, isLoading } = useGetOrgPlanTable(currentOrg?._id ?? "");
|
const { data, isLoading } = useGetOrgPlanTable(currentOrg?._id ?? "");
|
||||||
|
|
||||||
const displayCell = (value: null | number | string | boolean) => {
|
|
||||||
if (value === null) return "-";
|
|
||||||
|
|
||||||
if (typeof value === "boolean") {
|
|
||||||
if (value) return (
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faCircleCheck}
|
|
||||||
color='#2ecc71'
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
const displayCell = (value: null | number | string | boolean) => {
|
||||||
<FontAwesomeIcon
|
if (value === null) return "-";
|
||||||
icon={faCircleXmark}
|
|
||||||
color='#e74c3c'
|
if (typeof value === "boolean") {
|
||||||
/>
|
if (value) return <FontAwesomeIcon icon={faCircleCheck} color="#2ecc71" />;
|
||||||
);
|
|
||||||
}
|
return <FontAwesomeIcon icon={faCircleXmark} color="#e74c3c" />;
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return value;
|
||||||
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
|
};
|
||||||
<h2 className="text-xl font-semibold flex-1 text-white mb-8">Current Usage</h2>
|
|
||||||
<TableContainer className="mt-4">
|
return (
|
||||||
<Table>
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<THead>
|
<h2 className="mb-8 flex-1 text-xl font-semibold text-white">Current Usage</h2>
|
||||||
<Tr>
|
<TableContainer className="mt-4">
|
||||||
<Th className="w-1/3">Feature</Th>
|
<Table>
|
||||||
<Th className="w-1/3">Allowed</Th>
|
<THead>
|
||||||
<Th className="w-1/3">Used</Th>
|
<Tr>
|
||||||
</Tr>
|
<Th className="w-1/3">Feature</Th>
|
||||||
</THead>
|
<Th className="w-1/3">Allowed</Th>
|
||||||
<TBody>
|
<Th className="w-1/3">Used</Th>
|
||||||
{!isLoading && data && data?.rows?.length > 0 && data.rows.map(({
|
</Tr>
|
||||||
name,
|
</THead>
|
||||||
allowed,
|
<TBody>
|
||||||
used
|
{!isLoading &&
|
||||||
}) => {
|
data &&
|
||||||
return (
|
data?.rows?.length > 0 &&
|
||||||
<Tr key={`current-plan-row-${name}`} className="h-12">
|
data.rows.map(({ name, allowed, used }) => {
|
||||||
<Td>{name}</Td>
|
return (
|
||||||
<Td>{displayCell(allowed)}</Td>
|
<Tr key={`current-plan-row-${name}`} className="h-12">
|
||||||
<Td>{used}</Td>
|
<Td>{name}</Td>
|
||||||
</Tr>
|
<Td>{displayCell(allowed)}</Td>
|
||||||
);
|
<Td>{used}</Td>
|
||||||
})}
|
</Tr>
|
||||||
{isLoading && <TableSkeleton columns={5} key="invoices" />}
|
);
|
||||||
{!isLoading && data && data?.rows?.length === 0 && (
|
})}
|
||||||
<Tr>
|
{isLoading && <TableSkeleton columns={5} innerKey="invoices" />}
|
||||||
<Td colSpan={3}>
|
{!isLoading && data && data?.rows?.length === 0 && (
|
||||||
<EmptyState
|
<Tr>
|
||||||
title="No plan details found"
|
<Td colSpan={3}>
|
||||||
icon={faFileInvoice}
|
<EmptyState title="No plan details found" icon={faFileInvoice} />
|
||||||
/>
|
</Td>
|
||||||
</Td>
|
</Tr>
|
||||||
</Tr>
|
)}
|
||||||
)}
|
</TBody>
|
||||||
</TBody>
|
</Table>
|
||||||
</Table>
|
</TableContainer>
|
||||||
</TableContainer>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
};
|
||||||
}
|
|
||||||
|
|||||||
+117
-154
@@ -1,4 +1,4 @@
|
|||||||
import { faCircleCheck, faCircleXmark,faFileInvoice } from "@fortawesome/free-solid-svg-icons";
|
import { faCircleCheck, faCircleXmark, faFileInvoice } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -11,166 +11,129 @@ import {
|
|||||||
Td,
|
Td,
|
||||||
Th,
|
Th,
|
||||||
THead,
|
THead,
|
||||||
Tr,
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useOrganization,useSubscription } from "@app/context";
|
import { useOrganization, useSubscription } from "@app/context";
|
||||||
import {
|
import { useCreateCustomerPortalSession, useGetOrgPlansTable } from "@app/hooks/api";
|
||||||
useCreateCustomerPortalSession,
|
|
||||||
useGetOrgPlansTable} from "@app/hooks/api";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
billingCycle: "monthly" | "yearly"
|
billingCycle: "monthly" | "yearly";
|
||||||
}
|
};
|
||||||
|
|
||||||
export const ManagePlansTable = ({
|
export const ManagePlansTable = ({ billingCycle }: Props) => {
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
const { subscription } = useSubscription();
|
||||||
|
const { data: tableData, isLoading: isTableDataLoading } = useGetOrgPlansTable({
|
||||||
|
organizationId: currentOrg?._id ?? "",
|
||||||
billingCycle
|
billingCycle
|
||||||
}: Props) => {
|
});
|
||||||
const { currentOrg } = useOrganization();
|
const createCustomerPortalSession = useCreateCustomerPortalSession();
|
||||||
const { subscription } = useSubscription();
|
|
||||||
const { data: tableData, isLoading: isTableDataLoading } = useGetOrgPlansTable({
|
|
||||||
organizationId: currentOrg?._id ?? "",
|
|
||||||
billingCycle
|
|
||||||
});
|
|
||||||
const createCustomerPortalSession = useCreateCustomerPortalSession();
|
|
||||||
|
|
||||||
const displayCell = (value: null | number | string | boolean) => {
|
const displayCell = (value: null | number | string | boolean) => {
|
||||||
if (value === null) return "Unlimited";
|
if (value === null) return "Unlimited";
|
||||||
|
|
||||||
if (typeof value === "boolean") {
|
|
||||||
if (value) return (
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faCircleCheck}
|
|
||||||
color='#2ecc71'
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
if (typeof value === "boolean") {
|
||||||
<FontAwesomeIcon
|
if (value) return <FontAwesomeIcon icon={faCircleCheck} color="#2ecc71" />;
|
||||||
icon={faCircleXmark}
|
|
||||||
color='#e74c3c'
|
return <FontAwesomeIcon icon={faCircleXmark} color="#e74c3c" />;
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return value;
|
||||||
<TableContainer>
|
};
|
||||||
<Table>
|
|
||||||
<THead>
|
|
||||||
{subscription && !isTableDataLoading && tableData && (
|
|
||||||
<Tr>
|
|
||||||
<Th className="">Feature / Limit</Th>
|
|
||||||
{tableData.head.map(({
|
|
||||||
name,
|
|
||||||
priceLine
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<Th
|
|
||||||
key={`plans-feature-head-${billingCycle}-${name}`}
|
|
||||||
className="text-center flex-1"
|
|
||||||
>
|
|
||||||
<p>{name}</p>
|
|
||||||
<p>{priceLine}</p>
|
|
||||||
</Th>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Tr>
|
|
||||||
)}
|
|
||||||
</THead>
|
|
||||||
<TBody>
|
|
||||||
{subscription && !isTableDataLoading && tableData && tableData.rows.map(({
|
|
||||||
name,
|
|
||||||
starter,
|
|
||||||
team,
|
|
||||||
pro,
|
|
||||||
enterprise
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<Tr className="h-12" key={`plans-feature-row-${billingCycle}-${name}`}>
|
|
||||||
<Td>{displayCell(name)}</Td>
|
|
||||||
<Td className="text-center">
|
|
||||||
{displayCell(starter)}
|
|
||||||
</Td>
|
|
||||||
<Td className="text-center">
|
|
||||||
{displayCell(team)}
|
|
||||||
</Td>
|
|
||||||
<Td className="text-center">
|
|
||||||
{displayCell(pro)}
|
|
||||||
</Td>
|
|
||||||
<Td className="text-center">
|
|
||||||
{displayCell(enterprise)}
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{isTableDataLoading && <TableSkeleton columns={5} key="cloud-products" />}
|
|
||||||
{!isTableDataLoading && tableData?.rows.length === 0 && (
|
|
||||||
<Tr>
|
|
||||||
<Td colSpan={5}>
|
|
||||||
<EmptyState
|
|
||||||
title="No cloud product details found"
|
|
||||||
icon={faFileInvoice}
|
|
||||||
/>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
)}
|
|
||||||
{subscription && !isTableDataLoading && tableData && (
|
|
||||||
<Tr className="h-12">
|
|
||||||
<Td />
|
|
||||||
{tableData.head.map(({
|
|
||||||
slug,
|
|
||||||
tier
|
|
||||||
}) => {
|
|
||||||
|
|
||||||
const isCurrentPlan = slug === subscription.slug;
|
|
||||||
let subscriptionText = "Upgrade";
|
|
||||||
|
|
||||||
if (subscription.tier > tier) {
|
|
||||||
subscriptionText = "Downgrade"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tier === 3) {
|
|
||||||
subscriptionText = "Contact sales"
|
|
||||||
}
|
|
||||||
|
|
||||||
return isCurrentPlan ? (
|
return (
|
||||||
<Td>
|
<TableContainer>
|
||||||
<Button
|
<Table>
|
||||||
colorSchema="secondary"
|
<THead>
|
||||||
className="w-full"
|
{subscription && !isTableDataLoading && tableData && (
|
||||||
isDisabled
|
<Tr>
|
||||||
>
|
<Th className="">Feature / Limit</Th>
|
||||||
Current
|
{tableData.head.map(({ name, priceLine }) => {
|
||||||
</Button>
|
return (
|
||||||
</Td>
|
<Th
|
||||||
) : (
|
key={`plans-feature-head-${billingCycle}-${name}`}
|
||||||
<Td>
|
className="flex-1 text-center"
|
||||||
<Button
|
>
|
||||||
onClick={async () => {
|
<p>{name}</p>
|
||||||
if (!currentOrg?._id) return;
|
<p>{priceLine}</p>
|
||||||
|
</Th>
|
||||||
if (tier !== 3) {
|
);
|
||||||
const { url } = await createCustomerPortalSession.mutateAsync(currentOrg._id);
|
})}
|
||||||
window.location.href = url;
|
</Tr>
|
||||||
return;
|
)}
|
||||||
}
|
</THead>
|
||||||
|
<TBody>
|
||||||
window.location.href = "https://infisical.com/scheduledemo";
|
{subscription &&
|
||||||
}}
|
!isTableDataLoading &&
|
||||||
color="mineshaft"
|
tableData &&
|
||||||
className="w-full"
|
tableData.rows.map(({ name, starter, team, pro, enterprise }) => {
|
||||||
>
|
return (
|
||||||
{subscriptionText}
|
<Tr className="h-12" key={`plans-feature-row-${billingCycle}-${name}`}>
|
||||||
</Button>
|
<Td>{displayCell(name)}</Td>
|
||||||
</Td>
|
<Td className="text-center">{displayCell(starter)}</Td>
|
||||||
);
|
<Td className="text-center">{displayCell(team)}</Td>
|
||||||
})}
|
<Td className="text-center">{displayCell(pro)}</Td>
|
||||||
</Tr>
|
<Td className="text-center">{displayCell(enterprise)}</Td>
|
||||||
)}
|
</Tr>
|
||||||
</TBody>
|
);
|
||||||
</Table>
|
})}
|
||||||
</TableContainer>
|
{isTableDataLoading && <TableSkeleton columns={5} innerKey="cloud-products" />}
|
||||||
);
|
{!isTableDataLoading && tableData?.rows.length === 0 && (
|
||||||
}
|
<Tr>
|
||||||
|
<Td colSpan={5}>
|
||||||
|
<EmptyState title="No cloud product details found" icon={faFileInvoice} />
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
{subscription && !isTableDataLoading && tableData && (
|
||||||
|
<Tr className="h-12">
|
||||||
|
<Td />
|
||||||
|
{tableData.head.map(({ slug, tier }) => {
|
||||||
|
const isCurrentPlan = slug === subscription.slug;
|
||||||
|
let subscriptionText = "Upgrade";
|
||||||
|
|
||||||
|
if (subscription.tier > tier) {
|
||||||
|
subscriptionText = "Downgrade";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tier === 3) {
|
||||||
|
subscriptionText = "Contact sales";
|
||||||
|
}
|
||||||
|
|
||||||
|
return isCurrentPlan ? (
|
||||||
|
<Td>
|
||||||
|
<Button colorSchema="secondary" className="w-full" isDisabled>
|
||||||
|
Current
|
||||||
|
</Button>
|
||||||
|
</Td>
|
||||||
|
) : (
|
||||||
|
<Td>
|
||||||
|
<Button
|
||||||
|
onClick={async () => {
|
||||||
|
if (!currentOrg?._id) return;
|
||||||
|
|
||||||
|
if (tier !== 3) {
|
||||||
|
const { url } = await createCustomerPortalSession.mutateAsync(
|
||||||
|
currentOrg._id
|
||||||
|
);
|
||||||
|
window.location.href = url;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.href = "https://infisical.com/scheduledemo";
|
||||||
|
}}
|
||||||
|
color="mineshaft"
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{subscriptionText}
|
||||||
|
</Button>
|
||||||
|
</Td>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
+61
-71
@@ -14,78 +14,68 @@ import {
|
|||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useOrganization } from "@app/context";
|
import { useOrganization } from "@app/context";
|
||||||
import {
|
import { useDeleteOrgPmtMethod, useGetOrgPmtMethods } from "@app/hooks/api";
|
||||||
useDeleteOrgPmtMethod,
|
|
||||||
useGetOrgPmtMethods
|
|
||||||
} from "@app/hooks/api";
|
|
||||||
|
|
||||||
export const PmtMethodsTable = () => {
|
export const PmtMethodsTable = () => {
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const { data, isLoading } = useGetOrgPmtMethods(currentOrg?._id ?? "");
|
const { data, isLoading } = useGetOrgPmtMethods(currentOrg?._id ?? "");
|
||||||
const deleteOrgPmtMethod = useDeleteOrgPmtMethod();
|
const deleteOrgPmtMethod = useDeleteOrgPmtMethod();
|
||||||
|
|
||||||
const handleDeletePmtMethodBtnClick = async (pmtMethodId: string) => {
|
const handleDeletePmtMethodBtnClick = async (pmtMethodId: string) => {
|
||||||
if (!currentOrg?._id) return;
|
if (!currentOrg?._id) return;
|
||||||
await deleteOrgPmtMethod.mutateAsync({
|
await deleteOrgPmtMethod.mutateAsync({
|
||||||
organizationId: currentOrg._id,
|
organizationId: currentOrg._id,
|
||||||
pmtMethodId
|
pmtMethodId
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableContainer className="mt-4">
|
<TableContainer className="mt-4">
|
||||||
<Table>
|
<Table>
|
||||||
<THead>
|
<THead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th className="flex-1">Brand</Th>
|
<Th className="flex-1">Brand</Th>
|
||||||
<Th className="flex-1">Type</Th>
|
<Th className="flex-1">Type</Th>
|
||||||
<Th className="flex-1">Last 4 Digits</Th>
|
<Th className="flex-1">Last 4 Digits</Th>
|
||||||
<Th className="flex-1">Expiration</Th>
|
<Th className="flex-1">Expiration</Th>
|
||||||
<Th className="w-5" />
|
<Th className="w-5" />
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
{!isLoading && data && data?.length > 0 && data.map(({
|
{!isLoading &&
|
||||||
_id,
|
data &&
|
||||||
brand,
|
data?.length > 0 &&
|
||||||
exp_month,
|
data.map(({ _id, brand, exp_month, exp_year, funding, last4 }) => (
|
||||||
exp_year,
|
<Tr key={`pmt-method-${_id}`} className="h-10">
|
||||||
funding,
|
<Td>{brand.charAt(0).toUpperCase() + brand.slice(1)}</Td>
|
||||||
last4
|
<Td>{funding.charAt(0).toUpperCase() + funding.slice(1)}</Td>
|
||||||
}) => (
|
<Td>{last4}</Td>
|
||||||
<Tr key={`pmt-method-${_id}`} className="h-10">
|
<Td>{`${exp_month}/${exp_year}`}</Td>
|
||||||
<Td>{brand.charAt(0).toUpperCase() + brand.slice(1)}</Td>
|
<Td>
|
||||||
<Td>{funding.charAt(0).toUpperCase() + funding.slice(1)}</Td>
|
<IconButton
|
||||||
<Td>{last4}</Td>
|
onClick={async () => {
|
||||||
<Td>{`${exp_month}/${exp_year}`}</Td>
|
await handleDeletePmtMethodBtnClick(_id);
|
||||||
<Td>
|
}}
|
||||||
<IconButton
|
size="lg"
|
||||||
onClick={async () => {
|
colorSchema="danger"
|
||||||
await handleDeletePmtMethodBtnClick(_id);
|
variant="plain"
|
||||||
}}
|
ariaLabel="update"
|
||||||
size="lg"
|
>
|
||||||
colorSchema="danger"
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
variant="plain"
|
</IconButton>
|
||||||
ariaLabel="update"
|
</Td>
|
||||||
>
|
</Tr>
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
))}
|
||||||
</IconButton>
|
{isLoading && <TableSkeleton columns={5} innerKey="pmt-methods" />}
|
||||||
</Td>
|
{!isLoading && data && data?.length === 0 && (
|
||||||
</Tr>
|
<Tr>
|
||||||
))}
|
<Td colSpan={5}>
|
||||||
{isLoading && <TableSkeleton columns={5} key="pmt-methods" />}
|
<EmptyState title="No payment methods on file" icon={faCreditCard} />
|
||||||
{!isLoading && data && data?.length === 0 && (
|
</Td>
|
||||||
<Tr>
|
</Tr>
|
||||||
<Td colSpan={5}>
|
)}
|
||||||
<EmptyState
|
</TBody>
|
||||||
title="No payment methods on file"
|
</Table>
|
||||||
icon={faCreditCard}
|
</TableContainer>
|
||||||
/>
|
);
|
||||||
</Td>
|
};
|
||||||
</Tr>
|
|
||||||
)}
|
|
||||||
</TBody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
+109
-116
@@ -1,4 +1,4 @@
|
|||||||
import { faFileInvoice,faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faFileInvoice, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -11,127 +11,120 @@ import {
|
|||||||
Td,
|
Td,
|
||||||
Th,
|
Th,
|
||||||
THead,
|
THead,
|
||||||
Tr,
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useOrganization } from "@app/context";
|
import { useOrganization } from "@app/context";
|
||||||
import {
|
import { useDeleteOrgTaxId, useGetOrgTaxIds } from "@app/hooks/api";
|
||||||
useDeleteOrgTaxId,
|
|
||||||
useGetOrgTaxIds
|
|
||||||
} from "@app/hooks/api";
|
|
||||||
|
|
||||||
const taxIDTypeLabelMap: { [key: string]: string } = {
|
const taxIDTypeLabelMap: { [key: string]: string } = {
|
||||||
"au_abn": "Australia ABN",
|
au_abn: "Australia ABN",
|
||||||
"au_arn": "Australia ARN",
|
au_arn: "Australia ARN",
|
||||||
"bg_uic": "Bulgaria UIC",
|
bg_uic: "Bulgaria UIC",
|
||||||
"br_cnpj": "Brazil CNPJ",
|
br_cnpj: "Brazil CNPJ",
|
||||||
"br_cpf": "Brazil CPF",
|
br_cpf: "Brazil CPF",
|
||||||
"ca_bn": "Canada BN",
|
ca_bn: "Canada BN",
|
||||||
"ca_gst_hst": "Canada GST/HST",
|
ca_gst_hst: "Canada GST/HST",
|
||||||
"ca_pst_bc": "Canada PST BC",
|
ca_pst_bc: "Canada PST BC",
|
||||||
"ca_pst_mb": "Canada PST MB",
|
ca_pst_mb: "Canada PST MB",
|
||||||
"ca_pst_sk": "Canada PST SK",
|
ca_pst_sk: "Canada PST SK",
|
||||||
"ca_qst": "Canada QST",
|
ca_qst: "Canada QST",
|
||||||
"ch_vat": "Switzerland VAT",
|
ch_vat: "Switzerland VAT",
|
||||||
"cl_tin": "Chile TIN",
|
cl_tin: "Chile TIN",
|
||||||
"eg_tin": "Egypt TIN",
|
eg_tin: "Egypt TIN",
|
||||||
"es_cif": "Spain CIF",
|
es_cif: "Spain CIF",
|
||||||
"eu_oss_vat": "EU OSS VAT",
|
eu_oss_vat: "EU OSS VAT",
|
||||||
"eu_vat": "EU VAT",
|
eu_vat: "EU VAT",
|
||||||
"gb_vat": "GB VAT",
|
gb_vat: "GB VAT",
|
||||||
"ge_vat": "Georgia VAT",
|
ge_vat: "Georgia VAT",
|
||||||
"hk_br": "Hong Kong BR",
|
hk_br: "Hong Kong BR",
|
||||||
"hu_tin": "Hungary TIN",
|
hu_tin: "Hungary TIN",
|
||||||
"id_npwp": "Indonesia NPWP",
|
id_npwp: "Indonesia NPWP",
|
||||||
"il_vat": "Israel VAT",
|
il_vat: "Israel VAT",
|
||||||
"in_gst": "India GST",
|
in_gst: "India GST",
|
||||||
"is_vat": "Iceland VAT",
|
is_vat: "Iceland VAT",
|
||||||
"jp_cn": "Japan CN",
|
jp_cn: "Japan CN",
|
||||||
"jp_rn": "Japan RN",
|
jp_rn: "Japan RN",
|
||||||
"jp_trn": "Japan TRN",
|
jp_trn: "Japan TRN",
|
||||||
"ke_pin": "Kenya PIN",
|
ke_pin: "Kenya PIN",
|
||||||
"kr_brn": "South Korea BRN",
|
kr_brn: "South Korea BRN",
|
||||||
"li_uid": "Liechtenstein UID",
|
li_uid: "Liechtenstein UID",
|
||||||
"mx_rfc": "Mexico RFC",
|
mx_rfc: "Mexico RFC",
|
||||||
"my_frp": "Malaysia FRP",
|
my_frp: "Malaysia FRP",
|
||||||
"my_itn": "Malaysia ITN",
|
my_itn: "Malaysia ITN",
|
||||||
"my_sst": "Malaysia SST",
|
my_sst: "Malaysia SST",
|
||||||
"no_vat": "Norway VAT",
|
no_vat: "Norway VAT",
|
||||||
"nz_gst": "New Zealand GST",
|
nz_gst: "New Zealand GST",
|
||||||
"ph_tin": "Philippines TIN",
|
ph_tin: "Philippines TIN",
|
||||||
"ru_inn": "Russia INN",
|
ru_inn: "Russia INN",
|
||||||
"ru_kpp": "Russia KPP",
|
ru_kpp: "Russia KPP",
|
||||||
"sa_vat": "Saudi Arabia VAT",
|
sa_vat: "Saudi Arabia VAT",
|
||||||
"sg_gst": "Singapore GST",
|
sg_gst: "Singapore GST",
|
||||||
"sg_uen": "Singapore UEN",
|
sg_uen: "Singapore UEN",
|
||||||
"si_tin": "Slovenia TIN",
|
si_tin: "Slovenia TIN",
|
||||||
"th_vat": "Thailand VAT",
|
th_vat: "Thailand VAT",
|
||||||
"tr_tin": "Turkey TIN",
|
tr_tin: "Turkey TIN",
|
||||||
"tw_vat": "Taiwan VAT",
|
tw_vat: "Taiwan VAT",
|
||||||
"ua_vat": "Ukraine VAT",
|
ua_vat: "Ukraine VAT",
|
||||||
"us_ein": "US EIN",
|
us_ein: "US EIN",
|
||||||
"za_vat": "South Africa VAT"
|
za_vat: "South Africa VAT"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TaxIDTable = () => {
|
export const TaxIDTable = () => {
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const { data, isLoading } = useGetOrgTaxIds(currentOrg?._id ?? "");
|
const { data, isLoading } = useGetOrgTaxIds(currentOrg?._id ?? "");
|
||||||
const deleteOrgTaxId = useDeleteOrgTaxId();
|
const deleteOrgTaxId = useDeleteOrgTaxId();
|
||||||
|
|
||||||
const handleDeleteTaxIdBtnClick = async (taxId: string) => {
|
const handleDeleteTaxIdBtnClick = async (taxId: string) => {
|
||||||
if (!currentOrg?._id) return;
|
if (!currentOrg?._id) return;
|
||||||
await deleteOrgTaxId.mutateAsync({
|
await deleteOrgTaxId.mutateAsync({
|
||||||
organizationId: currentOrg._id,
|
organizationId: currentOrg._id,
|
||||||
taxId
|
taxId
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableContainer className="mt-4">
|
<TableContainer className="mt-4">
|
||||||
<Table>
|
<Table>
|
||||||
<THead>
|
<THead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th className="flex-1">Type</Th>
|
<Th className="flex-1">Type</Th>
|
||||||
<Th className="flex-1">Value</Th>
|
<Th className="flex-1">Value</Th>
|
||||||
<Th className="w-5" />
|
<Th className="w-5" />
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
{!isLoading && data && data?.length > 0 && data.map(({
|
{!isLoading &&
|
||||||
_id,
|
data &&
|
||||||
type,
|
data?.length > 0 &&
|
||||||
value
|
data.map(({ _id, type, value }) => (
|
||||||
}) => (
|
<Tr key={`tax-id-${_id}`} className="h-10">
|
||||||
<Tr key={`tax-id-${_id}`} className="h-10">
|
<Td>{taxIDTypeLabelMap[type]}</Td>
|
||||||
<Td>{taxIDTypeLabelMap[type]}</Td>
|
<Td>{value}</Td>
|
||||||
<Td>{value}</Td>
|
<Td>
|
||||||
<Td>
|
<IconButton
|
||||||
<IconButton
|
onClick={async () => {
|
||||||
onClick={async () => {
|
await handleDeleteTaxIdBtnClick(_id);
|
||||||
await handleDeleteTaxIdBtnClick(_id);
|
}}
|
||||||
}}
|
size="lg"
|
||||||
size="lg"
|
colorSchema="danger"
|
||||||
colorSchema="danger"
|
variant="plain"
|
||||||
variant="plain"
|
ariaLabel="update"
|
||||||
ariaLabel="update"
|
>
|
||||||
>
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
</IconButton>
|
||||||
</IconButton>
|
</Td>
|
||||||
</Td>
|
</Tr>
|
||||||
</Tr>
|
))}
|
||||||
))}
|
{isLoading && <TableSkeleton columns={3} innerKey="tax-ids" />}
|
||||||
{isLoading && <TableSkeleton columns={3} key="tax-ids" />}
|
{!isLoading && data && data?.length === 0 && (
|
||||||
{!isLoading && data && data?.length === 0 && (
|
<Tr>
|
||||||
<Tr>
|
<Td colSpan={5}>
|
||||||
<Td colSpan={5}>
|
<EmptyState title="No Tax IDs on file" icon={faFileInvoice} />
|
||||||
<EmptyState
|
</Td>
|
||||||
title="No Tax IDs on file"
|
</Tr>
|
||||||
icon={faFileInvoice}
|
)}
|
||||||
/>
|
</TBody>
|
||||||
</Td>
|
</Table>
|
||||||
</Tr>
|
</TableContainer>
|
||||||
)}
|
);
|
||||||
</TBody>
|
};
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
+61
-70
@@ -14,76 +14,67 @@ import {
|
|||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useOrganization } from "@app/context";
|
import { useOrganization } from "@app/context";
|
||||||
import {
|
import { useGetOrgInvoices } from "@app/hooks/api";
|
||||||
useGetOrgInvoices
|
|
||||||
} from "@app/hooks/api";
|
|
||||||
|
|
||||||
export const InvoicesTable = () => {
|
export const InvoicesTable = () => {
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const { data, isLoading } = useGetOrgInvoices(currentOrg?._id ?? "");
|
const { data, isLoading } = useGetOrgInvoices(currentOrg?._id ?? "");
|
||||||
return (
|
return (
|
||||||
<TableContainer className="mt-8">
|
<TableContainer className="mt-8">
|
||||||
<Table>
|
<Table>
|
||||||
<THead>
|
<THead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th className="flex-1">Invoice #</Th>
|
<Th className="flex-1">Invoice #</Th>
|
||||||
<Th className="flex-1">Date</Th>
|
<Th className="flex-1">Date</Th>
|
||||||
<Th className="flex-1">Status</Th>
|
<Th className="flex-1">Status</Th>
|
||||||
<Th className="flex-1">Amount</Th>
|
<Th className="flex-1">Amount</Th>
|
||||||
<Th className="w-5" />
|
<Th className="w-5" />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{!isLoading &&
|
||||||
|
data &&
|
||||||
|
data?.length > 0 &&
|
||||||
|
data.map(({ _id, created, paid, number, total, invoice_pdf }) => {
|
||||||
|
const formattedTotal = (Math.floor(total) / 100).toLocaleString("en-US", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "USD"
|
||||||
|
});
|
||||||
|
const createdDate = new Date(created * 1000);
|
||||||
|
const day: number = createdDate.getDate();
|
||||||
|
const month: number = createdDate.getMonth() + 1;
|
||||||
|
const year: number = createdDate.getFullYear();
|
||||||
|
const formattedDate: string = `${day}/${month}/${year}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tr key={`invoice-${_id}`} className="h-10">
|
||||||
|
<Td>{number}</Td>
|
||||||
|
<Td>{formattedDate}</Td>
|
||||||
|
<Td>{paid ? "Paid" : "Not Paid"}</Td>
|
||||||
|
<Td>{formattedTotal}</Td>
|
||||||
|
<Td>
|
||||||
|
<IconButton
|
||||||
|
onClick={async () => window.open(invoice_pdf)}
|
||||||
|
size="lg"
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="update"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faDownload} />
|
||||||
|
</IconButton>
|
||||||
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
);
|
||||||
<TBody>
|
})}
|
||||||
{!isLoading && data && data?.length > 0 && data.map(({
|
{isLoading && <TableSkeleton columns={5} innerKey="invoices" />}
|
||||||
_id,
|
{!isLoading && data && data?.length === 0 && (
|
||||||
created,
|
<Tr>
|
||||||
paid,
|
<Td colSpan={5}>
|
||||||
number,
|
<EmptyState title="No invoices on file" icon={faFileInvoice} />
|
||||||
total,
|
</Td>
|
||||||
invoice_pdf
|
</Tr>
|
||||||
}) => {
|
)}
|
||||||
const formattedTotal = (Math.floor(total) / 100).toLocaleString("en-US", {
|
</TBody>
|
||||||
style: "currency",
|
</Table>
|
||||||
currency: "USD",
|
</TableContainer>
|
||||||
});
|
);
|
||||||
const createdDate = new Date(created * 1000);
|
};
|
||||||
const day: number = createdDate.getDate();
|
|
||||||
const month: number = createdDate.getMonth() + 1;
|
|
||||||
const year: number = createdDate.getFullYear();
|
|
||||||
const formattedDate: string = `${day}/${month}/${year}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tr key={`invoice-${_id}`} className="h-10">
|
|
||||||
<Td>{number}</Td>
|
|
||||||
<Td>{formattedDate}</Td>
|
|
||||||
<Td>{paid ? "Paid" : "Not Paid"}</Td>
|
|
||||||
<Td>{formattedTotal}</Td>
|
|
||||||
<Td>
|
|
||||||
<IconButton
|
|
||||||
onClick={async () => window.open(invoice_pdf)}
|
|
||||||
size="lg"
|
|
||||||
variant="plain"
|
|
||||||
ariaLabel="update"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faDownload} />
|
|
||||||
</IconButton>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{isLoading && <TableSkeleton columns={5} key="invoices" />}
|
|
||||||
{!isLoading && data && data?.length === 0 && (
|
|
||||||
<Tr>
|
|
||||||
<Td colSpan={5}>
|
|
||||||
<EmptyState
|
|
||||||
title="No invoices on file"
|
|
||||||
icon={faFileInvoice}
|
|
||||||
/>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
)}
|
|
||||||
</TBody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
+386
-381
@@ -1,407 +1,412 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Controller,useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import {
|
import { faKey, faMagnifyingGlass, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
faKey,
|
|
||||||
faMagnifyingGlass,
|
|
||||||
faPlus,
|
|
||||||
faTrash} from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
|
|
||||||
import {
|
|
||||||
decryptAssymmetric,
|
|
||||||
encryptAssymmetric,
|
|
||||||
verifyPrivateKey} from "@app/components/utilities/cryptography/crypto";
|
|
||||||
import {
|
import {
|
||||||
Button,
|
decryptAssymmetric,
|
||||||
Checkbox,
|
encryptAssymmetric,
|
||||||
DeleteActionModal,
|
verifyPrivateKey
|
||||||
EmptyState,
|
} from "@app/components/utilities/cryptography/crypto";
|
||||||
FormControl,
|
import {
|
||||||
IconButton,
|
Button,
|
||||||
Input,
|
Checkbox,
|
||||||
Modal,
|
DeleteActionModal,
|
||||||
ModalClose,
|
EmptyState,
|
||||||
ModalContent,
|
FormControl,
|
||||||
Select,
|
IconButton,
|
||||||
SelectItem,
|
Input,
|
||||||
Table,
|
Modal,
|
||||||
TableContainer,
|
ModalClose,
|
||||||
TableSkeleton,
|
ModalContent,
|
||||||
TBody,
|
Select,
|
||||||
Td,
|
SelectItem,
|
||||||
Th,
|
Table,
|
||||||
THead,
|
TableContainer,
|
||||||
Tr} from "@app/components/v2";
|
TableSkeleton,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
useCreateServiceAccountProjectLevelPermission,
|
useCreateServiceAccountProjectLevelPermission,
|
||||||
useDeleteServiceAccountProjectLevelPermission,
|
useDeleteServiceAccountProjectLevelPermission,
|
||||||
useGetServiceAccountById,
|
useGetServiceAccountById,
|
||||||
useGetServiceAccountProjectLevelPermissions,
|
useGetServiceAccountProjectLevelPermissions,
|
||||||
useGetUserWorkspaces
|
useGetUserWorkspaces
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import getLatestFileKey from "@app/pages/api/workspace/getLatestFileKey";
|
import getLatestFileKey from "@app/pages/api/workspace/getLatestFileKey";
|
||||||
|
|
||||||
const createProjectLevelPermissionSchema = yup.object({
|
const createProjectLevelPermissionSchema = yup.object({
|
||||||
privateKey: yup.string().required().label("Private Key"),
|
privateKey: yup.string().required().label("Private Key"),
|
||||||
workspace: yup.string().required().label("Workspace"),
|
workspace: yup.string().required().label("Workspace"),
|
||||||
environment: yup.string().required().label("Environment"),
|
environment: yup.string().required().label("Environment"),
|
||||||
permissions: yup.object().shape({
|
permissions: yup
|
||||||
read: yup.boolean().required(),
|
.object()
|
||||||
write: yup.boolean().required()
|
.shape({
|
||||||
}).defined().required()
|
read: yup.boolean().required(),
|
||||||
|
write: yup.boolean().required()
|
||||||
|
})
|
||||||
|
.defined()
|
||||||
|
.required()
|
||||||
});
|
});
|
||||||
|
|
||||||
type CreateProjectLevelPermissionForm = yup.InferType<typeof createProjectLevelPermissionSchema>;
|
type CreateProjectLevelPermissionForm = yup.InferType<typeof createProjectLevelPermissionSchema>;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
serviceAccountId: string;
|
serviceAccountId: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const SAProjectLevelPermissionsTable = ({
|
export const SAProjectLevelPermissionsTable = ({ serviceAccountId }: Props): JSX.Element => {
|
||||||
serviceAccountId
|
const { data: serviceAccount } = useGetServiceAccountById(serviceAccountId);
|
||||||
}: Props): JSX.Element => {
|
const { data: userWorkspaces, isLoading: isUserWorkspacesLoading } = useGetUserWorkspaces();
|
||||||
const { data: serviceAccount } = useGetServiceAccountById(serviceAccountId);
|
const [searchPermissions, setSearchPermissions] = useState("");
|
||||||
const { data: userWorkspaces, isLoading: isUserWorkspacesLoading } = useGetUserWorkspaces();
|
|
||||||
const [searchPermissions, setSearchPermissions] = useState("");
|
|
||||||
|
|
||||||
const { data: serviceAccountWorkspacePermissions, isLoading: isPermissionsLoading } = useGetServiceAccountProjectLevelPermissions(serviceAccountId);
|
const { data: serviceAccountWorkspacePermissions, isLoading: isPermissionsLoading } =
|
||||||
|
useGetServiceAccountProjectLevelPermissions(serviceAccountId);
|
||||||
const createServiceAccountProjectLevelPermission = useCreateServiceAccountProjectLevelPermission();
|
|
||||||
const deleteServiceAccountProjectLevelPermission = useDeleteServiceAccountProjectLevelPermission();
|
|
||||||
|
|
||||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
const createServiceAccountProjectLevelPermission =
|
||||||
"addProjectLevelPermission",
|
useCreateServiceAccountProjectLevelPermission();
|
||||||
"removeProjectLevelPermission",
|
const deleteServiceAccountProjectLevelPermission =
|
||||||
] as const);
|
useDeleteServiceAccountProjectLevelPermission();
|
||||||
|
|
||||||
const [, setSelectedWorkspace] = useState<undefined | string>(undefined);
|
|
||||||
|
|
||||||
const {
|
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
control,
|
"addProjectLevelPermission",
|
||||||
handleSubmit,
|
"removeProjectLevelPermission"
|
||||||
reset,
|
] as const);
|
||||||
formState: { isSubmitting }
|
|
||||||
} = useForm<CreateProjectLevelPermissionForm>({ resolver: yupResolver(createProjectLevelPermissionSchema) })
|
|
||||||
|
|
||||||
const onAddProjectLevelPermission = async ({
|
const [, setSelectedWorkspace] = useState<undefined | string>(undefined);
|
||||||
privateKey,
|
|
||||||
workspace,
|
|
||||||
environment,
|
|
||||||
permissions: { read, write }
|
|
||||||
}: CreateProjectLevelPermissionForm) => {
|
|
||||||
|
|
||||||
// TODO: clean up / modularize this function
|
|
||||||
|
|
||||||
if (!serviceAccount) return;
|
|
||||||
|
|
||||||
const { latestKey } = await getLatestFileKey({
|
|
||||||
workspaceId: workspace
|
|
||||||
});
|
|
||||||
|
|
||||||
verifyPrivateKey({
|
const {
|
||||||
privateKey,
|
control,
|
||||||
publicKey: serviceAccount.publicKey
|
handleSubmit,
|
||||||
});
|
reset,
|
||||||
|
formState: { isSubmitting }
|
||||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
} = useForm<CreateProjectLevelPermissionForm>({
|
||||||
|
resolver: yupResolver(createProjectLevelPermissionSchema)
|
||||||
|
});
|
||||||
|
|
||||||
const key = decryptAssymmetric({
|
const onAddProjectLevelPermission = async ({
|
||||||
ciphertext: latestKey.encryptedKey,
|
privateKey,
|
||||||
nonce: latestKey.nonce,
|
workspace,
|
||||||
publicKey: latestKey.sender.publicKey,
|
environment,
|
||||||
privateKey: PRIVATE_KEY
|
permissions: { read, write }
|
||||||
});
|
}: CreateProjectLevelPermissionForm) => {
|
||||||
|
// TODO: clean up / modularize this function
|
||||||
const { ciphertext, nonce } = encryptAssymmetric({
|
|
||||||
plaintext: key,
|
|
||||||
publicKey: serviceAccount.publicKey,
|
|
||||||
privateKey
|
|
||||||
});
|
|
||||||
|
|
||||||
await createServiceAccountProjectLevelPermission.mutateAsync({
|
|
||||||
serviceAccountId,
|
|
||||||
workspaceId: workspace,
|
|
||||||
environment,
|
|
||||||
read,
|
|
||||||
write,
|
|
||||||
encryptedKey: ciphertext,
|
|
||||||
nonce
|
|
||||||
});
|
|
||||||
handlePopUpClose("addProjectLevelPermission");
|
|
||||||
}
|
|
||||||
|
|
||||||
const onRemoveProjectLevelPermission = async () => {
|
|
||||||
const serviceAccountWorkspacePermissionId = (popUp?.removeProjectLevelPermission?.data as { _id: string })?._id;
|
|
||||||
await deleteServiceAccountProjectLevelPermission.mutateAsync({
|
|
||||||
serviceAccountId,
|
|
||||||
serviceAccountWorkspacePermissionId
|
|
||||||
});
|
|
||||||
handlePopUpClose("removeProjectLevelPermission");
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
if (!serviceAccount) return;
|
||||||
<div className="w-full bg-white/5 p-6">
|
|
||||||
<p className="mb-4 text-xl font-semibold">Project-Level Permissions</p>
|
const { latestKey } = await getLatestFileKey({
|
||||||
<div className="mb-4 flex">
|
workspaceId: workspace
|
||||||
<div className="mr-4 flex-1">
|
});
|
||||||
<Input
|
|
||||||
value={searchPermissions}
|
verifyPrivateKey({
|
||||||
onChange={(e) => setSearchPermissions(e.target.value)}
|
privateKey,
|
||||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
publicKey: serviceAccount.publicKey
|
||||||
placeholder="Search service account project-level permissions..."
|
});
|
||||||
/>
|
|
||||||
</div>
|
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
||||||
<Button
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
const key = decryptAssymmetric({
|
||||||
onClick={() => {
|
ciphertext: latestKey.encryptedKey,
|
||||||
handlePopUpOpen("addProjectLevelPermission")
|
nonce: latestKey.nonce,
|
||||||
reset();
|
publicKey: latestKey.sender.publicKey,
|
||||||
}}
|
privateKey: PRIVATE_KEY
|
||||||
>
|
});
|
||||||
Add Permission
|
|
||||||
</Button>
|
const { ciphertext, nonce } = encryptAssymmetric({
|
||||||
</div>
|
plaintext: key,
|
||||||
<TableContainer>
|
publicKey: serviceAccount.publicKey,
|
||||||
<Table>
|
privateKey
|
||||||
<THead>
|
});
|
||||||
<Tr>
|
|
||||||
<Th>Project</Th>
|
await createServiceAccountProjectLevelPermission.mutateAsync({
|
||||||
<Th>Environment</Th>
|
serviceAccountId,
|
||||||
<Th>Read</Th>
|
workspaceId: workspace,
|
||||||
<Th>Write</Th>
|
environment,
|
||||||
<Th aria-label="actions" />
|
read,
|
||||||
</Tr>
|
write,
|
||||||
</THead>
|
encryptedKey: ciphertext,
|
||||||
<TBody>
|
nonce
|
||||||
{isPermissionsLoading && <TableSkeleton columns={6} key="service-account-project-level-permissions" />}
|
});
|
||||||
{!isPermissionsLoading && serviceAccountWorkspacePermissions && (
|
handlePopUpClose("addProjectLevelPermission");
|
||||||
serviceAccountWorkspacePermissions.map(({
|
};
|
||||||
_id,
|
|
||||||
workspace,
|
const onRemoveProjectLevelPermission = async () => {
|
||||||
environment,
|
const serviceAccountWorkspacePermissionId = (
|
||||||
read,
|
popUp?.removeProjectLevelPermission?.data as { _id: string }
|
||||||
write
|
)?._id;
|
||||||
}) => {
|
await deleteServiceAccountProjectLevelPermission.mutateAsync({
|
||||||
const environmentName = (workspace.environments.find((env) => env.slug === environment))?.name;
|
serviceAccountId,
|
||||||
return (
|
serviceAccountWorkspacePermissionId
|
||||||
<Tr key={`service-account-project-level-permission-${_id}`} className="w-full">
|
});
|
||||||
<Td>{workspace.name}</Td>
|
handlePopUpClose("removeProjectLevelPermission");
|
||||||
<Td>{environmentName}</Td>
|
};
|
||||||
<Td>
|
|
||||||
<Checkbox
|
return (
|
||||||
id="isReadPermissionEnabled"
|
<div className="w-full bg-white/5 p-6">
|
||||||
isChecked={read}
|
<p className="mb-4 text-xl font-semibold">Project-Level Permissions</p>
|
||||||
isDisabled
|
<div className="mb-4 flex">
|
||||||
>{/**/}</Checkbox>
|
<div className="mr-4 flex-1">
|
||||||
</Td>
|
<Input
|
||||||
<Td>
|
value={searchPermissions}
|
||||||
<Checkbox
|
onChange={(e) => setSearchPermissions(e.target.value)}
|
||||||
id="isWritePermissionEnabled"
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
isChecked={write}
|
placeholder="Search service account project-level permissions..."
|
||||||
isDisabled
|
/>
|
||||||
>{/**/}</Checkbox>
|
|
||||||
</Td>
|
|
||||||
<Td>
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="delete"
|
|
||||||
colorSchema="danger"
|
|
||||||
onClick={() => handlePopUpOpen("removeProjectLevelPermission", { _id })}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faTrash} />
|
|
||||||
</IconButton>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
{!isPermissionsLoading && serviceAccountWorkspacePermissions?.length === 0 && (
|
|
||||||
<Tr>
|
|
||||||
<Td colSpan={7} className="py-6 text-center text-bunker-400">
|
|
||||||
<EmptyState title="No permissions found" icon={faKey} />
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
)}
|
|
||||||
</TBody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
<Modal
|
|
||||||
isOpen={popUp?.addProjectLevelPermission?.isOpen}
|
|
||||||
onOpenChange={(isOpen) => {
|
|
||||||
handlePopUpToggle("addProjectLevelPermission", isOpen);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ModalContent
|
|
||||||
title="Add a Project-Level Permission"
|
|
||||||
subTitle="The service account will be granted scoped access to the specified project and environment"
|
|
||||||
>
|
|
||||||
<form onSubmit={handleSubmit(onAddProjectLevelPermission)}>
|
|
||||||
{!isUserWorkspacesLoading && userWorkspaces && (
|
|
||||||
<>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
defaultValue=""
|
|
||||||
name="privateKey"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Service Account Private Key"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input {...field} />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="workspace"
|
|
||||||
defaultValue={String(userWorkspaces?.[0]?._id)}
|
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Project"
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
defaultValue={field.value}
|
|
||||||
{...field}
|
|
||||||
onValueChange={(e) => {
|
|
||||||
onChange(e);
|
|
||||||
setSelectedWorkspace(e);
|
|
||||||
}}
|
|
||||||
className="w-full border border-mine-shaft-500"
|
|
||||||
>
|
|
||||||
{userWorkspaces && userWorkspaces.length > 0 ? (
|
|
||||||
userWorkspaces.map((userWorkspace) => {
|
|
||||||
return (
|
|
||||||
<SelectItem value={userWorkspace._id} key={`project-${userWorkspace._id}`}>
|
|
||||||
{userWorkspace.name}
|
|
||||||
</SelectItem>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
) : (
|
|
||||||
<SelectItem value="none" key="target-app-none">
|
|
||||||
No projects found
|
|
||||||
</SelectItem>
|
|
||||||
)}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="environment"
|
|
||||||
defaultValue={String(userWorkspaces?.[0]?.environments?.[0]?.slug)}
|
|
||||||
render={({ field: { onChange, ...field } }) => {
|
|
||||||
/* eslint-disable-next-line no-underscore-dangle */
|
|
||||||
const environments = userWorkspaces?.find((userWorkspace) => userWorkspace._id === control?._formValues?.workspace)?.environments ?? [];
|
|
||||||
return (
|
|
||||||
<FormControl
|
|
||||||
label="Environment"
|
|
||||||
className="mt-4"
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
defaultValue={field.value}
|
|
||||||
{...field}
|
|
||||||
onValueChange={(e) => onChange(e)}
|
|
||||||
className="w-full border border-mine-shaft-500"
|
|
||||||
>
|
|
||||||
{environments.length > 0 ? (
|
|
||||||
environments.map((environment) => {
|
|
||||||
return (
|
|
||||||
<SelectItem value={environment.slug} key={`environment-${environment.slug}`}>
|
|
||||||
{environment.name}
|
|
||||||
</SelectItem>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
) : (
|
|
||||||
<SelectItem value="none" key="target-app-none">
|
|
||||||
No environments found
|
|
||||||
</SelectItem>
|
|
||||||
)}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="permissions"
|
|
||||||
defaultValue={{
|
|
||||||
read: true,
|
|
||||||
write: false
|
|
||||||
}}
|
|
||||||
render={({ field: { onChange, value }, fieldState: { error }}) => {
|
|
||||||
const options = [
|
|
||||||
{
|
|
||||||
label: "Read (default)",
|
|
||||||
value: "read"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Write",
|
|
||||||
value: "write"
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormControl
|
|
||||||
label="Permissions"
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
>
|
|
||||||
<>
|
|
||||||
{options.map(({ label, value: optionValue }) => {
|
|
||||||
return (
|
|
||||||
<Checkbox
|
|
||||||
id={value[optionValue]}
|
|
||||||
key={optionValue}
|
|
||||||
className="data-[state=checked]:bg-primary"
|
|
||||||
isChecked={value[optionValue]}
|
|
||||||
isDisabled={optionValue === "read"}
|
|
||||||
onCheckedChange={(state) => {
|
|
||||||
onChange({
|
|
||||||
...value,
|
|
||||||
[optionValue]: state
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</Checkbox>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="mt-8 flex items-center">
|
|
||||||
<Button
|
|
||||||
className="mr-4"
|
|
||||||
type="submit"
|
|
||||||
isDisabled={isSubmitting}
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
>
|
|
||||||
Create
|
|
||||||
</Button>
|
|
||||||
<ModalClose asChild>
|
|
||||||
<Button variant="plain" colorSchema="secondary">
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</ModalClose>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
<DeleteActionModal
|
|
||||||
isOpen={popUp.removeProjectLevelPermission.isOpen}
|
|
||||||
deleteKey="remove"
|
|
||||||
title="Do you want to remove this permission from the service account?"
|
|
||||||
onChange={(isOpen) => handlePopUpToggle("removeProjectLevelPermission", isOpen)}
|
|
||||||
onDeleteApproved={onRemoveProjectLevelPermission}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
<Button
|
||||||
}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
onClick={() => {
|
||||||
|
handlePopUpOpen("addProjectLevelPermission");
|
||||||
|
reset();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add Permission
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th>Project</Th>
|
||||||
|
<Th>Environment</Th>
|
||||||
|
<Th>Read</Th>
|
||||||
|
<Th>Write</Th>
|
||||||
|
<Th aria-label="actions" />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isPermissionsLoading && (
|
||||||
|
<TableSkeleton columns={6} innerKey="service-account-project-level-permissions" />
|
||||||
|
)}
|
||||||
|
{!isPermissionsLoading &&
|
||||||
|
serviceAccountWorkspacePermissions &&
|
||||||
|
serviceAccountWorkspacePermissions.map(
|
||||||
|
({ _id, workspace, environment, read, write }) => {
|
||||||
|
const environmentName = workspace.environments.find(
|
||||||
|
(env) => env.slug === environment
|
||||||
|
)?.name;
|
||||||
|
return (
|
||||||
|
<Tr key={`service-account-project-level-permission-${_id}`} className="w-full">
|
||||||
|
<Td>{workspace.name}</Td>
|
||||||
|
<Td>{environmentName}</Td>
|
||||||
|
<Td>
|
||||||
|
<Checkbox id="isReadPermissionEnabled" isChecked={read} isDisabled>
|
||||||
|
{/**/}
|
||||||
|
</Checkbox>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<Checkbox id="isWritePermissionEnabled" isChecked={write} isDisabled>
|
||||||
|
{/**/}
|
||||||
|
</Checkbox>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="delete"
|
||||||
|
colorSchema="danger"
|
||||||
|
onClick={() => handlePopUpOpen("removeProjectLevelPermission", { _id })}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</IconButton>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
{!isPermissionsLoading && serviceAccountWorkspacePermissions?.length === 0 && (
|
||||||
|
<Tr>
|
||||||
|
<Td colSpan={7} className="py-6 text-center text-bunker-400">
|
||||||
|
<EmptyState title="No permissions found" icon={faKey} />
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp?.addProjectLevelPermission?.isOpen}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
handlePopUpToggle("addProjectLevelPermission", isOpen);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
title="Add a Project-Level Permission"
|
||||||
|
subTitle="The service account will be granted scoped access to the specified project and environment"
|
||||||
|
>
|
||||||
|
<form onSubmit={handleSubmit(onAddProjectLevelPermission)}>
|
||||||
|
{!isUserWorkspacesLoading && userWorkspaces && (
|
||||||
|
<>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name="privateKey"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Service Account Private Key"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="workspace"
|
||||||
|
defaultValue={String(userWorkspaces?.[0]?._id)}
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Project" errorText={error?.message}>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => {
|
||||||
|
onChange(e);
|
||||||
|
setSelectedWorkspace(e);
|
||||||
|
}}
|
||||||
|
className="border-mine-shaft-500 w-full border"
|
||||||
|
>
|
||||||
|
{userWorkspaces && userWorkspaces.length > 0 ? (
|
||||||
|
userWorkspaces.map((userWorkspace) => {
|
||||||
|
return (
|
||||||
|
<SelectItem
|
||||||
|
value={userWorkspace._id}
|
||||||
|
key={`project-${userWorkspace._id}`}
|
||||||
|
>
|
||||||
|
{userWorkspace.name}
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<SelectItem value="none" key="target-app-none">
|
||||||
|
No projects found
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="environment"
|
||||||
|
defaultValue={String(userWorkspaces?.[0]?.environments?.[0]?.slug)}
|
||||||
|
render={({ field: { onChange, ...field } }) => {
|
||||||
|
const environments =
|
||||||
|
userWorkspaces?.find(
|
||||||
|
/* eslint-disable-next-line no-underscore-dangle */
|
||||||
|
(userWorkspace) => userWorkspace._id === control?._formValues?.workspace
|
||||||
|
)?.environments ?? [];
|
||||||
|
return (
|
||||||
|
<FormControl label="Environment" className="mt-4">
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="border-mine-shaft-500 w-full border"
|
||||||
|
>
|
||||||
|
{environments.length > 0 ? (
|
||||||
|
environments.map((environment) => {
|
||||||
|
return (
|
||||||
|
<SelectItem
|
||||||
|
value={environment.slug}
|
||||||
|
key={`environment-${environment.slug}`}
|
||||||
|
>
|
||||||
|
{environment.name}
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<SelectItem value="none" key="target-app-none">
|
||||||
|
No environments found
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="permissions"
|
||||||
|
defaultValue={{
|
||||||
|
read: true,
|
||||||
|
write: false
|
||||||
|
}}
|
||||||
|
render={({ field: { onChange, value }, fieldState: { error } }) => {
|
||||||
|
const options = [
|
||||||
|
{
|
||||||
|
label: "Read (default)",
|
||||||
|
value: "read"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Write",
|
||||||
|
value: "write"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormControl
|
||||||
|
label="Permissions"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
{options.map(({ label, value: optionValue }) => {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
id={value[optionValue]}
|
||||||
|
key={optionValue}
|
||||||
|
className="data-[state=checked]:bg-primary"
|
||||||
|
isChecked={value[optionValue]}
|
||||||
|
isDisabled={optionValue === "read"}
|
||||||
|
onCheckedChange={(state) => {
|
||||||
|
onChange({
|
||||||
|
...value,
|
||||||
|
[optionValue]: state
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Checkbox>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="mt-8 flex items-center">
|
||||||
|
<Button
|
||||||
|
className="mr-4"
|
||||||
|
type="submit"
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
<ModalClose asChild>
|
||||||
|
<Button variant="plain" colorSchema="secondary">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</ModalClose>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.removeProjectLevelPermission.isOpen}
|
||||||
|
deleteKey="remove"
|
||||||
|
title="Do you want to remove this permission from the service account?"
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("removeProjectLevelPermission", isOpen)}
|
||||||
|
onDeleteApproved={onRemoveProjectLevelPermission}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
+60
-66
@@ -1,9 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import {
|
import { faContactBook, faMagnifyingGlass, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
faContactBook,
|
|
||||||
faMagnifyingGlass,
|
|
||||||
faTrash
|
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
@@ -23,13 +19,11 @@ import {
|
|||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useOrganization } from "@app/context";
|
import { useOrganization } from "@app/context";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import {
|
import { useDeleteIncidentContact, useGetOrgIncidentContact } from "@app/hooks/api";
|
||||||
useDeleteIncidentContact,
|
|
||||||
useGetOrgIncidentContact} from "@app/hooks/api";
|
|
||||||
|
|
||||||
export const OrgIncidentContactsTable = () => {
|
export const OrgIncidentContactsTable = () => {
|
||||||
const { createNotification } = useNotificationContext();
|
const { createNotification } = useNotificationContext();
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const { data: contacts, isLoading } = useGetOrgIncidentContact(currentOrg?._id ?? "");
|
const { data: contacts, isLoading } = useGetOrgIncidentContact(currentOrg?._id ?? "");
|
||||||
const [searchContact, setSearchContact] = useState("");
|
const [searchContact, setSearchContact] = useState("");
|
||||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
@@ -40,71 +34,71 @@ export const OrgIncidentContactsTable = () => {
|
|||||||
|
|
||||||
const onRemoveIncidentContact = async () => {
|
const onRemoveIncidentContact = async () => {
|
||||||
try {
|
try {
|
||||||
const incidentContactEmail = (popUp?.removeContact?.data as { email: string })?.email;
|
const incidentContactEmail = (popUp?.removeContact?.data as { email: string })?.email;
|
||||||
|
|
||||||
if (!currentOrg?._id) return;
|
|
||||||
await mutateAsync({
|
|
||||||
orgId: currentOrg._id,
|
|
||||||
email: incidentContactEmail
|
|
||||||
});
|
|
||||||
|
|
||||||
createNotification({
|
if (!currentOrg?._id) return;
|
||||||
text: "Successfully removed incident contact",
|
await mutateAsync({
|
||||||
type: "success"
|
orgId: currentOrg._id,
|
||||||
});
|
email: incidentContactEmail
|
||||||
|
});
|
||||||
handlePopUpClose("removeContact");
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully removed incident contact",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
handlePopUpClose("removeContact");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Failed to remove incident contact",
|
text: "Failed to remove incident contact",
|
||||||
type: "error"
|
type: "error"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredContacts = contacts ? contacts.filter(({ email }) =>
|
const filteredContacts = contacts
|
||||||
email.toLocaleLowerCase().includes(searchContact)
|
? contacts.filter(({ email }) => email.toLocaleLowerCase().includes(searchContact))
|
||||||
) : [];
|
: [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Input
|
<Input
|
||||||
value={searchContact}
|
value={searchContact}
|
||||||
onChange={(e) => setSearchContact(e.target.value)}
|
onChange={(e) => setSearchContact(e.target.value)}
|
||||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
placeholder="Search incident contact by email..."
|
placeholder="Search incident contact by email..."
|
||||||
/>
|
/>
|
||||||
<TableContainer className="mt-4">
|
<TableContainer className="mt-4">
|
||||||
<Table>
|
<Table>
|
||||||
<THead>
|
<THead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th>Email</Th>
|
<Th>Email</Th>
|
||||||
<Th aria-label="actions" />
|
<Th aria-label="actions" />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isLoading && <TableSkeleton columns={2} innerKey="incident-contact" />}
|
||||||
|
{filteredContacts?.map(({ email }) => (
|
||||||
|
<Tr key={email}>
|
||||||
|
<Td className="w-full">{email}</Td>
|
||||||
|
<Td className="mr-4">
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="delete"
|
||||||
|
colorSchema="danger"
|
||||||
|
onClick={() => handlePopUpOpen("removeContact", { email })}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</IconButton>
|
||||||
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
))}
|
||||||
<TBody>
|
</TBody>
|
||||||
{isLoading && <TableSkeleton columns={2} key="incident-contact" />}
|
</Table>
|
||||||
{filteredContacts?.map(({ email }) => (
|
{filteredContacts?.length === 0 && !isLoading && (
|
||||||
<Tr key={email}>
|
<EmptyState title="No incident contacts found" icon={faContactBook} />
|
||||||
<Td className="w-full">{email}</Td>
|
)}
|
||||||
<Td className="mr-4">
|
</TableContainer>
|
||||||
<IconButton
|
|
||||||
ariaLabel="delete"
|
|
||||||
colorSchema="danger"
|
|
||||||
onClick={() => handlePopUpOpen("removeContact", { email })}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faTrash} />
|
|
||||||
</IconButton>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
))}
|
|
||||||
</TBody>
|
|
||||||
</Table>
|
|
||||||
{filteredContacts?.length === 0 && !isLoading && (
|
|
||||||
<EmptyState title="No incident contacts found" icon={faContactBook} />
|
|
||||||
)}
|
|
||||||
</TableContainer>
|
|
||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
isOpen={popUp.removeContact.isOpen}
|
isOpen={popUp.removeContact.isOpen}
|
||||||
deleteKey="remove"
|
deleteKey="remove"
|
||||||
|
|||||||
+322
-323
@@ -1,17 +1,14 @@
|
|||||||
import { useEffect, useMemo,useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import {
|
|
||||||
// Controller,
|
|
||||||
// useForm
|
|
||||||
} from "react-hook-form";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import {
|
import {
|
||||||
faCheck,
|
faCheck,
|
||||||
faCopy,
|
faCopy,
|
||||||
faMagnifyingGlass,
|
faMagnifyingGlass,
|
||||||
faPencil,
|
faPencil,
|
||||||
faPlus,
|
faPlus,
|
||||||
faServer,
|
faServer,
|
||||||
faTrash} from "@fortawesome/free-solid-svg-icons";
|
faTrash
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
// import { yupResolver } from "@hookform/resolvers/yup";
|
// import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
@@ -21,13 +18,13 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
DeleteActionModal,
|
DeleteActionModal,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
// FormControl,
|
// FormControl,
|
||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
Modal,
|
Modal,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
// Select,
|
// Select,
|
||||||
// SelectItem,
|
// SelectItem,
|
||||||
Table,
|
Table,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
TableSkeleton,
|
TableSkeleton,
|
||||||
@@ -40,11 +37,15 @@ import {
|
|||||||
import { useOrganization, useWorkspace } from "@app/context";
|
import { useOrganization, useWorkspace } from "@app/context";
|
||||||
import { usePopUp, useToggle } from "@app/hooks";
|
import { usePopUp, useToggle } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
// useCreateServiceAccount,
|
// useCreateServiceAccount,
|
||||||
useDeleteServiceAccount,
|
useDeleteServiceAccount,
|
||||||
useGetServiceAccounts
|
useGetServiceAccounts
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
|
|
||||||
|
import // Controller,
|
||||||
|
// useForm
|
||||||
|
"react-hook-form";
|
||||||
|
|
||||||
// const serviceAccountExpiration = [
|
// const serviceAccountExpiration = [
|
||||||
// { label: "1 Day", value: 86400 },
|
// { label: "1 Day", value: 86400 },
|
||||||
// { label: "7 Days", value: 604800 },
|
// { label: "7 Days", value: 604800 },
|
||||||
@@ -62,314 +63,312 @@ import {
|
|||||||
// type TAddServiceAccountForm = yup.InferType<typeof addServiceAccountFormSchema>;
|
// type TAddServiceAccountForm = yup.InferType<typeof addServiceAccountFormSchema>;
|
||||||
|
|
||||||
export const OrgServiceAccountsTable = () => {
|
export const OrgServiceAccountsTable = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
const orgId = currentOrg?._id || "";
|
|
||||||
const [step, setStep] = useState(0);
|
|
||||||
const [isAccessKeyCopied, setIsAccessKeyCopied] = useToggle(false);
|
|
||||||
const [isPublicKeyCopied, setIsPublicKeyCopied] = useToggle(false);
|
|
||||||
const [isPrivateKeyCopied, setIsPrivateKeyCopied] = useToggle(false);
|
|
||||||
const [accessKey] = useState("");
|
|
||||||
const [publicKey] = useState("");
|
|
||||||
const [privateKey] = useState("");
|
|
||||||
const [searchServiceAccountFilter, setSearchServiceAccountFilter] = useState("");
|
|
||||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
|
||||||
"addServiceAccount",
|
|
||||||
"removeServiceAccount",
|
|
||||||
] as const);
|
|
||||||
|
|
||||||
const { data: serviceAccounts = [], isLoading: isServiceAccountsLoading } = useGetServiceAccounts(orgId);
|
const orgId = currentOrg?._id || "";
|
||||||
|
const [step, setStep] = useState(0);
|
||||||
// const createServiceAccount = useCreateServiceAccount();
|
const [isAccessKeyCopied, setIsAccessKeyCopied] = useToggle(false);
|
||||||
const removeServiceAccount = useDeleteServiceAccount();
|
const [isPublicKeyCopied, setIsPublicKeyCopied] = useToggle(false);
|
||||||
|
const [isPrivateKeyCopied, setIsPrivateKeyCopied] = useToggle(false);
|
||||||
useEffect(() => {
|
const [accessKey] = useState("");
|
||||||
let timer: NodeJS.Timeout;
|
const [publicKey] = useState("");
|
||||||
if (isAccessKeyCopied) {
|
const [privateKey] = useState("");
|
||||||
timer = setTimeout(() => setIsAccessKeyCopied.off(), 2000);
|
const [searchServiceAccountFilter, setSearchServiceAccountFilter] = useState("");
|
||||||
}
|
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
|
"addServiceAccount",
|
||||||
|
"removeServiceAccount"
|
||||||
|
] as const);
|
||||||
|
|
||||||
if (isPublicKeyCopied) {
|
const { data: serviceAccounts = [], isLoading: isServiceAccountsLoading } =
|
||||||
timer = setTimeout(() => setIsPublicKeyCopied.off(), 2000);
|
useGetServiceAccounts(orgId);
|
||||||
}
|
|
||||||
|
|
||||||
if (isPrivateKeyCopied) {
|
// const createServiceAccount = useCreateServiceAccount();
|
||||||
timer = setTimeout(() => setIsPrivateKeyCopied.off(), 2000);
|
const removeServiceAccount = useDeleteServiceAccount();
|
||||||
}
|
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
useEffect(() => {
|
||||||
}, [isAccessKeyCopied, isPublicKeyCopied, isPrivateKeyCopied]);
|
let timer: NodeJS.Timeout;
|
||||||
|
if (isAccessKeyCopied) {
|
||||||
// const {
|
timer = setTimeout(() => setIsAccessKeyCopied.off(), 2000);
|
||||||
// control,
|
|
||||||
// handleSubmit,
|
|
||||||
// reset,
|
|
||||||
// formState: { isSubmitting }
|
|
||||||
// } = useForm<TAddServiceAccountForm>({ resolver: yupResolver(addServiceAccountFormSchema) });
|
|
||||||
|
|
||||||
// const onAddServiceAccount = async ({ name, expiresIn }: TAddServiceAccountForm) => {
|
|
||||||
// if (!currentOrg?._id) return;
|
|
||||||
|
|
||||||
// const keyPair = generateKeyPair();
|
|
||||||
// setPublicKey(keyPair.publicKey);
|
|
||||||
// setPrivateKey(keyPair.privateKey);
|
|
||||||
|
|
||||||
// const serviceAccountDetails = await createServiceAccount.mutateAsync({
|
|
||||||
// name,
|
|
||||||
// organizationId: currentOrg?._id,
|
|
||||||
// publicKey: keyPair.publicKey,
|
|
||||||
// expiresIn: Number(expiresIn)
|
|
||||||
// });
|
|
||||||
|
|
||||||
// setAccessKey(serviceAccountDetails.serviceAccountAccessKey);
|
|
||||||
|
|
||||||
// setStep(1);
|
|
||||||
// reset();
|
|
||||||
// }
|
|
||||||
|
|
||||||
const onRemoveServiceAccount = async () => {
|
|
||||||
const serviceAccountId = (popUp?.removeServiceAccount?.data as { _id: string })?._id;
|
|
||||||
await removeServiceAccount.mutateAsync(serviceAccountId);
|
|
||||||
handlePopUpClose("removeServiceAccount");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredServiceAccounts = useMemo(
|
if (isPublicKeyCopied) {
|
||||||
() =>
|
timer = setTimeout(() => setIsPublicKeyCopied.off(), 2000);
|
||||||
serviceAccounts.filter(
|
|
||||||
({ name }) =>
|
|
||||||
name.toLowerCase().includes(searchServiceAccountFilter)
|
|
||||||
),
|
|
||||||
[serviceAccounts, searchServiceAccountFilter]
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderStep = (stepToRender: number) => {
|
|
||||||
switch (stepToRender) {
|
|
||||||
case 0:
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
We are currently revising the service account mechanism. In the meantime,
|
|
||||||
please use service tokens or API key to fetch secrets via API request.
|
|
||||||
</div>
|
|
||||||
// <form onSubmit={handleSubmit(onAddServiceAccount)}>
|
|
||||||
// <Controller
|
|
||||||
// control={control}
|
|
||||||
// defaultValue=""
|
|
||||||
// name="name"
|
|
||||||
// render={({ field, fieldState: { error } }) => (
|
|
||||||
// <FormControl label="Name" isError={Boolean(error)} errorText={error?.message}>
|
|
||||||
// <Input {...field} />
|
|
||||||
// </FormControl>
|
|
||||||
// )}
|
|
||||||
// />
|
|
||||||
// <Controller
|
|
||||||
// control={control}
|
|
||||||
// name="expiresIn"
|
|
||||||
// defaultValue={String(serviceAccountExpiration?.[0]?.value)}
|
|
||||||
// render={({ field: { onChange, ...field }, fieldState: { error } }) => {
|
|
||||||
// return (
|
|
||||||
// <FormControl
|
|
||||||
// label="Expiration"
|
|
||||||
// errorText={error?.message}
|
|
||||||
// isError={Boolean(error)}
|
|
||||||
// >
|
|
||||||
// <Select
|
|
||||||
// defaultValue={field.value}
|
|
||||||
// {...field}
|
|
||||||
// onValueChange={(e) => onChange(e)}
|
|
||||||
// className="w-full"
|
|
||||||
// >
|
|
||||||
// {serviceAccountExpiration.map(({ label, value }) => (
|
|
||||||
// <SelectItem value={String(value)} key={label}>
|
|
||||||
// {label}
|
|
||||||
// </SelectItem>
|
|
||||||
// ))}
|
|
||||||
// </Select>
|
|
||||||
// </FormControl>
|
|
||||||
// );
|
|
||||||
// }}
|
|
||||||
// />
|
|
||||||
// <div className="mt-8 flex items-center">
|
|
||||||
// <Button
|
|
||||||
// className="mr-4"
|
|
||||||
// size="sm"
|
|
||||||
// type="submit"
|
|
||||||
// isLoading={isSubmitting}
|
|
||||||
// isDisabled={isSubmitting}
|
|
||||||
// >
|
|
||||||
// Create Service Account
|
|
||||||
// </Button>
|
|
||||||
// <Button
|
|
||||||
// colorSchema="secondary"
|
|
||||||
// variant="plain"
|
|
||||||
// onClick={() => handlePopUpClose("addServiceAccount")}
|
|
||||||
// >
|
|
||||||
// Cancel
|
|
||||||
// </Button>
|
|
||||||
// </div>
|
|
||||||
// </form>
|
|
||||||
);
|
|
||||||
case 1:
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<p>Access Key</p>
|
|
||||||
<div className="flex items-center justify-end rounded-md p-2 text-base text-gray-400 bg-white/[0.07]">
|
|
||||||
<p className="mr-4 break-all">{accessKey}</p>
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="copy icon"
|
|
||||||
colorSchema="secondary"
|
|
||||||
className="group relative"
|
|
||||||
onClick={() => {
|
|
||||||
navigator.clipboard.writeText(accessKey);
|
|
||||||
setIsAccessKeyCopied.on();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={isAccessKeyCopied ? faCheck : faCopy} />
|
|
||||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
|
||||||
Copy
|
|
||||||
</span>
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
<p className="mt-4">Public Key</p>
|
|
||||||
<div className="flex items-center justify-end rounded-md p-2 text-base text-gray-400 bg-white/[0.07]">
|
|
||||||
<p className="mr-4 break-all">{publicKey}</p>
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="copy icon"
|
|
||||||
colorSchema="secondary"
|
|
||||||
className="group relative"
|
|
||||||
onClick={() => {
|
|
||||||
navigator.clipboard.writeText(publicKey);
|
|
||||||
setIsPublicKeyCopied.on();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={isPublicKeyCopied ? faCheck : faCopy} />
|
|
||||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
|
||||||
Copy
|
|
||||||
</span>
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
<p className="mt-4">Private Key</p>
|
|
||||||
<div className="flex items-center justify-end rounded-md p-2 text-base text-gray-400 bg-white/[0.07]">
|
|
||||||
<p className="mr-4 break-all">{privateKey}</p>
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="copy icon"
|
|
||||||
colorSchema="secondary"
|
|
||||||
className="group relative"
|
|
||||||
onClick={() => {
|
|
||||||
navigator.clipboard.writeText(privateKey);
|
|
||||||
setIsPrivateKeyCopied.on();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={isPrivateKeyCopied ? faCheck : faCopy} />
|
|
||||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
|
||||||
Copy
|
|
||||||
</span>
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return <div />
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
if (isPrivateKeyCopied) {
|
||||||
<div className="w-full">
|
timer = setTimeout(() => setIsPrivateKeyCopied.off(), 2000);
|
||||||
<div className="flex justify-between mb-4">
|
}
|
||||||
<p className="text-xl font-semibold text-mineshaft-100">Service Accounts</p>
|
|
||||||
<Button
|
return () => clearTimeout(timer);
|
||||||
colorSchema="secondary"
|
}, [isAccessKeyCopied, isPublicKeyCopied, isPrivateKeyCopied]);
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
onClick={() => {
|
// const {
|
||||||
setStep(0);
|
// control,
|
||||||
// reset();
|
// handleSubmit,
|
||||||
handlePopUpOpen("addServiceAccount");
|
// reset,
|
||||||
}}
|
// formState: { isSubmitting }
|
||||||
>
|
// } = useForm<TAddServiceAccountForm>({ resolver: yupResolver(addServiceAccountFormSchema) });
|
||||||
Add Service Account
|
|
||||||
</Button>
|
// const onAddServiceAccount = async ({ name, expiresIn }: TAddServiceAccountForm) => {
|
||||||
</div>
|
// if (!currentOrg?._id) return;
|
||||||
<Input
|
|
||||||
value={searchServiceAccountFilter}
|
// const keyPair = generateKeyPair();
|
||||||
onChange={(e) => setSearchServiceAccountFilter(e.target.value)}
|
// setPublicKey(keyPair.publicKey);
|
||||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
// setPrivateKey(keyPair.privateKey);
|
||||||
placeholder="Search service accounts..."
|
|
||||||
/>
|
// const serviceAccountDetails = await createServiceAccount.mutateAsync({
|
||||||
<TableContainer className="mt-4">
|
// name,
|
||||||
<Table>
|
// organizationId: currentOrg?._id,
|
||||||
<THead>
|
// publicKey: keyPair.publicKey,
|
||||||
<Th>Name</Th>
|
// expiresIn: Number(expiresIn)
|
||||||
<Th className="w-full">Valid Until</Th>
|
// });
|
||||||
<Th aria-label="actions" />
|
|
||||||
</THead>
|
// setAccessKey(serviceAccountDetails.serviceAccountAccessKey);
|
||||||
<TBody>
|
|
||||||
{isServiceAccountsLoading && <TableSkeleton columns={5} key="org-service-accounts" />}
|
// setStep(1);
|
||||||
{!isServiceAccountsLoading && (
|
// reset();
|
||||||
filteredServiceAccounts.map(({
|
// }
|
||||||
name,
|
|
||||||
expiresAt,
|
const onRemoveServiceAccount = async () => {
|
||||||
_id: serviceAccountId
|
const serviceAccountId = (popUp?.removeServiceAccount?.data as { _id: string })?._id;
|
||||||
}) => {
|
await removeServiceAccount.mutateAsync(serviceAccountId);
|
||||||
return (
|
handlePopUpClose("removeServiceAccount");
|
||||||
<Tr key={`org-service-account-${serviceAccountId}`}>
|
};
|
||||||
<Td>{name}</Td>
|
|
||||||
<Td>{new Date(expiresAt).toUTCString()}</Td>
|
const filteredServiceAccounts = useMemo(
|
||||||
<Td>
|
() =>
|
||||||
<div className="flex">
|
serviceAccounts.filter(({ name }) => name.toLowerCase().includes(searchServiceAccountFilter)),
|
||||||
<IconButton
|
[serviceAccounts, searchServiceAccountFilter]
|
||||||
ariaLabel="edit"
|
);
|
||||||
colorSchema="secondary"
|
|
||||||
onClick={() => {
|
const renderStep = (stepToRender: number) => {
|
||||||
if (currentWorkspace?._id) {
|
switch (stepToRender) {
|
||||||
router.push(`/settings/org/${currentWorkspace._id}/service-accounts/${serviceAccountId}`);
|
case 0:
|
||||||
}
|
return (
|
||||||
}}
|
<div>
|
||||||
className="mr-2"
|
We are currently revising the service account mechanism. In the meantime, please use
|
||||||
>
|
service tokens or API key to fetch secrets via API request.
|
||||||
<FontAwesomeIcon icon={faPencil} />
|
</div>
|
||||||
</IconButton>
|
// <form onSubmit={handleSubmit(onAddServiceAccount)}>
|
||||||
<IconButton
|
// <Controller
|
||||||
ariaLabel="delete"
|
// control={control}
|
||||||
colorSchema="danger"
|
// defaultValue=""
|
||||||
onClick={() => handlePopUpOpen("removeServiceAccount", { _id: serviceAccountId })}
|
// name="name"
|
||||||
>
|
// render={({ field, fieldState: { error } }) => (
|
||||||
<FontAwesomeIcon icon={faTrash} />
|
// <FormControl label="Name" isError={Boolean(error)} errorText={error?.message}>
|
||||||
</IconButton>
|
// <Input {...field} />
|
||||||
</div>
|
// </FormControl>
|
||||||
</Td>
|
// )}
|
||||||
</Tr>
|
// />
|
||||||
);
|
// <Controller
|
||||||
})
|
// control={control}
|
||||||
)}
|
// name="expiresIn"
|
||||||
</TBody>
|
// defaultValue={String(serviceAccountExpiration?.[0]?.value)}
|
||||||
</Table>
|
// render={({ field: { onChange, ...field }, fieldState: { error } }) => {
|
||||||
{!isServiceAccountsLoading && filteredServiceAccounts?.length === 0 && (
|
// return (
|
||||||
<EmptyState title="No service accounts found" icon={faServer} />
|
// <FormControl
|
||||||
)}
|
// label="Expiration"
|
||||||
</TableContainer>
|
// errorText={error?.message}
|
||||||
<Modal
|
// isError={Boolean(error)}
|
||||||
isOpen={popUp?.addServiceAccount?.isOpen}
|
// >
|
||||||
onOpenChange={(isOpen) => {
|
// <Select
|
||||||
handlePopUpToggle("addServiceAccount", isOpen);
|
// defaultValue={field.value}
|
||||||
// reset();
|
// {...field}
|
||||||
|
// onValueChange={(e) => onChange(e)}
|
||||||
|
// className="w-full"
|
||||||
|
// >
|
||||||
|
// {serviceAccountExpiration.map(({ label, value }) => (
|
||||||
|
// <SelectItem value={String(value)} key={label}>
|
||||||
|
// {label}
|
||||||
|
// </SelectItem>
|
||||||
|
// ))}
|
||||||
|
// </Select>
|
||||||
|
// </FormControl>
|
||||||
|
// );
|
||||||
|
// }}
|
||||||
|
// />
|
||||||
|
// <div className="mt-8 flex items-center">
|
||||||
|
// <Button
|
||||||
|
// className="mr-4"
|
||||||
|
// size="sm"
|
||||||
|
// type="submit"
|
||||||
|
// isLoading={isSubmitting}
|
||||||
|
// isDisabled={isSubmitting}
|
||||||
|
// >
|
||||||
|
// Create Service Account
|
||||||
|
// </Button>
|
||||||
|
// <Button
|
||||||
|
// colorSchema="secondary"
|
||||||
|
// variant="plain"
|
||||||
|
// onClick={() => handlePopUpClose("addServiceAccount")}
|
||||||
|
// >
|
||||||
|
// Cancel
|
||||||
|
// </Button>
|
||||||
|
// </div>
|
||||||
|
// </form>
|
||||||
|
);
|
||||||
|
case 1:
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p>Access Key</p>
|
||||||
|
<div className="flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||||
|
<p className="mr-4 break-all">{accessKey}</p>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="copy icon"
|
||||||
|
colorSchema="secondary"
|
||||||
|
className="group relative"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(accessKey);
|
||||||
|
setIsAccessKeyCopied.on();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ModalContent
|
<FontAwesomeIcon icon={isAccessKeyCopied ? faCheck : faCopy} />
|
||||||
title="Add Service Account"
|
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||||
subTitle="A service account represents a machine identity such as a VM or application client."
|
Copy
|
||||||
>
|
</span>
|
||||||
{renderStep(step)}
|
</IconButton>
|
||||||
</ModalContent>
|
</div>
|
||||||
</Modal>
|
<p className="mt-4">Public Key</p>
|
||||||
<DeleteActionModal
|
<div className="flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||||
isOpen={popUp.removeServiceAccount.isOpen}
|
<p className="mr-4 break-all">{publicKey}</p>
|
||||||
deleteKey="remove"
|
<IconButton
|
||||||
title="Do you want to remove this service account from the org?"
|
ariaLabel="copy icon"
|
||||||
onChange={(isOpen) => handlePopUpToggle("removeServiceAccount", isOpen)}
|
colorSchema="secondary"
|
||||||
onDeleteApproved={onRemoveServiceAccount}
|
className="group relative"
|
||||||
/>
|
onClick={() => {
|
||||||
</div>
|
navigator.clipboard.writeText(publicKey);
|
||||||
);
|
setIsPublicKeyCopied.on();
|
||||||
}
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={isPublicKeyCopied ? faCheck : faCopy} />
|
||||||
|
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||||
|
Copy
|
||||||
|
</span>
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
<p className="mt-4">Private Key</p>
|
||||||
|
<div className="flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||||
|
<p className="mr-4 break-all">{privateKey}</p>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="copy icon"
|
||||||
|
colorSchema="secondary"
|
||||||
|
className="group relative"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(privateKey);
|
||||||
|
setIsPrivateKeyCopied.on();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={isPrivateKeyCopied ? faCheck : faCopy} />
|
||||||
|
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||||
|
Copy
|
||||||
|
</span>
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return <div />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="mb-4 flex justify-between">
|
||||||
|
<p className="text-xl font-semibold text-mineshaft-100">Service Accounts</p>
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
onClick={() => {
|
||||||
|
setStep(0);
|
||||||
|
// reset();
|
||||||
|
handlePopUpOpen("addServiceAccount");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add Service Account
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
value={searchServiceAccountFilter}
|
||||||
|
onChange={(e) => setSearchServiceAccountFilter(e.target.value)}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
|
placeholder="Search service accounts..."
|
||||||
|
/>
|
||||||
|
<TableContainer className="mt-4">
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Th>Name</Th>
|
||||||
|
<Th className="w-full">Valid Until</Th>
|
||||||
|
<Th aria-label="actions" />
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isServiceAccountsLoading && (
|
||||||
|
<TableSkeleton columns={5} innerKey="org-service-accounts" />
|
||||||
|
)}
|
||||||
|
{!isServiceAccountsLoading &&
|
||||||
|
filteredServiceAccounts.map(({ name, expiresAt, _id: serviceAccountId }) => {
|
||||||
|
return (
|
||||||
|
<Tr key={`org-service-account-${serviceAccountId}`}>
|
||||||
|
<Td>{name}</Td>
|
||||||
|
<Td>{new Date(expiresAt).toUTCString()}</Td>
|
||||||
|
<Td>
|
||||||
|
<div className="flex">
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="edit"
|
||||||
|
colorSchema="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
if (currentWorkspace?._id) {
|
||||||
|
router.push(
|
||||||
|
`/settings/org/${currentWorkspace._id}/service-accounts/${serviceAccountId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="mr-2"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faPencil} />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="delete"
|
||||||
|
colorSchema="danger"
|
||||||
|
onClick={() =>
|
||||||
|
handlePopUpOpen("removeServiceAccount", { _id: serviceAccountId })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
{!isServiceAccountsLoading && filteredServiceAccounts?.length === 0 && (
|
||||||
|
<EmptyState title="No service accounts found" icon={faServer} />
|
||||||
|
)}
|
||||||
|
</TableContainer>
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp?.addServiceAccount?.isOpen}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
handlePopUpToggle("addServiceAccount", isOpen);
|
||||||
|
// reset();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
title="Add Service Account"
|
||||||
|
subTitle="A service account represents a machine identity such as a VM or application client."
|
||||||
|
>
|
||||||
|
{renderStep(step)}
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.removeServiceAccount.isOpen}
|
||||||
|
deleteKey="remove"
|
||||||
|
title="Do you want to remove this service account from the org?"
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("removeServiceAccount", isOpen)}
|
||||||
|
onDeleteApproved={onRemoveServiceAccount}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { faKey,faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faKey, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
@@ -14,99 +14,91 @@ import {
|
|||||||
THead,
|
THead,
|
||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import {
|
import { useDeleteAPIKey, useGetMyAPIKeys } from "@app/hooks/api";
|
||||||
useDeleteAPIKey,
|
|
||||||
useGetMyAPIKeys} from "@app/hooks/api";
|
|
||||||
|
|
||||||
export const APIKeyTable = () => {
|
export const APIKeyTable = () => {
|
||||||
const { createNotification } = useNotificationContext();
|
const { createNotification } = useNotificationContext();
|
||||||
const { data, isLoading } = useGetMyAPIKeys();
|
const { data, isLoading } = useGetMyAPIKeys();
|
||||||
const { mutateAsync } = useDeleteAPIKey();
|
const { mutateAsync } = useDeleteAPIKey();
|
||||||
|
|
||||||
const handleDeleteAPIKeyDataClick = async (apiKeyDataId: string) => {
|
const handleDeleteAPIKeyDataClick = async (apiKeyDataId: string) => {
|
||||||
try {
|
try {
|
||||||
await mutateAsync(apiKeyDataId);
|
await mutateAsync(apiKeyDataId);
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Successfully deleted API key",
|
text: "Successfully deleted API key",
|
||||||
type: "success"
|
type: "success"
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Failed to delete API key",
|
text: "Failed to delete API key",
|
||||||
type: "error"
|
type: "error"
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
const formatDate = (dateToFormat: string) => {
|
|
||||||
const date = new Date(dateToFormat);
|
const formatDate = (dateToFormat: string) => {
|
||||||
const year = date.getFullYear();
|
const date = new Date(dateToFormat);
|
||||||
const month = date.getMonth() + 1;
|
const year = date.getFullYear();
|
||||||
const day = date.getDate();
|
const month = date.getMonth() + 1;
|
||||||
|
const day = date.getDate();
|
||||||
const formattedDate = `${day}/${month}/${year}`;
|
|
||||||
|
const formattedDate = `${day}/${month}/${year}`;
|
||||||
return formattedDate;
|
|
||||||
}
|
return formattedDate;
|
||||||
|
};
|
||||||
return (
|
|
||||||
<div>
|
return (
|
||||||
<TableContainer className="">
|
<div>
|
||||||
<Table>
|
<TableContainer className="">
|
||||||
<THead>
|
<Table>
|
||||||
<Tr>
|
<THead>
|
||||||
<Th className="flex-1">Name</Th>
|
<Tr>
|
||||||
<Th className="flex-1">Last active</Th>
|
<Th className="flex-1">Name</Th>
|
||||||
<Th className="flex-1">Created</Th>
|
<Th className="flex-1">Last active</Th>
|
||||||
<Th className="flex-1">Expiration</Th>
|
<Th className="flex-1">Created</Th>
|
||||||
<Th className="w-5" />
|
<Th className="flex-1">Expiration</Th>
|
||||||
</Tr>
|
<Th className="w-5" />
|
||||||
</THead>
|
</Tr>
|
||||||
<TBody>
|
</THead>
|
||||||
{isLoading && <TableSkeleton columns={4} key="api-keys" />}
|
<TBody>
|
||||||
{!isLoading && data && data.length > 0 && data.map(({
|
{isLoading && <TableSkeleton columns={4} innerKey="api-keys" />}
|
||||||
_id,
|
{!isLoading &&
|
||||||
name,
|
data &&
|
||||||
createdAt,
|
data.length > 0 &&
|
||||||
expiresAt,
|
data.map(({ _id, name, createdAt, expiresAt, lastUsed }) => {
|
||||||
lastUsed
|
return (
|
||||||
}) => {
|
<Tr className="h-10" key={`api-key-${_id}`}>
|
||||||
return (
|
<Td>{name}</Td>
|
||||||
<Tr className="h-10" key={`api-key-${_id}`}>
|
<Td>{formatDate(lastUsed)}</Td>
|
||||||
<Td>{name}</Td>
|
<Td>{formatDate(createdAt)}</Td>
|
||||||
<Td>{formatDate(lastUsed)}</Td>
|
<Td>{formatDate(expiresAt)}</Td>
|
||||||
<Td>{formatDate(createdAt)}</Td>
|
<Td>
|
||||||
<Td>{formatDate(expiresAt)}</Td>
|
<IconButton
|
||||||
<Td>
|
onClick={async () => {
|
||||||
<IconButton
|
await handleDeleteAPIKeyDataClick(_id);
|
||||||
onClick={async () => {
|
}}
|
||||||
await handleDeleteAPIKeyDataClick(_id);
|
size="lg"
|
||||||
}}
|
colorSchema="danger"
|
||||||
size="lg"
|
variant="plain"
|
||||||
colorSchema="danger"
|
ariaLabel="update"
|
||||||
variant="plain"
|
>
|
||||||
ariaLabel="update"
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
>
|
</IconButton>
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
</Td>
|
||||||
</IconButton>
|
</Tr>
|
||||||
</Td>
|
);
|
||||||
</Tr>
|
})}
|
||||||
);
|
{!isLoading && data && data?.length === 0 && (
|
||||||
})}
|
<Tr>
|
||||||
{!isLoading && data && data?.length === 0 && (
|
<Td colSpan={5}>
|
||||||
<Tr>
|
<EmptyState title="No API Keys on file" icon={faKey} />
|
||||||
<Td colSpan={5}>
|
</Td>
|
||||||
<EmptyState
|
</Tr>
|
||||||
title="No API Keys on file"
|
)}
|
||||||
icon={faKey}
|
</TBody>
|
||||||
/>
|
</Table>
|
||||||
</Td>
|
</TableContainer>
|
||||||
</Tr>
|
</div>
|
||||||
)}
|
);
|
||||||
</TBody>
|
};
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
+50
-57
@@ -14,61 +14,54 @@ import {
|
|||||||
import { useGetMySessions } from "@app/hooks/api";
|
import { useGetMySessions } from "@app/hooks/api";
|
||||||
|
|
||||||
export const SessionsTable = () => {
|
export const SessionsTable = () => {
|
||||||
const { data, isLoading } = useGetMySessions();
|
const { data, isLoading } = useGetMySessions();
|
||||||
|
|
||||||
const formatDate = (dateToFormat: string) => {
|
const formatDate = (dateToFormat: string) => {
|
||||||
const date = new Date(dateToFormat);
|
const date = new Date(dateToFormat);
|
||||||
const year = date.getFullYear();
|
const year = date.getFullYear();
|
||||||
const month = date.getMonth() + 1;
|
const month = date.getMonth() + 1;
|
||||||
const day = date.getDate();
|
const day = date.getDate();
|
||||||
|
|
||||||
const formattedDate = `${day}/${month}/${year}`;
|
const formattedDate = `${day}/${month}/${year}`;
|
||||||
|
|
||||||
return formattedDate;
|
return formattedDate;
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableContainer className="mt-4">
|
<TableContainer className="mt-4">
|
||||||
<Table>
|
<Table>
|
||||||
<THead>
|
<THead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th>Created</Th>
|
<Th>Created</Th>
|
||||||
<Th>Last active</Th>
|
<Th>Last active</Th>
|
||||||
<Th>IP address</Th>
|
<Th>IP address</Th>
|
||||||
<Th>Device</Th>
|
<Th>Device</Th>
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
{isLoading && <TableSkeleton columns={4} key="sesssions" />}
|
{isLoading && <TableSkeleton columns={4} innerKey="sesssions" />}
|
||||||
{!isLoading && data && data.length > 0 && data.map(({
|
{!isLoading &&
|
||||||
_id,
|
data &&
|
||||||
createdAt,
|
data.length > 0 &&
|
||||||
lastUsed,
|
data.map(({ _id, createdAt, lastUsed, ip, userAgent }) => {
|
||||||
ip,
|
return (
|
||||||
userAgent
|
<Tr className="h-10" key={`session-${_id}`}>
|
||||||
}) => {
|
<Td>{formatDate(createdAt)}</Td>
|
||||||
return (
|
<Td>{formatDate(lastUsed)}</Td>
|
||||||
<Tr className="h-10" key={`session-${_id}`}>
|
<Td>{ip}</Td>
|
||||||
<Td>{formatDate(createdAt)}</Td>
|
<Td>{userAgent}</Td>
|
||||||
<Td>{formatDate(lastUsed)}</Td>
|
</Tr>
|
||||||
<Td>{ip}</Td>
|
);
|
||||||
<Td>{userAgent}</Td>
|
})}
|
||||||
</Tr>
|
{!isLoading && data && data?.length === 0 && (
|
||||||
);
|
<Tr>
|
||||||
})}
|
<Td colSpan={4}>
|
||||||
{!isLoading && data && data?.length === 0 && (
|
<EmptyState title="No sessions on file" icon={faServer} />
|
||||||
<Tr>
|
</Td>
|
||||||
<Td colSpan={4}>
|
</Tr>
|
||||||
<EmptyState
|
)}
|
||||||
title="No sessions on file"
|
</TBody>
|
||||||
icon={faServer}
|
</Table>
|
||||||
/>
|
</TableContainer>
|
||||||
</Td>
|
);
|
||||||
</Tr>
|
};
|
||||||
)}
|
|
||||||
</TBody>
|
|
||||||
</Table>
|
|
||||||
|
|
||||||
</TableContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
+69
-69
@@ -11,79 +11,79 @@ import {
|
|||||||
Td,
|
Td,
|
||||||
Th,
|
Th,
|
||||||
THead,
|
THead,
|
||||||
Tr,
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useWorkspace } from "@app/context";
|
import { useWorkspace } from "@app/context";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
handlePopUpOpen: (
|
handlePopUpOpen: (
|
||||||
popUpName: keyof UsePopUpState<["updateEnv", "deleteEnv", "upgradePlan"]>,
|
popUpName: keyof UsePopUpState<["updateEnv", "deleteEnv", "upgradePlan"]>,
|
||||||
{
|
{
|
||||||
name,
|
name,
|
||||||
slug
|
slug
|
||||||
}: {
|
}: {
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
}
|
}
|
||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EnvironmentTable = ({
|
export const EnvironmentTable = ({ handlePopUpOpen }: Props) => {
|
||||||
handlePopUpOpen
|
const { currentWorkspace, isLoading } = useWorkspace();
|
||||||
}: Props) => {
|
return (
|
||||||
const { currentWorkspace, isLoading } = useWorkspace();
|
<TableContainer>
|
||||||
return (
|
<Table>
|
||||||
<TableContainer>
|
<THead>
|
||||||
<Table>
|
<Tr>
|
||||||
<THead>
|
<Th>Name</Th>
|
||||||
<Tr>
|
<Th>Slug</Th>
|
||||||
<Th>Name</Th>
|
<Th aria-label="button" />
|
||||||
<Th>Slug</Th>
|
</Tr>
|
||||||
<Th aria-label="button" />
|
</THead>
|
||||||
</Tr>
|
<TBody>
|
||||||
</THead>
|
{isLoading && <TableSkeleton columns={3} innerKey="project-envs" />}
|
||||||
<TBody>
|
{!isLoading &&
|
||||||
{isLoading && <TableSkeleton columns={3} key="project-envs" />}
|
currentWorkspace &&
|
||||||
{!isLoading && currentWorkspace && currentWorkspace.environments.map(({ name, slug }) => (
|
currentWorkspace.environments.map(({ name, slug }) => (
|
||||||
<Tr key={name}>
|
<Tr key={name}>
|
||||||
<Td>{name}</Td>
|
<Td>{name}</Td>
|
||||||
<Td>{slug}</Td>
|
<Td>{slug}</Td>
|
||||||
<Td className="flex items-center justify-end">
|
<Td className="flex items-center justify-end">
|
||||||
<IconButton
|
<IconButton
|
||||||
className="mr-3 py-2"
|
className="mr-3 py-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handlePopUpOpen("updateEnv", { name, slug });
|
handlePopUpOpen("updateEnv", { name, slug });
|
||||||
}}
|
}}
|
||||||
colorSchema="primary"
|
colorSchema="primary"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
ariaLabel="update"
|
ariaLabel="update"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faPencil} />
|
<FontAwesomeIcon icon={faPencil} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handlePopUpOpen("deleteEnv", { name, slug });
|
handlePopUpOpen("deleteEnv", { name, slug });
|
||||||
}}
|
}}
|
||||||
size="lg"
|
size="lg"
|
||||||
colorSchema="danger"
|
colorSchema="danger"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
ariaLabel="update"
|
ariaLabel="update"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
))}
|
))}
|
||||||
{!isLoading && currentWorkspace && currentWorkspace.environments?.length === 0 && (
|
{!isLoading && currentWorkspace && currentWorkspace.environments?.length === 0 && (
|
||||||
<Tr>
|
<Tr>
|
||||||
<Td colSpan={3}>
|
<Td colSpan={3}>
|
||||||
<EmptyState title="No environments found" />
|
<EmptyState title="No environments found" />
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
)}
|
)}
|
||||||
</TBody>
|
</TBody>
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
+57
-57
@@ -18,65 +18,65 @@ import { useGetWsTags } from "@app/hooks/api";
|
|||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
handlePopUpOpen: (
|
handlePopUpOpen: (
|
||||||
popUpName: keyof UsePopUpState<["deleteTagConfirmation"]>,
|
popUpName: keyof UsePopUpState<["deleteTagConfirmation"]>,
|
||||||
{
|
{
|
||||||
name,
|
name,
|
||||||
id
|
id
|
||||||
}: {
|
}: {
|
||||||
name: string;
|
name: string;
|
||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SecretTagsTable = ({
|
export const SecretTagsTable = ({ handlePopUpOpen }: Props) => {
|
||||||
handlePopUpOpen
|
const { currentWorkspace } = useWorkspace();
|
||||||
}: Props) => {
|
const { data, isLoading } = useGetWsTags(currentWorkspace?._id ?? "");
|
||||||
const { currentWorkspace }= useWorkspace();
|
|
||||||
const { data, isLoading } = useGetWsTags(currentWorkspace?._id ?? "");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableContainer className="mt-4">
|
<TableContainer className="mt-4">
|
||||||
<Table>
|
<Table>
|
||||||
<THead>
|
<THead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th>Tag</Th>
|
<Th>Tag</Th>
|
||||||
<Th>Slug</Th>
|
<Th>Slug</Th>
|
||||||
<Th aria-label="button" />
|
<Th aria-label="button" />
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
{isLoading && <TableSkeleton columns={3} key="secret-tags" />}
|
{isLoading && <TableSkeleton columns={3} innerKey="secret-tags" />}
|
||||||
{!isLoading && data && data.map(({ _id, name, slug }) => (
|
{!isLoading &&
|
||||||
<Tr key={name}>
|
data &&
|
||||||
<Td>{name}</Td>
|
data.map(({ _id, name, slug }) => (
|
||||||
<Td>{slug}</Td>
|
<Tr key={name}>
|
||||||
<Td className="flex items-center justify-end">
|
<Td>{name}</Td>
|
||||||
<IconButton
|
<Td>{slug}</Td>
|
||||||
onClick={() =>
|
<Td className="flex items-center justify-end">
|
||||||
handlePopUpOpen("deleteTagConfirmation", {
|
<IconButton
|
||||||
name,
|
onClick={() =>
|
||||||
id: _id
|
handlePopUpOpen("deleteTagConfirmation", {
|
||||||
})
|
name,
|
||||||
}
|
id: _id
|
||||||
colorSchema="danger"
|
})
|
||||||
ariaLabel="update"
|
}
|
||||||
>
|
colorSchema="danger"
|
||||||
<FontAwesomeIcon icon={faTrashCan} />
|
ariaLabel="update"
|
||||||
</IconButton>
|
>
|
||||||
</Td>
|
<FontAwesomeIcon icon={faTrashCan} />
|
||||||
</Tr>
|
</IconButton>
|
||||||
))}
|
|
||||||
{!isLoading && data && data?.length === 0 && (
|
|
||||||
<Tr>
|
|
||||||
<Td colSpan={3}>
|
|
||||||
<EmptyState title="No secret tags found" icon={faTags} />
|
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
)}
|
))}
|
||||||
</TBody>
|
{!isLoading && data && data?.length === 0 && (
|
||||||
</Table>
|
<Tr>
|
||||||
</TableContainer>
|
<Td colSpan={3}>
|
||||||
);
|
<EmptyState title="No secret tags found" icon={faTags} />
|
||||||
}
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
+1
-1
@@ -48,7 +48,7 @@ export const ServiceTokenTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
{isLoading && <TableSkeleton columns={4} key="project-service-tokens" />}
|
{isLoading && <TableSkeleton columns={4} innerKey="project-service-tokens" />}
|
||||||
{!isLoading &&
|
{!isLoading &&
|
||||||
data &&
|
data &&
|
||||||
data.map((row) => (
|
data.map((row) => (
|
||||||
|
|||||||
+1
-1
@@ -160,7 +160,7 @@ export const WebhooksTab = () => {
|
|||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
{isWebhooksLoading && <TableSkeleton columns={5} key="webhooks-loading" />}
|
{isWebhooksLoading && <TableSkeleton columns={5} innerKey="webhooks-loading" />}
|
||||||
{!isWebhooksLoading && webhooks && webhooks?.length === 0 && (
|
{!isWebhooksLoading && webhooks && webhooks?.length === 0 && (
|
||||||
<Tr>
|
<Tr>
|
||||||
<Td colSpan={5}>
|
<Td colSpan={5}>
|
||||||
|
|||||||
Reference in New Issue
Block a user