mirror of
https://github.com/Kong/insomnia.git
synced 2026-04-20 22:27:24 -04:00
External Vault AWS: Add new settings page for cloud service credentials[INS-4623] (#2)
* Initial check-in for smaller part of AWS change, only including setting modal for add cloud service provider credentials. * fix issue from comment
This commit is contained in:
1648
package-lock.json
generated
1648
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -37,6 +37,8 @@
|
||||
"dependencies": {
|
||||
"@apideck/better-ajv-errors": "^0.3.6",
|
||||
"@apidevtools/swagger-parser": "10.1.0",
|
||||
"@aws-sdk/client-secrets-manager": "^3.686.0",
|
||||
"@aws-sdk/client-sts": "^3.686.0",
|
||||
"@bufbuild/protobuf": "^1.8.0",
|
||||
"@connectrpc/connect": "^1.4.0",
|
||||
"@connectrpc/connect-node": "^1.4.0",
|
||||
|
||||
@@ -13,6 +13,7 @@ import { SegmentEvent, trackSegmentEvent } from './main/analytics';
|
||||
import { registerInsomniaProtocols } from './main/api.protocol';
|
||||
import { backupIfNewerVersionAvailable } from './main/backup';
|
||||
import { registerGitServiceAPI } from './main/git-service';
|
||||
import { registerCloudServiceHandlers } from './main/ipc/cloud-service-integration/cloud-service';
|
||||
import { ipcMainOn, ipcMainOnce, registerElectronHandlers } from './main/ipc/electron';
|
||||
import { registergRPCHandlers } from './main/ipc/grpc';
|
||||
import { registerMainHandlers } from './main/ipc/main';
|
||||
@@ -69,6 +70,7 @@ app.on('ready', async () => {
|
||||
registerWebSocketHandlers();
|
||||
registerCurlHandlers();
|
||||
registerSecretStorageHandlers();
|
||||
registerCloudServiceHandlers();
|
||||
|
||||
/**
|
||||
* There's no option that prevents Electron from fetching spellcheck dictionaries from Chromium's CDN and passing a non-resolving URL is the only known way to prevent it from fetching.
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { GetCallerIdentityCommand, type GetCallerIdentityCommandOutput, STSClient, STSServiceException } from '@aws-sdk/client-sts';
|
||||
|
||||
import type { AWSTemporaryCredential, CloudProviderName } from '../../../models/cloud-credential';
|
||||
import type { AWSSecretConfig, CloudServiceResult, ICloudService } from './types';
|
||||
|
||||
export type AWSGetSecretConfig = Omit<AWSSecretConfig, 'SecretId' | 'SecretType' | 'SecretKey'>;
|
||||
export const providerName: CloudProviderName = 'aws';
|
||||
export class AWSService implements ICloudService {
|
||||
_credential: AWSTemporaryCredential;
|
||||
|
||||
constructor(credential: AWSTemporaryCredential) {
|
||||
this._credential = credential;
|
||||
}
|
||||
|
||||
async authenticate(): Promise<CloudServiceResult<GetCallerIdentityCommandOutput>> {
|
||||
const { region, accessKeyId, secretAccessKey, sessionToken } = this._credential;
|
||||
const stsClient = new STSClient({
|
||||
region,
|
||||
credentials: {
|
||||
accessKeyId, secretAccessKey, sessionToken,
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await stsClient.send(new GetCallerIdentityCommand({}));
|
||||
return {
|
||||
success: true,
|
||||
result: response,
|
||||
};
|
||||
} catch (error) {
|
||||
const errorDetail = {
|
||||
errorCode: error.code || 'UnknownError',
|
||||
errorMessage: error.message || 'Failed to authenticate with AWS. An unknown error occurred',
|
||||
};
|
||||
if (error instanceof STSServiceException) {
|
||||
errorDetail.errorCode = error.name || errorDetail.errorCode;
|
||||
errorDetail.errorMessage = error.message || errorDetail.errorMessage;
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
result: null,
|
||||
error: errorDetail,
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { AWSTemporaryCredential, BaseCloudCredential, CloudProviderName } from '../../../models/cloud-credential';
|
||||
import { ipcMainHandle } from '../electron';
|
||||
import { type AWSGetSecretConfig, AWSService } from './aws-service';
|
||||
|
||||
export interface cloudServiceBridgeAPI {
|
||||
authenticate: typeof cloudServiceProviderAuthentication;
|
||||
}
|
||||
export interface CloudServiceAuthOption {
|
||||
provider: CloudProviderName;
|
||||
credentials: BaseCloudCredential['credentials'];
|
||||
}
|
||||
export interface CloudServiceSecretOption<T extends {}> extends CloudServiceAuthOption {
|
||||
secretId: string;
|
||||
config: T;
|
||||
}
|
||||
export type CloudServiceGetSecretConfig = AWSGetSecretConfig;
|
||||
|
||||
export function registerCloudServiceHandlers() {
|
||||
ipcMainHandle('cloudService.authenticate', (_event, options) => cloudServiceProviderAuthentication(options));
|
||||
}
|
||||
|
||||
// factory pattern to create cloud service class based on its provider name
|
||||
class ServiceFactory {
|
||||
static createCloudService(name: CloudProviderName, credential: BaseCloudCredential['credentials']) {
|
||||
switch (name) {
|
||||
case 'aws':
|
||||
return new AWSService(credential as AWSTemporaryCredential);
|
||||
default:
|
||||
throw new Error('Invalid cloud service provider name');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// authenticate with cloud service provider
|
||||
const cloudServiceProviderAuthentication = (options: CloudServiceAuthOption) => {
|
||||
const { provider, credentials } = options;
|
||||
const cloudService = ServiceFactory.createCloudService(provider, credentials);
|
||||
return cloudService.authenticate();
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
export interface CloudServiceError {
|
||||
errorCode: string;
|
||||
errorMessage: string;
|
||||
}
|
||||
export interface CloudServiceResult<T extends Record<string, any>> {
|
||||
success: boolean;
|
||||
result?: T | null;
|
||||
error?: CloudServiceError;
|
||||
}
|
||||
export interface ICloudService {
|
||||
authenticate(...args: any[]): Promise<any>;
|
||||
}
|
||||
|
||||
export type AWSSecretType = 'kv' | 'plaintext';
|
||||
export interface AWSSecretConfig {
|
||||
SecretId: string;
|
||||
VersionId?: string;
|
||||
VersionStage?: string;
|
||||
SecretType: AWSSecretType;
|
||||
SecretKey?: string;
|
||||
};
|
||||
|
||||
export type ExternalVaultConfig = AWSSecretConfig;
|
||||
@@ -34,6 +34,7 @@ export type HandleChannels =
|
||||
| 'secretStorage.deleteSecret'
|
||||
| 'secretStorage.encryptString'
|
||||
| 'secretStorage.decryptString'
|
||||
| 'cloudService.authenticate'
|
||||
| 'git.loadGitRepository'
|
||||
| 'git.getGitBranches'
|
||||
| 'git.gitFetchAction'
|
||||
|
||||
@@ -15,6 +15,7 @@ import type { CurlBridgeAPI } from '../network/curl';
|
||||
import { cancelCurlRequest, curlRequest } from '../network/libcurl-promise';
|
||||
import { addExecutionStep, completeExecutionStep, getExecution, startExecution, type TimingStep, updateLatestStepName } from '../network/request-timing';
|
||||
import type { WebSocketBridgeAPI } from '../network/websocket';
|
||||
import type { cloudServiceBridgeAPI } from './cloud-service-integration/cloud-service';
|
||||
import { ipcMainHandle, ipcMainOn, ipcMainOnce, type RendererOnChannels } from './electron';
|
||||
import extractPostmanDataDumpHandler from './extractPostmanDataDump';
|
||||
import type { gRPCBridgeAPI } from './grpc';
|
||||
@@ -40,6 +41,7 @@ export interface RendererToMainBridgeAPI {
|
||||
curl: CurlBridgeAPI;
|
||||
git: GitServiceAPI;
|
||||
secretStorage: secretStorageBridgeAPI;
|
||||
cloudService: cloudServiceBridgeAPI;
|
||||
trackSegmentEvent: (options: { event: string; properties?: Record<string, unknown> }) => void;
|
||||
trackPageView: (options: { name: string }) => void;
|
||||
showNunjucksContextMenu: (options: { key: string; nunjucksTag?: { template: string; range: MarkerRange } }) => void;
|
||||
|
||||
79
packages/insomnia/src/models/cloud-credential.ts
Normal file
79
packages/insomnia/src/models/cloud-credential.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { database as db } from '../common/database';
|
||||
import type { BaseModel } from './index';
|
||||
|
||||
export type CloudProviderName = 'aws' | 'azure' | 'gcp';
|
||||
export enum AWSCredentialType {
|
||||
temp = 'temporary'
|
||||
}
|
||||
export interface AWSTemporaryCredential {
|
||||
type: AWSCredentialType.temp;
|
||||
accessKeyId: string;
|
||||
secretAccessKey: string;
|
||||
sessionToken: string;
|
||||
region: string;
|
||||
}
|
||||
interface IBaseCloudCredential {
|
||||
name: string;
|
||||
provider: CloudProviderName;
|
||||
}
|
||||
export interface AWSCloudCredential extends IBaseCloudCredential {
|
||||
name: string;
|
||||
provider: 'aws';
|
||||
credentials: AWSTemporaryCredential;
|
||||
}
|
||||
export type BaseCloudCredential = AWSCloudCredential;
|
||||
export type CloudProviderCredential = BaseModel & BaseCloudCredential;
|
||||
|
||||
export const name = 'Cloud Credential';
|
||||
export const type = 'CloudCredential';
|
||||
export const prefix = 'cloudCred';
|
||||
export const canDuplicate = false;
|
||||
export const canSync = false;
|
||||
|
||||
export const isCloudCredential = (model: Pick<BaseModel, 'type'>): model is CloudProviderCredential => (
|
||||
model.type === type
|
||||
);
|
||||
|
||||
export function getProviderDisplayName(provider: CloudProviderName) {
|
||||
return {
|
||||
aws: 'AWS',
|
||||
azure: 'Azure',
|
||||
gcp: 'GCP',
|
||||
}[provider] || '';
|
||||
};
|
||||
|
||||
export function init(): Partial<BaseCloudCredential> {
|
||||
return {
|
||||
name: '',
|
||||
provider: undefined,
|
||||
credentials: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function migrate(doc: BaseCloudCredential) {
|
||||
return doc;
|
||||
}
|
||||
|
||||
export function create(patch: Partial<CloudProviderCredential> = {}) {
|
||||
return db.docCreate<CloudProviderCredential>(type, patch);
|
||||
}
|
||||
|
||||
export async function getById(id: string) {
|
||||
return db.getWhere<CloudProviderCredential>(type, { _id: id });
|
||||
}
|
||||
|
||||
export function update(credential: CloudProviderCredential, patch: Partial<CloudProviderCredential>) {
|
||||
return db.docUpdate<CloudProviderCredential>(credential, patch);
|
||||
}
|
||||
|
||||
export function remove(credential: CloudProviderCredential) {
|
||||
return db.remove(credential);
|
||||
}
|
||||
|
||||
export function getByName(name: string, provider: CloudProviderName) {
|
||||
return db.find<CloudProviderCredential>(type, { name, provider });
|
||||
}
|
||||
|
||||
export function all() {
|
||||
return db.all<CloudProviderCredential>(type);
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import { generateId } from '../common/misc';
|
||||
import * as _apiSpec from './api-spec';
|
||||
import * as _caCertificate from './ca-certificate';
|
||||
import * as _clientCertificate from './client-certificate';
|
||||
import * as _cloudCredential from './cloud-credential';
|
||||
import * as _cookieJar from './cookie-jar';
|
||||
import * as _environment from './environment';
|
||||
import * as _gitCredentials from './git-credentials';
|
||||
@@ -101,6 +102,7 @@ export const workspace = _workspace;
|
||||
export const workspaceMeta = _workspaceMeta;
|
||||
export * as organization from './organization';
|
||||
export const userSession = _userSession;
|
||||
export const cloudCredential = _cloudCredential;
|
||||
|
||||
export function all() {
|
||||
// NOTE: This list should be from most to least specific (ie. parents above children)
|
||||
@@ -141,6 +143,7 @@ export function all() {
|
||||
webSocketRequest,
|
||||
webSocketResponse,
|
||||
userSession,
|
||||
cloudCredential,
|
||||
] as const;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { contextBridge, ipcRenderer, webUtils as _webUtils } from 'electron';
|
||||
|
||||
import type { GitServiceAPI } from './main/git-service';
|
||||
import type { cloudServiceBridgeAPI } from './main/ipc/cloud-service-integration/cloud-service';
|
||||
import type { gRPCBridgeAPI } from './main/ipc/grpc';
|
||||
import type { secretStorageBridgeAPI } from './main/ipc/secret-storage';
|
||||
import type { CurlBridgeAPI } from './main/network/curl';
|
||||
@@ -83,6 +84,10 @@ const git: GitServiceAPI = {
|
||||
completeSignInToGitLab: options => ipcRenderer.invoke('git.completeSignInToGitLab', options),
|
||||
};
|
||||
|
||||
const cloudService: cloudServiceBridgeAPI = {
|
||||
authenticate: options => ipcRenderer.invoke('cloudService.authenticate', options),
|
||||
};
|
||||
|
||||
const main: Window['main'] = {
|
||||
startExecution: options => ipcRenderer.send('startExecution', options),
|
||||
addExecutionStep: options => ipcRenderer.send('addExecutionStep', options),
|
||||
@@ -112,6 +117,7 @@ const main: Window['main'] = {
|
||||
grpc,
|
||||
curl,
|
||||
secretStorage,
|
||||
cloudService,
|
||||
trackSegmentEvent: options => ipcRenderer.send('trackSegmentEvent', options),
|
||||
trackPageView: options => ipcRenderer.send('trackPageView', options),
|
||||
showNunjucksContextMenu: options => ipcRenderer.send('show-nunjucks-context-menu', options),
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
/* eslint-disable react/no-unknown-property */
|
||||
import React, { memo, type SVGProps } from 'react';
|
||||
export const SvgIcnAzureLogo = memo<SVGProps<SVGSVGElement>>(props => (
|
||||
<svg viewBox="-0.4500000000000005 0.38 800.8891043012813 754.2299999999999" width="1em" height="1em" role="img" {...props}>
|
||||
<linearGradient id="a" gradientUnits="userSpaceOnUse" x1="353.1" x2="107.1" y1="56.3" y2="783"><stop offset="0" stop-color="#114a8b" /><stop offset="1" stop-color="#0669bc" /></linearGradient>
|
||||
<linearGradient id="b" gradientUnits="userSpaceOnUse" x1="429.8" x2="372.9" y1="394.9" y2="414.2"><stop offset="0" stop-opacity=".3" /><stop offset=".1" stop-opacity=".2" /><stop offset=".3" stop-opacity=".1" /><stop offset=".6" stop-opacity=".1" /><stop offset="1" stop-opacity="0" /></linearGradient>
|
||||
<linearGradient id="c" gradientUnits="userSpaceOnUse" x1="398.4" x2="668.4" y1="35.1" y2="754.4"><stop offset="0" stop-color="#3ccbf4" /><stop offset="1" stop-color="#2892df" /></linearGradient>
|
||||
<path d="M266.71.4h236.71L257.69 728.9a37.8 37.8 0 0 1-5.42 10.38c-2.33 3.16-5.14 5.93-8.33 8.22s-6.71 4.07-10.45 5.27-7.64 1.82-11.56 1.82H37.71c-5.98 0-11.88-1.42-17.2-4.16A37.636 37.636 0 0 1 7.1 738.87a37.762 37.762 0 0 1-6.66-16.41c-.89-5.92-.35-11.97 1.56-17.64L230.94 26.07c1.25-3.72 3.08-7.22 5.42-10.38 2.33-3.16 5.15-5.93 8.33-8.22 3.19-2.29 6.71-4.07 10.45-5.27S262.78.38 266.7.38v.01z" fill="url(#a)" />
|
||||
<path d="M703.07 754.59H490.52c-2.37 0-4.74-.22-7.08-.67-2.33-.44-4.62-1.1-6.83-1.97s-4.33-1.95-6.34-3.21a38.188 38.188 0 0 1-5.63-4.34l-241.2-225.26a17.423 17.423 0 0 1-5.1-8.88 17.383 17.383 0 0 1 7.17-18.21c2.89-1.96 6.3-3.01 9.79-3.01h375.36l92.39 265.56z" fill="#0078d4" />
|
||||
<path d="M504.27.4l-165.7 488.69 270.74-.06 92.87 265.56H490.43c-2.19-.02-4.38-.22-6.54-.61s-4.28-.96-6.34-1.72a38.484 38.484 0 0 1-11.36-6.51L303.37 593.79l-45.58 134.42c-1.18 3.36-2.8 6.55-4.82 9.48a40.479 40.479 0 0 1-16.05 13.67 40.03 40.03 0 0 1-10.13 3.23H37.82c-6.04.02-12-1.42-17.37-4.2A37.664 37.664 0 0 1 .43 722a37.77 37.77 0 0 1 1.87-17.79L230.87 26.58c1.19-3.79 2.98-7.36 5.3-10.58 2.31-3.22 5.13-6.06 8.33-8.4s6.76-4.16 10.53-5.38S262.75.38 266.72.4h237.56z" fill="url(#b)" />
|
||||
<path d="M797.99 704.82a37.847 37.847 0 0 1 1.57 17.64 37.867 37.867 0 0 1-6.65 16.41 37.691 37.691 0 0 1-30.61 15.72H498.48c5.98 0 11.88-1.43 17.21-4.16 5.32-2.73 9.92-6.7 13.41-11.56s5.77-10.49 6.66-16.41.35-11.97-1.56-17.64L305.25 26.05a37.713 37.713 0 0 0-13.73-18.58c-3.18-2.29-6.7-4.06-10.43-5.26S273.46.4 269.55.4h263.81c3.92 0 7.81.61 11.55 1.81 3.73 1.2 7.25 2.98 10.44 5.26 3.18 2.29 5.99 5.06 8.32 8.21s4.15 6.65 5.41 10.37l228.95 678.77z" fill="url(#c)" />
|
||||
</svg>
|
||||
));
|
||||
@@ -0,0 +1,8 @@
|
||||
import React, { memo, type SVGProps } from 'react';
|
||||
export const SvgIcnGCPLogo = memo<SVGProps<SVGSVGElement>>(props => (
|
||||
<svg viewBox="0 0 256 206" width="1em" height="1em" role="img" {...props}>
|
||||
<path d="m170.2517 56.8186 22.253-22.253 1.483-9.37c-40.551-36.873-105.012-32.692-141.567 8.724-10.154 11.503-17.687 25.844-21.704 40.653l7.97-1.123 44.505-7.339 3.436-3.514c19.797-21.742 53.27-24.667 76.128-6.168z" fill="#ea4335" />
|
||||
<path d="m224.2048 73.9182c-5.115-18.836-15.616-35.769-30.217-48.722l-31.232 31.232c13.188 10.776 20.701 27.01 20.379 44.037v5.544c15.351 0 27.797 12.445 27.797 27.796 0 15.352-12.446 27.485-27.797 27.485h-55.671l-5.466 5.934v33.34l5.466 5.231h55.671c39.93.311 72.552-31.494 72.863-71.424.188-24.203-11.745-46.893-31.793-60.453" fill="#4285f4" />
|
||||
<path d="m71.8704 205.7957h55.593v-44.506h-55.593c-3.961-.001-7.797-.852-11.399-2.498l-7.887 2.42-22.409 22.253-1.952 7.574c12.567 9.489 27.9 14.825 43.647 14.757" fill="#34a853" /><path d="m71.8704 61.4255c-39.931.238-72.107 32.802-71.869 72.732.133 22.298 10.547 43.288 28.222 56.881l32.248-32.247c-13.991-6.321-20.208-22.786-13.887-36.776 6.32-13.99 22.786-20.207 36.775-13.887 6.165 2.785 11.102 7.723 13.887 13.887l32.248-32.247c-13.721-17.937-35.041-28.424-57.624-28.343" fill="#fbbc05" />
|
||||
</svg>
|
||||
));
|
||||
@@ -0,0 +1,179 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button, Input, Label, TextField } from 'react-aria-components';
|
||||
|
||||
import { AWSCredentialType, type AWSTemporaryCredential, type BaseCloudCredential, type CloudProviderCredential, type CloudProviderName } from '../../../../models/cloud-credential';
|
||||
import { Icon } from '../../icon';
|
||||
|
||||
export interface AWSCredentialFormProps {
|
||||
data?: CloudProviderCredential;
|
||||
onSubmit: (newData: BaseCloudCredential) => void;
|
||||
isLoading: boolean;
|
||||
errorMessage?: string;
|
||||
}
|
||||
const initialFormValue = {
|
||||
name: '',
|
||||
credentials: {
|
||||
accessKeyId: '',
|
||||
secretAccessKey: '',
|
||||
sessionToken: '',
|
||||
region: '',
|
||||
},
|
||||
};
|
||||
export const providerType: CloudProviderName = 'aws';
|
||||
|
||||
const ToggleBtn = (props: { isHidden: boolean; onShowHideInput: () => void }) => {
|
||||
const { isHidden, onShowHideInput } = props;
|
||||
return (
|
||||
<Button
|
||||
className="px-4 h-8 min-w-[12ch] py-1 font-semibold border border-solid border-[--hl-md] flex items-center justify-center gap-2 aria-pressed:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all text-sm"
|
||||
onPress={onShowHideInput}
|
||||
>
|
||||
{isHidden ? <i className="fa fa-eye-slash" /> : <i className="fa fa-eye" />}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export const AWSCredentialForm = (props: AWSCredentialFormProps) => {
|
||||
const { data, onSubmit, isLoading, errorMessage } = props;
|
||||
const isEdit = !!data;
|
||||
const { name, credentials } = data || initialFormValue;
|
||||
const { accessKeyId, secretAccessKey, sessionToken, region } = credentials! as AWSTemporaryCredential;
|
||||
const [hideValueItemNames, setHideValueItemNames] = useState(['accessKeyId', 'secretAccessKey', 'sessionToken']);
|
||||
|
||||
const showOrHideItemValue = (name: string) => {
|
||||
if (hideValueItemNames.includes(name)) {
|
||||
setHideValueItemNames(hideValueItemNames.filter(n => n !== name));
|
||||
} else {
|
||||
setHideValueItemNames([...hideValueItemNames, name]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
className='flex flex-col gap-2 flex-shrink-0'
|
||||
onSubmit={e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const formData = new FormData(e.currentTarget);
|
||||
const { name, accessKeyId, secretAccessKey, sessionToken, region } = Object.fromEntries(formData.entries()) as Record<string, string>;
|
||||
// hard-code here since we only support AWS temporary token for now
|
||||
const type = AWSCredentialType.temp;
|
||||
const newData = {
|
||||
name,
|
||||
provider: providerType,
|
||||
credentials: { accessKeyId, secretAccessKey, sessionToken, region, type },
|
||||
};
|
||||
onSubmit(newData);
|
||||
}}
|
||||
>
|
||||
<div className='flex flex-col gap-2'>
|
||||
<TextField
|
||||
className="flex flex-col gap-2"
|
||||
defaultValue={name}
|
||||
>
|
||||
<Label className='col-span-4'>
|
||||
Credential Name:
|
||||
</Label>
|
||||
<Input
|
||||
required
|
||||
className='py-1 h-8 w-full pl-2 pr-7 rounded-sm border border-solid border-[--hl-sm] bg-[--color-bg] text-[--color-font] focus:outline-none focus:ring-1 focus:ring-[--hl-md] transition-colors flex-1 placeholder:italic placeholder:opacity-60 col-span-3'
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Credential name"
|
||||
/>
|
||||
</TextField>
|
||||
<TextField
|
||||
className="flex flex-col gap-2"
|
||||
defaultValue={accessKeyId}
|
||||
>
|
||||
<Label className='col-span-4'>
|
||||
Access Key Id:
|
||||
</Label>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Input
|
||||
required
|
||||
className='py-1 h-8 w-full pl-2 pr-7 rounded-sm border border-solid border-[--hl-sm] bg-[--color-bg] text-[--color-font] focus:outline-none focus:ring-1 focus:ring-[--hl-md] transition-colors flex-1 placeholder:italic placeholder:opacity-60 col-span-3'
|
||||
type={hideValueItemNames.includes('accessKeyId') ? 'password' : 'text'}
|
||||
name="accessKeyId"
|
||||
placeholder="Access Key Id"
|
||||
/>
|
||||
<ToggleBtn
|
||||
isHidden={hideValueItemNames.includes('accessKeyId')}
|
||||
onShowHideInput={() => showOrHideItemValue('accessKeyId')}
|
||||
/>
|
||||
</div>
|
||||
</TextField>
|
||||
<TextField
|
||||
className="flex flex-col gap-2"
|
||||
defaultValue={secretAccessKey}
|
||||
>
|
||||
<Label className='col-span-4'>
|
||||
Secret Access Key:
|
||||
</Label>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Input
|
||||
required
|
||||
className='py-1 h-8 w-full pl-2 pr-7 rounded-sm border border-solid border-[--hl-sm] bg-[--color-bg] text-[--color-font] focus:outline-none focus:ring-1 focus:ring-[--hl-md] transition-colors flex-1 placeholder:italic placeholder:opacity-60 col-span-3'
|
||||
type={hideValueItemNames.includes('secretAccessKey') ? 'password' : 'text'}
|
||||
name="secretAccessKey"
|
||||
placeholder="Secret Access Key"
|
||||
/>
|
||||
<ToggleBtn
|
||||
isHidden={hideValueItemNames.includes('secretAccessKey')}
|
||||
onShowHideInput={() => showOrHideItemValue('secretAccessKey')}
|
||||
/>
|
||||
</div>
|
||||
</TextField>
|
||||
<TextField
|
||||
className="flex flex-col gap-2"
|
||||
defaultValue={sessionToken}
|
||||
>
|
||||
<Label className='col-span-4'>
|
||||
Session Token:
|
||||
</Label>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Input
|
||||
required
|
||||
className='py-1 h-8 w-full pl-2 pr-7 rounded-sm border border-solid border-[--hl-sm] bg-[--color-bg] text-[--color-font] focus:outline-none focus:ring-1 focus:ring-[--hl-md] transition-colors flex-1 placeholder:italic placeholder:opacity-60 col-span-3'
|
||||
type={hideValueItemNames.includes('sessionToken') ? 'password' : 'text'}
|
||||
name="sessionToken"
|
||||
placeholder="AWS Secret Token"
|
||||
/>
|
||||
<ToggleBtn
|
||||
isHidden={hideValueItemNames.includes('sessionToken')}
|
||||
onShowHideInput={() => showOrHideItemValue('sessionToken')}
|
||||
/>
|
||||
</div>
|
||||
</TextField>
|
||||
<TextField
|
||||
className="flex flex-col gap-2"
|
||||
defaultValue={region}
|
||||
>
|
||||
<Label className='col-span-4'>
|
||||
Region:
|
||||
</Label>
|
||||
<Input
|
||||
required
|
||||
className='py-1 h-8 w-full pl-2 pr-7 rounded-sm border border-solid border-[--hl-sm] bg-[--color-bg] text-[--color-font] focus:outline-none focus:ring-1 focus:ring-[--hl-md] transition-colors flex-1 placeholder:italic placeholder:opacity-60 col-span-3'
|
||||
type="text"
|
||||
name="region"
|
||||
placeholder="Region"
|
||||
/>
|
||||
</TextField>
|
||||
</div>
|
||||
{errorMessage &&
|
||||
<p className="notice error margin-top-sm no-margin-bottom">{errorMessage}</p>
|
||||
}
|
||||
<div className='w-full flex flex-row items-center justify-end gap-[--padding-md] pt-[--padding-md]'>
|
||||
<Button
|
||||
className="hover:no-underline text-right bg-[--color-surprise] hover:bg-opacity-90 border border-solid border-[--hl-md] py-2 px-3 text-[--color-font-surprise] transition-colors rounded-sm"
|
||||
type='submit'
|
||||
isDisabled={isLoading}
|
||||
>
|
||||
{isLoading && <Icon icon="spinner" className="text-[--color-font] animate-spin m-auto inline-block mr-2" />}
|
||||
{isEdit ? 'Update' : 'Create'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { Button, Dialog, Heading, Modal, ModalOverlay } from 'react-aria-components';
|
||||
import { useFetcher } from 'react-router-dom';
|
||||
|
||||
import { type BaseCloudCredential, type CloudProviderCredential, type CloudProviderName, getProviderDisplayName } from '../../../../models/cloud-credential';
|
||||
import { Icon } from '../../icon';
|
||||
import { AWSCredentialForm } from './aws-credential-form';
|
||||
|
||||
export interface CloudCredentialModalProps {
|
||||
provider: CloudProviderName;
|
||||
providerCredential?: CloudProviderCredential;
|
||||
onClose: (data?: any) => void;
|
||||
onComplete?: (data?: any) => void;
|
||||
};
|
||||
|
||||
export const CloudCredentialModal = (props: CloudCredentialModalProps) => {
|
||||
const { provider, providerCredential, onClose, onComplete } = props;
|
||||
const providerDisplayName = getProviderDisplayName(provider);
|
||||
const cloudCredentialFetcher = useFetcher();
|
||||
const isEditing = !!providerCredential;
|
||||
|
||||
const fetchErrorMessage = useMemo(() => {
|
||||
if (cloudCredentialFetcher.data && 'error' in cloudCredentialFetcher.data && cloudCredentialFetcher.data.error && cloudCredentialFetcher.state === 'idle') {
|
||||
const errorMessage: string = cloudCredentialFetcher.data.error || `An unexpected error occurred while authenticating with ${getProviderDisplayName(provider)}.`;
|
||||
return errorMessage;
|
||||
}
|
||||
return undefined;
|
||||
}, [cloudCredentialFetcher.data, cloudCredentialFetcher.state, provider]);
|
||||
|
||||
const handleFormSubmit = (data: BaseCloudCredential & { isAuthenticated?: boolean }) => {
|
||||
const { name, credentials, isAuthenticated = false } = data;
|
||||
const formAction = isEditing ? `/cloud-credential/${providerCredential._id}/update` : '/cloud-credential/new';
|
||||
cloudCredentialFetcher.submit(
|
||||
JSON.stringify({ name, credentials, provider, isAuthenticated }),
|
||||
{
|
||||
action: formAction,
|
||||
method: 'post',
|
||||
encType: 'application/json',
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// close modal if submit success
|
||||
if (cloudCredentialFetcher.data && !cloudCredentialFetcher.data.error && cloudCredentialFetcher.state === 'idle') {
|
||||
const newCredentialData = cloudCredentialFetcher.data;
|
||||
onClose(newCredentialData);
|
||||
onComplete && onComplete(newCredentialData);
|
||||
};
|
||||
}, [cloudCredentialFetcher.data, cloudCredentialFetcher.state, onClose, onComplete]);
|
||||
|
||||
return (
|
||||
<ModalOverlay
|
||||
isOpen
|
||||
isDismissable
|
||||
onOpenChange={isOpen => {
|
||||
!isOpen && onClose();
|
||||
}}
|
||||
className="w-full h-[--visual-viewport-height] fixed z-[9999] top-0 left-0 flex items-start justify-center bg-black/30"
|
||||
>
|
||||
<Modal
|
||||
onOpenChange={isOpen => {
|
||||
!isOpen && onClose();
|
||||
}}
|
||||
className="max-h-[75%] overflow-auto flex flex-col w-full max-w-3xl rounded-md border border-solid border-[--hl-sm] p-[--padding-lg] bg-[--color-bg] text-[--color-font] m-24"
|
||||
>
|
||||
<Dialog
|
||||
className="outline-none flex-1 h-full flex flex-col overflow-hidden"
|
||||
>
|
||||
{({ close }) => (
|
||||
<div className='flex-1 flex flex-col gap-4 overflow-hidden'>
|
||||
<div className='flex gap-2 items-center justify-between'>
|
||||
<Heading slot="title" className='text-2xl'>{providerCredential ? `Edit ${providerDisplayName} credential` : `Authenticate With ${providerDisplayName}`}</Heading>
|
||||
<Button
|
||||
className="flex flex-shrink-0 items-center justify-center aspect-square h-6 aria-pressed:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all text-sm"
|
||||
id="close-add-cloud-crendeital-modal"
|
||||
onPress={close}
|
||||
>
|
||||
<Icon icon="x" />
|
||||
</Button>
|
||||
</div>
|
||||
{provider === 'aws' &&
|
||||
<AWSCredentialForm
|
||||
data={providerCredential}
|
||||
isLoading={cloudCredentialFetcher.state !== 'idle'}
|
||||
onSubmit={handleFormSubmit}
|
||||
errorMessage={fetchErrorMessage}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</ModalOverlay>
|
||||
);
|
||||
};
|
||||
@@ -8,6 +8,7 @@ import { ModalBody } from '../base/modal-body';
|
||||
import { ModalHeader } from '../base/modal-header';
|
||||
import { AI } from '../settings/ai';
|
||||
import { BooleanSetting } from '../settings/boolean-setting';
|
||||
import { CloudServiceCredentialList } from '../settings/cloud-service-credentials';
|
||||
import { General } from '../settings/general';
|
||||
import { ImportExport } from '../settings/import-export';
|
||||
import { MaskedSetting } from '../settings/masked-setting';
|
||||
@@ -27,6 +28,7 @@ export const TAB_INDEX_SHORTCUTS = 'keyboard';
|
||||
export const TAB_INDEX_THEMES = 'themes';
|
||||
export const TAB_INDEX_PLUGINS = 'plugins';
|
||||
export const TAB_INDEX_AI = 'ai';
|
||||
export const TAB_CLOUD_CREDENTIAL = 'cloudCred';
|
||||
|
||||
export const SettingsModal = forwardRef<SettingsModalHandle, ModalProps>((props, ref) => {
|
||||
const [defaultTabKey, setDefaultTabKey] = useState('general');
|
||||
@@ -98,6 +100,12 @@ export const SettingsModal = forwardRef<SettingsModalHandle, ModalProps>((props,
|
||||
>
|
||||
Plugins
|
||||
</Tab>
|
||||
<Tab
|
||||
className='flex-shrink-0 h-full flex items-center justify-between cursor-pointer gap-2 outline-none select-none px-3 py-1 text-[--hl] aria-selected:text-[--color-font] hover:bg-[--hl-sm] hover:text-[--color-font] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] transition-colors duration-300'
|
||||
id='cloudCred'
|
||||
>
|
||||
Cloud Credentials
|
||||
</Tab>
|
||||
<Tab
|
||||
className='flex-shrink-0 h-full flex items-center justify-between cursor-pointer gap-2 outline-none select-none px-3 py-1 text-[--hl] aria-selected:text-[--color-font] hover:bg-[--hl-sm] hover:text-[--color-font] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] transition-colors duration-300'
|
||||
id='ai'
|
||||
@@ -153,6 +161,9 @@ export const SettingsModal = forwardRef<SettingsModalHandle, ModalProps>((props,
|
||||
<TabPanel className='w-full h-full overflow-y-auto p-4' id='plugins'>
|
||||
<Plugins />
|
||||
</TabPanel>
|
||||
<TabPanel className='w-full h-full overflow-y-auto p-4' id='cloudCred'>
|
||||
<CloudServiceCredentialList />
|
||||
</TabPanel>
|
||||
<TabPanel className='w-full h-full overflow-y-auto p-4' id='ai'>
|
||||
<AI />
|
||||
</TabPanel>
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button, Menu, MenuItem, MenuTrigger, Popover } from 'react-aria-components';
|
||||
import { useFetcher } from 'react-router-dom';
|
||||
|
||||
import { type CloudProviderCredential, type CloudProviderName, getProviderDisplayName } from '../../../models/cloud-credential';
|
||||
import { usePlanData } from '../../hooks/use-plan';
|
||||
import { useRootLoaderData } from '../../routes/root';
|
||||
import { Icon } from '../icon';
|
||||
import { showModal } from '../modals';
|
||||
import { AskModal } from '../modals/ask-modal';
|
||||
import { CloudCredentialModal } from '../modals/cloud-credential-modal/cloud-credential-modal';
|
||||
import { UpgradeNotice } from '../upgrade-notice';
|
||||
|
||||
interface createCredentialItemType {
|
||||
name: string;
|
||||
id: CloudProviderName;
|
||||
icon: JSX.Element;
|
||||
}
|
||||
const createCredentialItemList: createCredentialItemType[] = [
|
||||
{
|
||||
id: 'aws',
|
||||
name: getProviderDisplayName('aws'),
|
||||
icon: <i className="ml-1 fa-brands fa-aws" />,
|
||||
},
|
||||
];
|
||||
const buttonClassName = 'disabled:opacity-50 h-7 aspect-square aria-pressed:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] transition-all text-sm py-1 px-2';
|
||||
|
||||
export const CloudServiceCredentialList = () => {
|
||||
const { isOwner, isEnterprisePlan } = usePlanData();
|
||||
const { cloudCredentials } = useRootLoaderData();
|
||||
const [modalState, setModalState] = useState<{ show: boolean; provider: CloudProviderName; credential?: CloudProviderCredential }>();
|
||||
const deleteCredentialFetcher = useFetcher();
|
||||
|
||||
const handleDeleteItem = (id: string, name: string) => {
|
||||
showModal(AskModal, {
|
||||
title: 'Delete Cloud Credential?',
|
||||
message: `Are you sure to delete ${name}?`,
|
||||
onDone: async (isYes: boolean) => {
|
||||
if (isYes) {
|
||||
deleteCredentialFetcher.submit({}, {
|
||||
action: `/cloud-credential/${id}/delete`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const hideModal = () => {
|
||||
setModalState(prevState => {
|
||||
const newState = {
|
||||
show: false,
|
||||
provider: prevState!.provider,
|
||||
credentials: undefined,
|
||||
};
|
||||
return newState;
|
||||
});
|
||||
};
|
||||
|
||||
const handleCreateCloudServiceCredential = (key: CloudProviderName) => {
|
||||
setModalState({ show: true, provider: key as CloudProviderName });
|
||||
};
|
||||
|
||||
if (!isEnterprisePlan) {
|
||||
return (
|
||||
<UpgradeNotice
|
||||
isOwner={isOwner}
|
||||
featureName='Cloud Credentials feature'
|
||||
newPlan='enterprise'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='flex justify-between items-end'>
|
||||
<h2 className='font-bold text-lg bg-[--color-bg] z-10'>Service Provider Credential List</h2>
|
||||
<MenuTrigger>
|
||||
<Button
|
||||
aria-label="Create in project"
|
||||
className="flex items-center justify-center px-4 py-2 gap-2 h-full bg-[--hl-xxs] aria-pressed:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all text-sm"
|
||||
>
|
||||
<Icon icon="plus-circle" /> Add Credential
|
||||
</Button>
|
||||
<Popover
|
||||
className="min-w-max"
|
||||
placement='bottom right'
|
||||
>
|
||||
<Menu
|
||||
aria-label="Create cloud service credential actions"
|
||||
selectionMode="single"
|
||||
onAction={key => handleCreateCloudServiceCredential(key as CloudProviderName)}
|
||||
items={createCredentialItemList}
|
||||
className="border select-none text-sm min-w-max border-solid border-[--hl-sm] shadow-lg bg-[--color-bg] py-2 rounded-md overflow-y-auto max-h-[85vh] focus:outline-none"
|
||||
>
|
||||
{item => (
|
||||
<MenuItem
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
className="flex gap-2 px-[--padding-md] aria-selected:font-bold items-center text-[--color-font] h-[--line-height-xxs] w-full text-md whitespace-nowrap bg-transparent hover:bg-[--hl-sm] disabled:cursor-not-allowed focus:bg-[--hl-xs] focus:outline-none transition-colors"
|
||||
aria-label={item.name}
|
||||
>
|
||||
{item.icon}
|
||||
<span>{item.name}</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
</Popover>
|
||||
</MenuTrigger>
|
||||
</div>
|
||||
{cloudCredentials.length === 0 ?
|
||||
<div className="text-center faint italic pad">No cloud servicie provider credentials found</div> :
|
||||
<table className="table--fancy table--striped table--valign-middle margin-top margin-bottom">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className='normal-case'>Name</th>
|
||||
<th className='normal-case'>Service Provider</th>
|
||||
<th className='normal-case'>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{cloudCredentials.map(cloudCred => {
|
||||
const { _id, name, provider } = cloudCred;
|
||||
const credentialItem = createCredentialItemList.find(item => item.id === provider);
|
||||
return (
|
||||
<tr key={_id}>
|
||||
<td >
|
||||
{name}
|
||||
</td>
|
||||
<td className='w-36'>
|
||||
{credentialItem && (
|
||||
<div className='flex items-center gap-2'>
|
||||
{credentialItem.icon}
|
||||
<span>{credentialItem.name}</span>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td className='w-52 whitespace-nowrap'>
|
||||
<div className='flex gap-2'>
|
||||
<Button
|
||||
className={`${buttonClassName} w-16`}
|
||||
onPress={() => setModalState({ show: true, provider: provider!, credential: cloudCred })}
|
||||
>
|
||||
<Icon icon="edit" /> Edit
|
||||
</Button>
|
||||
<Button
|
||||
className={`${buttonClassName} w-20`}
|
||||
onPress={() => handleDeleteItem(_id, name)}
|
||||
>
|
||||
<Icon icon="trash" /> Delete
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
{modalState && modalState.show &&
|
||||
<CloudCredentialModal
|
||||
provider={modalState.provider}
|
||||
providerCredential={modalState.credential}
|
||||
onClose={hideModal}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import React, { type NamedExoticComponent, type SVGProps } from 'react';
|
||||
import type { ValueOf } from 'type-fest';
|
||||
|
||||
import { SvgIcnArrowRight } from './assets/svgr/IcnArrowRight';
|
||||
import { SvgIcnAzureLogo } from './assets/svgr/IcnAzureLogo';
|
||||
import { SvgIcnBitbucketLogo } from './assets/svgr/IcnBitbucketLogo';
|
||||
import { SvgIcnBrackets } from './assets/svgr/IcnBrackets';
|
||||
import { SvgIcnBug } from './assets/svgr/IcnBug';
|
||||
@@ -23,6 +24,7 @@ import { SvgIcnErrors } from './assets/svgr/IcnErrors';
|
||||
import { SvgIcnFile } from './assets/svgr/IcnFile';
|
||||
import { SvgIcnFolder } from './assets/svgr/IcnFolder';
|
||||
import { SvgIcnFolderOpen } from './assets/svgr/IcnFolderOpen';
|
||||
import { SvgIcnGCPLogo } from './assets/svgr/IcnGCPLogo';
|
||||
import { SvgIcnGear } from './assets/svgr/IcnGear';
|
||||
import { SvgIcnGitBranch } from './assets/svgr/IcnGitBranch';
|
||||
import { SvgIcnGithubLogo } from './assets/svgr/IcnGithubLogo';
|
||||
@@ -131,6 +133,8 @@ export const IconEnum = {
|
||||
receive: 'receive',
|
||||
sent: 'sent',
|
||||
systemEvent: 'system-event',
|
||||
gcpLogo: 'gcp-logo',
|
||||
azureLogo: 'azure-logo',
|
||||
/** Blank icon */
|
||||
empty: 'empty',
|
||||
} as const;
|
||||
@@ -193,6 +197,8 @@ const icons: Record<IconId, [ThemeKeys, NamedExoticComponent<SVGProps<SVGSVGElem
|
||||
[IconEnum.sent]: [ThemeEnum.default, SvgIcnSent],
|
||||
[IconEnum.checkmarkCircle]: [ThemeEnum.default, SvgIcnCheckmarkCircle],
|
||||
[IconEnum.systemEvent]: [ThemeEnum.default, SvgIcnSystemEvent],
|
||||
[IconEnum.gcpLogo]: [ThemeEnum.default, SvgIcnGCPLogo],
|
||||
[IconEnum.azureLogo]: [ThemeEnum.default, SvgIcnAzureLogo],
|
||||
};
|
||||
|
||||
export type IconId = ValueOf<typeof IconEnum>;
|
||||
|
||||
@@ -205,6 +205,32 @@ async function renderApp() {
|
||||
action: async (...args) =>
|
||||
(await import('./routes/actions')).updateSettingsAction(...args),
|
||||
},
|
||||
{
|
||||
path: 'cloud-credential',
|
||||
children: [
|
||||
{
|
||||
path: 'new',
|
||||
action: async (...args) =>
|
||||
(
|
||||
await import('./routes/actions')
|
||||
).createCloudCredentialAction(...args),
|
||||
},
|
||||
{
|
||||
path: ':cloudCredentialId/update',
|
||||
action: async (...args) =>
|
||||
(
|
||||
await import('./routes/actions')
|
||||
).updateCloudCredentialAction(...args),
|
||||
},
|
||||
{
|
||||
path: ':cloudCredentialId/delete',
|
||||
action: async (...args) =>
|
||||
(
|
||||
await import('./routes/actions')
|
||||
).deleteCloudCredentialAction(...args),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'untracked-projects',
|
||||
loader: async (...args) => (await import('./routes/untracked-projects')).loader(...args),
|
||||
|
||||
@@ -1547,3 +1547,67 @@ export const toggleExpandAllRequestGroupsAction: ActionFunction = async ({ param
|
||||
}));
|
||||
return null;
|
||||
};
|
||||
|
||||
export const createCloudCredentialAction: ActionFunction = async ({ request }) => {
|
||||
const patch = await request.json();
|
||||
const { name, provider, credentials, isAuthenticated } = patch;
|
||||
invariant(typeof name === 'string', 'Name is required');
|
||||
invariant(provider, 'Cloud Provier name is required');
|
||||
if (name && provider && credentials) {
|
||||
if (isAuthenticated) {
|
||||
// find credential with same name for oauth authenticated cloud service
|
||||
const existingCredential = await models.cloudCrendential.getByName(name, provider);
|
||||
if (existingCredential.length === 0) {
|
||||
await models.cloudCrendential.create(patch);
|
||||
} else {
|
||||
await models.cloudCrendential.update(existingCredential[0], patch);
|
||||
}
|
||||
return credentials;
|
||||
} else {
|
||||
const authenciateResponse = await window.main.cloudService.authenticate({ provider, credentials });
|
||||
const { success, error, result } = authenciateResponse!;
|
||||
if (success) {
|
||||
await models.cloudCrendential.create(patch);
|
||||
} else {
|
||||
return {
|
||||
error: error?.errorMessage,
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return { error: 'Invalid paramters for creating cloud credential' };
|
||||
};
|
||||
|
||||
export const updateCloudCredentialAction: ActionFunction = async ({ request, params }) => {
|
||||
const { cloudCredentialId } = params;
|
||||
invariant(typeof cloudCredentialId === 'string', 'Credential ID is required');
|
||||
const patch = await request.json();
|
||||
const { name, provider, credentials } = patch;
|
||||
invariant(typeof name === 'string', 'Name is required');
|
||||
invariant(provider, 'Cloud Provier name is required');
|
||||
if (name && provider && credentials) {
|
||||
const authenciateResponse = await window.main.cloudService.authenticate({ provider, credentials });
|
||||
const { success, error, result } = authenciateResponse!;
|
||||
if (success) {
|
||||
const originCredential = await models.cloudCrendential.getById(cloudCredentialId);
|
||||
invariant(originCredential, 'No Cloud Credential found');
|
||||
await models.cloudCrendential.update(originCredential, patch);
|
||||
} else {
|
||||
return {
|
||||
error: error?.errorMessage,
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return { error: 'Invalid paramters for updating cloud credential' };
|
||||
};
|
||||
|
||||
export const deleteCloudCredentialAction: ActionFunction = async ({ params }) => {
|
||||
const { cloudCredentialId } = params;
|
||||
invariant(typeof cloudCredentialId === 'string', 'Cloud Credential ID is required');
|
||||
const cloudCredential = await models.cloudCrendential.getById(cloudCredentialId);
|
||||
invariant(cloudCredential, 'Cloud Credential not found');
|
||||
await models.cloudCrendential.remove(cloudCredential);
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import { type LoaderFunction, Outlet, useFetcher, useNavigate, useParams, useRou
|
||||
|
||||
import { isDevelopment } from '../../common/constants';
|
||||
import * as models from '../../models';
|
||||
import type { CloudProviderCredential } from '../../models/cloud-credential';
|
||||
import type { Settings } from '../../models/settings';
|
||||
import type { UserSession } from '../../models/user-session';
|
||||
import { reloadPlugins } from '../../plugins';
|
||||
@@ -31,6 +32,7 @@ export interface RootLoaderData {
|
||||
settings: Settings;
|
||||
workspaceCount: number;
|
||||
userSession: UserSession;
|
||||
cloudCredentials: CloudProviderCredential[];
|
||||
}
|
||||
|
||||
export const useRootLoaderData = () => {
|
||||
@@ -41,11 +43,13 @@ export const loader: LoaderFunction = async (): Promise<RootLoaderData> => {
|
||||
const settings = await models.settings.get();
|
||||
const workspaceCount = await models.workspace.count();
|
||||
const userSession = await models.userSession.getOrCreate();
|
||||
const cloudCredentials = await models.cloudCrendential.all();
|
||||
|
||||
return {
|
||||
settings,
|
||||
workspaceCount,
|
||||
userSession,
|
||||
cloudCredentials,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user