Merge pull request #6389 from ToolJet/copilot-scope-changed

improvement - changing scope of API key from user to workspace
This commit is contained in:
Akshay 2023-05-16 12:30:42 +05:30 committed by GitHub
commit f8df7d1568
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 172 additions and 86 deletions

View file

@ -8,6 +8,7 @@ export const ApiKeyContainer = ({
isLoading = false,
darkMode,
isCopilotEnabled,
isAdmin = false,
}) => {
const [inputValue, setInputValue] = useState(copilotApiKey);
@ -19,57 +20,82 @@ export const ApiKeyContainer = ({
setInputValue(copilotApiKey);
}, [copilotApiKey]);
return (
<div className="container-xl mt-3">
<div className="row">
<small className="text-green">
<img className="encrypted-icon" src="assets/images/icons/padlock2.svg" width="12" height="12" />
<span className="text-success mx-2 font-500">API KEY</span>
</small>
<div className="mb-3 col-6">
<input
disabled={!isCopilotEnabled}
type="password"
class="form-control mt-2"
name="example-text-input"
placeholder=""
value={inputValue}
onChange={handleOnchange}
/>
const AdminInfoComponent = () => {
return (
<>
<h4 class="alert-title"> Don&apos;t have an API key?</h4>
<div class="text-muted">
<strong style={{ fontWeight: 700, color: '#3E63DD' }}>ToolJet Copilot </strong>
is currently in <strong style={{ fontWeight: 700, color: '#3E63DD' }}>beta</strong> and provided on request.
Join our waitlist to be notified when API keys become available, or sign up for beta access to get started
today.
</div>
<div className="col-auto mt-1">
<div className="mt-2 w-25">
<Button
onClick={() => handleOnSave(inputValue)}
onClick={() => window.open('https://tooljet.com/copilot', '_blank')}
darkMode={darkMode}
size="md"
isLoading={isLoading}
styles={{ backgroundColor: '#3E63DD', color: '#fff' }}
disabled={!isCopilotEnabled}
size="sm"
styles={{ width: '100%', fontSize: '12px', fontWeight: 500, borderColor: darkMode && 'transparent' }}
>
<Button.Content title={'Save'} iconSrc={'assets/images/icons/save.svg'} />
<Button.Content title={' Sign up for Beta Access'} />
</Button>
</div>
</div>
</>
);
};
return (
<div className="container-xl mt-3">
{isAdmin && (
<div className="row">
<small className="text-green">
<img className="encrypted-icon" src="assets/images/icons/padlock2.svg" width="12" height="12" />
<span className="text-success mx-2 font-500">API KEY</span>
</small>
<div className="mb-3 col-6">
<input
disabled={!isCopilotEnabled}
type="password"
class="form-control mt-2"
name="example-text-input"
placeholder=""
value={inputValue}
onChange={handleOnchange}
/>
</div>
<div className="col-auto mt-1">
<Button
onClick={() => handleOnSave(inputValue)}
darkMode={darkMode}
size="md"
isLoading={isLoading}
styles={{ backgroundColor: '#3E63DD', color: '#fff' }}
disabled={!isCopilotEnabled}
>
<Button.Content title={'Save'} iconSrc={'assets/images/icons/save.svg'} />
</Button>
</div>
</div>
)}
<div className="alert-container">
<Alert svg="alert-info" cls="copilot-alert" data-cy={`copilot-alert-info`}>
<h4 class="alert-title"> Don&apos;t have an API key?</h4>
<div class="text-muted">
<strong style={{ fontWeight: 700, color: '#3E63DD' }}>ToolJet Copilot </strong>
is currently in <strong style={{ fontWeight: 700, color: '#3E63DD' }}>beta</strong> and provided on request.
Join our waitlist to be notified when API keys become available, or sign up for beta access to get started
today.
</div>
<div className="mt-2 w-25">
<Button
onClick={() => window.open('https://tooljet.com/copilot', '_blank')}
darkMode={darkMode}
size="sm"
styles={{ width: '100%', fontSize: '12px', fontWeight: 500, borderColor: darkMode && 'transparent' }}
>
<Button.Content title={' Sign up for Beta Access'} />
</Button>
</div>
{isAdmin ? (
<AdminInfoComponent />
) : (
<>
<div class="text-muted">
<strong style={{ fontWeight: 700, color: '#3E63DD' }}>ToolJet Copilot </strong>
is currently in <strong style={{ fontWeight: 700, color: '#3E63DD' }}>beta</strong> and provided on
request. Join our waitlist to be notified when API keys become available, or sign up for beta access to
get started today.
</div>
<div class="text-muted mt-2">
<strong style={{ fontWeight: 700, color: '#df4759' }}>Please note : </strong>
Copilot functionality is dependent on your workspace admin completing the setup process.
</div>
</>
)}
</Alert>
</div>
</div>

View file

@ -8,10 +8,12 @@ import { Button } from '@/_ui/LeftSidebar';
import { useLocalStorageState } from '@/_hooks/use-local-storage';
export const CopilotSetting = () => {
const { current_organization_id } = authenticationService.currentSessionValue;
const { current_organization_id, current_organization_name, admin } = authenticationService.currentSessionValue;
const currentOrgName = current_organization_name.replace(/\s/g, '').toLowerCase();
const [copilotApiKey, setCopilotApiKey] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [state, setState] = useLocalStorageState(`copilotEnabled-${current_organization_id}`, false);
const [state, setState] = useLocalStorageState(`copilotEnabled-${currentOrgName}`, false);
const [copilotWorkspaceVarId, set] = useState(null);
const saveCopilotApiKey = async (apikey) => {
@ -30,7 +32,10 @@ export const CopilotSetting = () => {
console.log(err);
return toast.error('Something went wrong');
})
.finally(() => setIsLoading(false));
.finally(() => {
setIsLoading(false);
orgEnvironmentVariableService.create(`copilot_enabled-${current_organization_id}`, 'true', 'client', false);
});
}
if (isCopilotApiKeyPresent === true && copilotWorkspaceVarId) {
@ -58,7 +63,7 @@ export const CopilotSetting = () => {
const validateApiKey = (apiKey) => {
return new Promise((resolve, reject) => {
copilotService
.validateCopilotAPIKey(apiKey)
.validateCopilotAPIKey(apiKey, current_organization_id)
.then(({ status }) => {
if (status === 'ok') {
return resolve(true);
@ -72,33 +77,56 @@ export const CopilotSetting = () => {
});
};
useEffect(() => {
if (!state) {
return;
}
orgEnvironmentVariableService.getVariables().then((data) => {
const isCopilotApiKeyPresent = data.variables.some(
(variable) => variable.variable_name === `copilot_api_key-${current_organization_id}`
);
const shouldUpdate = isCopilotApiKeyPresent;
if (shouldUpdate) {
const copilotVariableId = data.variables.find(
(variable) => variable.variable_name === `copilot_api_key-${current_organization_id}`
)?.id;
const key = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
set(copilotVariableId);
setCopilotApiKey(key);
}
const updateCopilotEnabled = (id, value, variableName) => {
return orgEnvironmentVariableService.update(id, variableName, `${value}`).catch((err) => {
console.log(err);
});
};
useEffect(() => {
if (!admin) {
orgEnvironmentVariableService.getVariables().then((data) => {
const { value } = data.variables.find(
(variable) => variable.variable_name === `copilot_enabled-${current_organization_id}`
);
setState(value === 'true');
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (admin) {
orgEnvironmentVariableService.getVariables().then((data) => {
const isCopilotApiKeyPresent = data.variables.some(
(variable) => variable.variable_name === `copilot_api_key-${current_organization_id}`
);
const { id, variable_name, value } = data.variables.find(
(variable) => variable.variable_name === `copilot_enabled-${current_organization_id}`
);
if (value !== `${state}`) updateCopilotEnabled(id, state, variable_name);
const shouldUpdate = state && isCopilotApiKeyPresent;
if (shouldUpdate) {
const copilotVariableId = data.variables.find(
(variable) => variable.variable_name === `copilot_api_key-${current_organization_id}`
)?.id;
const key = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
set(copilotVariableId);
setCopilotApiKey(key);
}
});
}
return () => {
setCopilotApiKey('');
set(null);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [state]);
const darkMode = localStorage.getItem('darkMode') === 'true';
@ -111,7 +139,7 @@ export const CopilotSetting = () => {
padding: '4rem',
}}
>
<Container isCopilotEnabled={state} handleCopilotToggle={handleCopilotToggle}>
<Container isCopilotEnabled={state} handleCopilotToggle={handleCopilotToggle} isAdmin={admin}>
<div className="row">
<div className="col-12">
<ApiKeyContainer
@ -120,6 +148,7 @@ export const CopilotSetting = () => {
isLoading={isLoading}
darkMode={darkMode}
isCopilotEnabled={state}
isAdmin={admin}
/>
</div>
</div>
@ -130,7 +159,7 @@ export const CopilotSetting = () => {
);
};
const Container = ({ children, isCopilotEnabled, handleCopilotToggle, darkMode }) => {
const Container = ({ children, isCopilotEnabled, handleCopilotToggle, darkMode, isAdmin }) => {
return (
<div className="card p-2 card-container">
<div className="card-header row">
@ -151,6 +180,7 @@ const Container = ({ children, isCopilotEnabled, handleCopilotToggle, darkMode }
toggleSwitchFunction={handleCopilotToggle}
action="enableTransformation"
dataCy={'copilot'}
disabled={!isAdmin}
/>
<span className="mx-2 mt-3 font-weight-400 tranformation-label" data-cy={'label-query-transformation'}>

View file

@ -69,6 +69,7 @@ export function CodeHinter({
popOverCallback,
cyLabel = '',
callgpt = () => null,
isCopilotEnabled = false,
}) {
const darkMode = localStorage.getItem('darkMode') === 'true';
const options = {
@ -323,6 +324,7 @@ export function CodeHinter({
/>
)}
<CodeHinter.Portal
isCopilotEnabled={isCopilotEnabled}
isOpen={isOpen}
callback={setIsOpen}
componentName={componentName}

View file

@ -1,4 +1,5 @@
import React from 'react';
import { Tooltip as ReactTooltip } from 'react-tooltip';
export const CustomToggleSwitch = ({
isChecked,
@ -7,9 +8,14 @@ export const CustomToggleSwitch = ({
darkMode = false,
label = '',
dataCy = '',
disabled = false,
}) => {
return (
<div className={`custom-toggle-switch d-flex col gap-2 align-items-center ${darkMode && 'theme-dark'}`}>
<div
data-tooltip-id="tooltip-for-active-copilot"
data-tooltip-content="Only workspace admins can enable or disable Copilot."
className={`custom-toggle-switch d-flex col gap-2 align-items-center ${darkMode && 'theme-dark'}`}
>
<label className="switch">
<input
type="checkbox"
@ -23,6 +29,7 @@ export const CustomToggleSwitch = ({
}
}}
data-cy={`${dataCy}-toggle-switch`}
disabled={disabled}
/>
<label htmlFor={action} className="slider round"></label>
</label>
@ -31,6 +38,13 @@ export const CustomToggleSwitch = ({
{label}
</span>
)}
{disabled && dataCy === 'copilot' && (
<ReactTooltip
id="tooltip-for-active-copilot"
className="tooltip"
style={{ backgroundColor: '#e6eefe', color: '#222' }}
/>
)}
</div>
);
};

View file

@ -19,7 +19,8 @@ import { authenticationService } from '@/_services';
export const Transformation = ({ changeOption, currentState, options, darkMode, queryId }) => {
const { t } = useTranslation();
const { current_organization_id } = authenticationService.currentSessionValue;
const { current_organization_name } = authenticationService.currentSessionValue;
const currentOrgName = current_organization_name.replace(/\s/g, '').toLowerCase();
const [lang, setLang] = React.useState(options?.transformationLanguage ?? 'javascript');
@ -39,7 +40,7 @@ return [row for row in data if row['amount'] > 1000]
const [state, setState] = useLocalStorageState('transformation', defaultValue);
const [fetchingRecommendation, setFetchingRecommendation] = useState(false);
const isCopilotEnabled = localStorage.getItem(`copilotEnabled-${current_organization_id}`) === 'true';
const isCopilotEnabled = localStorage.getItem(`copilotEnabled-${currentOrgName}`) === 'true';
const handleCallToGPT = async () => {
setFetchingRecommendation(true);
@ -291,7 +292,7 @@ return [row for row in data if row['amount'] > 1000]
styles={{ width: '100%', fontSize: '12px', fontWeight: 500, borderColor: darkMode && 'transparent' }}
disabled={!isCopilotEnabled}
>
<Button.Content title={'Generate code ⌘+L'} />
<Button.Content title={'Generate code'} />
</Button>
</div>
@ -317,6 +318,7 @@ return [row for row in data if row['amount'] > 1000]
componentName={`transformation`}
cyLabel={'transformation-input'}
callgpt={handleCallToGPT}
isCopilotEnabled={isCopilotEnabled}
/>
</div>
)}

View file

@ -74,9 +74,7 @@ class ManageOrgVarsComponent extends React.Component {
});
orgEnvironmentVariableService.getVariables().then((data) => {
const variables = _.cloneDeep(data.variables)?.filter(
({ variable_name }) => !/copilot_api_key/.test(variable_name)
);
const variables = _.cloneDeep(data.variables)?.filter(({ variable_name }) => !/copilot_/.test(variable_name));
this.setState({
variables: variables,
isLoading: false,

View file

@ -53,7 +53,7 @@ export function OrganizationSettings(props) {
{sideBarNavs.map((item, index) => {
return (
<>
{(admin || item == 'Workspace variables') && (
{(admin || item == 'Workspace variables' || item == 'Copilot') && (
<FolderList
className="workspace-settings-nav-items"
key={index}

View file

@ -4,7 +4,8 @@ import { Rnd } from 'react-rnd';
import { Button } from '@/_ui/LeftSidebar';
const Portal = ({ children, ...restProps }) => {
const { isOpen, trigger, styles, className, componentName, dragResizePortal, callgpt } = restProps;
const { isOpen, trigger, styles, className, componentName, dragResizePortal, callgpt, isCopilotEnabled } = restProps;
const [name, setName] = React.useState(componentName);
const handleClose = (e) => {
e.stopPropagation();
@ -45,6 +46,7 @@ const Portal = ({ children, ...restProps }) => {
componentName={name}
dragResizePortal={dragResizePortal}
callgpt={callgpt}
isCopilotEnabled={isCopilotEnabled}
>
{children}
</Portal.Modal>
@ -57,7 +59,17 @@ const Container = ({ children, ...restProps }) => {
return <ReactPortal {...restProps}>{children}</ReactPortal>;
};
const Modal = ({ children, handleClose, portalStyles, styles, componentName, darkMode, dragResizePortal, callgpt }) => {
const Modal = ({
children,
handleClose,
portalStyles,
styles,
componentName,
darkMode,
dragResizePortal,
callgpt,
isCopilotEnabled,
}) => {
const [loading, setLoading] = React.useState(false);
const handleCallGpt = () => {
@ -66,7 +78,6 @@ const Modal = ({ children, handleClose, portalStyles, styles, componentName, dar
callgpt().then(() => setLoading(false));
};
const isCopilotEnabled = localStorage.getItem('copilotEnabled') === 'true';
const includeGPT = ['Runjs', 'Runpy', 'transformation'].includes(componentName) && isCopilotEnabled;
const renderModalContent = () => (
@ -95,7 +106,7 @@ const Modal = ({ children, handleClose, portalStyles, styles, componentName, dar
classNames={`${loading ? (darkMode ? 'btn-loading' : 'button-loading') : ''}`}
styles={{ width: '100%', fontSize: '12px', fontWeight: 500, borderColor: darkMode && 'transparent' }}
>
<Button.Content title={'Generate code ⌘+L'} />
<Button.Content title={'Generate code'} />
</Button>
</div>
)}

View file

@ -13,6 +13,7 @@ const usePortal = ({ children, ...restProps }) => {
selectors = {},
dragResizePortal = false,
callgpt,
isCopilotEnabled = false,
} = restProps;
const renderCustomComponent = ({ component, ...restProps }) => {
@ -38,6 +39,7 @@ const usePortal = ({ children, ...restProps }) => {
componentName={componentName}
dragResizePortal={dragResizePortal}
callgpt={callgpt}
isCopilotEnabled={isCopilotEnabled}
>
<div className={`editor-container ${optionalProps.cls ?? ''}`} key={key}>
{React.cloneElement(children, { ...styleProps })}

View file

@ -20,9 +20,10 @@ async function getCopilotRecommendations(options) {
return data || {};
}
function validateCopilotAPIKey(key) {
function validateCopilotAPIKey(key, organizationId) {
const body = {
secretKey: key,
organizationId,
};
const requestOptions = { method: 'POST', headers: authHeader(), credentials: 'include', body: JSON.stringify(body) };

View file

@ -30,7 +30,7 @@ export class CopilotController {
@UseGuards(JwtAuthGuard)
@Post('api-key')
async validateCopilotAPIKey(@User() user, @Body() body: { secretKey: string }) {
return await this.copilotService.validateCopilotAPIKey(user.id, body.secretKey);
async validateCopilotAPIKey(@User() user, @Body() body: { secretKey: string; organizationId: string }) {
return await this.copilotService.validateCopilotAPIKey(body.organizationId, body.secretKey);
}
}

View file

@ -32,7 +32,7 @@ export class CopilotService {
query: query,
context: context,
language: language,
userId: userId,
workspaceId: orgnaizationId,
}),
});
@ -42,13 +42,13 @@ export class CopilotService {
};
}
async validateCopilotAPIKey(userId: string, secretKey: string) {
async validateCopilotAPIKey(workspaceId: string, secretKey: string) {
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ userId: userId, action: 'get', apiKey: secretKey }),
body: JSON.stringify({ workspaceId: workspaceId, action: 'get', apiKey: secretKey }),
};
const response = await fetch(`${process.env.COPILOT_API_ENDPOINT}/api-key`, options);