mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 21:28:18 +00:00
Merge pull request #169 from Infisical/dashboard-sidebar
Added dashboard sidebar
This commit is contained in:
@@ -0,0 +1,81 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Switch } from "@headlessui/react";
|
||||||
|
|
||||||
|
|
||||||
|
interface OverrideProps {
|
||||||
|
id: string;
|
||||||
|
keyName: string;
|
||||||
|
value: string;
|
||||||
|
pos: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ToggleProps {
|
||||||
|
enabled: boolean;
|
||||||
|
setEnabled: (value: boolean) => void;
|
||||||
|
addOverride: (value: OverrideProps) => void;
|
||||||
|
keyName: string;
|
||||||
|
value: string;
|
||||||
|
pos: number;
|
||||||
|
id: string;
|
||||||
|
deleteOverride: (id: string) => void;
|
||||||
|
sharedToHide: string[];
|
||||||
|
setSharedToHide: (values: string[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a typical 'iPhone' toggle (e.g., user for overriding secrets with personal values)
|
||||||
|
* @param obj
|
||||||
|
* @param {boolean} obj.enabled - whether the toggle is turned on or off
|
||||||
|
* @param {function} obj.setEnabled - change the state of the toggle
|
||||||
|
* @param {function} obj.addOverride - a function that adds an override to a certain secret
|
||||||
|
* @param {string} obj.keyName - key of a certain secret
|
||||||
|
* @param {string} obj.value - value of a certain secret
|
||||||
|
* @param {number} obj.pos - position of a certain secret
|
||||||
|
#TODO: make the secret id persistent?
|
||||||
|
* @param {string} obj.id - id of a certain secret
|
||||||
|
* @param {function} obj.deleteOverride - a function that deleted an override for a certain secret
|
||||||
|
* @param {string[]} obj.sharedToHide - an array of shared secrets that we want to hide visually because they are overriden.
|
||||||
|
* @param {function} obj.setSharedToHide - a function that updates the array of secrets that we want to hide visually
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export default function Toggle ({
|
||||||
|
enabled,
|
||||||
|
setEnabled,
|
||||||
|
addOverride,
|
||||||
|
keyName,
|
||||||
|
value,
|
||||||
|
pos,
|
||||||
|
id,
|
||||||
|
deleteOverride,
|
||||||
|
sharedToHide,
|
||||||
|
setSharedToHide
|
||||||
|
}: ToggleProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<Switch
|
||||||
|
checked={enabled}
|
||||||
|
onChange={() => {
|
||||||
|
if (enabled == false) {
|
||||||
|
addOverride({ id, keyName, value, pos });
|
||||||
|
setSharedToHide([
|
||||||
|
...sharedToHide!,
|
||||||
|
id
|
||||||
|
])
|
||||||
|
} else {
|
||||||
|
setSharedToHide(sharedToHide!.filter(tempId => tempId != id))
|
||||||
|
deleteOverride(id);
|
||||||
|
}
|
||||||
|
setEnabled(!enabled);
|
||||||
|
}}
|
||||||
|
className={`${
|
||||||
|
enabled ? 'bg-primary' : 'bg-bunker-400'
|
||||||
|
} relative inline-flex h-5 w-9 items-center rounded-full`}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Enable notifications</span>
|
||||||
|
<span
|
||||||
|
className={`${
|
||||||
|
enabled ? 'translate-x-[1.26rem]' : 'translate-x-0.5'
|
||||||
|
} inline-block h-3.5 w-3.5 transform rounded-full bg-bunker-800 transition`}
|
||||||
|
/>
|
||||||
|
</Switch>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ type ButtonProps = {
|
|||||||
size: string;
|
size: string;
|
||||||
icon?: IconProp;
|
icon?: IconProp;
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
iconDisabled?: string;
|
iconDisabled?: IconProp;
|
||||||
textDisabled?: string;
|
textDisabled?: string;
|
||||||
type?: ButtonHTMLAttributes<any>['type'];
|
type?: ButtonHTMLAttributes<any>['type'];
|
||||||
};
|
};
|
||||||
@@ -73,15 +73,16 @@ export default function Button(props: ButtonProps): JSX.Element {
|
|||||||
|
|
||||||
// Setting the text color for the text and icon
|
// Setting the text color for the text and icon
|
||||||
props.color == "mineshaft" && "text-gray-400",
|
props.color == "mineshaft" && "text-gray-400",
|
||||||
props.color != "mineshaft" && props.color != "red" && "text-black",
|
props.color != "mineshaft" && props.color != "red" && props.color != "none" && "text-black",
|
||||||
props.color == "red" && "text-gray-200",
|
props.color == "red" && "text-gray-200",
|
||||||
activityStatus && props.color != "red" ? "group-hover:text-black" : "",
|
props.color == "none" && "text-gray-200 text-xl",
|
||||||
|
activityStatus && props.color != "red" && props.color != "none" ? "group-hover:text-black" : "",
|
||||||
|
|
||||||
props.size == "icon" && "flex items-center justify-center"
|
props.size == "icon" && "flex items-center justify-center"
|
||||||
);
|
);
|
||||||
|
|
||||||
const textStyle = classNames(
|
const textStyle = classNames(
|
||||||
"relative duration-200",
|
"relative duration-200 text-center w-full",
|
||||||
|
|
||||||
// Show the loading sign if the loading indicator is on
|
// Show the loading sign if the loading indicator is on
|
||||||
props.loading ? "opacity-0" : "opacity-100",
|
props.loading ? "opacity-0" : "opacity-100",
|
||||||
|
|||||||
+16
-6
@@ -3,6 +3,16 @@ import { faXmark } from "@fortawesome/free-solid-svg-icons";
|
|||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
|
||||||
|
interface PopupProps {
|
||||||
|
buttonText: string;
|
||||||
|
buttonLink: string;
|
||||||
|
titleText: string;
|
||||||
|
emoji: string;
|
||||||
|
textLine1: string;
|
||||||
|
textLine2: string;
|
||||||
|
setCheckDocsPopUpVisible: (value: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the notification that pops up at the bottom right when a user performs a certain action
|
* This is the notification that pops up at the bottom right when a user performs a certain action
|
||||||
* @param {object} org
|
* @param {object} org
|
||||||
@@ -23,16 +33,16 @@ export default function BottonRightPopup({
|
|||||||
textLine1,
|
textLine1,
|
||||||
textLine2,
|
textLine2,
|
||||||
setCheckDocsPopUpVisible,
|
setCheckDocsPopUpVisible,
|
||||||
}) {
|
}: PopupProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class="z-50 drop-shadow-xl border-gray-600/50 border flex flex-col items-start bg-bunker max-w-xl text-gray-200 pt-3 pb-4 rounded-xl absolute bottom-0 right-0 mr-6 mb-6"
|
className="z-50 drop-shadow-xl border-gray-600/50 border flex flex-col items-start bg-bunker max-w-xl text-gray-200 pt-3 pb-4 rounded-xl absolute bottom-0 right-0 mr-6 mb-6"
|
||||||
role="alert"
|
role="alert"
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center justify-between w-full border-b border-gray-600/70 pb-3 px-6">
|
<div className="flex flex-row items-center justify-between w-full border-b border-gray-600/70 pb-3 px-6">
|
||||||
<div className="font-bold text-xl mr-2 mt-0.5 flex flex-row">
|
<div className="font-bold text-xl mr-2 mt-0.5 flex flex-row">
|
||||||
<div>{titleText}</div>
|
<div>{titleText}</div>
|
||||||
<div class="ml-2.5">{emoji}</div>
|
<div className="ml-2.5">{emoji}</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
@@ -44,14 +54,14 @@ export default function BottonRightPopup({
|
|||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="block sm:inline px-6 mt-4 mb-0.5 text-gray-300">
|
<div className="block sm:inline px-6 mt-4 mb-0.5 text-gray-300">
|
||||||
{textLine1}
|
{textLine1}
|
||||||
</div>
|
</div>
|
||||||
<div class="block sm:inline mb-4 px-6">{textLine2}</div>
|
<div className="block sm:inline mb-4 px-6">{textLine2}</div>
|
||||||
<div className="flex flex-row px-6 w-full">
|
<div className="flex flex-row px-6 w-full">
|
||||||
{/*eslint-disable-next-line react/jsx-no-target-blank */}
|
{/*eslint-disable-next-line react/jsx-no-target-blank */}
|
||||||
<a
|
<a
|
||||||
class="font-bold p-2 bg-white/10 rounded-md w-full hover:bg-primary duration-200 hover:text-black flex justify-center"
|
className="font-bold p-2 bg-white/10 rounded-md w-full hover:bg-primary duration-200 hover:text-black flex justify-center"
|
||||||
href={buttonLink}
|
href={buttonLink}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
@@ -11,19 +11,21 @@ interface DashboardInputFieldProps {
|
|||||||
onChangeHandler: (value: string, position: number) => void;
|
onChangeHandler: (value: string, position: number) => void;
|
||||||
value: string;
|
value: string;
|
||||||
type: 'varName' | 'value';
|
type: 'varName' | 'value';
|
||||||
blurred: boolean;
|
blurred?: boolean;
|
||||||
duplicates: string[];
|
isDuplicate?: boolean;
|
||||||
|
override?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders the input fields on the dashboard
|
* This component renders the input fields on the dashboard
|
||||||
* @param {object} obj - the order number of a keyPair
|
* @param {object} obj - the order number of a keyPair
|
||||||
* @param {number} obj.pos - the order number of a keyPair
|
* @param {number} obj.position - the order number of a keyPair
|
||||||
* @param {function} obj.onChangeHandler - what happens when the input is modified
|
* @param {function} obj.onChangeHandler - what happens when the input is modified
|
||||||
* @param {string} obj.type - whether the input field is for a Key Name or for a Key Value
|
* @param {string} obj.type - whether the input field is for a Key Name or for a Key Value
|
||||||
* @param {string} obj.value - value of the InputField
|
* @param {string} obj.value - value of the InputField
|
||||||
* @param {boolean} obj.blurred - whether the input field should be blurred (behind the gray dots) or not; this can be turned on/off in the dashboard
|
* @param {boolean} obj.blurred - whether the input field should be blurred (behind the gray dots) or not; this can be turned on/off in the dashboard
|
||||||
* @param {string[]} obj.duplicates - list of all the duplicated key names on the dashboard
|
* @param {boolean} obj.isDuplicate - if the key name is duplicated
|
||||||
|
* @param {boolean} obj.override - whether a secret/row should be displalyed as overriden
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -33,7 +35,8 @@ const DashboardInputField = ({
|
|||||||
type,
|
type,
|
||||||
value,
|
value,
|
||||||
blurred,
|
blurred,
|
||||||
duplicates
|
isDuplicate,
|
||||||
|
override
|
||||||
}: DashboardInputFieldProps) => {
|
}: DashboardInputFieldProps) => {
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
const syncScroll = (e: SyntheticEvent<HTMLDivElement>) => {
|
const syncScroll = (e: SyntheticEvent<HTMLDivElement>) => {
|
||||||
@@ -45,8 +48,7 @@ const DashboardInputField = ({
|
|||||||
|
|
||||||
if (type === 'varName') {
|
if (type === 'varName') {
|
||||||
const startsWithNumber = !isNaN(Number(value.charAt(0))) && value != '';
|
const startsWithNumber = !isNaN(Number(value.charAt(0))) && value != '';
|
||||||
const hasDuplicates = duplicates?.includes(value);
|
const error = startsWithNumber || isDuplicate;
|
||||||
const error = startsWithNumber || hasDuplicates;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-col w-full">
|
<div className="flex-col w-full">
|
||||||
@@ -72,7 +74,7 @@ const DashboardInputField = ({
|
|||||||
Should not start with a number
|
Should not start with a number
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{hasDuplicates && !startsWithNumber && (
|
{isDuplicate && !startsWithNumber && (
|
||||||
<p className="text-red text-xs mt-0.5 mx-1 mb-2 max-w-xs">
|
<p className="text-red text-xs mt-0.5 mx-1 mb-2 max-w-xs">
|
||||||
Secret names should be unique
|
Secret names should be unique
|
||||||
</p>
|
</p>
|
||||||
@@ -85,6 +87,7 @@ const DashboardInputField = ({
|
|||||||
<div
|
<div
|
||||||
className={`group relative whitespace-pre flex flex-col justify-center w-full max-w-2xl border border-mineshaft-500 rounded-md`}
|
className={`group relative whitespace-pre flex flex-col justify-center w-full max-w-2xl border border-mineshaft-500 rounded-md`}
|
||||||
>
|
>
|
||||||
|
{override == true && <div className='bg-primary-300 absolute top-[0.1rem] right-[0.1rem] z-10 w-min text-xxs px-1 text-black opacity-80 rounded-md'>Override enabled</div>}
|
||||||
<input
|
<input
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => onChangeHandler(e.target.value, position)}
|
onChange={(e) => onChangeHandler(e.target.value, position)}
|
||||||
@@ -99,10 +102,13 @@ const DashboardInputField = ({
|
|||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={`${
|
className={`${
|
||||||
blurred
|
blurred && !override
|
||||||
? 'text-bunker-800 group-hover:text-gray-400 peer-focus:text-gray-400 peer-active:text-gray-400'
|
? 'text-bunker-800 group-hover:text-gray-400 peer-focus:text-gray-400 peer-active:text-gray-400'
|
||||||
: ''
|
: ''
|
||||||
} absolute flex flex-row whitespace-pre font-mono z-0 ph-no-capture max-w-2xl overflow-x-scroll bg-bunker-800 h-9 rounded-md text-gray-400 text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-2 focus:ring-primary/50 duration-100 no-scrollbar no-scrollbar::-webkit-scrollbar`}
|
} ${
|
||||||
|
override ? 'text-primary-300' : 'text-gray-400'
|
||||||
|
}
|
||||||
|
absolute flex flex-row whitespace-pre font-mono z-0 ph-no-capture max-w-2xl overflow-x-scroll bg-bunker-800 h-9 rounded-md text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-2 focus:ring-primary/50 duration-100 no-scrollbar no-scrollbar::-webkit-scrollbar`}
|
||||||
>
|
>
|
||||||
{value.split(REGEX).map((word, id) => {
|
{value.split(REGEX).map((word, id) => {
|
||||||
if (word.match(REGEX) !== null) {
|
if (word.match(REGEX) !== null) {
|
||||||
@@ -153,4 +159,8 @@ const DashboardInputField = ({
|
|||||||
return <>Something Wrong</>;
|
return <>Something Wrong</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(DashboardInputField);
|
function inputPropsAreEqual(prev: DashboardInputFieldProps, next: DashboardInputFieldProps) {
|
||||||
|
return prev.value === next.value && prev.type === next.type && prev.position === next.position && prev.blurred === next.blurred && prev.override === next.override && prev.isDuplicate === next.isDuplicate;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(DashboardInputField, inputPropsAreEqual);
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import { Fragment,useState } from 'react';
|
||||||
|
import { faShuffle } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { Menu, Transition } from '@headlessui/react';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the menu that is used to (re)generate secrets (currently we only have ranom hex, in future we will have more options)
|
||||||
|
* @returns the popup-menu for randomly generating secrets
|
||||||
|
*/
|
||||||
|
const GenerateSecretMenu = ({ modifyValue, position }: { modifyValue: (value: string, position: number) => void; position: number; }) => {
|
||||||
|
const [randomStringLength, setRandomStringLength] = useState(32);
|
||||||
|
|
||||||
|
return <Menu as="div" className="relative inline-block text-left">
|
||||||
|
<div>
|
||||||
|
<Menu.Button className="inline-flex w-full justify-center rounded-md text-sm font-medium text-gray-200 rounded-md hover:bg-white/10 duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
|
||||||
|
<div className='py-1 px-2 rounded-md bg-bunker-800 hover:bg-bunker-500'>
|
||||||
|
<FontAwesomeIcon icon={faShuffle} className='text-bunker-300'/>
|
||||||
|
</div>
|
||||||
|
</Menu.Button>
|
||||||
|
</div>
|
||||||
|
<Transition
|
||||||
|
as={Fragment}
|
||||||
|
enter="transition ease-out duration-100"
|
||||||
|
enterFrom="transform opacity-0 scale-95"
|
||||||
|
enterTo="transform opacity-100 scale-100"
|
||||||
|
leave="transition ease-in duration-75"
|
||||||
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
|
leaveTo="transform opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<Menu.Items className="absolute z-50 drop-shadow-xl right-0 mt-0.5 w-[20rem] origin-top-right rounded-md bg-bunker border border-mineshaft-500 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none px-1 py-1">
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
if (randomStringLength > 32) {
|
||||||
|
setRandomStringLength(32);
|
||||||
|
} else if (randomStringLength < 2) {
|
||||||
|
setRandomStringLength(2);
|
||||||
|
} else {
|
||||||
|
modifyValue(
|
||||||
|
[...Array(randomStringLength)]
|
||||||
|
.map(() => Math.floor(Math.random() * 16).toString(16))
|
||||||
|
.join(''),
|
||||||
|
position
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="relative flex flex-row justify-start items-center cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/10 duration-200 hover:text-gray-200 w-full"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
className="text-lg pl-1.5 pr-3"
|
||||||
|
icon={faShuffle}
|
||||||
|
/>
|
||||||
|
<div className="text-sm justify-between flex flex-row w-full">
|
||||||
|
<p>Generate Random Hex</p>
|
||||||
|
<p>digits</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row absolute bottom-[0.4rem] right-[3.3rem] w-16 bg-bunker-800 border border-chicago-700 rounded-md text-bunker-200 ">
|
||||||
|
<div
|
||||||
|
className="m-0.5 px-1 cursor-pointer rounded-md hover:bg-chicago-700"
|
||||||
|
onClick={() => {
|
||||||
|
if (randomStringLength > 1) {
|
||||||
|
setRandomStringLength(randomStringLength - 1);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
-
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
onChange={(e) =>
|
||||||
|
setRandomStringLength(parseInt(e.target.value))
|
||||||
|
}
|
||||||
|
value={randomStringLength}
|
||||||
|
className="text-center z-20 peer text-sm bg-transparent w-full outline-none"
|
||||||
|
spellCheck="false"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="m-0.5 px-1 pb-0.5 cursor-pointer rounded-md hover:bg-chicago-700"
|
||||||
|
onClick={() => {
|
||||||
|
if (randomStringLength < 32) {
|
||||||
|
setRandomStringLength(randomStringLength + 1);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Menu.Items>
|
||||||
|
</Transition>
|
||||||
|
</Menu>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GenerateSecretMenu;
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { faEllipsis, faShuffle, faX } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
|
||||||
|
import Button from '../basic/buttons/Button';
|
||||||
|
import DashboardInputField from './DashboardInputField';
|
||||||
|
|
||||||
|
interface SecretDataProps {
|
||||||
|
type: 'personal' | 'shared';
|
||||||
|
pos: number;
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KeyPairProps {
|
||||||
|
keyPair: SecretDataProps;
|
||||||
|
deleteRow: (id: string) => void;
|
||||||
|
modifyKey: (value: string, position: number) => void;
|
||||||
|
modifyValue: (value: string, position: number) => void;
|
||||||
|
isBlurred: boolean;
|
||||||
|
isDuplicate: boolean;
|
||||||
|
toggleSidebar: (id: string) => void;
|
||||||
|
sidebarSecretId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component represent a single row for an environemnt variable on the dashboard
|
||||||
|
* @param {object} obj
|
||||||
|
* @param {String[]} obj.keyPair - data related to the environment variable (id, pos, key, value, public/private)
|
||||||
|
* @param {function} obj.deleteRow - a function to delete a certain keyPair
|
||||||
|
* @param {function} obj.modifyKey - modify the key of a certain environment variable
|
||||||
|
* @param {function} obj.modifyValue - modify the value of a certain environment variable
|
||||||
|
* @param {boolean} obj.isBlurred - if the blurring setting is turned on
|
||||||
|
* @param {boolean} obj.isDuplicate - list of all the duplicates secret names on the dashboard
|
||||||
|
* @param {function} obj.toggleSidebar - open/close/switch sidebar
|
||||||
|
* @param {string} obj.sidebarSecretId - the id of a secret for the side bar is displayed
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const KeyPair = ({
|
||||||
|
keyPair,
|
||||||
|
deleteRow,
|
||||||
|
modifyKey,
|
||||||
|
modifyValue,
|
||||||
|
isBlurred,
|
||||||
|
isDuplicate,
|
||||||
|
toggleSidebar,
|
||||||
|
sidebarSecretId
|
||||||
|
}: KeyPairProps) => {
|
||||||
|
return (
|
||||||
|
<div className={`mx-1 flex flex-col items-center ml-1 ${keyPair.id == sidebarSecretId && "bg-mineshaft-500 duration-200"} rounded-md`}>
|
||||||
|
<div className="relative flex flex-row justify-between w-full max-w-5xl mr-auto max-h-14 my-1 items-start px-1">
|
||||||
|
{keyPair.type == "personal" && <div className="group font-normal group absolute top-[1rem] left-[0.2rem] z-40 inline-block text-gray-300 underline hover:text-primary duration-200">
|
||||||
|
<div className='w-1 h-1 rounded-full bg-primary z-40'></div>
|
||||||
|
<span className="absolute z-50 hidden group-hover:flex group-hover:animate-popdown duration-200 w-[10.5rem] -left-[0.4rem] -top-[1.7rem] translate-y-full px-2 py-2 bg-mineshaft-500 rounded-b-md rounded-r-md text-center text-gray-100 text-sm after:content-[''] after:absolute after:left-0 after:bottom-[100%] after:-translate-x-0 after:border-8 after:border-x-transparent after:border-t-transparent after:border-b-mineshaft-500">
|
||||||
|
This secret is overriden
|
||||||
|
</span>
|
||||||
|
</div>}
|
||||||
|
<div className="min-w-xl w-96">
|
||||||
|
<div className="flex pr-1 items-center rounded-lg mt-4 md:mt-0 max-h-16">
|
||||||
|
<DashboardInputField
|
||||||
|
onChangeHandler={modifyKey}
|
||||||
|
type="varName"
|
||||||
|
position={keyPair.pos}
|
||||||
|
value={keyPair.key}
|
||||||
|
isDuplicate={isDuplicate}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-full min-w-5xl">
|
||||||
|
<div className="flex min-w-7xl items-center pl-1 pr-1.5 rounded-lg mt-4 md:mt-0 max-h-10 ">
|
||||||
|
<DashboardInputField
|
||||||
|
onChangeHandler={modifyValue}
|
||||||
|
type="value"
|
||||||
|
position={keyPair.pos}
|
||||||
|
value={keyPair.value}
|
||||||
|
blurred={isBlurred}
|
||||||
|
override={keyPair.type == "personal"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div onClick={() => toggleSidebar(keyPair.id)} className="cursor-pointer w-9 h-9 bg-mineshaft-700 hover:bg-chicago-700 rounded-md flex flex-row justify-center items-center duration-200">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
className="text-gray-300 px-2.5 text-lg mt-0.5"
|
||||||
|
icon={faEllipsis}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-2"></div>
|
||||||
|
<div className="bg-[#9B3535] hover:bg-red rounded-md duration-200">
|
||||||
|
<Button
|
||||||
|
onButtonPressed={() => deleteRow(keyPair.id)}
|
||||||
|
color="none"
|
||||||
|
size="icon-sm"
|
||||||
|
icon={faX}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(KeyPair);
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import SecretVersionList from 'ee/components/SecretVersionList';
|
||||||
|
|
||||||
|
import Button from '../basic/buttons/Button';
|
||||||
|
import Toggle from '../basic/Toggle';
|
||||||
|
import DashboardInputField from './DashboardInputField';
|
||||||
|
import GenerateSecretMenu from './GenerateSecretMenu';
|
||||||
|
|
||||||
|
|
||||||
|
interface SecretProps {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
pos: number;
|
||||||
|
type: string;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OverrideProps {
|
||||||
|
id: string;
|
||||||
|
keyName: string;
|
||||||
|
value: string;
|
||||||
|
pos: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SideBarProps {
|
||||||
|
toggleSidebar: (value: string) => void;
|
||||||
|
data: SecretProps[];
|
||||||
|
modifyKey: (value: string, position: number) => void;
|
||||||
|
modifyValue: (value: string, position: number) => void;
|
||||||
|
addOverride: (value: OverrideProps) => void;
|
||||||
|
deleteOverride: (id: string) => void;
|
||||||
|
buttonReady: boolean;
|
||||||
|
savePush: () => void;
|
||||||
|
sharedToHide: string[];
|
||||||
|
setSharedToHide: (values: string[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} obj
|
||||||
|
* @param {function} obj.toggleSidebar - function that opens or closes the sidebar
|
||||||
|
* @param {SecretProps[]} obj.data - data of a certain key valeu pair
|
||||||
|
* @param {function} obj.modifyKey - function that modifies the secret key
|
||||||
|
* @param {function} obj.modifyValue - function that modifies the secret value
|
||||||
|
* @param {function} obj.addOverride - override a certain secret
|
||||||
|
* @param {function} obj.deleteOverride - delete the personal override for a certain secret
|
||||||
|
* @param {boolean} obj.buttonReady - is the button for saving chagnes active
|
||||||
|
* @param {function} obj.savePush - save changes andp ush secrets
|
||||||
|
* @param {string[]} obj.sharedToHide - an array of shared secrets that we want to hide visually because they are overriden.
|
||||||
|
* @param {function} obj.setSharedToHide - a function that updates the array of secrets that we want to hide visually
|
||||||
|
* @returns the sidebar with 'secret's settings'
|
||||||
|
*/
|
||||||
|
const SideBar = ({
|
||||||
|
toggleSidebar,
|
||||||
|
data,
|
||||||
|
modifyKey,
|
||||||
|
modifyValue,
|
||||||
|
addOverride,
|
||||||
|
deleteOverride,
|
||||||
|
buttonReady,
|
||||||
|
savePush,
|
||||||
|
sharedToHide,
|
||||||
|
setSharedToHide
|
||||||
|
}: SideBarProps) => {
|
||||||
|
const [overrideEnabled, setOverrideEnabled] = useState(data.map(secret => secret.type).includes("personal"));
|
||||||
|
|
||||||
|
return <div className='absolute border-l border-mineshaft-500 bg-bunker fixed h-full w-96 top-14 right-0 z-50 shadow-xl flex flex-col justify-between'>
|
||||||
|
<div className='h-min overflow-y-auto'>
|
||||||
|
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
|
||||||
|
<p className="font-semibold text-lg text-bunker-200">Secret</p>
|
||||||
|
<div className='p-1' onClick={() => toggleSidebar("None")}>
|
||||||
|
<FontAwesomeIcon icon={faX} className='w-4 h-4 text-bunker-300 cursor-pointer'/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='mt-4 px-4 pointer-events-none'>
|
||||||
|
<p className='text-sm text-bunker-300'>Key</p>
|
||||||
|
<DashboardInputField
|
||||||
|
onChangeHandler={modifyKey}
|
||||||
|
type="varName"
|
||||||
|
position={data[0].pos}
|
||||||
|
value={data[0].key}
|
||||||
|
isDuplicate={false}
|
||||||
|
blurred={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{data.filter(secret => secret.type == "shared")[0]?.value
|
||||||
|
? <div className={`relative mt-2 px-4 ${overrideEnabled && "opacity-40 pointer-events-none"} duration-200`}>
|
||||||
|
<p className='text-sm text-bunker-300'>Value</p>
|
||||||
|
<DashboardInputField
|
||||||
|
onChangeHandler={modifyValue}
|
||||||
|
type="value"
|
||||||
|
position={data.filter(secret => secret.type == "shared")[0]?.pos}
|
||||||
|
value={data.filter(secret => secret.type == "shared")[0]?.value}
|
||||||
|
isDuplicate={false}
|
||||||
|
blurred={true}
|
||||||
|
/>
|
||||||
|
<div className='absolute bg-bunker-800 right-[1.07rem] top-[1.6rem] z-50'>
|
||||||
|
<GenerateSecretMenu modifyValue={modifyValue} position={data.filter(secret => secret.type == "shared")[0]?.pos} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
: <div className='px-4 text-sm text-bunker-300 pt-4'>
|
||||||
|
<span className='py-0.5 px-1 rounded-md bg-primary-200/10 mr-1'>Note:</span>
|
||||||
|
This secret is personal. It is not shared with any of your teammates.
|
||||||
|
</div>}
|
||||||
|
<div className='mt-4 px-4'>
|
||||||
|
{data.filter(secret => secret.type == "shared")[0]?.value &&
|
||||||
|
<div className='flex flex-row items-center justify-between my-2 pl-1 pr-2'>
|
||||||
|
<p className='text-sm text-bunker-300'>Override value with a personal value</p>
|
||||||
|
<Toggle
|
||||||
|
enabled={overrideEnabled}
|
||||||
|
setEnabled={setOverrideEnabled}
|
||||||
|
addOverride={addOverride}
|
||||||
|
keyName={data[0].key}
|
||||||
|
value={data[0].value}
|
||||||
|
pos={data[0].pos}
|
||||||
|
id={data[0].id}
|
||||||
|
deleteOverride={deleteOverride}
|
||||||
|
sharedToHide={sharedToHide}
|
||||||
|
setSharedToHide={setSharedToHide}
|
||||||
|
/>
|
||||||
|
</div>}
|
||||||
|
<div className={`relative ${!overrideEnabled && "opacity-40 pointer-events-none"} duration-200`}>
|
||||||
|
<DashboardInputField
|
||||||
|
onChangeHandler={modifyValue}
|
||||||
|
type="value"
|
||||||
|
position={overrideEnabled ? data.filter(secret => secret.type == "personal")[0].pos : data[0].pos}
|
||||||
|
value={overrideEnabled ? data.filter(secret => secret.type == "personal")[0].value : data[0].value}
|
||||||
|
isDuplicate={false}
|
||||||
|
blurred={true}
|
||||||
|
/>
|
||||||
|
<div className='absolute right-[0.57rem] top-[0.3rem] z-50'>
|
||||||
|
<GenerateSecretMenu modifyValue={modifyValue} position={overrideEnabled ? data.filter(secret => secret.type == "personal")[0].pos : data[0].pos} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* <div className={`relative mt-4 px-4 opacity-80 duration-200`}>
|
||||||
|
<p className='text-sm text-bunker-200'>Group</p>
|
||||||
|
<ListBox
|
||||||
|
selected={"Database Secrets"}
|
||||||
|
onChange={() => {}}
|
||||||
|
data={["Group1"]}
|
||||||
|
isFull={true}
|
||||||
|
/>
|
||||||
|
</div> */}
|
||||||
|
<div className={`relative mt-4 px-4 pt-4`}>
|
||||||
|
<div className='flex flex-row justify-between'>
|
||||||
|
<p className='text-sm text-bunker-300'>Comments & notes</p>
|
||||||
|
<div className="bg-yellow rounded-md h-min">
|
||||||
|
<p className="relative text-black text-xs px-1.5 h-min">Coming soon!</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='h-32 opacity-50 w-full bg-bunker-800 p-2 rounded-md border border-mineshaft-500 rounded-md text-sm text-bunker-300'>
|
||||||
|
Leave your comment here...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={`flex justify-start max-w-sm mt-4 px-4 mt-full mb-[4.7rem]`}>
|
||||||
|
<Button
|
||||||
|
text="Save Changes"
|
||||||
|
onButtonPressed={savePush}
|
||||||
|
color="primary"
|
||||||
|
size="md"
|
||||||
|
active={buttonReady}
|
||||||
|
textDisabled="Saved"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SideBar;
|
||||||
@@ -39,7 +39,7 @@ const getSecretsForProject = async ({
|
|||||||
|
|
||||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
|
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
|
||||||
|
|
||||||
const tempFileState: { key: string; value: string; type: string }[] = [];
|
const tempFileState: { key: string; value: string; type: 'personal' | 'shared'; }[] = [];
|
||||||
if (file.key) {
|
if (file.key) {
|
||||||
// assymmetrically decrypt symmetric key with local private key
|
// assymmetrically decrypt symmetric key with local private key
|
||||||
const key = decryptAssymmetric({
|
const key = decryptAssymmetric({
|
||||||
@@ -97,7 +97,7 @@ const getSecretsForProject = async ({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Something went wrong during accessing or decripting secrets.');
|
console.log('Something went wrong during accessing or decripting secrets.');
|
||||||
}
|
}
|
||||||
return true;
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
export default getSecretsForProject;
|
export default getSecretsForProject;
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ const pushKeys = async({ obj, workspaceId, env }: { obj: object; workspaceId: st
|
|||||||
iv: ivKey,
|
iv: ivKey,
|
||||||
tag: tagKey,
|
tag: tagKey,
|
||||||
} = encryptSymmetric({
|
} = encryptSymmetric({
|
||||||
plaintext: key,
|
plaintext: key.slice(1),
|
||||||
key: randomBytes,
|
key: randomBytes,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -65,13 +65,13 @@ const pushKeys = async({ obj, workspaceId, env }: { obj: object; workspaceId: st
|
|||||||
key: randomBytes,
|
key: randomBytes,
|
||||||
});
|
});
|
||||||
|
|
||||||
const visibility = obj[key as keyof typeof obj][1] != null ? obj[key as keyof typeof obj][1] : "personal";
|
const visibility = key.charAt(0) == "p" ? "personal" : "shared";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ciphertextKey,
|
ciphertextKey,
|
||||||
ivKey,
|
ivKey,
|
||||||
tagKey,
|
tagKey,
|
||||||
hashKey: crypto.createHash("sha256").update(key).digest("hex"),
|
hashKey: crypto.createHash("sha256").update(key.slice(1)).digest("hex"),
|
||||||
ciphertextValue,
|
ciphertextValue,
|
||||||
ivValue,
|
ivValue,
|
||||||
tagValue,
|
tagValue,
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { faCircle, faDotCircle } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||||
|
interface SecretVersionListProps {}
|
||||||
|
|
||||||
|
const versionData = [{
|
||||||
|
value: "Value1",
|
||||||
|
date: "Date1",
|
||||||
|
user: "[email protected]"
|
||||||
|
}, {
|
||||||
|
value: "Value2",
|
||||||
|
date: "Date2",
|
||||||
|
user: "[email protected]"
|
||||||
|
}]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns a list of the versions for a specific secret
|
||||||
|
*/
|
||||||
|
const SecretVersionList = () => {
|
||||||
|
return <div className='w-full h-52 px-4 mt-4 text-sm text-bunker-300 overflow-x-none'>
|
||||||
|
<p className=''>Version History</p>
|
||||||
|
<div className='p-1 rounded-md bg-bunker-800 border border-mineshaft-500 overflow-x-none'>
|
||||||
|
<div className='h-48 overflow-y-scroll overflow-x-none'>
|
||||||
|
{versionData.map((version, index) =>
|
||||||
|
<div key={index} className='flex flex-row'>
|
||||||
|
<div className='pr-1 flex flex-col items-center'>
|
||||||
|
<div className='p-1'><FontAwesomeIcon icon={index == 0 ? faDotCircle : faCircle} /></div>
|
||||||
|
<div className='w-0 h-full border-l mt-1'></div>
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-col w-full max-w-[calc(100%-2.3rem)]'>
|
||||||
|
<div className='pr-2 pt-1'>{version.date}</div>
|
||||||
|
<div className=''><p className='break-words'><span className='py-0.5 px-1 rounded-md bg-primary-200/10 mr-1.5'>Value:</span>{version.value}</p></div>
|
||||||
|
<div className=''><p className='break-words'><span className='py-0.5 px-1 rounded-md bg-primary-200/10 mr-1.5'>Updated by:</span>{version.user}</p></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SecretVersionList;
|
||||||
@@ -6,208 +6,68 @@ import {
|
|||||||
faArrowDownAZ,
|
faArrowDownAZ,
|
||||||
faArrowDownZA,
|
faArrowDownZA,
|
||||||
faCheck,
|
faCheck,
|
||||||
faCircleInfo,
|
|
||||||
faCopy,
|
faCopy,
|
||||||
faDownload,
|
faDownload,
|
||||||
faEllipsis,
|
|
||||||
faEye,
|
faEye,
|
||||||
faEyeSlash,
|
faEyeSlash,
|
||||||
faFolderOpen,
|
faFolderOpen,
|
||||||
faMagnifyingGlass,
|
faMagnifyingGlass,
|
||||||
faPeopleGroup,
|
|
||||||
faPerson,
|
|
||||||
faPlus,
|
faPlus,
|
||||||
faShuffle,
|
|
||||||
faX
|
|
||||||
} 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 { Menu, Transition } from '@headlessui/react';
|
|
||||||
|
|
||||||
import Button from '~/components/basic/buttons/Button';
|
import Button from '~/components/basic/buttons/Button';
|
||||||
import ListBox from '~/components/basic/Listbox';
|
import ListBox from '~/components/basic/Listbox';
|
||||||
import BottonRightPopup from '~/components/basic/popups/BottomRightPopup';
|
import BottonRightPopup from '~/components/basic/popups/BottomRightPopup';
|
||||||
import { useNotificationContext } from '~/components/context/Notifications/NotificationProvider';
|
import { useNotificationContext } from '~/components/context/Notifications/NotificationProvider';
|
||||||
import DashboardInputField from '~/components/dashboard/DashboardInputField';
|
|
||||||
import DropZone from '~/components/dashboard/DropZone';
|
import DropZone from '~/components/dashboard/DropZone';
|
||||||
|
import KeyPair from '~/components/dashboard/KeyPair';
|
||||||
|
import SideBar from '~/components/dashboard/SideBar';
|
||||||
import NavHeader from '~/components/navigation/NavHeader';
|
import NavHeader from '~/components/navigation/NavHeader';
|
||||||
import getSecretsForProject from '~/components/utilities/secrets/getSecretsForProject';
|
import getSecretsForProject from '~/components/utilities/secrets/getSecretsForProject';
|
||||||
import pushKeys from '~/components/utilities/secrets/pushKeys';
|
import pushKeys from '~/components/utilities/secrets/pushKeys';
|
||||||
import pushKeysIntegration from '~/components/utilities/secrets/pushKeysIntegration';
|
|
||||||
import guidGenerator from '~/utilities/randomId';
|
import guidGenerator from '~/utilities/randomId';
|
||||||
|
|
||||||
import { envMapping } from '../../public/data/frequentConstants';
|
import { envMapping } from '../../public/data/frequentConstants';
|
||||||
import getWorkspaceIntegrations from '../api/integrations/getWorkspaceIntegrations';
|
|
||||||
import getUser from '../api/user/getUser';
|
import getUser from '../api/user/getUser';
|
||||||
import checkUserAction from '../api/userActions/checkUserAction';
|
import checkUserAction from '../api/userActions/checkUserAction';
|
||||||
import registerUserAction from '../api/userActions/registerUserAction';
|
import registerUserAction from '../api/userActions/registerUserAction';
|
||||||
import getWorkspaces from '../api/workspace/getWorkspaces';
|
import getWorkspaces from '../api/workspace/getWorkspaces';
|
||||||
|
|
||||||
/**
|
|
||||||
* This component represent a single row for an environemnt variable on the dashboard
|
|
||||||
* @param {object} obj
|
|
||||||
* @param {String[]} obj.keyPair - data related to the environment variable (id, pos, key, value, public/private)
|
|
||||||
* @param {function} obj.deleteRow - a function to delete a certain keyPair
|
|
||||||
* @param {function} obj.modifyKey - modify the key of a certain environment variable
|
|
||||||
* @param {function} obj.modifyValue - modify the value of a certain environment variable
|
|
||||||
* @param {function} obj.modifyVisibility - switch between public/private visibility
|
|
||||||
* @param {boolean} obj.isBlurred - if the blurring setting is turned on
|
|
||||||
* @param {string[]} obj.duplicates - list of all the duplicates secret names on the dashboard
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
const KeyPair = ({
|
|
||||||
keyPair,
|
|
||||||
deleteRow,
|
|
||||||
modifyKey,
|
|
||||||
modifyValue,
|
|
||||||
modifyVisibility,
|
|
||||||
isBlurred,
|
|
||||||
duplicates
|
|
||||||
}) => {
|
|
||||||
const [randomStringLength, setRandomStringLength] = useState(32);
|
|
||||||
|
|
||||||
return (
|
interface SecretDataProps {
|
||||||
<div className="px-1 flex flex-col items-center ml-1">
|
type: 'personal' | 'shared';
|
||||||
<div className="relative flex flex-row justify-between w-full max-w-5xl mr-auto max-h-14 my-1 items-start px-2">
|
pos: number;
|
||||||
<div className="min-w-xl w-96">
|
key: string;
|
||||||
<div className="flex items-center md:px-1 rounded-lg mt-4 md:mt-0 max-h-16">
|
value: string;
|
||||||
<DashboardInputField
|
id: string;
|
||||||
onChangeHandler={modifyKey}
|
}
|
||||||
type="varName"
|
|
||||||
position={keyPair.pos}
|
/**
|
||||||
value={keyPair.key}
|
* this function finds the teh duplicates in an array
|
||||||
duplicates={duplicates}
|
* @param arr - array of anything (e.g., with secret keys and types (personal/shared))
|
||||||
/>
|
* @returns - a list with duplicates
|
||||||
</div>
|
*/
|
||||||
</div>
|
function findDuplicates(arr: any[]) {
|
||||||
<div className="w-full min-w-5xl">
|
const map = new Map();
|
||||||
<div className="flex min-w-7xl items-center pl-1 pr-1.5 rounded-lg mt-4 md:mt-0 max-h-10 ">
|
return arr.filter((item) => {
|
||||||
<DashboardInputField
|
if (map.has(item)) {
|
||||||
onChangeHandler={modifyValue}
|
map.set(item, false);
|
||||||
type="value"
|
return true;
|
||||||
position={keyPair.pos}
|
} else {
|
||||||
value={keyPair.value}
|
map.set(item, true);
|
||||||
blurred={isBlurred}
|
return false;
|
||||||
/>
|
}
|
||||||
</div>
|
});
|
||||||
</div>
|
}
|
||||||
<Menu as="div" className="relative inline-block text-left">
|
|
||||||
<div>
|
|
||||||
<Menu.Button className="inline-flex w-full justify-center rounded-md text-sm font-medium text-gray-200 rounded-md hover:bg-white/10 duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
|
|
||||||
<div className="cursor-pointer w-9 h-9 bg-white/10 rounded-md flex flex-row justify-center items-center opacity-50 hover:opacity-100 duration-200">
|
|
||||||
<FontAwesomeIcon
|
|
||||||
className="text-gray-300 px-2.5 text-lg mt-0.5"
|
|
||||||
icon={faEllipsis}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Menu.Button>
|
|
||||||
</div>
|
|
||||||
<Transition
|
|
||||||
as={Fragment}
|
|
||||||
enter="transition ease-out duration-100"
|
|
||||||
enterFrom="transform opacity-0 scale-95"
|
|
||||||
enterTo="transform opacity-100 scale-100"
|
|
||||||
leave="transition ease-in duration-75"
|
|
||||||
leaveFrom="transform opacity-100 scale-100"
|
|
||||||
leaveTo="transform opacity-0 scale-95"
|
|
||||||
>
|
|
||||||
<Menu.Items className="absolute z-50 drop-shadow-xl right-0 mt-0.5 w-[20rem] origin-top-right rounded-md bg-bunker border border-mineshaft-500 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none px-1 py-1">
|
|
||||||
<div
|
|
||||||
onClick={() =>
|
|
||||||
modifyVisibility(
|
|
||||||
keyPair.type == 'personal' ? 'shared' : 'personal',
|
|
||||||
keyPair.pos
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="relative flex justify-start items-center cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/10 duration-200 hover:text-gray-200 w-full"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
className="text-lg pl-1.5 pr-3"
|
|
||||||
icon={keyPair.type == 'personal' ? faPeopleGroup : faPerson}
|
|
||||||
/>
|
|
||||||
<div className="text-sm">
|
|
||||||
{keyPair.type == 'personal' ? 'Make Shared' : 'Make Personal'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
onClick={() => {
|
|
||||||
if (randomStringLength > 32) {
|
|
||||||
setRandomStringLength(32);
|
|
||||||
} else if (randomStringLength < 2) {
|
|
||||||
setRandomStringLength(2);
|
|
||||||
} else {
|
|
||||||
modifyValue(
|
|
||||||
[...Array(randomStringLength)]
|
|
||||||
.map(() => Math.floor(Math.random() * 16).toString(16))
|
|
||||||
.join(''),
|
|
||||||
keyPair.pos
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="relative flex flex-row justify-start items-center cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/10 duration-200 hover:text-gray-200 w-full"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
className="text-lg pl-1.5 pr-3"
|
|
||||||
icon={keyPair.value == '' ? faPlus : faShuffle}
|
|
||||||
/>
|
|
||||||
<div className="text-sm justify-between flex flex-row w-full">
|
|
||||||
<p>Generate Random Hex</p>
|
|
||||||
<p>digits</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-row absolute bottom-[0.4rem] right-[3.3rem] w-16 bg-bunker-800 border border-chicago-700 rounded-md text-bunker-200 ">
|
|
||||||
<div
|
|
||||||
className="m-0.5 px-1 cursor-pointer rounded-md hover:bg-chicago-700"
|
|
||||||
onClick={() => {
|
|
||||||
if (randomStringLength > 1) {
|
|
||||||
setRandomStringLength(randomStringLength - 1);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
-
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
onChange={(e) =>
|
|
||||||
setRandomStringLength(parseInt(e.target.value))
|
|
||||||
}
|
|
||||||
value={randomStringLength}
|
|
||||||
className="text-center z-20 peer text-sm bg-transparent w-full outline-none"
|
|
||||||
spellCheck="false"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="m-0.5 px-1 pb-0.5 cursor-pointer rounded-md hover:bg-chicago-700"
|
|
||||||
onClick={() => {
|
|
||||||
if (randomStringLength < 32) {
|
|
||||||
setRandomStringLength(randomStringLength + 1);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
+
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Menu.Items>
|
|
||||||
</Transition>
|
|
||||||
</Menu>
|
|
||||||
<div className="w-2"></div>
|
|
||||||
<div className="opacity-50 hover:opacity-100 duration-200">
|
|
||||||
<Button
|
|
||||||
onButtonPressed={() => deleteRow(keyPair.id)}
|
|
||||||
color="red"
|
|
||||||
size="icon-sm"
|
|
||||||
icon={faX}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the main component for the dashboard (aka the screen with all the encironemnt variable & secrets)
|
* This is the main component for the dashboard (aka the screen with all the encironemnt variable & secrets)
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const [data, setData] = useState();
|
const [data, setData] = useState<SecretDataProps[] | null>();
|
||||||
const [fileState, setFileState] = useState([]);
|
const [fileState, setFileState] = useState<SecretDataProps[]>([]);
|
||||||
const [buttonReady, setButtonReady] = useState(false);
|
const [buttonReady, setButtonReady] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [workspaceId, setWorkspaceId] = useState('');
|
const [workspaceId, setWorkspaceId] = useState('');
|
||||||
@@ -227,6 +87,8 @@ export default function Dashboard() {
|
|||||||
const [sortMethod, setSortMethod] = useState('alphabetical');
|
const [sortMethod, setSortMethod] = useState('alphabetical');
|
||||||
const [checkDocsPopUpVisible, setCheckDocsPopUpVisible] = useState(false);
|
const [checkDocsPopUpVisible, setCheckDocsPopUpVisible] = useState(false);
|
||||||
const [hasUserEverPushed, setHasUserEverPushed] = useState(false);
|
const [hasUserEverPushed, setHasUserEverPushed] = useState(false);
|
||||||
|
const [sidebarSecretId, toggleSidebar] = useState("None");
|
||||||
|
const [sharedToHide, setSharedToHide] = useState<string[]>([]);
|
||||||
|
|
||||||
const { createNotification } = useNotificationContext();
|
const { createNotification } = useNotificationContext();
|
||||||
|
|
||||||
@@ -249,7 +111,7 @@ export default function Dashboard() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const warningText =
|
const warningText =
|
||||||
'Do you want to save your results before leaving this page?';
|
'Do you want to save your results before leaving this page?';
|
||||||
const handleWindowClose = (e) => {
|
const handleWindowClose = (e: any) => {
|
||||||
if (!buttonReady) return;
|
if (!buttonReady) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return (e.returnValue = warningText);
|
return (e.returnValue = warningText);
|
||||||
@@ -265,18 +127,18 @@ export default function Dashboard() {
|
|||||||
/**
|
/**
|
||||||
* Reorder rows alphabetically or in the opprosite order
|
* Reorder rows alphabetically or in the opprosite order
|
||||||
*/
|
*/
|
||||||
const reorderRows = (dataToReorder) => {
|
const reorderRows = (dataToReorder: SecretDataProps[] | 1) => {
|
||||||
setSortMethod((prevSort) =>
|
setSortMethod((prevSort) =>
|
||||||
prevSort == 'alphabetical' ? '-alphabetical' : 'alphabetical'
|
prevSort == 'alphabetical' ? '-alphabetical' : 'alphabetical'
|
||||||
);
|
);
|
||||||
|
|
||||||
sortValuesHandler(dataToReorder);
|
sortValuesHandler(dataToReorder, undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
let userWorkspaces = await getWorkspaces();
|
const userWorkspaces = await getWorkspaces();
|
||||||
const listWorkspaces = userWorkspaces.map((workspace) => workspace._id);
|
const listWorkspaces = userWorkspaces.map((workspace) => workspace._id);
|
||||||
if (
|
if (
|
||||||
!listWorkspaces.includes(router.asPath.split('/')[2].split('?')[0])
|
!listWorkspaces.includes(router.asPath.split('/')[2].split('?')[0])
|
||||||
@@ -288,31 +150,41 @@ export default function Dashboard() {
|
|||||||
router.push(router.asPath.split('?')[0] + '?' + env);
|
router.push(router.asPath.split('?')[0] + '?' + env);
|
||||||
}
|
}
|
||||||
setBlurred(true);
|
setBlurred(true);
|
||||||
setWorkspaceId(router.query.id);
|
setWorkspaceId(String(router.query.id));
|
||||||
|
|
||||||
const dataToSort = await getSecretsForProject({
|
const dataToSort = await getSecretsForProject({
|
||||||
env,
|
env,
|
||||||
setFileState,
|
setFileState,
|
||||||
setIsKeyAvailable,
|
setIsKeyAvailable,
|
||||||
setData,
|
setData,
|
||||||
workspaceId: router.query.id
|
workspaceId: String(router.query.id)
|
||||||
});
|
});
|
||||||
reorderRows(dataToSort);
|
reorderRows(dataToSort);
|
||||||
|
|
||||||
|
setSharedToHide(
|
||||||
|
dataToSort?.filter(row => (dataToSort
|
||||||
|
?.map((item) => item.key)
|
||||||
|
.filter(
|
||||||
|
(item, index) =>
|
||||||
|
index !==
|
||||||
|
dataToSort?.map((item) => item.key).indexOf(item)
|
||||||
|
).includes(row.key) && row.type == 'shared'))?.map((item) => item.id)
|
||||||
|
)
|
||||||
|
|
||||||
const user = await getUser();
|
const user = await getUser();
|
||||||
setIsNew(
|
setIsNew(
|
||||||
(Date.parse(new Date()) - Date.parse(user.createdAt)) / 60000 < 3
|
(Date.parse(String(new Date())) - Date.parse(user.createdAt)) / 60000 < 3
|
||||||
? true
|
? true
|
||||||
: false
|
: false
|
||||||
);
|
);
|
||||||
|
|
||||||
let userAction = await checkUserAction({
|
const userAction = await checkUserAction({
|
||||||
action: 'first_time_secrets_pushed'
|
action: 'first_time_secrets_pushed'
|
||||||
});
|
});
|
||||||
setHasUserEverPushed(userAction ? true : false);
|
setHasUserEverPushed(userAction ? true : false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Error', error);
|
console.log('Error', error);
|
||||||
setData([]);
|
setData(undefined);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
@@ -321,10 +193,10 @@ export default function Dashboard() {
|
|||||||
const addRow = () => {
|
const addRow = () => {
|
||||||
setIsNew(false);
|
setIsNew(false);
|
||||||
setData([
|
setData([
|
||||||
...data,
|
...data!,
|
||||||
{
|
{
|
||||||
id: guidGenerator(),
|
id: guidGenerator(),
|
||||||
pos: data.length,
|
pos: data!.length,
|
||||||
key: '',
|
key: '',
|
||||||
value: '',
|
value: '',
|
||||||
type: 'shared'
|
type: 'shared'
|
||||||
@@ -332,45 +204,85 @@ export default function Dashboard() {
|
|||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteRow = (id) => {
|
interface overrideProps {
|
||||||
setButtonReady(true);
|
id: string;
|
||||||
setData(data.filter((row) => row.id !== id));
|
keyName: string;
|
||||||
|
value: string;
|
||||||
|
pos: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function add an ovverrided version of a certain secret to the current user
|
||||||
|
* @param {object} obj
|
||||||
|
* @param {string} obj.id - if of this secret that is about to be overriden
|
||||||
|
* @param {string} obj.keyName - key name of this secret
|
||||||
|
* @param {string} obj.value - value of this secret
|
||||||
|
* @param {string} obj.pos - position of this secret on the dashboard
|
||||||
|
*/
|
||||||
|
const addOverride = ({ id, keyName, value, pos }: overrideProps) => {
|
||||||
|
setIsNew(false);
|
||||||
|
const tempdata: SecretDataProps[] | 1 = [
|
||||||
|
...data!,
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
pos: pos,
|
||||||
|
key: keyName,
|
||||||
|
value: value,
|
||||||
|
type: 'personal'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
sortValuesHandler(tempdata, sortMethod == "alhpabetical" ? "-alphabetical" : "alphabetical");
|
||||||
};
|
};
|
||||||
|
|
||||||
const modifyValue = (value, pos) => {
|
const deleteRow = (id: string) => {
|
||||||
|
setButtonReady(true);
|
||||||
|
setData(data!.filter((row: SecretDataProps) => row.id !== id));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function deleted the override of a certain secrer
|
||||||
|
* @param {string} id - id of a secret to be deleted
|
||||||
|
*/
|
||||||
|
const deleteOverride = (id: string) => {
|
||||||
|
setButtonReady(true);
|
||||||
|
const tempData = data!.filter((row: SecretDataProps) => !(row.id == id && row.type == 'personal'))
|
||||||
|
sortValuesHandler(tempData, sortMethod == "alhpabetical" ? "-alphabetical" : "alphabetical")
|
||||||
|
};
|
||||||
|
|
||||||
|
const modifyValue = (value: string, pos: number) => {
|
||||||
setData((oldData) => {
|
setData((oldData) => {
|
||||||
oldData[pos].value = value;
|
oldData![pos].value = value;
|
||||||
return [...oldData];
|
return [...oldData!];
|
||||||
});
|
});
|
||||||
setButtonReady(true);
|
setButtonReady(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const modifyKey = (value, pos) => {
|
const modifyKey = (value: string, pos: number) => {
|
||||||
setData((oldData) => {
|
setData((oldData) => {
|
||||||
oldData[pos].key = value;
|
oldData![pos].key = value;
|
||||||
return [...oldData];
|
return [...oldData!];
|
||||||
});
|
});
|
||||||
setButtonReady(true);
|
setButtonReady(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const modifyVisibility = (value, pos) => {
|
const modifyVisibility = (value: "shared" | "personal", pos: number) => {
|
||||||
setData((oldData) => {
|
setData((oldData) => {
|
||||||
oldData[pos].type = value;
|
oldData![pos].type = value;
|
||||||
return [...oldData];
|
return [...oldData!];
|
||||||
});
|
});
|
||||||
setButtonReady(true);
|
setButtonReady(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// For speed purposes and better perforamance, we are using useCallback
|
// For speed purposes and better perforamance, we are using useCallback
|
||||||
const listenChangeValue = useCallback((value, pos) => {
|
const listenChangeValue = useCallback((value: string, pos: number) => {
|
||||||
modifyValue(value, pos);
|
modifyValue(value, pos);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const listenChangeKey = useCallback((value, pos) => {
|
const listenChangeKey = useCallback((value: string, pos: number) => {
|
||||||
modifyKey(value, pos);
|
modifyKey(value, pos);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const listenChangeVisibility = useCallback((value, pos) => {
|
const listenChangeVisibility = useCallback((value: "shared" | "personal", pos: number) => {
|
||||||
modifyVisibility(value, pos);
|
modifyVisibility(value, pos);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -379,21 +291,16 @@ export default function Dashboard() {
|
|||||||
*/
|
*/
|
||||||
const savePush = async () => {
|
const savePush = async () => {
|
||||||
// Format the new object with environment variables
|
// Format the new object with environment variables
|
||||||
let obj = Object.assign(
|
const obj = Object.assign(
|
||||||
{},
|
{},
|
||||||
...data.map((row) => ({ [row.key]: [row.value, row.type] }))
|
...data!.map((row: SecretDataProps) => ({ [row.type.charAt(0) + row.key]: [row.value, row.type] }))
|
||||||
);
|
);
|
||||||
|
|
||||||
// Checking if any of the secret keys start with a number - if so, don't do anything
|
// Checking if any of the secret keys start with a number - if so, don't do anything
|
||||||
const nameErrors = !Object.keys(obj)
|
const nameErrors = !Object.keys(obj)
|
||||||
.map((key) => !isNaN(key.charAt(0)))
|
.map((key) => !isNaN(Number(key[0].charAt(0))))
|
||||||
.every((v) => v === false);
|
.every((v) => v === false);
|
||||||
const duplicatesExist =
|
const duplicatesExist = findDuplicates(data!.map((item: SecretDataProps) => item.key + item.type)).length > 0;
|
||||||
data
|
|
||||||
?.map((item) => item.key)
|
|
||||||
.filter(
|
|
||||||
(item, index) => index !== data?.map((item) => item.key).indexOf(item)
|
|
||||||
).length > 0;
|
|
||||||
|
|
||||||
if (nameErrors) {
|
if (nameErrors) {
|
||||||
return createNotification({
|
return createNotification({
|
||||||
@@ -409,9 +316,11 @@ export default function Dashboard() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('pushing', obj)
|
||||||
|
|
||||||
// Once "Save changed is clicked", disable that button
|
// Once "Save changed is clicked", disable that button
|
||||||
setButtonReady(false);
|
setButtonReady(false);
|
||||||
pushKeys({ obj, workspaceId: router.query.id, env });
|
pushKeys({ obj, workspaceId: String(router.query.id), env });
|
||||||
|
|
||||||
// If this user has never saved environment variables before, show them a prompt to read docs
|
// If this user has never saved environment variables before, show them a prompt to read docs
|
||||||
if (!hasUserEverPushed) {
|
if (!hasUserEverPushed) {
|
||||||
@@ -420,8 +329,8 @@ export default function Dashboard() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const addData = (newData) => {
|
const addData = (newData: SecretDataProps[]) => {
|
||||||
setData(data.concat(newData));
|
setData(data!.concat(newData));
|
||||||
setButtonReady(true);
|
setButtonReady(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -429,37 +338,39 @@ export default function Dashboard() {
|
|||||||
setBlurred(!blurred);
|
setBlurred(!blurred);
|
||||||
};
|
};
|
||||||
|
|
||||||
const sortValuesHandler = (dataToSort) => {
|
const sortValuesHandler = (dataToSort: SecretDataProps[] | 1, specificSortMethod?: 'alphabetical' | '-alphabetical') => {
|
||||||
const sortedData = (dataToSort != 1 ? dataToSort : data)
|
const howToSort = specificSortMethod == undefined ? sortMethod : specificSortMethod;
|
||||||
.sort((a, b) =>
|
const sortedData = (dataToSort != 1 ? dataToSort : data)!
|
||||||
sortMethod == 'alphabetical'
|
.sort((a, b) =>
|
||||||
? a.key.localeCompare(b.key)
|
howToSort == 'alphabetical'
|
||||||
: b.key.localeCompare(a.key)
|
? a.key.localeCompare(b.key)
|
||||||
)
|
: b.key.localeCompare(a.key)
|
||||||
.map((item, index) => {
|
)
|
||||||
return {
|
.map((item: SecretDataProps, index: number) => {
|
||||||
...item,
|
return {
|
||||||
pos: index
|
...item,
|
||||||
};
|
pos: index
|
||||||
});
|
};
|
||||||
|
});
|
||||||
|
console.log('override', sortedData)
|
||||||
|
|
||||||
setData(sortedData);
|
setData(sortedData);
|
||||||
};
|
};
|
||||||
|
|
||||||
// This function downloads the secrets as a .env file
|
// This function downloads the secrets as a .env file
|
||||||
const download = () => {
|
const download = () => {
|
||||||
const file = data
|
const file = data!
|
||||||
.map((item) => [item.key, item.value].join('='))
|
.map((item: SecretDataProps) => [item.key, item.value].join('='))
|
||||||
.join('\n');
|
.join('\n');
|
||||||
const blob = new Blob([file]);
|
const blob = new Blob([file]);
|
||||||
const fileDownloadUrl = URL.createObjectURL(blob);
|
const fileDownloadUrl = URL.createObjectURL(blob);
|
||||||
let alink = document.createElement('a');
|
const alink = document.createElement('a');
|
||||||
alink.href = fileDownloadUrl;
|
alink.href = fileDownloadUrl;
|
||||||
alink.download = envMapping[env] + '.env';
|
alink.download = envMapping[env] + '.env';
|
||||||
alink.click();
|
alink.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteCertainRow = (id) => {
|
const deleteCertainRow = (id: string) => {
|
||||||
deleteRow(id);
|
deleteRow(id);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -467,15 +378,17 @@ export default function Dashboard() {
|
|||||||
* This function copies the project id to the clipboard
|
* This function copies the project id to the clipboard
|
||||||
*/
|
*/
|
||||||
function copyToClipboard() {
|
function copyToClipboard() {
|
||||||
var copyText = document.getElementById('myInput');
|
const copyText = document.getElementById('myInput') as HTMLInputElement;
|
||||||
|
|
||||||
|
if (copyText) {
|
||||||
|
copyText.select();
|
||||||
|
copyText.setSelectionRange(0, 99999); // For mobile devices
|
||||||
|
|
||||||
copyText.select();
|
navigator.clipboard.writeText(copyText.value);
|
||||||
copyText.setSelectionRange(0, 99999); // For mobile devices
|
|
||||||
|
setProjectIdCopied(true);
|
||||||
navigator.clipboard.writeText(copyText.value);
|
setTimeout(() => setProjectIdCopied(false), 2000);
|
||||||
|
}
|
||||||
setProjectIdCopied(true);
|
|
||||||
setTimeout(() => setProjectIdCopied(false), 2000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return data ? (
|
return data ? (
|
||||||
@@ -491,6 +404,18 @@ export default function Dashboard() {
|
|||||||
/>
|
/>
|
||||||
</Head>
|
</Head>
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
|
{sidebarSecretId != "None" && <SideBar
|
||||||
|
toggleSidebar={toggleSidebar}
|
||||||
|
data={data.filter((row: SecretDataProps) => row.id == sidebarSecretId)}
|
||||||
|
modifyKey={listenChangeKey}
|
||||||
|
modifyValue={listenChangeValue}
|
||||||
|
addOverride={addOverride}
|
||||||
|
deleteOverride={deleteOverride}
|
||||||
|
buttonReady={buttonReady}
|
||||||
|
savePush={savePush}
|
||||||
|
sharedToHide={sharedToHide}
|
||||||
|
setSharedToHide={setSharedToHide}
|
||||||
|
/>}
|
||||||
<div className="w-full max-h-96 pb-2">
|
<div className="w-full max-h-96 pb-2">
|
||||||
<NavHeader pageName="Secrets" isProjectRelated={true} />
|
<NavHeader pageName="Secrets" isProjectRelated={true} />
|
||||||
{checkDocsPopUpVisible && (
|
{checkDocsPopUpVisible && (
|
||||||
@@ -513,7 +438,6 @@ export default function Dashboard() {
|
|||||||
data={['Development', 'Staging', 'Production', 'Testing']}
|
data={['Development', 'Staging', 'Production', 'Testing']}
|
||||||
// ref={useRef(123)}
|
// ref={useRef(123)}
|
||||||
onChange={setEnv}
|
onChange={setEnv}
|
||||||
className="z-40"
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -568,7 +492,6 @@ export default function Dashboard() {
|
|||||||
data={['Development', 'Staging', 'Production', 'Testing']}
|
data={['Development', 'Staging', 'Production', 'Testing']}
|
||||||
// ref={useRef(123)}
|
// ref={useRef(123)}
|
||||||
onChange={setEnv}
|
onChange={setEnv}
|
||||||
className="z-40"
|
|
||||||
/>
|
/>
|
||||||
<div className="h-10 w-full bg-white/5 hover:bg-white/10 ml-2 flex items-center rounded-md flex flex-row items-center">
|
<div className="h-10 w-full bg-white/5 hover:bg-white/10 ml-2 flex items-center rounded-md flex flex-row items-center">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
@@ -630,144 +553,55 @@ export default function Dashboard() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{data?.length !== 0 ? (
|
{data?.length !== 0 ? (
|
||||||
<div
|
<div className="flex flex-col w-full mt-1 mb-2">
|
||||||
id="dataall"
|
<div className='bg-mineshaft-800 rounded-md px-2 py-2 max-w-5xl'>
|
||||||
className="flex flex-col max-h-40 grow max-h-[calc(100vh-240px)] w-full overflow-y-scroll no-scrollbar no-scrollbar::-webkit-scrollbar"
|
<div
|
||||||
>
|
className={`mt-1 max-h-[calc(100vh-280px)] overflow-hidden overflow-y-scroll no-scrollbar no-scrollbar::-webkit-scrollbar`}
|
||||||
<div
|
>
|
||||||
className={`bg-white/5 mt-1 mb-1 rounded-md pb-2 max-w-5xl overflow-visible`}
|
<div className="px-1 pt-2">
|
||||||
>
|
{data?.filter(row => !(sharedToHide.includes(row.id) && row.type == 'shared')).map((keyPair) => (
|
||||||
<div className="rounded-t-md sticky top-0 z-20 bg-bunker flex flex-row pl-4 pr-6 pt-4 pb-2 items-center justify-between text-gray-300 font-bold">
|
<KeyPair
|
||||||
{/* <FontAwesomeIcon icon={faAngleDown} /> */}
|
|
||||||
<div className="flex flex-row items-center">
|
|
||||||
<p className="pl-2 text-lg">Personal</p>
|
|
||||||
<div className="group font-normal group relative inline-block text-gray-300 underline hover:text-primary duration-200">
|
|
||||||
<FontAwesomeIcon
|
|
||||||
className="ml-3 mt-1 text-lg"
|
|
||||||
icon={faCircleInfo}
|
|
||||||
/>
|
|
||||||
<span className="absolute hidden group-hover:flex group-hover:animate-popdown duration-300 w-44 -left-16 -top-7 translate-y-full px-2 py-2 bg-gray-700 rounded-md text-center text-gray-100 text-sm after:content-[''] after:absolute after:left-1/2 after:bottom-[100%] after:-translate-x-1/2 after:border-8 after:border-x-transparent after:border-t-transparent after:border-b-gray-700">
|
|
||||||
Personal keys are only visible to you
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="data1" className="">
|
|
||||||
{data
|
|
||||||
.filter(
|
|
||||||
(keyPair) =>
|
|
||||||
keyPair.key
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(searchKeys.toLowerCase()) &&
|
|
||||||
keyPair.type == 'personal'
|
|
||||||
)
|
|
||||||
?.map((keyPair) => (
|
|
||||||
<KeyPair
|
|
||||||
key={keyPair.id}
|
key={keyPair.id}
|
||||||
keyPair={keyPair}
|
keyPair={keyPair}
|
||||||
deleteRow={deleteCertainRow}
|
deleteRow={deleteCertainRow}
|
||||||
modifyValue={listenChangeValue}
|
modifyValue={listenChangeValue}
|
||||||
modifyKey={listenChangeKey}
|
modifyKey={listenChangeKey}
|
||||||
modifyVisibility={listenChangeVisibility}
|
|
||||||
isBlurred={blurred}
|
isBlurred={blurred}
|
||||||
duplicates={data
|
isDuplicate={findDuplicates(data?.map((item) => item.key + item.type))?.includes(keyPair.key + keyPair.type)}
|
||||||
?.map((item) => item.key)
|
toggleSidebar={toggleSidebar}
|
||||||
.filter(
|
sidebarSecretId={sidebarSecretId}
|
||||||
(item, index) =>
|
|
||||||
index !==
|
|
||||||
data?.map((item) => item.key).indexOf(item)
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="w-full max-w-5xl px-2 pt-2">
|
||||||
<div
|
<DropZone
|
||||||
className={`bg-white/5 mt-1 mb-2 rounded-md p-1 pb-2 max-w-5xl ${
|
setData={addData}
|
||||||
data?.length > 8 ? 'h-3/4' : 'h-min'
|
setErrorDragAndDrop={setErrorDragAndDrop}
|
||||||
}`}
|
createNewFile={addRow}
|
||||||
>
|
errorDragAndDrop={errorDragAndDrop}
|
||||||
<div className="sticky top-0 z-40 bg-bunker flex flex-row pl-4 pr-5 pt-4 pb-2 items-center justify-between text-gray-300 font-bold">
|
setButtonReady={setButtonReady}
|
||||||
{/* <FontAwesomeIcon icon={faAngleDown} /> */}
|
keysExist={true}
|
||||||
<div className="flex flex-row items-center">
|
numCurrentRows={data.length}
|
||||||
<p className="pl-2 text-lg">Shared</p>
|
/>
|
||||||
<div className="group font-normal group relative inline-block text-gray-300 underline hover:text-primary duration-200">
|
|
||||||
<FontAwesomeIcon
|
|
||||||
className="ml-3 text-lg mt-1"
|
|
||||||
icon={faCircleInfo}
|
|
||||||
/>
|
|
||||||
<span className="absolute hidden group-hover:flex group-hover:animate-popdown duration-300 w-44 -left-16 -top-7 translate-y-full px-2 py-2 bg-gray-700 rounded-md text-center text-gray-100 text-sm after:content-[''] after:absolute after:left-1/2 after:bottom-[100%] after:-translate-x-1/2 after:border-8 after:border-x-transparent after:border-t-transparent after:border-b-gray-700">
|
|
||||||
Shared keys are visible to your whole team
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="data2" className="data2">
|
|
||||||
{data
|
|
||||||
.filter(
|
|
||||||
(keyPair) =>
|
|
||||||
keyPair.key
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(searchKeys.toLowerCase()) &&
|
|
||||||
keyPair.type == 'shared'
|
|
||||||
)
|
|
||||||
?.map((keyPair) => (
|
|
||||||
<KeyPair
|
|
||||||
key={keyPair.id}
|
|
||||||
keyPair={keyPair}
|
|
||||||
deleteRow={deleteCertainRow}
|
|
||||||
modifyValue={listenChangeValue}
|
|
||||||
modifyKey={listenChangeKey}
|
|
||||||
modifyVisibility={listenChangeVisibility}
|
|
||||||
isBlurred={blurred}
|
|
||||||
duplicates={data
|
|
||||||
?.map((item) => item.key)
|
|
||||||
.filter(
|
|
||||||
(item, index) =>
|
|
||||||
index !==
|
|
||||||
data?.map((item) => item.key).indexOf(item)
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-full max-w-5xl">
|
|
||||||
<DropZone
|
|
||||||
setData={addData}
|
|
||||||
setErrorDragAndDrop={setErrorDragAndDrop}
|
|
||||||
createNewFile={addRow}
|
|
||||||
errorDragAndDrop={errorDragAndDrop}
|
|
||||||
setButtonReady={setButtonReady}
|
|
||||||
keysExist={true}
|
|
||||||
numCurrentRows={data.length}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center justify-center h-full text-xl text-gray-400 max-w-5xl mt-28">
|
<div className="flex flex-col items-center justify-center h-full text-xl text-gray-400 max-w-5xl mt-28">
|
||||||
{fileState.message != "There's nothing to pull" &&
|
{isKeyAvailable && (
|
||||||
fileState.message != undefined && (
|
<DropZone
|
||||||
<FontAwesomeIcon
|
setData={setData}
|
||||||
className="text-7xl mb-8"
|
setErrorDragAndDrop={setErrorDragAndDrop}
|
||||||
icon={faFolderOpen}
|
createNewFile={addRow}
|
||||||
/>
|
errorDragAndDrop={errorDragAndDrop}
|
||||||
)}
|
setButtonReady={setButtonReady}
|
||||||
{(fileState.message == "There's nothing to pull" ||
|
numCurrentRows={data.length}
|
||||||
fileState.message == undefined) &&
|
keysExist={false}
|
||||||
isKeyAvailable && (
|
/>
|
||||||
<DropZone
|
|
||||||
setData={setData}
|
|
||||||
setErrorDragAndDrop={setErrorDragAndDrop}
|
|
||||||
createNewFile={addRow}
|
|
||||||
errorDragAndDrop={errorDragAndDrop}
|
|
||||||
setButtonReady={setButtonReady}
|
|
||||||
numCurrentRows={data.length}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{fileState.message ==
|
|
||||||
'Failed membership validation for workspace' && (
|
|
||||||
<p>You are not authorized to view this project.</p>
|
|
||||||
)}
|
)}
|
||||||
{fileState.message == 'Access needed to pull the latest file' ||
|
{
|
||||||
|
// fileState.message == 'Access needed to pull the latest file' ||
|
||||||
(!isKeyAvailable && (
|
(!isKeyAvailable && (
|
||||||
<>
|
<>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
Reference in New Issue
Block a user