mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 19:28:16 +00:00
Merge pull request #692 from akhilmhdh/feat/multi-line-secrets
multi line support for secrets
This commit is contained in:
@@ -54,23 +54,20 @@ export const getSecretVersions = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
const { secretId, workspaceId, environment, folderId } = req.params;
|
const { secretId } = req.params;
|
||||||
|
|
||||||
const offset: number = parseInt(req.query.offset as string);
|
const offset: number = parseInt(req.query.offset as string);
|
||||||
const limit: number = parseInt(req.query.limit as string);
|
const limit: number = parseInt(req.query.limit as string);
|
||||||
|
|
||||||
const secretVersions = await SecretVersion.find({
|
const secretVersions = await SecretVersion.find({
|
||||||
secret: secretId,
|
secret: secretId
|
||||||
workspace: workspaceId,
|
|
||||||
environment,
|
|
||||||
folder: folderId,
|
|
||||||
})
|
})
|
||||||
.sort({ createdAt: -1 })
|
.sort({ createdAt: -1 })
|
||||||
.skip(offset)
|
.skip(offset)
|
||||||
.limit(limit);
|
.limit(limit);
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
secretVersions,
|
secretVersions
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -135,7 +132,7 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
|
|||||||
// validate secret version
|
// validate secret version
|
||||||
const oldSecretVersion = await SecretVersion.findOne({
|
const oldSecretVersion = await SecretVersion.findOne({
|
||||||
secret: secretId,
|
secret: secretId,
|
||||||
version,
|
version
|
||||||
}).select("+secretBlindIndex");
|
}).select("+secretBlindIndex");
|
||||||
|
|
||||||
if (!oldSecretVersion) throw new Error("Failed to find secret version");
|
if (!oldSecretVersion) throw new Error("Failed to find secret version");
|
||||||
@@ -154,7 +151,7 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
|
|||||||
secretValueTag,
|
secretValueTag,
|
||||||
algorithm,
|
algorithm,
|
||||||
folder,
|
folder,
|
||||||
keyEncoding,
|
keyEncoding
|
||||||
} = oldSecretVersion;
|
} = oldSecretVersion;
|
||||||
|
|
||||||
// update secret
|
// update secret
|
||||||
@@ -162,7 +159,7 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
|
|||||||
secretId,
|
secretId,
|
||||||
{
|
{
|
||||||
$inc: {
|
$inc: {
|
||||||
version: 1,
|
version: 1
|
||||||
},
|
},
|
||||||
workspace,
|
workspace,
|
||||||
type,
|
type,
|
||||||
@@ -177,10 +174,10 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
|
|||||||
secretValueTag,
|
secretValueTag,
|
||||||
folderId: folder,
|
folderId: folder,
|
||||||
algorithm,
|
algorithm,
|
||||||
keyEncoding,
|
keyEncoding
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
new: true,
|
new: true
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -204,17 +201,17 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
|
|||||||
secretValueTag,
|
secretValueTag,
|
||||||
folder,
|
folder,
|
||||||
algorithm,
|
algorithm,
|
||||||
keyEncoding,
|
keyEncoding
|
||||||
}).save();
|
}).save();
|
||||||
|
|
||||||
// take secret snapshot
|
// take secret snapshot
|
||||||
await EESecretService.takeSecretSnapshot({
|
await EESecretService.takeSecretSnapshot({
|
||||||
workspaceId: secret.workspace,
|
workspaceId: secret.workspace,
|
||||||
environment,
|
environment,
|
||||||
folderId: folder,
|
folderId: folder
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
secret,
|
secret
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -81,76 +81,79 @@ export const useGetProjectSecrets = ({
|
|||||||
enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused,
|
enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused,
|
||||||
queryKey: secretKeys.getProjectSecret(workspaceId, env, folderId),
|
queryKey: secretKeys.getProjectSecret(workspaceId, env, folderId),
|
||||||
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId),
|
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId),
|
||||||
select: useCallback((data: EncryptedSecret[]) => {
|
select: useCallback(
|
||||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
(data: EncryptedSecret[]) => {
|
||||||
const latestKey = decryptFileKey;
|
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
||||||
const key = decryptAssymmetric({
|
const latestKey = decryptFileKey;
|
||||||
ciphertext: latestKey.encryptedKey,
|
const key = decryptAssymmetric({
|
||||||
nonce: latestKey.nonce,
|
ciphertext: latestKey.encryptedKey,
|
||||||
publicKey: latestKey.sender.publicKey,
|
nonce: latestKey.nonce,
|
||||||
privateKey: PRIVATE_KEY
|
publicKey: latestKey.sender.publicKey,
|
||||||
});
|
privateKey: PRIVATE_KEY
|
||||||
|
|
||||||
const sharedSecrets: DecryptedSecret[] = [];
|
|
||||||
const personalSecrets: Record<string, { id: string; value: string }> = {};
|
|
||||||
// this used for add-only mode in dashboard
|
|
||||||
// type won't be there thus only one key is shown
|
|
||||||
const duplicateSecretKey: Record<string, boolean> = {};
|
|
||||||
data.forEach((encSecret: EncryptedSecret) => {
|
|
||||||
const secretKey = decryptSymmetric({
|
|
||||||
ciphertext: encSecret.secretKeyCiphertext,
|
|
||||||
iv: encSecret.secretKeyIV,
|
|
||||||
tag: encSecret.secretKeyTag,
|
|
||||||
key
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretValue = decryptSymmetric({
|
const sharedSecrets: DecryptedSecret[] = [];
|
||||||
ciphertext: encSecret.secretValueCiphertext,
|
const personalSecrets: Record<string, { id: string; value: string }> = {};
|
||||||
iv: encSecret.secretValueIV,
|
// this used for add-only mode in dashboard
|
||||||
tag: encSecret.secretValueTag,
|
// type won't be there thus only one key is shown
|
||||||
key
|
const duplicateSecretKey: Record<string, boolean> = {};
|
||||||
});
|
data.forEach((encSecret: EncryptedSecret) => {
|
||||||
|
const secretKey = decryptSymmetric({
|
||||||
|
ciphertext: encSecret.secretKeyCiphertext,
|
||||||
|
iv: encSecret.secretKeyIV,
|
||||||
|
tag: encSecret.secretKeyTag,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
|
||||||
const secretComment = decryptSymmetric({
|
const secretValue = decryptSymmetric({
|
||||||
ciphertext: encSecret.secretCommentCiphertext,
|
ciphertext: encSecret.secretValueCiphertext,
|
||||||
iv: encSecret.secretCommentIV,
|
iv: encSecret.secretValueIV,
|
||||||
tag: encSecret.secretCommentTag,
|
tag: encSecret.secretValueTag,
|
||||||
key
|
key
|
||||||
});
|
});
|
||||||
|
|
||||||
const decryptedSecret = {
|
const secretComment = decryptSymmetric({
|
||||||
_id: encSecret._id,
|
ciphertext: encSecret.secretCommentCiphertext,
|
||||||
env: encSecret.environment,
|
iv: encSecret.secretCommentIV,
|
||||||
key: secretKey,
|
tag: encSecret.secretCommentTag,
|
||||||
value: secretValue,
|
key
|
||||||
tags: encSecret.tags,
|
});
|
||||||
comment: secretComment,
|
|
||||||
createdAt: encSecret.createdAt,
|
|
||||||
updatedAt: encSecret.updatedAt
|
|
||||||
};
|
|
||||||
|
|
||||||
if (encSecret.type === "personal") {
|
const decryptedSecret = {
|
||||||
personalSecrets[`${decryptedSecret.key}-${decryptedSecret.env}`] = {
|
_id: encSecret._id,
|
||||||
id: encSecret._id,
|
env: encSecret.environment,
|
||||||
value: secretValue
|
key: secretKey,
|
||||||
|
value: secretValue,
|
||||||
|
tags: encSecret.tags,
|
||||||
|
comment: secretComment,
|
||||||
|
createdAt: encSecret.createdAt,
|
||||||
|
updatedAt: encSecret.updatedAt
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
if (!duplicateSecretKey?.[`${decryptedSecret.key}-${decryptedSecret.env}`]) {
|
if (encSecret.type === "personal") {
|
||||||
sharedSecrets.push(decryptedSecret);
|
personalSecrets[`${decryptedSecret.key}-${decryptedSecret.env}`] = {
|
||||||
|
id: encSecret._id,
|
||||||
|
value: secretValue
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
if (!duplicateSecretKey?.[`${decryptedSecret.key}-${decryptedSecret.env}`]) {
|
||||||
|
sharedSecrets.push(decryptedSecret);
|
||||||
|
}
|
||||||
|
duplicateSecretKey[`${decryptedSecret.key}-${decryptedSecret.env}`] = true;
|
||||||
}
|
}
|
||||||
duplicateSecretKey[`${decryptedSecret.key}-${decryptedSecret.env}`] = true;
|
});
|
||||||
}
|
sharedSecrets.forEach((val) => {
|
||||||
});
|
const dupKey = `${val.key}-${val.env}`;
|
||||||
sharedSecrets.forEach((val) => {
|
if (personalSecrets?.[dupKey]) {
|
||||||
const dupKey = `${val.key}-${val.env}`;
|
val.idOverride = personalSecrets[dupKey].id;
|
||||||
if (personalSecrets?.[dupKey]) {
|
val.valueOverride = personalSecrets[dupKey].value;
|
||||||
val.idOverride = personalSecrets[dupKey].id;
|
val.overrideAction = "modified";
|
||||||
val.valueOverride = personalSecrets[dupKey].value;
|
}
|
||||||
val.overrideAction = "modified";
|
});
|
||||||
}
|
return { secrets: sharedSecrets };
|
||||||
});
|
},
|
||||||
return { secrets: sharedSecrets };
|
[decryptFileKey]
|
||||||
}, [decryptFileKey])
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useGetProjectSecretsByKey = ({
|
export const useGetProjectSecretsByKey = ({
|
||||||
@@ -167,82 +170,85 @@ export const useGetProjectSecretsByKey = ({
|
|||||||
// right now secretpath is passed as folderid as only this is used in overview
|
// right now secretpath is passed as folderid as only this is used in overview
|
||||||
queryKey: secretKeys.getProjectSecret(workspaceId, env, secretPath),
|
queryKey: secretKeys.getProjectSecret(workspaceId, env, secretPath),
|
||||||
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId, secretPath),
|
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId, secretPath),
|
||||||
select: useCallback((data: EncryptedSecret[]) => {
|
select: useCallback(
|
||||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
(data: EncryptedSecret[]) => {
|
||||||
const latestKey = decryptFileKey;
|
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
||||||
const key = decryptAssymmetric({
|
const latestKey = decryptFileKey;
|
||||||
ciphertext: latestKey.encryptedKey,
|
const key = decryptAssymmetric({
|
||||||
nonce: latestKey.nonce,
|
ciphertext: latestKey.encryptedKey,
|
||||||
publicKey: latestKey.sender.publicKey,
|
nonce: latestKey.nonce,
|
||||||
privateKey: PRIVATE_KEY
|
publicKey: latestKey.sender.publicKey,
|
||||||
});
|
privateKey: PRIVATE_KEY
|
||||||
|
|
||||||
const sharedSecrets: Record<string, DecryptedSecret[]> = {};
|
|
||||||
const personalSecrets: Record<string, { id: string; value: string }> = {};
|
|
||||||
// this used for add-only mode in dashboard
|
|
||||||
// type won't be there thus only one key is shown
|
|
||||||
const duplicateSecretKey: Record<string, boolean> = {};
|
|
||||||
const uniqSecKeys: Record<string, boolean> = {};
|
|
||||||
data.forEach((encSecret: EncryptedSecret) => {
|
|
||||||
const secretKey = decryptSymmetric({
|
|
||||||
ciphertext: encSecret.secretKeyCiphertext,
|
|
||||||
iv: encSecret.secretKeyIV,
|
|
||||||
tag: encSecret.secretKeyTag,
|
|
||||||
key
|
|
||||||
});
|
|
||||||
if (!uniqSecKeys?.[secretKey]) uniqSecKeys[secretKey] = true;
|
|
||||||
|
|
||||||
const secretValue = decryptSymmetric({
|
|
||||||
ciphertext: encSecret.secretValueCiphertext,
|
|
||||||
iv: encSecret.secretValueIV,
|
|
||||||
tag: encSecret.secretValueTag,
|
|
||||||
key
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretComment = decryptSymmetric({
|
const sharedSecrets: Record<string, DecryptedSecret[]> = {};
|
||||||
ciphertext: encSecret.secretCommentCiphertext,
|
const personalSecrets: Record<string, { id: string; value: string }> = {};
|
||||||
iv: encSecret.secretCommentIV,
|
// this used for add-only mode in dashboard
|
||||||
tag: encSecret.secretCommentTag,
|
// type won't be there thus only one key is shown
|
||||||
key
|
const duplicateSecretKey: Record<string, boolean> = {};
|
||||||
});
|
const uniqSecKeys: Record<string, boolean> = {};
|
||||||
|
data.forEach((encSecret: EncryptedSecret) => {
|
||||||
|
const secretKey = decryptSymmetric({
|
||||||
|
ciphertext: encSecret.secretKeyCiphertext,
|
||||||
|
iv: encSecret.secretKeyIV,
|
||||||
|
tag: encSecret.secretKeyTag,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
if (!uniqSecKeys?.[secretKey]) uniqSecKeys[secretKey] = true;
|
||||||
|
|
||||||
const decryptedSecret = {
|
const secretValue = decryptSymmetric({
|
||||||
_id: encSecret._id,
|
ciphertext: encSecret.secretValueCiphertext,
|
||||||
env: encSecret.environment,
|
iv: encSecret.secretValueIV,
|
||||||
key: secretKey,
|
tag: encSecret.secretValueTag,
|
||||||
value: secretValue,
|
key
|
||||||
tags: encSecret.tags,
|
});
|
||||||
comment: secretComment,
|
|
||||||
createdAt: encSecret.createdAt,
|
|
||||||
updatedAt: encSecret.updatedAt
|
|
||||||
};
|
|
||||||
|
|
||||||
if (encSecret.type === "personal") {
|
const secretComment = decryptSymmetric({
|
||||||
personalSecrets[`${decryptedSecret.key}-${decryptedSecret.env}`] = {
|
ciphertext: encSecret.secretCommentCiphertext,
|
||||||
id: encSecret._id,
|
iv: encSecret.secretCommentIV,
|
||||||
value: secretValue
|
tag: encSecret.secretCommentTag,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedSecret = {
|
||||||
|
_id: encSecret._id,
|
||||||
|
env: encSecret.environment,
|
||||||
|
key: secretKey,
|
||||||
|
value: secretValue,
|
||||||
|
tags: encSecret.tags,
|
||||||
|
comment: secretComment,
|
||||||
|
createdAt: encSecret.createdAt,
|
||||||
|
updatedAt: encSecret.updatedAt
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
if (!duplicateSecretKey?.[`${decryptedSecret.key}-${decryptedSecret.env}`]) {
|
if (encSecret.type === "personal") {
|
||||||
if (!sharedSecrets?.[secretKey]) sharedSecrets[secretKey] = [];
|
personalSecrets[`${decryptedSecret.key}-${decryptedSecret.env}`] = {
|
||||||
sharedSecrets[secretKey].push(decryptedSecret);
|
id: encSecret._id,
|
||||||
}
|
value: secretValue
|
||||||
duplicateSecretKey[`${decryptedSecret.key}-${decryptedSecret.env}`] = true;
|
};
|
||||||
}
|
} else {
|
||||||
});
|
if (!duplicateSecretKey?.[`${decryptedSecret.key}-${decryptedSecret.env}`]) {
|
||||||
Object.keys(sharedSecrets).forEach((secName) => {
|
if (!sharedSecrets?.[secretKey]) sharedSecrets[secretKey] = [];
|
||||||
sharedSecrets[secName].forEach((val) => {
|
sharedSecrets[secretKey].push(decryptedSecret);
|
||||||
const dupKey = `${val.key}-${val.env}`;
|
}
|
||||||
if (personalSecrets?.[dupKey]) {
|
duplicateSecretKey[`${decryptedSecret.key}-${decryptedSecret.env}`] = true;
|
||||||
val.idOverride = personalSecrets[dupKey].id;
|
|
||||||
val.valueOverride = personalSecrets[dupKey].value;
|
|
||||||
val.overrideAction = "modified";
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
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 };
|
return { secrets: sharedSecrets, uniqueSecCount: Object.keys(uniqSecKeys).length };
|
||||||
}, [decryptFileKey])
|
},
|
||||||
|
[decryptFileKey]
|
||||||
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchEncryptedSecretVersion = async (secretId: string, offset: number, limit: number) => {
|
const fetchEncryptedSecretVersion = async (secretId: string, offset: number, limit: number) => {
|
||||||
@@ -263,29 +269,32 @@ export const useGetSecretVersion = (dto: GetSecretVersionsDTO) =>
|
|||||||
enabled: Boolean(dto.secretId && dto.decryptFileKey),
|
enabled: Boolean(dto.secretId && dto.decryptFileKey),
|
||||||
queryKey: secretKeys.getSecretVersion(dto.secretId),
|
queryKey: secretKeys.getSecretVersion(dto.secretId),
|
||||||
queryFn: () => fetchEncryptedSecretVersion(dto.secretId, dto.offset, dto.limit),
|
queryFn: () => fetchEncryptedSecretVersion(dto.secretId, dto.offset, dto.limit),
|
||||||
select: useCallback((data: EncryptedSecretVersion[]) => {
|
select: useCallback(
|
||||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
(data: EncryptedSecretVersion[]) => {
|
||||||
const latestKey = dto.decryptFileKey;
|
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
||||||
const key = decryptAssymmetric({
|
const latestKey = dto.decryptFileKey;
|
||||||
ciphertext: latestKey.encryptedKey,
|
const key = decryptAssymmetric({
|
||||||
nonce: latestKey.nonce,
|
ciphertext: latestKey.encryptedKey,
|
||||||
publicKey: latestKey.sender.publicKey,
|
nonce: latestKey.nonce,
|
||||||
privateKey: PRIVATE_KEY
|
publicKey: latestKey.sender.publicKey,
|
||||||
});
|
privateKey: PRIVATE_KEY
|
||||||
|
});
|
||||||
|
|
||||||
return data
|
return data
|
||||||
.map((el) => ({
|
.map((el) => ({
|
||||||
createdAt: el.createdAt,
|
createdAt: el.createdAt,
|
||||||
id: el._id,
|
id: el._id,
|
||||||
value: decryptSymmetric({
|
value: decryptSymmetric({
|
||||||
ciphertext: el.secretValueCiphertext,
|
ciphertext: el.secretValueCiphertext,
|
||||||
iv: el.secretValueIV,
|
iv: el.secretValueIV,
|
||||||
tag: el.secretValueTag,
|
tag: el.secretValueTag,
|
||||||
key
|
key
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
||||||
}, [])
|
},
|
||||||
|
[dto.decryptFileKey]
|
||||||
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useBatchSecretsOp = () => {
|
export const useBatchSecretsOp = () => {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export { useLeaveConfirm } from "./useLeaveConfirm";
|
export { useLeaveConfirm } from "./useLeaveConfirm";
|
||||||
export { usePersistentState } from "./usePersistentState";
|
export { usePersistentState } from "./usePersistentState";
|
||||||
export { usePopUp } from "./usePopUp";
|
export { usePopUp } from "./usePopUp";
|
||||||
|
export { useSyntaxHighlight } from "./useSyntaxHighlight";
|
||||||
export { useToggle } from "./useToggle";
|
export { useToggle } from "./useToggle";
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
import { faCircle } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
const REGEX = /([$]{.*?})/g;
|
||||||
|
|
||||||
|
export const useSyntaxHighlight = () => {
|
||||||
|
const syntaxHighlight = useCallback((text: string, isHidden?: boolean) => {
|
||||||
|
if (isHidden) {
|
||||||
|
return text
|
||||||
|
.split("")
|
||||||
|
.slice(0, 200)
|
||||||
|
.map((el, i) =>
|
||||||
|
el === "\n" ? (
|
||||||
|
el
|
||||||
|
) : (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
key={`${text}_${el}_${i + 1}`}
|
||||||
|
className="mr-0.5 text-xxs"
|
||||||
|
icon={faCircle}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// append a space on last new line this to show new line in ui for code component
|
||||||
|
const val = text.at(-1) === "\n" ? text.concat(" ") : text;
|
||||||
|
if (val?.length === 0) return <span className="font-mono text-bunker-400/80">EMPTY</span>;
|
||||||
|
return val?.split(REGEX).map((word, i) =>
|
||||||
|
word.match(REGEX) !== null ? (
|
||||||
|
<span className="ph-no-capture text-yellow" key={`${val}-${i + 1}`}>
|
||||||
|
${
|
||||||
|
<span className="ph-no-capture text-yellow-200/80">{word.slice(2, word.length - 1)}</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span key={`${word}_${i + 1}`} className="ph-no-capture">
|
||||||
|
{word}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return syntaxHighlight;
|
||||||
|
};
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
/* eslint-disable react/jsx-no-useless-fragment */
|
/* eslint-disable react/jsx-no-useless-fragment */
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useRef, useState } from "react";
|
||||||
import { faCircle, faEye, faEyeSlash, faKey, faMinus } from "@fortawesome/free-solid-svg-icons";
|
import { faEye, faEyeSlash, faKey, faMinus } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { twMerge } from "tailwind-merge";
|
|
||||||
|
import { useSyntaxHighlight } from "@app/hooks";
|
||||||
|
|
||||||
|
import { useToggle } from "~/hooks/useToggle";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
secrets: any[] | undefined;
|
secrets: any[] | undefined;
|
||||||
@@ -12,7 +15,8 @@ type Props = {
|
|||||||
userAvailableEnvs?: any[];
|
userAvailableEnvs?: any[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const REGEX = /([$]{.*?})/g;
|
const SEC_VAL_LINE_HEIGHT = 21;
|
||||||
|
const MAX_MULTI_LINE = 6;
|
||||||
|
|
||||||
const DashboardInput = ({
|
const DashboardInput = ({
|
||||||
isOverridden,
|
isOverridden,
|
||||||
@@ -25,84 +29,60 @@ const DashboardInput = ({
|
|||||||
isReadOnly?: boolean;
|
isReadOnly?: boolean;
|
||||||
secret?: any;
|
secret?: any;
|
||||||
}): JSX.Element => {
|
}): JSX.Element => {
|
||||||
const syntaxHighlight = useCallback((val: string) => {
|
const ref = useRef<HTMLElement | null>(null);
|
||||||
if (val === undefined)
|
const [isFocused, setIsFocused] = useToggle();
|
||||||
return (
|
const syntaxHighlight = useSyntaxHighlight();
|
||||||
<span className="cursor-default font-sans text-xs italic text-red-500/80">
|
|
||||||
<FontAwesomeIcon icon={faMinus} className="mt-1" />
|
const value = isOverridden ? secret.valueOverride : secret?.value;
|
||||||
</span>
|
const multilineExpandUnit = ((value?.match(/\n/g)?.length || 0) + 1) * SEC_VAL_LINE_HEIGHT;
|
||||||
);
|
const maxMultilineHeight = Math.min(multilineExpandUnit, 21 * MAX_MULTI_LINE);
|
||||||
if (val?.length === 0)
|
|
||||||
return <span className="w-full font-sans text-bunker-400/80">EMPTY</span>;
|
|
||||||
return val?.split(REGEX).map((word, index) =>
|
|
||||||
word.match(REGEX) !== null ? (
|
|
||||||
<span className="ph-no-capture text-yellow" key={`${val}-${index + 1}`}>
|
|
||||||
{word.slice(0, 2)}
|
|
||||||
<span className="ph-no-capture text-yellow-200/80">{word.slice(2, word.length - 1)}</span>
|
|
||||||
{word.slice(word.length - 1, word.length) === "}" ? (
|
|
||||||
<span className="ph-no-capture text-yellow">
|
|
||||||
{word.slice(word.length - 1, word.length)}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="ph-no-capture text-yellow-400">
|
|
||||||
{word.slice(word.length - 1, word.length)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span key={word} className="ph-no-capture">
|
|
||||||
{word}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<td
|
<td
|
||||||
key={`row-${secret?.key || ""}--`}
|
key={`row-${secret?.key || ""}--`}
|
||||||
className={`flex h-10 w-full cursor-default flex-row items-center justify-center ${
|
className={`flex w-full cursor-default flex-row ${
|
||||||
!(secret?.value || secret?.value === "") ? "bg-red-800/10" : "bg-mineshaft-900/30"
|
!(secret?.value || secret?.value === "") ? "bg-red-800/10" : "bg-mineshaft-900/30"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="group relative flex w-full cursor-default flex-col justify-center whitespace-pre">
|
<div className="group relative flex w-full flex-col whitespace-pre px-1.5 pt-1.5">
|
||||||
<input
|
<textarea
|
||||||
value={isOverridden ? secret.valueOverride : secret?.value || ""}
|
|
||||||
readOnly={isReadOnly}
|
readOnly={isReadOnly}
|
||||||
className={twMerge(
|
value={value}
|
||||||
"ph-no-capture no-scrollbar::-webkit-scrollbar duration-50 peer z-10 w-full cursor-default bg-transparent px-2 py-2 font-mono text-sm text-transparent caret-transparent outline-none no-scrollbar",
|
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"
|
||||||
isSecretValueHidden && "text-transparent focus:text-transparent active:text-transparent"
|
style={{ height: `${maxMultilineHeight}px` }}
|
||||||
)}
|
|
||||||
spellCheck="false"
|
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;
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<div
|
<pre className="whitespace-pre-wrap break-words">
|
||||||
className={twMerge(
|
<code
|
||||||
"ph-no-capture min-w-16 no-scrollbar::-webkit-scrollbar duration-50 absolute z-0 mt-0.5 flex h-10 w-full cursor-default flex-row overflow-x-scroll whitespace-pre bg-transparent px-2 py-2 font-mono text-sm outline-none no-scrollbar peer-focus:visible",
|
ref={ref}
|
||||||
isSecretValueHidden && secret?.value ? "invisible" : "visible",
|
className={`absolute top-1.5 left-3.5 z-10 overflow-auto font-mono text-sm transition-all no-scrollbar ${
|
||||||
isSecretValueHidden &&
|
isOverridden && "text-primary-300"
|
||||||
secret?.value &&
|
}`}
|
||||||
"duration-50 text-bunker-800 group-hover:text-gray-400 peer-focus:text-gray-100 peer-active:text-gray-400",
|
style={{ height: `${maxMultilineHeight}px`, width: "calc(100% - 12px)" }}
|
||||||
!secret?.value && "justify-center text-bunker-400"
|
>
|
||||||
)}
|
{value === undefined ? (
|
||||||
>
|
<span className="cursor-default font-sans text-xs italic text-red-500/80">
|
||||||
{syntaxHighlight(secret?.value)}
|
<FontAwesomeIcon icon={faMinus} className="mt-1" />
|
||||||
</div>
|
</span>
|
||||||
{isSecretValueHidden && secret?.value && (
|
) : (
|
||||||
<div className="duration-50 peer absolute z-0 flex h-10 w-full flex-row items-center justify-between text-clip pr-2 text-bunker-400 group-hover:bg-white/[0.00] peer-focus:hidden peer-active:hidden">
|
syntaxHighlight(value || "", isSecretValueHidden ? !isFocused : isSecretValueHidden)
|
||||||
<div className="no-scrollbar::-webkit-scrollbar flex flex-row items-center overflow-x-scroll px-2 no-scrollbar">
|
)}
|
||||||
{(isOverridden ? secret.valueOverride : secret?.value || "")
|
</code>
|
||||||
?.split("")
|
</pre>
|
||||||
.map((_a: string, index: number) => (
|
|
||||||
<FontAwesomeIcon
|
|
||||||
key={`${secret?.value}_${index + 1}`}
|
|
||||||
className="mr-0.5 text-xxs"
|
|
||||||
icon={faCircle}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{(isOverridden ? secret.valueOverride : secret?.value || "")?.split("").length ===
|
|
||||||
0 && <span className="text-sm text-bunker-400/80">EMPTY</span>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
@@ -122,14 +102,14 @@ export const EnvComparisonRow = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr className="group flex min-w-full flex-row items-center hover:bg-mineshaft-800">
|
<tr className="group flex min-w-full flex-row hover:bg-mineshaft-800">
|
||||||
<td className="flex h-10 w-10 items-center justify-center border-none px-4">
|
<td className="flex w-10 justify-center border-none px-4">
|
||||||
<div className="w-10 text-center text-xs text-bunker-400">
|
<div className="flex h-8 w-10 items-center justify-center text-center text-xs text-bunker-400">
|
||||||
<FontAwesomeIcon icon={faKey} />
|
<FontAwesomeIcon icon={faKey} />
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="flex h-full min-w-[200px] flex-row items-center justify-between lg:min-w-[220px] xl:min-w-[250px]">
|
<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 truncate">
|
<div className="flex h-8 cursor-default flex-row items-center justify-center truncate">
|
||||||
{secrets![0].key || ""}
|
{secrets![0].key || ""}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
|||||||
+2
-2
@@ -193,8 +193,8 @@ export const SecretDetailDrawer = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-1.5 flex items-center space-x-2 border-l border-bunker-300 pl-4">
|
<div className="ml-1.5 flex items-center space-x-2 border-l border-bunker-300 pl-4">
|
||||||
<div className="rounded-sm bg-primary-500/30 px-1">Value:</div>
|
<div className="self-start rounded-sm bg-primary-500/30 px-1">Value:</div>
|
||||||
<div className="font-mono">{value}</div>
|
<div className="break-all font-mono">{value}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { useCallback } from "react";
|
import { useRef } from "react";
|
||||||
import { useFormContext, useWatch } from "react-hook-form";
|
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||||
import { faCircle } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { useSyntaxHighlight, useToggle } from "@app/hooks";
|
||||||
import { twMerge } from "tailwind-merge";
|
|
||||||
|
|
||||||
import { FormData } from "../../DashboardPage.utils";
|
import { FormData } from "../../DashboardPage.utils";
|
||||||
|
|
||||||
@@ -13,95 +12,94 @@ type Props = {
|
|||||||
index: number;
|
index: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const REGEX = /([$]{.*?})/g;
|
const SEC_VAL_LINE_HEIGHT = 21;
|
||||||
|
const MAX_MULTI_LINE = 6;
|
||||||
|
|
||||||
export const MaskedInput = ({ isReadOnly, isSecretValueHidden, index, isOverridden }: Props) => {
|
export const MaskedInput = ({ isReadOnly, isSecretValueHidden, index, isOverridden }: Props) => {
|
||||||
const { register, control } = useFormContext<FormData>();
|
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 secretValue = useWatch({ control, name: `secrets.${index}.value` });
|
||||||
const secretValueOverride = useWatch({ control, name: `secrets.${index}.valueOverride` });
|
const secretValueOverride = useWatch({ control, name: `secrets.${index}.valueOverride` });
|
||||||
const value = isOverridden ? secretValueOverride : secretValue;
|
const value = isOverridden ? secretValueOverride : secretValue;
|
||||||
|
|
||||||
const syntaxHighlight = useCallback((val: string) => {
|
const multilineExpandUnit = ((value?.match(/\n/g)?.length || 0) + 1) * SEC_VAL_LINE_HEIGHT;
|
||||||
if (val?.length === 0) return <span className="font-sans text-bunker-400/80">EMPTY</span>;
|
const maxMultilineHeight = Math.min(multilineExpandUnit, 21 * MAX_MULTI_LINE);
|
||||||
return val?.split(REGEX).map((word, i) =>
|
|
||||||
word.match(REGEX) !== null ? (
|
|
||||||
<span className="ph-no-capture text-yellow" key={`${val}-${i + 1}`}>
|
|
||||||
{word.slice(0, 2)}
|
|
||||||
<span className="ph-no-capture text-yellow-200/80">{word.slice(2, word.length - 1)}</span>
|
|
||||||
{word.slice(word.length - 1, word.length) === "}" ? (
|
|
||||||
<span className="ph-no-capture text-yellow">
|
|
||||||
{word.slice(word.length - 1, word.length)}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="ph-no-capture text-yellow-400">
|
|
||||||
{word.slice(word.length - 1, word.length)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span key={`${word}_${i + 1}`} className="ph-no-capture">
|
|
||||||
{word}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group relative flex w-full flex-col justify-center whitespace-pre px-1.5">
|
<div className="group relative flex w-full flex-col whitespace-pre px-1.5 pt-1.5">
|
||||||
{isOverridden ? (
|
{isOverridden ? (
|
||||||
<input
|
<Controller
|
||||||
{...register(`secrets.${index}.valueOverride`)}
|
control={control}
|
||||||
readOnly={isReadOnly}
|
name={`secrets.${index}.valueOverride`}
|
||||||
className={twMerge(
|
render={({ field }) => (
|
||||||
"ph-no-capture min-w-16 no-scrollbar::-webkit-scrollbar duration-50 peer z-10 w-full bg-transparent px-2 py-2 font-mono text-sm text-transparent caret-white outline-none no-scrollbar",
|
<textarea
|
||||||
!isSecretValueHidden &&
|
key={`secrets.${index}.valueOverride`}
|
||||||
"text-transparent focus:text-transparent active:text-transparent"
|
{...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;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
spellCheck="false"
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<input
|
<Controller
|
||||||
{...register(`secrets.${index}.value`)}
|
control={control}
|
||||||
readOnly={isReadOnly}
|
name={`secrets.${index}.value`}
|
||||||
className={twMerge(
|
key={`secrets.${index}.value`}
|
||||||
"ph-no-capture min-w-16 no-scrollbar::-webkit-scrollbar duration-50 peer z-10 w-full bg-transparent px-2 py-2 font-mono text-sm text-transparent caret-white outline-none no-scrollbar",
|
render={({ field }) => (
|
||||||
!isSecretValueHidden &&
|
<textarea
|
||||||
"text-transparent focus:text-transparent active:text-transparent"
|
{...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;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
spellCheck="false"
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div
|
<pre className="whitespace-pre-wrap break-words">
|
||||||
className={twMerge(
|
<code
|
||||||
"ph-no-capture min-w-16 no-scrollbar::-webkit-scrollbar duration-50 absolute z-0 mt-0.5 flex h-10 w-full flex-row overflow-x-scroll whitespace-pre bg-transparent px-2 py-2 font-mono text-sm outline-none no-scrollbar peer-focus:visible",
|
ref={ref}
|
||||||
isSecretValueHidden ? "invisible" : "visible",
|
className={`absolute top-1.5 left-3.5 z-10 w-full overflow-auto font-mono text-sm transition-all no-scrollbar ${
|
||||||
isOverridden
|
isOverridden && "text-primary-300"
|
||||||
? "text-primary-300"
|
}`}
|
||||||
: "duration-50 text-gray-400 group-hover:text-gray-400 peer-focus:text-gray-100 peer-active:text-gray-400"
|
style={{ height: `${maxMultilineHeight}px`, width: "calc(100% - 12px)" }}
|
||||||
)}
|
>
|
||||||
>
|
{syntaxHighlight(value || "", isSecretValueHidden ? !isFocused : isSecretValueHidden)}
|
||||||
{syntaxHighlight(value || "")}
|
</code>
|
||||||
</div>
|
</pre>
|
||||||
<div
|
|
||||||
className={twMerge(
|
|
||||||
"duration-50 peer absolute z-0 flex h-10 w-full flex-row items-center justify-between text-clip pr-2 text-bunker-400 group-hover:bg-white/[0.00] peer-focus:hidden peer-active:hidden",
|
|
||||||
!isSecretValueHidden ? "invisible" : "visible"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="no-scrollbar::-webkit-scrollbar flex flex-row items-center overflow-x-scroll px-2 no-scrollbar">
|
|
||||||
{value?.split("").map((val, i) => (
|
|
||||||
<FontAwesomeIcon
|
|
||||||
key={`${value}_${val}_${i + 1}`}
|
|
||||||
className="mr-0.5 text-xxs"
|
|
||||||
icon={faCircle}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{value?.split("").length === 0 && (
|
|
||||||
<span className="text-sm text-bunker-400/80">EMPTY</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ export const SecretInputRow = memo(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr className="group flex flex-row items-center" key={index}>
|
<tr className="group flex flex-row" key={index}>
|
||||||
<td className="flex h-10 w-10 items-center justify-center border-none px-4">
|
<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">{index + 1}</div>
|
<div className="w-10 text-center text-xs text-bunker-400">{index + 1}</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -198,7 +198,7 @@ export const SecretInputRow = memo(
|
|||||||
</HoverCard>
|
</HoverCard>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<td className="flex h-10 w-full flex-grow flex-row items-center justify-center border-r border-none border-red">
|
<td className="flex w-full flex-grow flex-row border-r border-none border-red">
|
||||||
<MaskedInput
|
<MaskedInput
|
||||||
isReadOnly={
|
isReadOnly={
|
||||||
isReadOnly || isRollbackMode || (isOverridden ? isAddOnly : shouldBeBlockedInAddOnly)
|
isReadOnly || isRollbackMode || (isOverridden ? isAddOnly : shouldBeBlockedInAddOnly)
|
||||||
@@ -208,8 +208,8 @@ export const SecretInputRow = memo(
|
|||||||
index={index}
|
index={index}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="min-w-sm flex h-10 items-center">
|
<td className="min-w-sm flex">
|
||||||
<div className="flex items-center pl-2">
|
<div className="flex h-8 items-center pl-2">
|
||||||
{secretTags.map(({ id, slug }, i) => (
|
{secretTags.map(({ id, slug }, i) => (
|
||||||
<Tag
|
<Tag
|
||||||
className={cx(
|
className={cx(
|
||||||
@@ -289,7 +289,7 @@ export const SecretInputRow = memo(
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex h-full flex-row items-center pr-2">
|
<div className="flex h-8 flex-row items-center pr-2">
|
||||||
{!isAddOnly && (
|
{!isAddOnly && (
|
||||||
<div>
|
<div>
|
||||||
<Tooltip content="Override with a personal value">
|
<Tooltip content="Override with a personal value">
|
||||||
@@ -346,35 +346,37 @@ export const SecretInputRow = memo(
|
|||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<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">
|
<div className="duration-0 flex w-16 justify-center overflow-hidden border-l border-mineshaft-600 pl-2 transition-all">
|
||||||
{!isAddOnly && (
|
<div className="flex h-8 items-center space-x-2.5">
|
||||||
|
{!isAddOnly && (
|
||||||
|
<div className="opacity-0 group-hover:opacity-100">
|
||||||
|
<Tooltip content="Settings">
|
||||||
|
<IconButton
|
||||||
|
size="lg"
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="plain"
|
||||||
|
onClick={onRowExpand}
|
||||||
|
ariaLabel="expand"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faEllipsis} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="opacity-0 group-hover:opacity-100">
|
<div className="opacity-0 group-hover:opacity-100">
|
||||||
<Tooltip content="Settings">
|
<Tooltip content="Delete">
|
||||||
<IconButton
|
<IconButton
|
||||||
size="lg"
|
size="md"
|
||||||
colorSchema="primary"
|
|
||||||
variant="plain"
|
variant="plain"
|
||||||
onClick={onRowExpand}
|
colorSchema="danger"
|
||||||
ariaLabel="expand"
|
ariaLabel="delete"
|
||||||
|
isDisabled={isReadOnly || isRollbackMode}
|
||||||
|
onClick={() => onSecretDelete(index, secId, idOverride)}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faEllipsis} />
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
<div className="opacity-0 group-hover:opacity-100">
|
|
||||||
<Tooltip content="Delete">
|
|
||||||
<IconButton
|
|
||||||
size="md"
|
|
||||||
variant="plain"
|
|
||||||
colorSchema="danger"
|
|
||||||
ariaLabel="delete"
|
|
||||||
isDisabled={isReadOnly || isRollbackMode}
|
|
||||||
onClick={() => onSecretDelete(index, secId, idOverride)}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
Reference in New Issue
Block a user