Skip to content

Commit

Permalink
Merge pull request #2 from davelopez/ui-add-secrets-to-tools
Browse files Browse the repository at this point in the history
Add UI for Tool secrets
  • Loading branch information
arash77 authored Dec 9, 2024
2 parents 6a9cb7e + cb24b9a commit 8e389b0
Show file tree
Hide file tree
Showing 6 changed files with 421 additions and 0 deletions.
34 changes: 34 additions & 0 deletions client/src/api/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,37 @@ export async function fetchCurrentUserQuotaSourceUsage(quotaSourceLabel?: string

return toQuotaUsage(data);
}

// TODO: Temporarily using these interfaces until the new API is implemented
export interface CredentialsDefinition {
name: string;
reference: string;
optional: boolean;
multiple: boolean;
label?: string;
description?: string;
}
export interface UserCredentials extends CredentialsDefinition {
variables: Variable[];
secrets: Secret[];
}

export interface ToolCredentialsDefinition extends CredentialsDefinition {
variables: CredentialDetail[];
secrets: CredentialDetail[];
}

export interface CredentialDetail {
name: string;
label?: string;
description?: string;
}

export interface Secret extends CredentialDetail {
alreadySet: boolean;
value?: string;
}

export interface Variable extends CredentialDetail {
value?: string;
}
7 changes: 7 additions & 0 deletions client/src/components/Tool/ToolCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { useUserStore } from "@/stores/userStore";
import ToolSelectPreferredObjectStore from "./ToolSelectPreferredObjectStore";
import ToolTargetPreferredObjectStorePopover from "./ToolTargetPreferredObjectStorePopover";
import ToolCredentials from "./ToolCredentials.vue";
import ToolHelpForum from "./ToolHelpForum.vue";
import ToolTutorialRecommendations from "./ToolTutorialRecommendations.vue";
import ToolFavoriteButton from "components/Tool/Buttons/ToolFavoriteButton.vue";
Expand Down Expand Up @@ -174,6 +175,12 @@ const showHelpForum = computed(() => isConfigLoaded.value && config.value.enable
</div>
</div>
<ToolCredentials
v-if="props.options.credentials"
:tool-id="props.id"
:tool-version="props.version"
:tool-credentials-definition="props.options.credentials" />
<div id="tool-card-body">
<FormMessage variant="danger" :message="errorText" :persistent="true" />
<FormMessage :variant="props.messageVariant" :message="props.messageText" />
Expand Down
183 changes: 183 additions & 0 deletions client/src/components/Tool/ToolCredentials.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
<script setup lang="ts">
import { BAlert, BButton, BModal } from "bootstrap-vue";
import { computed, ref } from "vue";
import type { ToolCredentialsDefinition, UserCredentials } from "@/api/users";
import { useUserCredentialsStore } from "@/stores/userCredentials";
import { useUserStore } from "@/stores/userStore";
import LoadingSpan from "@/components/LoadingSpan.vue";
import ManageToolCredentials from "@/components/User/Credentials/ManageToolCredentials.vue";
interface Props {
toolId: string;
toolVersion: string;
toolCredentialsDefinition: ToolCredentialsDefinition[];
}
const props = defineProps<Props>();
const userStore = useUserStore();
const userCredentialsStore = useUserCredentialsStore();
const isBusy = ref(true);
const busyMessage = ref<string>("");
const userCredentials = ref<UserCredentials[] | undefined>(undefined);
const hasUserProvidedRequiredCredentials = computed<boolean>(() => {
if (!userCredentials.value) {
return false;
}
return userCredentials.value.every((credentials) => credentials.optional || areSetByUser(credentials));
});
const hasUserProvidedAllCredentials = computed<boolean>(() => {
if (!userCredentials.value) {
return false;
}
return userCredentials.value.every(areSetByUser);
});
const hasSomeOptionalCredentials = computed<boolean>(() => {
return props.toolCredentialsDefinition.some((credentials) => credentials.optional);
});
const hasSomeRequiredCredentials = computed<boolean>(() => {
return props.toolCredentialsDefinition.some((credentials) => !credentials.optional);
});
const provideCredentialsButtonTitle = computed(() => {
return hasUserProvidedRequiredCredentials.value ? "Manage credentials" : "Provide credentials";
});
const bannerVariant = computed(() => {
if (isBusy.value) {
return "info";
}
return hasUserProvidedRequiredCredentials.value ? "success" : "warning";
});
const showModal = ref(false);
/**
* Check if the user has credentials for the tool.
* @param providedCredentials - The provided credentials to check. If not provided, the function will fetch the
* credentials from the store if they exist.
*/
async function checkUserCredentials(providedCredentials?: UserCredentials[]) {
busyMessage.value = "Checking your credentials...";
isBusy.value = true;
try {
if (userStore.isAnonymous) {
return;
}
if (!providedCredentials) {
providedCredentials =
userCredentialsStore.getAllUserCredentialsForTool(props.toolId) ??
(await userCredentialsStore.fetchAllUserCredentialsForTool(
props.toolId,
props.toolCredentialsDefinition
));
}
userCredentials.value = providedCredentials;
} catch (error) {
// TODO: Implement error handling.
console.error("Error checking user credentials", error);
} finally {
isBusy.value = false;
}
}
function areSetByUser(credentials: UserCredentials): boolean {
return (
credentials.variables.every((variable) => variable.value) &&
credentials.secrets.every((secret) => secret.alreadySet)
);
}
function provideCredentials() {
showModal.value = true;
}
async function onSavedCredentials(providedCredentials: UserCredentials[]) {
showModal.value = false;
busyMessage.value = "Saving your credentials...";
try {
isBusy.value = true;
userCredentials.value = await userCredentialsStore.saveUserCredentialsForTool(
props.toolId,
providedCredentials
);
} catch (error) {
// TODO: Implement error handling.
console.error("Error saving user credentials", error);
} finally {
isBusy.value = false;
}
}
checkUserCredentials();
</script>

<template>
<div>
<BAlert show :variant="bannerVariant" class="tool-credentials-banner">
<LoadingSpan v-if="isBusy" :message="busyMessage" />
<div v-else-if="userStore.isAnonymous">
<span v-if="hasSomeRequiredCredentials">
<strong>
This tool requires credentials to access its services and you need to be logged in to provide
them.
</strong>
</span>
<span v-else>
This tool <strong>can use additional credentials</strong> to access its services
<strong>or you can use it anonymously</strong>.
</span>
<br />
Please <a href="/login/start">log in or register here</a>.
</div>
<div v-else class="d-flex justify-content-between align-items-center">
<div class="credentials-info">
<span v-if="hasUserProvidedRequiredCredentials">
<strong>You have already provided credentials for this tool.</strong> You can update or delete
your credentials, using the <i>{{ provideCredentialsButtonTitle }}</i> button.
<span v-if="hasSomeOptionalCredentials && !hasUserProvidedAllCredentials">
<br />
You can still provide some optional credentials for this tool.
</span>
</span>
<span v-else-if="hasSomeRequiredCredentials">
This tool <strong>requires you to enter credentials</strong> to access its services. Please
provide your credentials before using the tool using the
<i>{{ provideCredentialsButtonTitle }}</i> button.
</span>
<span v-else>
This tool <strong>can use credentials</strong> to access its services. If you don't provide
credentials, you can still use the tool, but you will access its services
<strong>anonymously</strong> and in some cases, with limited functionality.
</span>
</div>

<BButton variant="primary" size="sm" class="provide-credentials-btn" @click="provideCredentials">
{{ provideCredentialsButtonTitle }}
</BButton>
</div>
</BAlert>
<BModal v-model="showModal" title="Manage Tool Credentials" hide-footer>
<ManageToolCredentials
:tool-id="props.toolId"
:tool-version="props.toolVersion"
:credentials="userCredentials"
@save-credentials="onSavedCredentials" />
</BModal>
</div>
</template>

<style scoped>
.tool-credentials-banner {
margin-bottom: 1rem;
}
</style>
55 changes: 55 additions & 0 deletions client/src/components/User/Credentials/CredentialsInput.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<script setup lang="ts">
import { BBadge, BCard } from "bootstrap-vue";
import type { UserCredentials } from "@/api/users";
interface Props {
credential: UserCredentials;
}
defineProps<Props>();
</script>

<template>
<BCard>
<h3>
{{ credential.label || credential.name }}
<BBadge
v-if="credential.optional"
variant="secondary"
class="optional-credentials"
title="These credentials are optional. If you do not provide them, the tool will use default values or
anonymous access.">
Optional
</BBadge>
<BBadge
v-else
variant="danger"
class="required-credentials"
title="These credentials are required. You must provide them to use the tool.">
Required
</BBadge>
</h3>
<p>{{ credential.description }}</p>
<div v-for="variable in credential.variables" :key="variable.name">
<label :for="variable.name">{{ variable.label || variable.name }}</label>
<input :id="variable.name" v-model="variable.value" type="text" autocomplete="off" />
</div>
<div v-for="secret in credential.secrets" :key="secret.name" class="secret-input">
<label :for="secret.name">{{ secret.label || secret.name }}</label>
<input :id="secret.name" v-model="secret.value" type="password" autocomplete="off" />
<span v-if="secret.alreadySet" class="tick-icon">✔️</span>
</div>
</BCard>
</template>

<style scoped>
.secret-input {
display: flex;
align-items: center;
}
.tick-icon {
color: green;
margin-left: 0.5em;
}
</style>
61 changes: 61 additions & 0 deletions client/src/components/User/Credentials/ManageToolCredentials.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<script setup lang="ts">
import { ref } from "vue";
import type { UserCredentials } from "@/api/users";
import CredentialsInput from "@/components/User/Credentials/CredentialsInput.vue";
interface ManageToolCredentialsProps {
toolId: string;
toolVersion: string;
credentials?: UserCredentials[];
}
const props = defineProps<ManageToolCredentialsProps>();
const providedCredentials = ref<UserCredentials[]>(initializeCredentials());
const emit = defineEmits<{
(e: "save-credentials", credentials: UserCredentials[]): void;
}>();
function saveCredentials() {
emit("save-credentials", providedCredentials.value);
}
function initializeCredentials(): UserCredentials[] {
// If credentials are provided, clone them to avoid modifying the original data
return props.credentials ? JSON.parse(JSON.stringify(props.credentials)) : [];
}
</script>

<template>
<div>
<p>
Here you can manage your credentials for the tool <strong>{{ toolId }}</strong> version
<strong> {{ toolVersion }} </strong>.
</p>
<CredentialsInput
v-for="credential in providedCredentials"
:key="credential.reference"
:credential="credential" />
<button @click="saveCredentials">Save Credentials</button>
</div>
</template>

<style scoped>
.credential-card {
border: 1px solid #ccc;
padding: 1em;
margin-bottom: 1em;
border-radius: 5px;
}
.secret-input {
display: flex;
align-items: center;
}
.tick-icon {
color: green;
margin-left: 0.5em;
}
</style>
Loading

0 comments on commit 8e389b0

Please sign in to comment.