blob: 09b5d50f1fe16e35e6ee700094918cbcd593b360 [file] [log] [blame]
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<script lang="ts" module>
type GlobalPermissionsSnakeCase = keyof KeysToSnakeCase<GlobalPermissions>;
type StreamPermissionsSnakeCase = Exclude<keyof KeysToSnakeCase<StreamPermissions>, 'topics'>;
type TopicsPerms = Record<
Topic['id'],
Record<keyof TopicPermissions, { name: string; checked: false }>
>;
type StreamsPerms = Record<
Stream['id'],
Record<
StreamPermissionsSnakeCase,
{
name: string;
checked: boolean;
disabled: boolean;
globalPermsKey: GlobalPermissionsSnakeCase;
}
> & { topicPerms: TopicsPerms }
>;
type GlobalPerms = Record<
GlobalPermissionsSnakeCase,
{ name: string; checked: boolean; relatesTo?: StreamPermissionsSnakeCase }
>;
</script>
<script lang="ts">
import { run } from 'svelte/legacy';
import Icon from './Icon.svelte';
import Combobox from './Combobox.svelte';
import type { Stream } from '$lib/domain/Stream';
import { topicMapper, type Topic } from '$lib/domain/Topic';
import { fetchRouteApi } from '$lib/api/fetchRouteApi';
import { showToast } from './AppToasts.svelte';
import type { KeysToSnakeCase } from '$lib/utils/utilTypes';
import type {
GlobalPermissions,
StreamPermissions,
TopicPermissions
} from '$lib/domain/Permissions';
import Checkbox from './Checkbox.svelte';
import { twMerge } from 'tailwind-merge';
import { noTypeCheck } from '$lib/utils/noTypeCheck';
import { fade } from 'svelte/transition';
import { SvelteSet } from 'svelte/reactivity';
interface Props {
streams: Stream[];
value?: any;
}
let { streams, value = $bindable() }: Props = $props();
let topics: Topic[] = $state([]);
let fetchingTopics = $state(false);
let selectedStream: { id: number; name: string } | undefined = $state(undefined);
let selectedTopic: { id: number; name: string } | undefined = $state(undefined);
if (streams.length > 0) {
selectedStream = { name: streams[0].name, id: streams[0].id };
}
const fetchTopics = async (id: number) => {
fetchingTopics = true;
const { data, ok } = await fetchRouteApi({
method: 'GET',
path: `/streams/${id}/topics`
});
if (!ok) {
showToast({ type: 'error', description: 'Something went wrong' });
return;
}
fetchingTopics = false;
const newTopics = data.map(topicMapper) as Topic[];
if (newTopics.length > 0) {
selectedTopic = { name: newTopics[0].name, id: newTopics[0].id };
} else {
selectedTopic = undefined;
}
topics = newTopics;
};
const buildTopicsPerms = (newTopics: Topic[]) => {
const tempTopicPerms: TopicsPerms = {};
if (!selectedStream || Object.keys(streamsPerms[selectedStream.id].topicPerms).length > 0) {
return;
}
newTopics.forEach((t) => {
tempTopicPerms[t.id] = {
manageTopic: {
checked: false,
name: 'Manage topic'
},
pollMessages: {
checked: false,
name: 'Poll messages'
},
readTopic: {
checked: false,
name: 'Read topic'
},
sendMessages: {
checked: false,
name: 'Send messages'
}
};
});
streamsPerms[selectedStream.id].topicPerms = tempTopicPerms;
streamsPerms = streamsPerms;
};
const onGlobalPermChanged = (key: GlobalPermissionsSnakeCase, checked: boolean) => {
const relatesTo = globalPerms[key].relatesTo;
if (relatesTo) {
Object.keys(streamsPerms).forEach((k) => {
streamsPerms[k][relatesTo] = { ...streamsPerms[k][relatesTo], checked, disabled: checked };
streamsPerms = streamsPerms;
});
}
};
const globalPerms: GlobalPerms = $state({
manage_servers: {
name: 'Manage servers',
checked: false
},
read_servers: {
name: 'Read servers',
checked: false
},
manage_users: {
name: 'Manage users',
checked: false
},
read_users: {
name: 'Read users',
checked: false
},
manage_streams: {
name: 'Manage streams',
relatesTo: 'manage_stream',
checked: false
},
read_streams: {
name: 'Read streams',
relatesTo: 'read_stream',
checked: false
},
manage_topics: {
name: 'Manage topics',
relatesTo: 'manage_topics',
checked: false
},
read_topics: {
name: 'Read topics',
relatesTo: 'read_topics',
checked: false
},
poll_messages: {
name: 'Pool messages',
relatesTo: 'poll_messages',
checked: false
},
send_messages: {
name: 'Send messages',
relatesTo: 'send_messages',
checked: false
}
});
let streamsPerms = $state(
(() => {
const tempPerms: StreamsPerms = {};
streams.forEach((s) => {
tempPerms[s.id] = {
manage_stream: {
name: 'Manage stream',
globalPermsKey: 'manage_streams',
checked: false,
disabled: false
},
read_stream: {
name: 'Read stream',
globalPermsKey: 'read_streams',
checked: false,
disabled: false
},
read_topics: {
name: 'Read topics',
globalPermsKey: 'read_topics',
checked: false,
disabled: false
},
poll_messages: {
name: 'Poll messages',
globalPermsKey: 'poll_messages',
checked: false,
disabled: false
},
send_messages: {
name: 'Send messages',
globalPermsKey: 'send_messages',
checked: false,
disabled: false
},
manage_topics: {
name: 'Manage topics',
globalPermsKey: 'manage_topics',
checked: false,
disabled: false
},
topicPerms: {}
};
});
return tempPerms;
})() satisfies StreamsPerms
);
run(() => {
if (selectedStream) fetchTopics(selectedStream.id);
});
run(() => {
buildTopicsPerms(topics);
});
let taintedStreams = $derived(
(() => {
const tainted: Set<number> = new SvelteSet([]);
Object.keys(streamsPerms).forEach((streamId) => {
Object.keys(streamsPerms[streamId]).forEach((permissionKey) => {
if (permissionKey === 'topicPerms') {
const perm = streamsPerms[streamId][permissionKey];
Object.keys(perm).forEach((topicId) => {
const topicPerm = perm[topicId];
const isTopicTained = Object.keys(topicPerm)
.map((k) => topicPerm[k])
.some((p) => p.checked);
if (isTopicTained) tainted.add(streamId);
});
} else {
const perm = streamsPerms[streamId][permissionKey];
if (perm.checked && !perm.disabled) tainted.add(streamId);
}
});
});
return Array.from(tainted);
})().map((taintedStreamId) => {
const name = streams.find((stream) => stream.id === +taintedStreamId)!.name;
return { name, id: +taintedStreamId };
})
);
$effect(() => {
function formatGlobalPermissions() {
return Object.keys(globalPerms).reduce((result, key) => {
result[key] = globalPerms[key].checked;
return result;
}, {});
}
function hasAnyPermissionChecked(
permissionsObj: Record<string, any>,
excludeKeys: string[] = []
) {
return Object.keys(permissionsObj)
.filter((key) => !excludeKeys.includes(key))
.some((key) => permissionsObj[key].checked);
}
function formatTopicPermissions(topicPerms: Record<string, any>) {
const result = {};
Object.keys(topicPerms).forEach((topicId) => {
const topicPerm = topicPerms[topicId];
if (!hasAnyPermissionChecked(topicPerm)) {
return;
}
result[topicId] = {
manage_topic: topicPerm.manageTopic.checked,
read_topic: topicPerm.readTopic.checked,
poll_messages: topicPerm.pollMessages.checked,
send_messages: topicPerm.sendMessages.checked
};
});
return result;
}
function formatStreamPermissions() {
const result = {};
Object.keys(streamsPerms).forEach((streamId) => {
const streamPerm = streamsPerms[streamId];
const topicPerms = streamPerm.topicPerms;
const hasStreamPermissions = hasAnyPermissionChecked(streamPerm, ['topicPerms']);
const formattedTopics = formatTopicPermissions(topicPerms);
const hasTopicPermissions = Object.keys(formattedTopics).length > 0;
if (!hasStreamPermissions && !hasTopicPermissions) {
return;
}
result[streamId] = {
manage_stream: streamPerm.manage_stream.checked,
read_stream: streamPerm.read_stream.checked,
manage_topics: streamPerm.manage_topics.checked,
read_topics: streamPerm.read_topics.checked,
poll_messages: streamPerm.poll_messages.checked,
send_messages: streamPerm.send_messages.checked
};
if (hasTopicPermissions) {
result[streamId].topics = formattedTopics;
}
});
return result;
}
value = {
global: formatGlobalPermissions(),
streams: formatStreamPermissions()
};
});
</script>
<h4 class="ml-1 text-lg text-color mt-7">Global permissions</h4>
<div class="grid grid-cols-4 mt-4">
{#each Object.keys(globalPerms) as key (key)}
<label
class="flex gap-2 items-center text-color cursor-pointer"
for={`global-permissions-${key}`}
>
<Checkbox
bind:checked={globalPerms[key].checked}
value={globalPerms[key].name}
id={`global-permissions-${key}`}
name={globalPerms[key].name}
onclick={(e) => onGlobalPermChanged(key, noTypeCheck(e).target.checked)}
/>
<span class="text-sm">{globalPerms[key].name}</span>
</label>
{/each}
</div>
{#if selectedStream}
<div class="flex flex-wrap gap-3 mt-6 items-center">
<h4 class="text-lg text-color mr-2">Granular permissions</h4>
{#each taintedStreams as { id, name } (id)}
<button
type="button"
onclick={() => (selectedStream = { name, id })}
transition:fade={{ duration: 80 }}
class={twMerge(
'rounded-3xl px-3 py-1 whitespace-nowrap text-xs hover:shadow-lg hover:ring-2 transition-all dark:text-white ring-1 ring-green-500 shadow-md hover:cursor-pointer',
selectedStream?.id === id && 'bg-green-500 text-white'
)}
>id: {id}, {name}
</button>
{/each}
</div>
<div class="grid grid-cols-[1fr_auto_1fr] gap-5 mt-4">
<div class="w-full flex flex-col">
<Combobox
items={streams}
formatter={(item) => `id: ${item.id}, ${item.name}`}
label="Stream"
bind:selectedValue={selectedStream}
/>
<div class="grid grid-cols-2 mt-4">
{#each Object.keys(streamsPerms[selectedStream.id]) as key (key)}
{#if key !== 'topicPerms'}
<label
class={twMerge(
'flex gap-2 items-center text-color cursor-pointer',
streamsPerms[selectedStream.id][key].disabled &&
'cursor-not-allowed text-shade-l800'
)}
for={`stream-${key}-permission`}
>
<Checkbox
bind:checked={streamsPerms[selectedStream.id][key].checked}
value={streamsPerms[selectedStream.id][key].name}
disabled={streamsPerms[selectedStream.id][key].disabled}
id={`stream-${key}-permission`}
/>
<span class={twMerge('text-sm')}>{streamsPerms[selectedStream.id][key].name}</span>
</label>
{/if}
{/each}
</div>
</div>
<div class="h-[68px] w-[40px] flex flex-col justify-end">
<div class="w-fit h-fit">
<Icon name="chevronRight" class="h-[40px] dark:stroke-white mt-auto w-auto" />
</div>
</div>
<div class="w-full flex flex-col">
{#if topics.length === 0 && !streamsPerms[selectedStream.id].manage_topics.checked}
<em class="italic dark:text-white text-center block mt-[34px]">
This stream has no topics.
</em>
{/if}
{#if streamsPerms[selectedStream.id].manage_topics.checked}
<div class="dark:text-white mt-9 text-center">
<span> Every topic in stream </span>
<em class="text-green-500">{selectedStream.name}</em>
<span> {topics.length === 0 ? 'will have' : 'has'} full permissions </span>
</div>
{/if}
{#if selectedTopic && Object.keys(streamsPerms[selectedStream.id].topicPerms).length > 0 && !streamsPerms[selectedStream.id].manage_topics.checked}
<div>
<Combobox
isLoading={fetchingTopics}
items={topics}
formatter={(item) => `id: ${item.id}, ${item.name}`}
label="Topic"
bind:selectedValue={selectedTopic}
/>
<div class="grid grid-cols-2 mt-4">
{#each Object.keys(streamsPerms[selectedStream.id].topicPerms[selectedTopic.id]) as key (key)}
<label class="flex gap-2 items-center text-color cursor-pointer">
<Checkbox
bind:checked={
streamsPerms[selectedStream.id].topicPerms[selectedTopic.id][key].checked
}
value=""
/>
<span class="text-sm"
>{streamsPerms[selectedStream.id].topicPerms[selectedTopic.id][key].name}</span
>
</label>
{/each}
</div>
</div>
{/if}
</div>
</div>
{/if}