diff --git a/cmd/e2e/api_test.go b/cmd/e2e/api_test.go index 6b02c54f..33fc719a 100644 --- a/cmd/e2e/api_test.go +++ b/cmd/e2e/api_test.go @@ -168,7 +168,7 @@ func (suite *basicSuite) TestTenantsAPI() { Body: map[string]interface{}{ "id": tenantID, "destinations_count": 1, - "topics": suite.config.Topics, + "topics": []string{"*"}, }, }, }, diff --git a/internal/apirouter/tenant_handlers.go b/internal/apirouter/tenant_handlers.go index 9fc52983..db225d4e 100644 --- a/internal/apirouter/tenant_handlers.go +++ b/internal/apirouter/tenant_handlers.go @@ -209,7 +209,7 @@ func (h *TenantHandlers) RetrievePortal(c *gin.Context) { theme = "" } - portalURL := scheme + "://" + c.Request.Host + "?token=" + jwtToken + "&tenant_id=" + tenant.ID + portalURL := scheme + "://" + c.Request.Host + "?token=" + jwtToken if theme != "" { portalURL += "&theme=" + theme } diff --git a/internal/apirouter/tenant_handlers_test.go b/internal/apirouter/tenant_handlers_test.go index 9d1e740b..5291068e 100644 --- a/internal/apirouter/tenant_handlers_test.go +++ b/internal/apirouter/tenant_handlers_test.go @@ -240,7 +240,7 @@ func TestTenantRetrievePortalHandler(t *testing.T) { router, _, redisClient := setupTestRouter(t, apiKey, jwtSecret) entityStore := setupTestEntityStore(t, redisClient, nil) - t.Run("should return redirect_url with tenant_id and tenant_id in body", func(t *testing.T) { + t.Run("should return redirect_url with token and tenant_id in body", func(t *testing.T) { t.Parallel() // Setup @@ -261,7 +261,7 @@ func TestTenantRetrievePortalHandler(t *testing.T) { // Test assert.Equal(t, http.StatusOK, w.Code) assert.NotEmpty(t, response["redirect_url"]) - assert.Contains(t, response["redirect_url"], "tenant_id="+existingResource.ID) + assert.Contains(t, response["redirect_url"], "token=") assert.Equal(t, existingResource.ID, response["tenant_id"]) // Cleanup @@ -288,7 +288,7 @@ func TestTenantRetrievePortalHandler(t *testing.T) { // Test assert.Equal(t, http.StatusOK, w.Code) - assert.Contains(t, response["redirect_url"], "tenant_id="+existingResource.ID) + assert.Contains(t, response["redirect_url"], "token=") assert.Contains(t, response["redirect_url"], "theme=dark") assert.Equal(t, existingResource.ID, response["tenant_id"]) diff --git a/internal/portal/src/common/TopicPicker/TopicPicker.scss b/internal/portal/src/common/TopicPicker/TopicPicker.scss index 70b635ad..983b2242 100644 --- a/internal/portal/src/common/TopicPicker/TopicPicker.scss +++ b/internal/portal/src/common/TopicPicker/TopicPicker.scss @@ -83,4 +83,10 @@ font-size: var(--font-size-m); line-height: var(--line-height-m); } + + &__flat-topic { + padding-left: var(--spacing-6); + margin-top: var(--spacing-3); + margin-bottom: var(--spacing-3); + } } diff --git a/internal/portal/src/common/TopicPicker/TopicPicker.tsx b/internal/portal/src/common/TopicPicker/TopicPicker.tsx index a2449af3..e1cb71a3 100644 --- a/internal/portal/src/common/TopicPicker/TopicPicker.tsx +++ b/internal/portal/src/common/TopicPicker/TopicPicker.tsx @@ -17,39 +17,32 @@ interface TopicPickerProps { onTopicsChange: (topics: string[]) => void; } -const detectSeparator = (topics: string[]): string => { - // Common separators to check - const possibleSeparators = ["/", ".", "-"]; +const possibleSeparators = ["/", ".", "-"]; - // Find the first separator that appears in all topics - // and is the first occurring separator in each topic - return ( - possibleSeparators.find((sep) => - topics.every((topic) => { - const sepIndex = topic.indexOf(sep); - if (sepIndex === -1) return false; - - // Check if any other separator appears before this one - const otherSepsIndex = possibleSeparators - .filter((s) => s !== sep) - .map((s) => topic.indexOf(s)) - .filter((idx) => idx !== -1); - - return otherSepsIndex.every((idx) => idx === -1 || idx > sepIndex); - }), - ) || "-" - ); // Fallback to '-' if no consistent separator is found +const findFirstSeparator = (topic: string): string | null => { + let firstIndex = -1; + let firstSep: string | null = null; + + for (const sep of possibleSeparators) { + const idx = topic.indexOf(sep); + if (idx !== -1 && (firstIndex === -1 || idx < firstIndex)) { + firstIndex = idx; + firstSep = sep; + } + } + + return firstSep; }; const topics: Topic[] = (() => { const topicsList = CONFIGS.TOPICS.split(","); - const separator = detectSeparator(topicsList); return topicsList.map((topic) => { - const parts = topic.split(separator); + const separator = findFirstSeparator(topic); + return { id: topic, - category: parts[0], + category: separator ? topic.split(separator)[0] : topic, }; }); })(); @@ -162,6 +155,28 @@ const TopicPicker = ({ const areAllSelected = selectedCount === categoryTopics.length; const isIndeterminate = selectedCount > 0 && !areAllSelected; + // Check if this is a flat topic (no actual nesting) + const isFlatTopic = + categoryTopics.length === 1 && categoryTopics[0].id === category; + + if (isFlatTopic) { + const topic = categoryTopics[0]; + return ( +
+ toggleTopic(topic.id)} + label={topic.id} + monospace + disabled={isEverythingSelected} + /> +
+ ); + } + return (
diff --git a/internal/portal/vite.config.ts b/internal/portal/vite.config.ts index 07865a8d..665caa01 100644 --- a/internal/portal/vite.config.ts +++ b/internal/portal/vite.config.ts @@ -3,7 +3,7 @@ import react from "@vitejs/plugin-react-swc"; import { sentryVitePlugin } from "@sentry/vite-plugin"; export default defineConfig(({ mode }) => { - let plugins: UserConfig["plugins"] = [react()]; + const plugins: UserConfig["plugins"] = [react()]; if (process.env.SENTRY_AUTH_TOKEN && mode === "production") { plugins.push( @@ -28,9 +28,13 @@ export default defineConfig(({ mode }) => { plugins, server: { port: 3334, - // hmr: { - // port: 3334, - // }, + host: true, + watch: { + usePolling: true, // Required for Docker volume mounts + }, + hmr: { + clientPort: 3334, // Ensure HMR WebSocket connects to the right port + }, }, build: { sourcemap: true,