teams improvements if there are a large number of connected teams (#5694)

* teams improvements if there are a large number of connected teams / channels

* move sync to backend

* formatting

* update ee ref
This commit is contained in:
Alexander Petric
2025-05-07 23:42:05 +02:00
committed by GitHub
parent 242a565428
commit 5589135cb0
6 changed files with 344 additions and 145 deletions
+1 -1
View File
@@ -1 +1 @@
868ccad87afb804fe22818ecd3d5a091199bcdbf
19b5681d1985ee5214f768e3b818faf6ca7d20b2
@@ -0,0 +1,86 @@
<script lang="ts">
import Select from '$lib/components/apps/svelte-select/lib/Select.svelte'
import { SELECT_INPUT_DEFAULT_STYLE } from '$lib/defaults'
import { createEventDispatcher, onMount } from 'svelte'
import DarkModeObserver from './DarkModeObserver.svelte'
interface ChannelItem {
channel_id: string
channel_name: string
}
export let disabled = false
export let placeholder = 'Select channel'
export let selectedChannel: ChannelItem | undefined = undefined
export let containerClass = 'w-64'
export let minWidth = '160px'
export let channels: ChannelItem[] = []
let darkMode: boolean = false
const dispatch = createEventDispatcher<{
change: ChannelItem
}>()
function onThemeChange() {
darkMode = document.documentElement.classList.contains('dark')
}
onMount(() => {
onThemeChange()
})
function handleChannelSelect(event) {
selectedChannel = event.detail
if (selectedChannel) {
dispatch('change', selectedChannel)
}
}
function filterChannels(filterText: string | unknown) {
if (!channels) return channels
const searchText = typeof filterText === 'string' ? filterText : ''
const filtered = searchText
? channels.filter((channel) =>
channel.channel_name.toLowerCase().includes(searchText.toLowerCase())
)
: channels
return filtered
}
</script>
<div class={containerClass}>
<div class="flex items-center gap-2">
<div class="flex-grow" style="min-width: {minWidth};">
<Select
inputStyles={SELECT_INPUT_DEFAULT_STYLE.inputStyles}
containerStyles={'border-color: lightgray; min-width: ' +
minWidth +
';' +
(darkMode
? SELECT_INPUT_DEFAULT_STYLE.containerStylesDark
: SELECT_INPUT_DEFAULT_STYLE.containerStyles)}
itemId="channel_id"
label="channel_name"
items={channels || []}
on:change={handleChannelSelect}
{placeholder}
searchable={true}
disabled={disabled || channels.length === 0}
on:input={async (e) => {
const filterText = e.detail
return filterChannels(filterText)
}}
/>
</div>
</div>
{#if channels.length === 0 && !disabled}
<div class="text-xs text-tertiary mt-1"> No channels available </div>
{/if}
</div>
<DarkModeObserver on:change={onThemeChange} />
@@ -1,15 +1,21 @@
<script lang="ts">
import { Badge, Button } from '$lib/components/common'
import Description from '$lib/components/Description.svelte'
import { Slack, Code2, RefreshCcw } from 'lucide-svelte'
import { Slack, Code2 } from 'lucide-svelte'
import MsTeamsIcon from '$lib/components/icons/MSTeamsIcon.svelte'
import BarsStaggered from '$lib/components/icons/BarsStaggered.svelte'
import { hubBaseUrlStore, workspaceStore, enterpriseLicense } from '$lib/stores'
import ScriptPicker from '$lib/components/ScriptPicker.svelte'
import { WorkspaceService } from '$lib/gen'
import type { ListAvailableTeamsIdsResponse } from '$lib/gen/types.gen'
export let platform: 'slack' | 'teams'
import { sendUserToast } from '$lib/utils'
import TeamSelector from './TeamSelector.svelte'
interface TeamItem {
team_id: string
team_name: string
}
export let platform: 'slack' | 'teams'
export let teamName: string | undefined
export let display_name: string | undefined
export let scriptPath: string
@@ -23,34 +29,25 @@
export let onLoadSettings: () => void
export let itemKind: 'flow' | 'script' = 'script'
let isFetching = false
let teams: ListAvailableTeamsIdsResponse = []
let selected_teams_team: string | undefined = undefined
async function loadTeams() {
isFetching = true
selected_teams_team = undefined
teams = (await WorkspaceService.listAvailableTeamsIds({ workspace: $workspaceStore! })) ?? []
isFetching = false
}
$: workspaceStore && platform && $enterpriseLicense && loadTeams()
let selectedTeam: TeamItem | undefined = undefined
async function connectTeams() {
const selectedTeam = teams.find((team) => team.team_id === selected_teams_team)
const selectedTeamName = selectedTeam ? selectedTeam.team_name : undefined
if (!selectedTeam) return
await WorkspaceService.connectTeams({
workspace: $workspaceStore!,
requestBody: {
team_id: selected_teams_team,
team_name: selectedTeamName
}
})
sendUserToast('Connected to Teams to Workspace')
onLoadSettings()
loadTeams()
try {
await WorkspaceService.connectTeams({
workspace: $workspaceStore!,
requestBody: {
team_id: selectedTeam.team_id,
team_name: selectedTeam.team_name
}
})
sendUserToast('Connected to Teams successfully')
onLoadSettings()
} catch (error) {
sendUserToast('Failed to connect to Teams', true)
console.error('Error connecting to Teams:', error)
}
}
</script>
@@ -92,49 +89,35 @@
{/if}
</div>
{:else}
<div class="flex flex-row gap-2">
{#if platform === 'teams'}
<Button
size="xs"
color="dark"
on:click={connectTeams}
startIcon={{ icon: MsTeamsIcon }}
disabled={!selected_teams_team || !enterpriseLicense}
>
Connect to {platform.charAt(0).toUpperCase() + platform.slice(1)}
{$enterpriseLicense ? '' : '(EE only)'}
</Button>
{#if $enterpriseLicense}
<div class="w-64 flex flex-row gap-2">
<select bind:value={selected_teams_team}>
{#if !isFetching}
{#if teams.length === 0}
<option value="" disabled selected>No unassigned teams found</option>
{:else}
<option value="" disabled selected>Select team</option>
{#each teams as team}
<option value={team.team_id}>
{team.team_name}
</option>
{/each}
{/if}
{:else}
<option value="" disabled selected>Loading...</option>
{/if}
</select>
</div>
<div class="pt-1">
<button on:click={loadTeams} class="flex items-center gap-1 mt-2">
<RefreshCcw size={16} class={isFetching ? 'animate-spin' : ''} />
</button>
</div>
<div class="flex flex-col gap-2">
<div class="flex flex-row gap-2 items-center">
{#if platform === 'teams'}
<Button
size="xs"
color="dark"
on:click={connectTeams}
startIcon={{ icon: MsTeamsIcon }}
disabled={!selectedTeam || !$enterpriseLicense}
>
Connect to {platform.charAt(0).toUpperCase() + platform.slice(1)}
{$enterpriseLicense ? '' : '(EE only)'}
</Button>
{#if $enterpriseLicense}
<TeamSelector
bind:selectedTeam
minWidth="180px"
disabled={!$enterpriseLicense}
on:error={(e) => sendUserToast('Failed to load teams: ' + e.detail.message, true)}
/>
{/if}
{:else}
<Button size="xs" color="dark" href={connectHref} startIcon={{ icon: Slack }}>
Connect to {platform.charAt(0).toUpperCase() + platform.slice(1)}
</Button>
{/if}
{:else}
<Button size="xs" color="dark" href={connectHref} startIcon={{ icon: Slack }}>
Connect to {platform.charAt(0).toUpperCase() + platform.slice(1)}
</Button>
{/if}
<Badge color="red">Not connected</Badge>
<Badge color="red">Not connected</Badge>
</div>
</div>
{/if}
@@ -3,6 +3,7 @@
import ScriptPicker from '$lib/components/ScriptPicker.svelte'
import Toggle from '$lib/components/Toggle.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
import ChannelSelector from '$lib/components/ChannelSelector.svelte'
import type { Schema, SupportedLanguage } from '$lib/common'
import { base } from '$lib/base'
@@ -439,34 +440,40 @@
/>
</span>
{#if workspaceConnectedToTeams}
<div class="w-2/3 flex flex-row items-center gap-2">
<div class="pt-1 flex-shrink-0">
<MsTeamsIcon height="24px" width="24px" />
<div class="w-2/3 flex flex-col gap-2">
<div class="flex flex-row items-center gap-2">
<div class="pt-1 flex-shrink-0">
<MsTeamsIcon height="24px" width="24px" />
</div>
<p class="text-sm">Teams Channel</p>
</div>
<p class="text-sm">Teams Channel</p>
<div class="flex-grow">
<select class="w-full" bind:value={handlerExtraArgs['channel']}>
{#if teams_channels.length === 0}
<option value="" disabled selected>Bot not connected to any channel</option>
{:else}
<option value="" disabled selected>Select Teams channel</option>
{#each teams_channels as channel}
<option value={channel.channel_id}>
{channel.channel_name}
</option>
{/each}
{/if}
</select>
</div>
<div class="flex-shrink-0">
<button on:click={loadTeamsResources} class="flex items-center gap-1 mt-2">
<RefreshCcw size={16} class={isFetching ? 'animate-spin' : ''} />
</button>
<div class="flex flex-row gap-2 items-start">
<ChannelSelector
containerClass="flex-grow"
minWidth="200px"
placeholder="Select Teams channel"
channels={teams_channels as any[]}
on:change={(e) => (handlerExtraArgs['channel'] = e.detail.channel_id)}
selectedChannel={handlerExtraArgs['channel']
? (teams_channels.find((ch) => ch.channel_id === handlerExtraArgs['channel']) as any)
: undefined}
/>
<div class="flex-shrink-0">
<button
on:click={loadTeamsResources}
class="flex items-center gap-1 p-1.5 rounded hover:bg-surface-hover focus:bg-surface-hover"
>
<RefreshCcw size={16} class={isFetching ? 'animate-spin' : ''} />
</button>
</div>
</div>
</div>
<div class="flex flex-row gap-2 pb-4">
<p class="text-sm">
This workspace is connected to Team: <Badge color="blue" size="xs" class="mt-2">{teams_team_name}</Badge>
This workspace is connected to Team: <Badge color="blue" size="xs" class="mt-2"
>{teams_team_name}</Badge
>
</p>
<Tooltip text={teams_team_name}>
Each workspace can only be connected to one Microsoft Teams team. You can configure it under <a
@@ -499,7 +506,7 @@
{:else}
<Button
disabled={emptyString(handlerExtraArgs['channel'])}
btnClasses="w-32 text-center"
btnClasses="w-32 text-center mt-2"
color="dark"
on:click={() => sendTeamsMessage(handlerExtraArgs['channel'] ?? '')}
size="xs">Send test message</Button
@@ -507,7 +514,7 @@
{#if connectionTestJob !== undefined}
<p class="text-normal text-2xs mt-1 gap-2">
{#if connectionTestJob.in_progress}
<RotateCw size={14} />
<RotateCw size={14} class="animate-spin" />
{:else if connectionTestJob.is_success}
<CheckCircle2 size={14} class="text-green-600" />
{:else}
@@ -30,6 +30,8 @@
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
import SimpleEditor from './SimpleEditor.svelte'
import TeamSelector from './TeamSelector.svelte'
import ChannelSelector from './ChannelSelector.svelte'
export let setting: Setting
export let version: string
@@ -134,9 +136,8 @@
}
}
function handleTeamChange(event: Event, i: number) {
const teamId = (event.target as HTMLSelectElement).value
const team = $values['teams'].find((team) => team.team_id === teamId) || null
function handleTeamChange(teamItem: { team_id: string; team_name: string }, i: number) {
const team = $values['teams'].find((team) => team.team_id === teamItem.team_id) || null
$values['critical_error_channels'][i] = {
teams_channel: {
team_id: team?.team_id,
@@ -147,13 +148,9 @@
}
}
function handleChannelChange(event: Event, setting: Setting, i: number) {
const channelId = (event.target as HTMLSelectElement).value
const team = $values['teams'].find(
(team) => team.team_id === $values['critical_error_channels'][i]?.teams_channel?.team_id
)
const channel = team?.channels.find((channel) => channel.channel_id === channelId) || null
if (channelId) {
function handleChannelChange(channel: { channel_id: string; channel_name: string }, i: number) {
const team = $values['critical_error_channels'][i]?.teams_channel
if (team) {
$values['critical_error_channels'][i] = {
teams_channel: {
team_id: team?.team_id,
@@ -458,46 +455,45 @@
/>
{:else if v && 'teams_channel' in v}
<div class="flex flex-row gap-2 w-full">
<select on:change={(e) => handleTeamChange(e, i)}>
<option
value=""
disabled
selected={!$values['critical_error_channels'][i]?.teams_channel
?.team_id}>Select team</option
>
{#if $values['teams']}
{#each $values['teams'] as team}
<option
value={team.team_id}
selected={$values['critical_error_channels'][i]?.teams_channel
?.team_id === team.team_id}
>
{team.team_name}
</option>
{/each}
{/if}
</select>
<TeamSelector
containerClass="w-44"
minWidth="140px"
showRefreshButton={false}
placeholder="Select team"
teams={$values['teams']}
on:change={(e) => handleTeamChange(e.detail, i)}
selectedTeam={$values['critical_error_channels'][i]?.teams_channel
? {
team_id:
$values['critical_error_channels'][i]?.teams_channel?.team_id,
team_name:
$values['critical_error_channels'][i]?.teams_channel?.team_name
}
: undefined}
/>
{#if $values['critical_error_channels'][i]?.teams_channel?.team_id}
<select
id="channel-select"
on:change={(e) => handleChannelChange(e, setting, i)}
>
<option
value=""
disabled
selected={!$values['critical_error_channels'][i]?.teams_channel
?.channel_id}>Select channel</option
>
{#each $values['teams'].find((team) => team.team_id === $values['critical_error_channels'][i]?.teams_channel?.team_id)?.channels ?? [] as channel}
<option
value={channel.channel_id}
selected={$values['critical_error_channels'][i]?.teams_channel
?.channel_id === channel.channel_id}
>
{channel.channel_name}
</option>
{/each}
</select>
<ChannelSelector
containerClass=""
placeholder="Select channel"
channels={$values['teams'].find(
(team) =>
team.team_id ===
$values['critical_error_channels'][i]?.teams_channel?.team_id
)?.channels ?? []}
on:change={(e) => handleChannelChange(e.detail, i)}
selectedChannel={$values['critical_error_channels'][i]?.teams_channel
?.channel_id
? {
channel_id:
$values['critical_error_channels'][i]?.teams_channel
?.channel_id,
channel_name:
$values['critical_error_channels'][i]?.teams_channel
?.channel_name
}
: undefined}
/>
{/if}
<div>
<button on:click={fetchTeams} class="flex items-center gap-1 mt-2">
@@ -0,0 +1,127 @@
<script lang="ts">
import Select from '$lib/components/apps/svelte-select/lib/Select.svelte'
import { workspaceStore } from '$lib/stores'
import { SELECT_INPUT_DEFAULT_STYLE } from '$lib/defaults'
import { createEventDispatcher, onMount } from 'svelte'
import DarkModeObserver from './DarkModeObserver.svelte'
import { RefreshCcw } from 'lucide-svelte'
import { WorkspaceService } from '$lib/gen'
interface TeamItem {
team_id: string
team_name: string
}
export let disabled = false
export let placeholder = 'Select a team'
export let selectedTeam: TeamItem | undefined = undefined
export let containerClass = 'w-64'
export let showRefreshButton = true
export let teams: TeamItem[] | undefined = undefined
export let minWidth = '160px'
let isFetching = false
let darkMode: boolean = false
const dispatch = createEventDispatcher<{
change: TeamItem
error: Error
}>()
function onThemeChange() {
darkMode = document.documentElement.classList.contains('dark')
}
onMount(() => {
onThemeChange()
if (!teams) {
loadTeams()
}
})
async function loadTeams() {
isFetching = true
try {
const response = (await WorkspaceService.listAvailableTeamsIds({
workspace: $workspaceStore!
})) as unknown as TeamItem[]
teams = response || []
isFetching = false
console.log('Teams loaded:', teams.length)
return teams
} catch (error) {
isFetching = false
dispatch('error', error)
console.error('Error loading teams:', error)
return []
}
}
function handleTeamSelect(event) {
selectedTeam = event.detail
if (selectedTeam) {
dispatch('change', selectedTeam)
}
}
function filterTeams(filterText: string | unknown) {
if (!teams) return teams
const searchText = typeof filterText === 'string' ? filterText : ''
const filtered = searchText
? teams.filter((team) => team.team_name.toLowerCase().includes(searchText.toLowerCase()))
: teams
return filtered
}
</script>
<div class={containerClass}>
<div class="flex items-center gap-2">
<div class="flex-grow" style="min-width: {minWidth};">
<Select
inputStyles={SELECT_INPUT_DEFAULT_STYLE.inputStyles}
containerStyles={'border-color: lightgray; min-width: ' +
minWidth +
';' +
(darkMode
? SELECT_INPUT_DEFAULT_STYLE.containerStylesDark
: SELECT_INPUT_DEFAULT_STYLE.containerStyles)}
itemId="team_id"
label="team_name"
items={teams || []}
on:change={handleTeamSelect}
{placeholder}
searchable={true}
loading={isFetching}
disabled={disabled || isFetching}
on:input={(e) => filterTeams(e.detail)}
/>
</div>
{#if showRefreshButton}
<button
on:click={loadTeams}
disabled={isFetching || disabled}
class="flex items-center justify-center p-1.5 rounded hover:bg-surface-hover focus:bg-surface-hover disabled:opacity-50"
title="Refresh teams from Microsoft"
>
<RefreshCcw size={16} class={isFetching ? 'animate-spin' : ''} />
</button>
{/if}
</div>
{#if isFetching || ((!teams || teams.length === 0) && !isFetching)}
<div class="text-xs text-tertiary mt-1">
{#if isFetching}
Fetching teams from Microsoft...
{:else}
No available teams found
{/if}
</div>
{/if}
</div>
<DarkModeObserver on:change={onThemeChange} />