Skip to main content
Glama
WorkspaceDetailsPage.vue17 kB
<template> <div class="overflow-hidden"> <template v-if="loadWorkspacesReqStatus.isSuccess || createMode"> <WorkspacePageHeader :subtitle=" createMode ? 'Fill out this form to create a new workspace.' : 'From here you can manage this workspace and invite users to be part of it.' " :title=" draftWorkspace.displayName || (createMode ? 'Create New Workspace' : 'Workspace Details') " > <template v-if="isDefaultWorkspace"> <div :class=" clsx( 'rounded text-sm px-xs py-2xs my-2xs w-fit', themeClasses( 'bg-success-600 text-shade-0', 'bg-success-500 text-shade-100', ), ) " > DEFAULT </div> </template> <div v-if="featureFlagStore.APPROVALS_OPT_IN_OUT" :class=" clsx( 'rounded text-sm px-xs py-2xs my-2xs w-fit', themeClasses( 'bg-success-600 text-shade-0', 'bg-success-500 text-shade-100', ), ) " > Approvals {{ approvalStatus }} </div> <IconButton v-if="!createMode" class="flex-none" icon="key-tilted" iconIdleTone="shade" size="lg" tooltip="API Tokens" tooltipPlacement="top" @click="openApiTokens" /> </WorkspacePageHeader> <Stack> <ErrorMessage :requestStatus=" createMode ? createWorkspaceReqStatus : editWorkspaceReqStatus " /> <VormInput v-model="draftWorkspace.displayName" :disabled="!isWorkspaceOwner && !createMode" :maxLength="500" label="Display Name" placeholder="A display name for this workspace" required :regex="ALLOWED_INPUT_REGEX" /> <VormInput v-if="createMode" v-model="createWorkspaceType" :options="createWorkspaceTypeDropdownOptions" label="Workspace Type" placeholder="Choose what kind of workspace to create" required type="dropdown" /> <VormInput v-if="createWorkspaceType === 'url'" ref="urlInputRef" v-model="draftWorkspace.instanceUrl" :disabled="!isWorkspaceOwner" autocomplete="url" label="URL" placeholder="The instance url for this workspace" :regex="ALLOWED_URL_REGEX" required /> <VormInput v-model="draftWorkspace.description" :disabled="!isWorkspaceOwner && !createMode" :required="false" label="Description" placeholder="A description for this workspace" :regex="ALLOWED_INPUT_REGEX" /> <div class="flex flex-row flex-wrap items-center w-full gap-xs"> <VButton v-if="!createMode || createWorkspaceType" :class=" clsx( 'basis-[calc(75%-0.5rem)]', createMode ? 'flex-grow' : 'flex-grow-0', ) " :disabled=" validationState.isError || (!isWorkspaceOwner && !createMode) " :loadingText="createMode ? 'Creating...' : 'Applying...'" :requestStatus=" createMode ? createWorkspaceReqStatus : editWorkspaceReqStatus " iconRight="chevron--right" tone="action" variant="solid" @click="() => (createMode ? createWorkspace() : editWorkspace())" > {{ createMode ? "Create Workspace" : "Save Workspace" }} </VButton> <VButton v-if="!createMode && featureFlagStore.APPROVALS_OPT_IN_OUT" class="basis-[calc(25%-0.5rem)] flex-grow-0" iconRight="chevron--right" loadingText="Changing approval status..." tone="action" variant="solid" :label=" draftWorkspace.approvalsEnabled ? 'Disable Workspace Approvals' : 'Enable Workspace Approvals' " @click="changeApprovalsStatus" /> </div> </Stack> <div v-if="!createMode" class="mt-sm"> <template v-if="loadWorkspaceMembersReqStatus.isPending"> <Icon name="loader" /> </template> <template v-else-if="loadWorkspaceMembersReqStatus.isError"> <ErrorMessage :requestStatus="loadWorkspaceMembersReqStatus" /> </template> <template v-else-if="loadWorkspaceMembersReqStatus.isSuccess"> <div class="relative"> <Stack> <div class="text-lg font-bold">Members of this workspace:</div> <table class="w-full divide-y divide-neutral-400 dark:divide-neutral-600 border-b border-neutral-400 dark:border-neutral-600" > <thead> <tr class="children:pb-xs children:px-md children:font-bold text-left text-xs uppercase" > <th scope="col">Email</th> <th scope="col">Role</th> <th scope="col">Remove User</th> </tr> </thead> <tbody class="divide-y divide-neutral-300 dark:divide-neutral-700" > <MemberListItem v-for="memUser in members" :key="memUser.userId" :draftWorkspace="draftWorkspace" :memUser="memUser" :workspaceId="workspaceId" /> </tbody> </table> </Stack> </div> </template> </div> <div v-if="!createMode" class="pt-md"> <Stack> <ErrorMessage :requestStatus="inviteUserReqStatus" :message="inviteUserReqStatus.error?.data.error" /> <VormInput ref="newMemberEmailInput" v-model="newMember.email" label="User Email to Grant Workspace Access" type="email" @enterPressed="inviteButtonHandler" /> <VButton :disabled="!canInviteMember" :requestStatus="inviteUserReqStatus" class="flex-none" tone="action" variant="solid" @click="inviteButtonHandler" > Add User To Workspace </VButton> <div v-if="latestInviteEmail" class="p-sm border border-neutral-400 rounded-lg transition-opacity" > we have notified {{ latestInviteEmail }} that you invited them to collaborate on this workspace. They will be able to see this workspace in their workspace list. </div> </Stack> </div> <div v-if="!createMode" class="flex justify-between items-center pt-md"> <div class="flex flex-row gap-xs"> <VButton v-if="isWorkspaceOwner" :disabled="!isWorkspaceOwner" :requestStatus="deleteWorkspaceReqStatus" iconRight="chevron--right" loadingText="Deleting..." tone="action" variant="solid" @click="() => deleteWorkspace()" > Delete Workspace </VButton> <VButton v-else :requestStatus="leaveWorkspaceReqStatus" iconRight="chevron--right" loadingText="Leaving..." tone="destructive" variant="solid" @click="() => leaveWorkspace()" > Leave Workspace </VButton> </div> <VButton label="Go to workspace" tone="action" variant="solid" @click="() => launchWorkspace()" > </VButton> </div> </template> </div> </template> <script lang="ts" setup> import * as _ from "lodash-es"; import { computed, PropType, reactive, ref, watch, onMounted } from "vue"; import { Icon, VormInput, Stack, ErrorMessage, VButton, useValidatedInputGroup, IconButton, themeClasses, } from "@si/vue-lib/design-system"; import clsx from "clsx"; import { useHead } from "@vueuse/head"; import { useRouter } from "vue-router"; import { useWorkspacesStore, WorkspaceId } from "@/store/workspaces.store"; import { useAuthStore } from "@/store/auth.store"; import { useFeatureFlagsStore } from "@/store/feature_flags.store"; import { tracker } from "@/lib/posthog"; import { API_HTTP_URL } from "@/store/api"; import MemberListItem from "@/components/MemberListItem.vue"; import WorkspacePageHeader from "@/components/WorkspacePageHeader.vue"; import { ALLOWED_INPUT_REGEX, ALLOWED_URL_REGEX } from "@/lib/validations"; const workspacesStore = useWorkspacesStore(); const router = useRouter(); const authStore = useAuthStore(); const featureFlagStore = useFeatureFlagsStore(); const props = defineProps({ workspaceId: { type: String as PropType<WorkspaceId>, required: true }, }); const urlInputRef = ref<InstanceType<typeof VormInput>>(); const { validationState, validationMethods } = useValidatedInputGroup(); const members = computed(() => { const members = workspacesStore.selectedWorkspaceMembers; return members.slice().sort((a, b) => { // "OWNER" should come first if (a.role === "OWNER" && b.role !== "OWNER") { return -1; } if (a.role !== "OWNER" && b.role === "OWNER") { return 1; } return a.email.localeCompare(b.email); }); }); const blankWorkspace = { instanceUrl: "", displayName: "", isDefault: false, description: "", isFavourite: false, isHidden: false, approvalsEnabled: false, }; const draftWorkspace = reactive(_.cloneDeep(blankWorkspace)); const newMember = reactive({ email: "", role: "editor" }); useHead({ title: "Workspace Details" }); const createWorkspaceReqStatus = workspacesStore.getRequestStatus("CREATE_WORKSPACE"); const editWorkspaceReqStatus = workspacesStore.getRequestStatus("EDIT_WORKSPACE"); const loadWorkspaceMembersReqStatus = workspacesStore.getRequestStatus( "LOAD_WORKSPACE_MEMBERS", ); const inviteUserReqStatus = workspacesStore.getRequestStatus("INVITE_USER"); const deleteWorkspaceReqStatus = workspacesStore.getRequestStatus("DELETE_WORKSPACE"); const leaveWorkspaceReqStatus = workspacesStore.getRequestStatus("LEAVE_WORKSPACE"); const createMode = computed(() => props.workspaceId === "new"); const isWorkspaceOwner = computed( () => props.workspaceId === "new" || workspacesStore.workspacesById[props.workspaceId]?.role === "OWNER", ); const canInviteMember = computed(() => { if (props.workspaceId === "new") { return true; } const workspace = workspacesStore.workspacesById[props.workspaceId]; return workspace?.role === "OWNER" || workspace?.role === "APPROVER"; }); const isDefaultWorkspace = computed( () => props.workspaceId === "new" || workspacesStore.workspacesById[props.workspaceId]?.isDefault, ); const setDefaultReqStatus = workspacesStore.getRequestStatus( "SET_DEFAULT_WORKSPACE", ); // Reload the workspace on page load, when user logs in, when workspace ID changes or // default workspace is set (TODO last one probably not needed, in which case a computed // value for workspace will suffice rather than a watch) const loadWorkspacesReqStatus = workspacesStore.refreshWorkspaces(); watch( [() => props.workspaceId, setDefaultReqStatus], async () => { if (!setDefaultReqStatus.value.isSuccess) return; await workspacesStore.LOAD_WORKSPACES(); }, { immediate: true }, ); watch( [() => props.workspaceId, loadWorkspacesReqStatus], () => { if (!loadWorkspacesReqStatus.value.isSuccess) return; _.assign( draftWorkspace, _.cloneDeep( createMode.value ? blankWorkspace : workspacesStore.workspacesById[props.workspaceId], ), ); if (!createMode.value) { // eslint-disable-next-line @typescript-eslint/no-floating-promises workspacesStore.LOAD_WORKSPACE_MEMBERS(props.workspaceId); } }, { immediate: true }, ); // const setDefaultWorkspace = async () => { // if (!props.workspaceId) return; // await workspacesStore.SET_DEFAULT_WORKSPACE(props.workspaceId); // }; const createWorkspace = async () => { if (!draftWorkspace.description) { draftWorkspace.description = ""; } if (createWorkspaceType.value === "saas") { draftWorkspace.instanceUrl = "https://app.systeminit.com"; } else if (createWorkspaceType.value === "local") { draftWorkspace.instanceUrl = "http://localhost:8080"; } else { if (draftWorkspace.instanceUrl.includes("app.systeminit")) { // Can't create a Remote URL workspace with our URL! urlInputRef.value?.setError( 'You cannot use an "app.systeminit" URL for a Remote URL Workspace. Use "Managed By System Initiative" instead.', ); return; } else if ( draftWorkspace.instanceUrl.includes("localhost") || draftWorkspace.instanceUrl.includes("127.0.0.1") ) { // Can't create a Remote URL workspace with localhost! urlInputRef.value?.setError( 'You cannot use a "localhost" URL for a Remote URL Workspace. Use "Local Dev Instance" instead.', ); return; } } if (validationMethods.hasError()) return; const res = await workspacesStore.CREATE_WORKSPACE(draftWorkspace); if (res.result.success) { // eslint-disable-next-line @typescript-eslint/no-floating-promises await router.push({ name: "workspace-settings", params: { workspaceId: res.result.data.newWorkspaceId }, }); } }; const editWorkspace = async () => { if (validationMethods.hasError()) return; const res = await workspacesStore.EDIT_WORKSPACE(draftWorkspace); if (res.result.success) { return; } }; const openApiTokens = async () => { await router.push({ name: "workspace-api-tokens", params: { workspaceId: props.workspaceId }, }); }; const changeApprovalsStatus = async () => { if (!props.workspaceId) return; let newStatus = false; if (!draftWorkspace.approvalsEnabled) { newStatus = true; } const res = await workspacesStore.CHANGE_WORKSPACE_APPROVAL_STATUS( props.workspaceId, newStatus, ); draftWorkspace.approvalsEnabled = newStatus; if (res.result.success) { if (!draftWorkspace.instanceUrl.includes("localhost")) { window.location.href = `${draftWorkspace.instanceUrl}/refresh-auth?workspaceId=${props.workspaceId}`; } } }; const approvalStatus = computed(() => { if (!props.workspaceId) return "disabled"; if (draftWorkspace.approvalsEnabled) return "enabled"; return "disabled"; }); const deleteWorkspace = async () => { const res = await workspacesStore.DELETE_WORKSPACE(props.workspaceId); if (res.result.success) { // eslint-disable-next-line @typescript-eslint/no-floating-promises await router.push({ name: "workspaces", params: {}, }); } }; const leaveWorkspace = async () => { const res = await workspacesStore.LEAVE_WORKSPACE(props.workspaceId); if (res.result.success) { // eslint-disable-next-line @typescript-eslint/no-floating-promises await router.push({ name: "workspaces", params: {}, }); } }; const launchWorkspace = async () => { if (props.workspaceId) { tracker.trackEvent("workspace_launcher_widget_click"); window.location.href = `${API_HTTP_URL}/workspaces/${props.workspaceId}/go`; } }; const latestInviteEmail = ref<string | undefined>(); const newMemberEmailInput = ref<InstanceType<typeof VormInput>>(); const inviteButtonHandler = async () => { if (!newMember.email || newMember.email === "") return; if (newMemberEmailInput.value?.validationState.isInvalid === true) return; const res = await workspacesStore.INVITE_USER(newMember, props.workspaceId); if (res.result.success) { latestInviteEmail.value = newMember.email; newMember.email = ""; setTimeout(() => { latestInviteEmail.value = undefined; }, 20000); } }; type WorkspaceType = "saas" | "local" | "url"; const createWorkspaceType = ref<WorkspaceType | undefined>("saas"); const createWorkspaceTypeDropdownOptions = [ { label: "Managed By System Initiative", value: "saas" }, { label: "Local Dev Instance", value: "local" }, { label: "Remote URL", value: "url" }, ]; const user = computed(() => authStore.user); onMounted(() => { if (!user.value || !user.value?.emailVerified) { return router.push({ name: "profile" }); } }); </script>

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/systeminit/si'

If you have feedback or need assistance with the MCP directory API, please join our Discord server