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

View file

@ -8,10 +8,12 @@ import { Button } from '@/_ui/LeftSidebar';
import { useLocalStorageState } from '@/_hooks/use-local-storage'; import { useLocalStorageState } from '@/_hooks/use-local-storage';
export const CopilotSetting = () => { 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 [copilotApiKey, setCopilotApiKey] = useState('');
const [isLoading, setIsLoading] = useState(false); 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 [copilotWorkspaceVarId, set] = useState(null);
const saveCopilotApiKey = async (apikey) => { const saveCopilotApiKey = async (apikey) => {
@ -30,7 +32,10 @@ export const CopilotSetting = () => {
console.log(err); console.log(err);
return toast.error('Something went wrong'); 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) { if (isCopilotApiKeyPresent === true && copilotWorkspaceVarId) {
@ -58,7 +63,7 @@ export const CopilotSetting = () => {
const validateApiKey = (apiKey) => { const validateApiKey = (apiKey) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
copilotService copilotService
.validateCopilotAPIKey(apiKey) .validateCopilotAPIKey(apiKey, current_organization_id)
.then(({ status }) => { .then(({ status }) => {
if (status === 'ok') { if (status === 'ok') {
return resolve(true); return resolve(true);
@ -72,33 +77,56 @@ export const CopilotSetting = () => {
}); });
}; };
useEffect(() => { const updateCopilotEnabled = (id, value, variableName) => {
if (!state) { return orgEnvironmentVariableService.update(id, variableName, `${value}`).catch((err) => {
return; console.log(err);
}
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);
}
}); });
};
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 () => { return () => {
setCopilotApiKey(''); setCopilotApiKey('');
set(null); set(null);
}; };
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, [state]);
const darkMode = localStorage.getItem('darkMode') === 'true'; const darkMode = localStorage.getItem('darkMode') === 'true';
@ -111,7 +139,7 @@ export const CopilotSetting = () => {
padding: '4rem', padding: '4rem',
}} }}
> >
<Container isCopilotEnabled={state} handleCopilotToggle={handleCopilotToggle}> <Container isCopilotEnabled={state} handleCopilotToggle={handleCopilotToggle} isAdmin={admin}>
<div className="row"> <div className="row">
<div className="col-12"> <div className="col-12">
<ApiKeyContainer <ApiKeyContainer
@ -120,6 +148,7 @@ export const CopilotSetting = () => {
isLoading={isLoading} isLoading={isLoading}
darkMode={darkMode} darkMode={darkMode}
isCopilotEnabled={state} isCopilotEnabled={state}
isAdmin={admin}
/> />
</div> </div>
</div> </div>
@ -130,7 +159,7 @@ export const CopilotSetting = () => {
); );
}; };
const Container = ({ children, isCopilotEnabled, handleCopilotToggle, darkMode }) => { const Container = ({ children, isCopilotEnabled, handleCopilotToggle, darkMode, isAdmin }) => {
return ( return (
<div className="card p-2 card-container"> <div className="card p-2 card-container">
<div className="card-header row"> <div className="card-header row">
@ -151,6 +180,7 @@ const Container = ({ children, isCopilotEnabled, handleCopilotToggle, darkMode }
toggleSwitchFunction={handleCopilotToggle} toggleSwitchFunction={handleCopilotToggle}
action="enableTransformation" action="enableTransformation"
dataCy={'copilot'} dataCy={'copilot'}
disabled={!isAdmin}
/> />
<span className="mx-2 mt-3 font-weight-400 tranformation-label" data-cy={'label-query-transformation'}> <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, popOverCallback,
cyLabel = '', cyLabel = '',
callgpt = () => null, callgpt = () => null,
isCopilotEnabled = false,
}) { }) {
const darkMode = localStorage.getItem('darkMode') === 'true'; const darkMode = localStorage.getItem('darkMode') === 'true';
const options = { const options = {
@ -323,6 +324,7 @@ export function CodeHinter({
/> />
)} )}
<CodeHinter.Portal <CodeHinter.Portal
isCopilotEnabled={isCopilotEnabled}
isOpen={isOpen} isOpen={isOpen}
callback={setIsOpen} callback={setIsOpen}
componentName={componentName} componentName={componentName}

View file

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { Tooltip as ReactTooltip } from 'react-tooltip';
export const CustomToggleSwitch = ({ export const CustomToggleSwitch = ({
isChecked, isChecked,
@ -7,9 +8,14 @@ export const CustomToggleSwitch = ({
darkMode = false, darkMode = false,
label = '', label = '',
dataCy = '', dataCy = '',
disabled = false,
}) => { }) => {
return ( 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"> <label className="switch">
<input <input
type="checkbox" type="checkbox"
@ -23,6 +29,7 @@ export const CustomToggleSwitch = ({
} }
}} }}
data-cy={`${dataCy}-toggle-switch`} data-cy={`${dataCy}-toggle-switch`}
disabled={disabled}
/> />
<label htmlFor={action} className="slider round"></label> <label htmlFor={action} className="slider round"></label>
</label> </label>
@ -31,6 +38,13 @@ export const CustomToggleSwitch = ({
{label} {label}
</span> </span>
)} )}
{disabled && dataCy === 'copilot' && (
<ReactTooltip
id="tooltip-for-active-copilot"
className="tooltip"
style={{ backgroundColor: '#e6eefe', color: '#222' }}
/>
)}
</div> </div>
); );
}; };

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -32,7 +32,7 @@ export class CopilotService {
query: query, query: query,
context: context, context: context,
language: language, 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 = { const options = {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', '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); const response = await fetch(`${process.env.COPILOT_API_ENDPOINT}/api-key`, options);