mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-07 16:59:51 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52a90fbd2b |
@@ -1,28 +0,0 @@
|
|||||||
import { CommandModule } from "yargs";
|
|
||||||
import { db, certificates } from "@server/db";
|
|
||||||
|
|
||||||
type ClearCertificatesArgs = {};
|
|
||||||
|
|
||||||
export const clearCertificates: CommandModule<{}, ClearCertificatesArgs> = {
|
|
||||||
command: "clear-certificates",
|
|
||||||
describe: "Delete all entries from the certificates table",
|
|
||||||
builder: (yargs) => {
|
|
||||||
return yargs;
|
|
||||||
},
|
|
||||||
handler: async (argv: {}) => {
|
|
||||||
try {
|
|
||||||
console.log("Clearing all certificates from the database...");
|
|
||||||
|
|
||||||
const deleted = await db.delete(certificates).returning();
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`Deleted ${deleted.length} certificate(s) from the database`
|
|
||||||
);
|
|
||||||
|
|
||||||
process.exit(0);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error:", error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -9,7 +9,6 @@ import { rotateServerSecret } from "./commands/rotateServerSecret";
|
|||||||
import { clearLicenseKeys } from "./commands/clearLicenseKeys";
|
import { clearLicenseKeys } from "./commands/clearLicenseKeys";
|
||||||
import { deleteClient } from "./commands/deleteClient";
|
import { deleteClient } from "./commands/deleteClient";
|
||||||
import { generateOrgCaKeys } from "./commands/generateOrgCaKeys";
|
import { generateOrgCaKeys } from "./commands/generateOrgCaKeys";
|
||||||
import { clearCertificates } from "./commands/clearCertificates";
|
|
||||||
|
|
||||||
yargs(hideBin(process.argv))
|
yargs(hideBin(process.argv))
|
||||||
.scriptName("pangctl")
|
.scriptName("pangctl")
|
||||||
@@ -20,6 +19,5 @@ yargs(hideBin(process.argv))
|
|||||||
.command(clearLicenseKeys)
|
.command(clearLicenseKeys)
|
||||||
.command(deleteClient)
|
.command(deleteClient)
|
||||||
.command(generateOrgCaKeys)
|
.command(generateOrgCaKeys)
|
||||||
.command(clearCertificates)
|
|
||||||
.demandCommand()
|
.demandCommand()
|
||||||
.help().argv;
|
.help().argv;
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
services:
|
|
||||||
mailer:
|
|
||||||
image: axllent/mailpit
|
|
||||||
ports:
|
|
||||||
- 8025:8025
|
|
||||||
- 1025:1025
|
|
||||||
volumes:
|
|
||||||
- mailpit-storage:/data
|
|
||||||
environment:
|
|
||||||
- MP_DATABASE=/data/mailpit.db
|
|
||||||
volumes:
|
|
||||||
mailpit-storage:
|
|
||||||
@@ -2660,19 +2660,19 @@
|
|||||||
"noMoreAuthMethods": "Няма валидни методи за удостоверение",
|
"noMoreAuthMethods": "Няма валидни методи за удостоверение",
|
||||||
"ip": "IP",
|
"ip": "IP",
|
||||||
"reason": "Причина",
|
"reason": "Причина",
|
||||||
"requestLogs": "Логове за HTTP заявки",
|
"requestLogs": "Заявка за логове",
|
||||||
"requestAnalytics": "Анализи На Заявки",
|
"requestAnalytics": "Анализи На Заявки",
|
||||||
"host": "Хост",
|
"host": "Хост",
|
||||||
"location": "Местоположение",
|
"location": "Местоположение",
|
||||||
"actionLogs": "Дневници на действията",
|
"actionLogs": "Дневници на действията",
|
||||||
"sidebarLogsRequest": "Логове за HTTP заявки",
|
"sidebarLogsRequest": "Заявка за логове",
|
||||||
"sidebarLogsAccess": "Достъп до логове",
|
"sidebarLogsAccess": "Достъп до логове",
|
||||||
"sidebarLogsAction": "Дневници на действията",
|
"sidebarLogsAction": "Дневници на действията",
|
||||||
"logRetention": "Задържане на логове",
|
"logRetention": "Задържане на логове",
|
||||||
"logRetentionDescription": "Управлявайте времето за задържане на различни видове логове за тази организация или ги деактивирайте",
|
"logRetentionDescription": "Управлявайте времето за задържане на различни видове логове за тази организация или ги деактивирайте",
|
||||||
"requestLogsDescription": "Прегледайте подробни логове на заявки за ресурси в тази организация",
|
"requestLogsDescription": "Прегледайте подробни логове на заявки за ресурси в тази организация",
|
||||||
"requestAnalyticsDescription": "Вижте подробни анализи на заявки за ресурсите в тази организация",
|
"requestAnalyticsDescription": "Вижте подробни анализи на заявки за ресурсите в тази организация",
|
||||||
"logRetentionRequestLabel": "Задържане на логове за HTTP заявки",
|
"logRetentionRequestLabel": "Задържане на логове на заявки",
|
||||||
"logRetentionRequestDescription": "Колко дълго да се задържат логовете на заявките",
|
"logRetentionRequestDescription": "Колко дълго да се задържат логовете на заявките",
|
||||||
"logRetentionAccessLabel": "Задържане на логове за достъп",
|
"logRetentionAccessLabel": "Задържане на логове за достъп",
|
||||||
"logRetentionAccessDescription": "Колко дълго да се задържат логовете за достъп",
|
"logRetentionAccessDescription": "Колко дълго да се задържат логовете за достъп",
|
||||||
@@ -3134,7 +3134,7 @@
|
|||||||
"httpDestActionLogsDescription": "Административни действия, извършени от потребители в организацията.",
|
"httpDestActionLogsDescription": "Административни действия, извършени от потребители в организацията.",
|
||||||
"httpDestConnectionLogsTitle": "Логове на връзката",
|
"httpDestConnectionLogsTitle": "Логове на връзката",
|
||||||
"httpDestConnectionLogsDescription": "Събития на свързване и прекъсване на сайта и тунела, включително свръзки и прекъсвания.",
|
"httpDestConnectionLogsDescription": "Събития на свързване и прекъсване на сайта и тунела, включително свръзки и прекъсвания.",
|
||||||
"httpDestRequestLogsTitle": "Логове за HTTP заявки",
|
"httpDestRequestLogsTitle": "Заявки за логове",
|
||||||
"httpDestRequestLogsDescription": "Регистри за HTTP заявките към проксирани ресурси, включително метод, път и код на отговор.",
|
"httpDestRequestLogsDescription": "Регистри за HTTP заявките към проксирани ресурси, включително метод, път и код на отговор.",
|
||||||
"httpDestSaveChanges": "Запази промените",
|
"httpDestSaveChanges": "Запази промените",
|
||||||
"httpDestCreateDestination": "Създаване на дестинация",
|
"httpDestCreateDestination": "Създаване на дестинация",
|
||||||
@@ -3208,48 +3208,5 @@
|
|||||||
"domainPickerWildcardCertWarning": "Ресурсите с уайлдкард може да изискват допълнителна конфигурация за правилна работа.",
|
"domainPickerWildcardCertWarning": "Ресурсите с уайлдкард може да изискват допълнителна конфигурация за правилна работа.",
|
||||||
"domainPickerWildcardCertWarningLink": "Научете повече",
|
"domainPickerWildcardCertWarningLink": "Научете повече",
|
||||||
"health": "Здраве",
|
"health": "Здраве",
|
||||||
"domainPendingErrorTitle": "Проблем при проверка",
|
"domainPendingErrorTitle": "Проблем при проверка"
|
||||||
"memberPortalTitle": "Ресурси",
|
|
||||||
"memberPortalDescription": "Ресурси, до които имате достъп в тази организация",
|
|
||||||
"memberPortalSortBy": "Сортиране по...",
|
|
||||||
"memberPortalSortNameAsc": "Име А-Я",
|
|
||||||
"memberPortalSortNameDesc": "Име Я-А",
|
|
||||||
"memberPortalSortDomainAsc": "Домен А-Я",
|
|
||||||
"memberPortalSortDomainDesc": "Домен Я-А",
|
|
||||||
"memberPortalSortEnabledFirst": "Активирани Първи",
|
|
||||||
"memberPortalSortDisabledFirst": "Деактивирани Първи",
|
|
||||||
"memberPortalRefresh": "Обнови",
|
|
||||||
"memberPortalRefreshResources": "Обнови ресурсите",
|
|
||||||
"memberPortalFailedToLoad": "Грешка при зареждане на ресурсите",
|
|
||||||
"memberPortalFailedToLoadDescription": "Грешка при зареждане на ресурсите. Моля, проверете връзката си и опитайте отново.",
|
|
||||||
"memberPortalUnableToLoad": "Неуспешно зареждане на ресурси",
|
|
||||||
"memberPortalTryAgain": "Опитай отново",
|
|
||||||
"memberPortalNoResourcesFound": "Няма намерени ресурси",
|
|
||||||
"memberPortalNoResourcesAvailable": "Няма налични ресурси",
|
|
||||||
"memberPortalNoResourcesMatchSearch": "Няма ресурси, съвпадащи с \"{query}\". Опитайте да промените търсените условия или нулирайте търсенето, за да видите всички ресурси.",
|
|
||||||
"memberPortalNoResourcesAccess": "Още нямате достъп до ресурси. Свържете се с вашия администратор, за да получите достъп до нужните ресурси.",
|
|
||||||
"memberPortalClearSearch": "Изчисти търсенето",
|
|
||||||
"memberPortalPublicResources": "Публични ресурси",
|
|
||||||
"memberPortalPublicResourcesDescription": "Уеб приложения и услуги, достъпни през браузър",
|
|
||||||
"memberPortalCopiedToClipboard": "Копирано в клипборда",
|
|
||||||
"memberPortalCopiedUrlDescription": "URL адресът на ресурса е копиран в клипборда.",
|
|
||||||
"memberPortalOpenResource": "Отвори ресурса",
|
|
||||||
"memberPortalPrivateResources": "Частни ресурси",
|
|
||||||
"memberPortalPrivateResourcesDescription": "Ресурси на вътрешната мрежа, достъпни чрез клиент",
|
|
||||||
"memberPortalResourceDetails": "Детайли за ресурса",
|
|
||||||
"memberPortalMode": "Режим",
|
|
||||||
"memberPortalDestination": "Дестинация",
|
|
||||||
"memberPortalAlias": "Алиас",
|
|
||||||
"memberPortalCopiedAliasDescription": "Алиасът на ресурса е копиран в клипборда.",
|
|
||||||
"memberPortalCopiedDestinationDescription": "Дестинацията на ресурса е копирана в клипборда.",
|
|
||||||
"memberPortalRequiresClientConnection": "Изисква връзка с клиента",
|
|
||||||
"memberPortalAuthMethods": "Методи на удостоверяване",
|
|
||||||
"memberPortalSso": "Единно вход (SSO)",
|
|
||||||
"memberPortalPasswordProtected": "Защитено с парола",
|
|
||||||
"memberPortalPinCode": "ПИН код",
|
|
||||||
"memberPortalEmailWhitelist": "Бял списък на имейли",
|
|
||||||
"memberPortalResourceDisabled": "Ресурсът е деактивиран",
|
|
||||||
"memberPortalShowingResources": "Показва {start}-{end} от {total} ресурси",
|
|
||||||
"memberPortalPrevious": "Предишен",
|
|
||||||
"memberPortalNext": "Следващ"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2660,19 +2660,19 @@
|
|||||||
"noMoreAuthMethods": "No Valid Auth",
|
"noMoreAuthMethods": "No Valid Auth",
|
||||||
"ip": "IP adresa",
|
"ip": "IP adresa",
|
||||||
"reason": "Důvod",
|
"reason": "Důvod",
|
||||||
"requestLogs": "Záznamy HTTP požadavků",
|
"requestLogs": "Záznamy požadavků",
|
||||||
"requestAnalytics": "Vyžádat analýzu",
|
"requestAnalytics": "Vyžádat analýzu",
|
||||||
"host": "Hostitel",
|
"host": "Hostitel",
|
||||||
"location": "Poloha",
|
"location": "Poloha",
|
||||||
"actionLogs": "Záznamy akcí",
|
"actionLogs": "Záznamy akcí",
|
||||||
"sidebarLogsRequest": "Záznamy HTTP požadavků",
|
"sidebarLogsRequest": "Záznamy požadavků",
|
||||||
"sidebarLogsAccess": "Protokoly přístupu",
|
"sidebarLogsAccess": "Protokoly přístupu",
|
||||||
"sidebarLogsAction": "Záznamy akcí",
|
"sidebarLogsAction": "Záznamy akcí",
|
||||||
"logRetention": "Zaznamenávání záznamu",
|
"logRetention": "Zaznamenávání záznamu",
|
||||||
"logRetentionDescription": "Spravovat, jak dlouho jsou různé typy logů uloženy pro tuto organizaci nebo je zakázat",
|
"logRetentionDescription": "Spravovat, jak dlouho jsou různé typy logů uloženy pro tuto organizaci nebo je zakázat",
|
||||||
"requestLogsDescription": "Zobrazit podrobné protokoly požadavků pro zdroje v této organizaci",
|
"requestLogsDescription": "Zobrazit podrobné protokoly požadavků pro zdroje v této organizaci",
|
||||||
"requestAnalyticsDescription": "Zobrazit podrobnou analýzu požadavků pro zdroje v této organizaci",
|
"requestAnalyticsDescription": "Zobrazit podrobnou analýzu požadavků pro zdroje v této organizaci",
|
||||||
"logRetentionRequestLabel": "Zachování logu HTTP požadavků",
|
"logRetentionRequestLabel": "Zachování logu žádosti",
|
||||||
"logRetentionRequestDescription": "Jak dlouho uchovávat záznamy požadavků",
|
"logRetentionRequestDescription": "Jak dlouho uchovávat záznamy požadavků",
|
||||||
"logRetentionAccessLabel": "Zachování záznamu přístupu",
|
"logRetentionAccessLabel": "Zachování záznamu přístupu",
|
||||||
"logRetentionAccessDescription": "Jak dlouho uchovávat přístupové záznamy",
|
"logRetentionAccessDescription": "Jak dlouho uchovávat přístupové záznamy",
|
||||||
@@ -3134,7 +3134,7 @@
|
|||||||
"httpDestActionLogsDescription": "Správní opatření prováděná uživateli v rámci organizace.",
|
"httpDestActionLogsDescription": "Správní opatření prováděná uživateli v rámci organizace.",
|
||||||
"httpDestConnectionLogsTitle": "Protokoly připojení",
|
"httpDestConnectionLogsTitle": "Protokoly připojení",
|
||||||
"httpDestConnectionLogsDescription": "Události týkající se připojení lokality a tunelu, včetně připojení a odpojení.",
|
"httpDestConnectionLogsDescription": "Události týkající se připojení lokality a tunelu, včetně připojení a odpojení.",
|
||||||
"httpDestRequestLogsTitle": "Záznamy HTTP požadavků",
|
"httpDestRequestLogsTitle": "Záznamy požadavků",
|
||||||
"httpDestRequestLogsDescription": "HTTP záznamy požadavků pro proxy zdroje, včetně metod, cesty a kódu odpovědi.",
|
"httpDestRequestLogsDescription": "HTTP záznamy požadavků pro proxy zdroje, včetně metod, cesty a kódu odpovědi.",
|
||||||
"httpDestSaveChanges": "Uložit změny",
|
"httpDestSaveChanges": "Uložit změny",
|
||||||
"httpDestCreateDestination": "Vytvořit cíl",
|
"httpDestCreateDestination": "Vytvořit cíl",
|
||||||
@@ -3208,48 +3208,5 @@
|
|||||||
"domainPickerWildcardCertWarning": "Zástupné zdroje mohou vyžadovat dodatečnou konfiguraci pro správnou funkci.",
|
"domainPickerWildcardCertWarning": "Zástupné zdroje mohou vyžadovat dodatečnou konfiguraci pro správnou funkci.",
|
||||||
"domainPickerWildcardCertWarningLink": "Zjistit více",
|
"domainPickerWildcardCertWarningLink": "Zjistit více",
|
||||||
"health": "Zdraví",
|
"health": "Zdraví",
|
||||||
"domainPendingErrorTitle": "Problém s ověřením",
|
"domainPendingErrorTitle": "Problém s ověřením"
|
||||||
"memberPortalTitle": "Zdroje",
|
|
||||||
"memberPortalDescription": "Zdroje, ke kterým máte v této organizaci přístup",
|
|
||||||
"memberPortalSortBy": "Řadit podle...",
|
|
||||||
"memberPortalSortNameAsc": "Názvu A-Z",
|
|
||||||
"memberPortalSortNameDesc": "Názvu Z-A",
|
|
||||||
"memberPortalSortDomainAsc": "Domény A-Z",
|
|
||||||
"memberPortalSortDomainDesc": "Domény Z-A",
|
|
||||||
"memberPortalSortEnabledFirst": "Nejprve povoleno",
|
|
||||||
"memberPortalSortDisabledFirst": "Nejprve zakázáno",
|
|
||||||
"memberPortalRefresh": "Aktualizovat",
|
|
||||||
"memberPortalRefreshResources": "Aktualizovat zdroje",
|
|
||||||
"memberPortalFailedToLoad": "Nepodařilo se načíst zdroje",
|
|
||||||
"memberPortalFailedToLoadDescription": "Nepodařilo se načíst zdroje. Zkontrolujte prosím své připojení a zkuste to znovu.",
|
|
||||||
"memberPortalUnableToLoad": "Nelze načíst zdroje",
|
|
||||||
"memberPortalTryAgain": "Zkusit znovu",
|
|
||||||
"memberPortalNoResourcesFound": "Žádné zdroje nebyly nalezeny",
|
|
||||||
"memberPortalNoResourcesAvailable": "Žádné zdroje nejsou k dispozici",
|
|
||||||
"memberPortalNoResourcesMatchSearch": "Žádné zdroje neodpovídají \"{query}\". Zkuste přizpůsobit své vyhledávací termíny nebo vyčistit hledání, abyste viděli všechny zdroje.",
|
|
||||||
"memberPortalNoResourcesAccess": "Zatím nemáte přístup k žádným zdrojům. Kontaktujte svého správce, aby vám poskytl přístup k potřebným zdrojům.",
|
|
||||||
"memberPortalClearSearch": "Vymazat hledání",
|
|
||||||
"memberPortalPublicResources": "Veřejné zdroje",
|
|
||||||
"memberPortalPublicResourcesDescription": "Webové aplikace a služby přístupné přes prohlížeč",
|
|
||||||
"memberPortalCopiedToClipboard": "Zkopírováno do schránky",
|
|
||||||
"memberPortalCopiedUrlDescription": "URL zdroje byla zkopírována do vaší schránky.",
|
|
||||||
"memberPortalOpenResource": "Otevřít zdroj",
|
|
||||||
"memberPortalPrivateResources": "Soukromé zdroje",
|
|
||||||
"memberPortalPrivateResourcesDescription": "Interní síťové zdroje přístupné přes klienta",
|
|
||||||
"memberPortalResourceDetails": "Podrobnosti o zdroji",
|
|
||||||
"memberPortalMode": "Režim",
|
|
||||||
"memberPortalDestination": "Cíl",
|
|
||||||
"memberPortalAlias": "Přezdívka",
|
|
||||||
"memberPortalCopiedAliasDescription": "Alias zdroje byl zkopírován do vaší schránky.",
|
|
||||||
"memberPortalCopiedDestinationDescription": "Cíl zdroje byl zkopírován do vaší schránky.",
|
|
||||||
"memberPortalRequiresClientConnection": "Vyžaduje klientské připojení",
|
|
||||||
"memberPortalAuthMethods": "Metody ověřování",
|
|
||||||
"memberPortalSso": "Jedno přihlášení (SSO)",
|
|
||||||
"memberPortalPasswordProtected": "Heslo chráněno",
|
|
||||||
"memberPortalPinCode": "PIN kód",
|
|
||||||
"memberPortalEmailWhitelist": "Seznam povolených emailů",
|
|
||||||
"memberPortalResourceDisabled": "Zdroj je zakázán",
|
|
||||||
"memberPortalShowingResources": "Zobrazeny {start}-{end} z {total} zdrojů",
|
|
||||||
"memberPortalPrevious": "Předchozí",
|
|
||||||
"memberPortalNext": "Následující"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2660,19 +2660,19 @@
|
|||||||
"noMoreAuthMethods": "Keine gültige Authentifizierungsmethode verfügbar",
|
"noMoreAuthMethods": "Keine gültige Authentifizierungsmethode verfügbar",
|
||||||
"ip": "IP",
|
"ip": "IP",
|
||||||
"reason": "Grund",
|
"reason": "Grund",
|
||||||
"requestLogs": "HTTP Anforderungsprotokolle",
|
"requestLogs": "Logs anfordern",
|
||||||
"requestAnalytics": "Anfrage-Analyse anzeigen",
|
"requestAnalytics": "Anfrage-Analyse anzeigen",
|
||||||
"host": "Host",
|
"host": "Host",
|
||||||
"location": "Standort",
|
"location": "Standort",
|
||||||
"actionLogs": "Aktionsprotokolle",
|
"actionLogs": "Aktionsprotokolle",
|
||||||
"sidebarLogsRequest": "HTTP Anforderungsprotokolle",
|
"sidebarLogsRequest": "Logs anfordern",
|
||||||
"sidebarLogsAccess": "Zugriffsprotokolle",
|
"sidebarLogsAccess": "Zugriffsprotokolle",
|
||||||
"sidebarLogsAction": "Aktionsprotokolle",
|
"sidebarLogsAction": "Aktionsprotokolle",
|
||||||
"logRetention": "Log-Speicherung",
|
"logRetention": "Log-Speicherung",
|
||||||
"logRetentionDescription": "Verwalten, wie lange verschiedene Logs für diese Organisation gespeichert werden oder deaktivieren",
|
"logRetentionDescription": "Verwalten, wie lange verschiedene Logs für diese Organisation gespeichert werden oder deaktivieren",
|
||||||
"requestLogsDescription": "Detaillierte Request-Logs für Ressourcen in dieser Organisation anzeigen",
|
"requestLogsDescription": "Detaillierte Request-Logs für Ressourcen in dieser Organisation anzeigen",
|
||||||
"requestAnalyticsDescription": "Detaillierte Anfrage-Analyse für Ressourcen in dieser Organisation anzeigen",
|
"requestAnalyticsDescription": "Detaillierte Anfrage-Analyse für Ressourcen in dieser Organisation anzeigen",
|
||||||
"logRetentionRequestLabel": "HTTP Anforderungsprotokoll Aufbewahrung",
|
"logRetentionRequestLabel": "Log-Speicherung anfordern",
|
||||||
"logRetentionRequestDescription": "Wie lange sollen Request-Logs gespeichert werden",
|
"logRetentionRequestDescription": "Wie lange sollen Request-Logs gespeichert werden",
|
||||||
"logRetentionAccessLabel": "Zugriffsprotokoll-Speicherung",
|
"logRetentionAccessLabel": "Zugriffsprotokoll-Speicherung",
|
||||||
"logRetentionAccessDescription": "Wie lange Zugriffsprotokolle beibehalten werden sollen",
|
"logRetentionAccessDescription": "Wie lange Zugriffsprotokolle beibehalten werden sollen",
|
||||||
@@ -3134,7 +3134,7 @@
|
|||||||
"httpDestActionLogsDescription": "Administrative Maßnahmen, die von Benutzern innerhalb der Organisation durchgeführt werden.",
|
"httpDestActionLogsDescription": "Administrative Maßnahmen, die von Benutzern innerhalb der Organisation durchgeführt werden.",
|
||||||
"httpDestConnectionLogsTitle": "Verbindungsprotokolle",
|
"httpDestConnectionLogsTitle": "Verbindungsprotokolle",
|
||||||
"httpDestConnectionLogsDescription": "Site- und Tunnelverbindungen, einschließlich Verbindungen und Trennungen.",
|
"httpDestConnectionLogsDescription": "Site- und Tunnelverbindungen, einschließlich Verbindungen und Trennungen.",
|
||||||
"httpDestRequestLogsTitle": "HTTP Anforderungsprotokolle",
|
"httpDestRequestLogsTitle": "Logs anfordern",
|
||||||
"httpDestRequestLogsDescription": "HTTP-Request-Protokolle für proxiierte Ressourcen, einschließlich Methode, Pfad und Antwort-Code.",
|
"httpDestRequestLogsDescription": "HTTP-Request-Protokolle für proxiierte Ressourcen, einschließlich Methode, Pfad und Antwort-Code.",
|
||||||
"httpDestSaveChanges": "Änderungen speichern",
|
"httpDestSaveChanges": "Änderungen speichern",
|
||||||
"httpDestCreateDestination": "Ziel erstellen",
|
"httpDestCreateDestination": "Ziel erstellen",
|
||||||
@@ -3208,48 +3208,5 @@
|
|||||||
"domainPickerWildcardCertWarning": "Wildcard-Ressourcen erfordern möglicherweise zusätzliche Konfigurationen, um ordnungsgemäß zu funktionieren.",
|
"domainPickerWildcardCertWarning": "Wildcard-Ressourcen erfordern möglicherweise zusätzliche Konfigurationen, um ordnungsgemäß zu funktionieren.",
|
||||||
"domainPickerWildcardCertWarningLink": "Mehr erfahren",
|
"domainPickerWildcardCertWarningLink": "Mehr erfahren",
|
||||||
"health": "Gesundheit",
|
"health": "Gesundheit",
|
||||||
"domainPendingErrorTitle": "Verifizierungsproblem",
|
"domainPendingErrorTitle": "Verifizierungsproblem"
|
||||||
"memberPortalTitle": "Ressourcen",
|
|
||||||
"memberPortalDescription": "Ressourcen, auf die Sie in dieser Organisation Zugriff haben",
|
|
||||||
"memberPortalSortBy": "Sortieren nach...",
|
|
||||||
"memberPortalSortNameAsc": "Name A-Z",
|
|
||||||
"memberPortalSortNameDesc": "Name Z-A",
|
|
||||||
"memberPortalSortDomainAsc": "Domain A-Z",
|
|
||||||
"memberPortalSortDomainDesc": "Domain Z-A",
|
|
||||||
"memberPortalSortEnabledFirst": "Zuerst aktiviert",
|
|
||||||
"memberPortalSortDisabledFirst": "Zuerst deaktiviert",
|
|
||||||
"memberPortalRefresh": "Aktualisieren",
|
|
||||||
"memberPortalRefreshResources": "Ressourcen aktualisieren",
|
|
||||||
"memberPortalFailedToLoad": "Fehler beim Laden der Ressourcen",
|
|
||||||
"memberPortalFailedToLoadDescription": "Fehler beim Laden der Ressourcen. Bitte überprüfen Sie Ihre Verbindung und versuchen Sie es erneut.",
|
|
||||||
"memberPortalUnableToLoad": "Ressourcen konnten nicht geladen werden",
|
|
||||||
"memberPortalTryAgain": "Nochmal versuchen",
|
|
||||||
"memberPortalNoResourcesFound": "Keine Ressourcen gefunden",
|
|
||||||
"memberPortalNoResourcesAvailable": "Keine Ressourcen verfügbar",
|
|
||||||
"memberPortalNoResourcesMatchSearch": "Keine Ressourcen passen zu \"{query}\". Versuchen Sie, Ihre Suchbegriffe anzupassen oder die Suche zu löschen, um alle Ressourcen anzuzeigen.",
|
|
||||||
"memberPortalNoResourcesAccess": "Sie haben noch keinen Zugriff auf Ressourcen. Wenden Sie sich an Ihren Administrator, um Zugriff auf die benötigten Ressourcen zu erhalten.",
|
|
||||||
"memberPortalClearSearch": "Suchverlauf löschen",
|
|
||||||
"memberPortalPublicResources": "Öffentliche Ressourcen",
|
|
||||||
"memberPortalPublicResourcesDescription": "Webanwendungen und Dienste, die über den Browser zugänglich sind",
|
|
||||||
"memberPortalCopiedToClipboard": "In die Zwischenablage kopiert",
|
|
||||||
"memberPortalCopiedUrlDescription": "Ressourcen-URL wurde in Ihre Zwischenablage kopiert.",
|
|
||||||
"memberPortalOpenResource": "Ressource öffnen",
|
|
||||||
"memberPortalPrivateResources": "Private Ressourcen",
|
|
||||||
"memberPortalPrivateResourcesDescription": "Interne Netzwerkressourcen, die über den Client zugänglich sind",
|
|
||||||
"memberPortalResourceDetails": "Ressourcendetails",
|
|
||||||
"memberPortalMode": "Modus",
|
|
||||||
"memberPortalDestination": "Ziel",
|
|
||||||
"memberPortalAlias": "Alias",
|
|
||||||
"memberPortalCopiedAliasDescription": "Ressourcenalias wurde in Ihre Zwischenablage kopiert.",
|
|
||||||
"memberPortalCopiedDestinationDescription": "Ressourcenziel wurde in Ihre Zwischenablage kopiert.",
|
|
||||||
"memberPortalRequiresClientConnection": "Erfordert Client-Verbindung",
|
|
||||||
"memberPortalAuthMethods": "Authentifizierungsmethoden",
|
|
||||||
"memberPortalSso": "Single Sign-On (SSO)",
|
|
||||||
"memberPortalPasswordProtected": "Passwortgeschützt",
|
|
||||||
"memberPortalPinCode": "PIN-Code",
|
|
||||||
"memberPortalEmailWhitelist": "E-Mail-Whitelist",
|
|
||||||
"memberPortalResourceDisabled": "Ressource deaktiviert",
|
|
||||||
"memberPortalShowingResources": "Zeige {start}-{end} von {total} Ressourcen",
|
|
||||||
"memberPortalPrevious": "Vorherige",
|
|
||||||
"memberPortalNext": "Nächste"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2660,19 +2660,19 @@
|
|||||||
"noMoreAuthMethods": "No Valid Auth",
|
"noMoreAuthMethods": "No Valid Auth",
|
||||||
"ip": "IP",
|
"ip": "IP",
|
||||||
"reason": "Reason",
|
"reason": "Reason",
|
||||||
"requestLogs": "HTTP Request Logs",
|
"requestLogs": "HTTPS Request Logs",
|
||||||
"requestAnalytics": "Request Analytics",
|
"requestAnalytics": "Request Analytics",
|
||||||
"host": "Host",
|
"host": "Host",
|
||||||
"location": "Location",
|
"location": "Location",
|
||||||
"actionLogs": "Admin Action Logs",
|
"actionLogs": "Admin Action Logs",
|
||||||
"sidebarLogsRequest": "HTTP Request Logs",
|
"sidebarLogsRequest": "HTTPS Request Logs",
|
||||||
"sidebarLogsAccess": "Authentication Logs",
|
"sidebarLogsAccess": "Authentication Logs",
|
||||||
"sidebarLogsAction": "Admin Action Logs",
|
"sidebarLogsAction": "Admin Action Logs",
|
||||||
"logRetention": "Log Retention",
|
"logRetention": "Log Retention",
|
||||||
"logRetentionDescription": "Manage how long different types of logs are retained for this organization or disable them",
|
"logRetentionDescription": "Manage how long different types of logs are retained for this organization or disable them",
|
||||||
"requestLogsDescription": "View detailed request logs for HTTPS resources in this organization",
|
"requestLogsDescription": "View detailed request logs for HTTPS resources in this organization",
|
||||||
"requestAnalyticsDescription": "View detailed request analytics for resources in this organization",
|
"requestAnalyticsDescription": "View detailed request analytics for resources in this organization",
|
||||||
"logRetentionRequestLabel": "HTTP Request Log Retention",
|
"logRetentionRequestLabel": "HTTPS Request Log Retention",
|
||||||
"logRetentionRequestDescription": "How long to retain request logs",
|
"logRetentionRequestDescription": "How long to retain request logs",
|
||||||
"logRetentionAccessLabel": "Authentication Log Retention",
|
"logRetentionAccessLabel": "Authentication Log Retention",
|
||||||
"logRetentionAccessDescription": "How long to retain access logs",
|
"logRetentionAccessDescription": "How long to retain access logs",
|
||||||
@@ -3134,7 +3134,7 @@
|
|||||||
"httpDestActionLogsDescription": "Administrative actions performed by users within the organization.",
|
"httpDestActionLogsDescription": "Administrative actions performed by users within the organization.",
|
||||||
"httpDestConnectionLogsTitle": "Network Logs",
|
"httpDestConnectionLogsTitle": "Network Logs",
|
||||||
"httpDestConnectionLogsDescription": "Site and tunnel connection events, including connects and disconnects.",
|
"httpDestConnectionLogsDescription": "Site and tunnel connection events, including connects and disconnects.",
|
||||||
"httpDestRequestLogsTitle": "HTTP Request Logs",
|
"httpDestRequestLogsTitle": "HTTPS Request Logs",
|
||||||
"httpDestRequestLogsDescription": "HTTP request logs for proxied resources, including method, path, and response code.",
|
"httpDestRequestLogsDescription": "HTTP request logs for proxied resources, including method, path, and response code.",
|
||||||
"httpDestSaveChanges": "Save Changes",
|
"httpDestSaveChanges": "Save Changes",
|
||||||
"httpDestCreateDestination": "Create Destination",
|
"httpDestCreateDestination": "Create Destination",
|
||||||
@@ -3208,48 +3208,5 @@
|
|||||||
"domainPickerWildcardCertWarning": "Wildcard resources may require additional configuration to work properly.",
|
"domainPickerWildcardCertWarning": "Wildcard resources may require additional configuration to work properly.",
|
||||||
"domainPickerWildcardCertWarningLink": "Learn more",
|
"domainPickerWildcardCertWarningLink": "Learn more",
|
||||||
"health": "Health",
|
"health": "Health",
|
||||||
"domainPendingErrorTitle": "Verification Issue",
|
"domainPendingErrorTitle": "Verification Issue"
|
||||||
"memberPortalTitle": "Resources",
|
|
||||||
"memberPortalDescription": "Resources you have access to in this organization",
|
|
||||||
"memberPortalSortBy": "Sort by...",
|
|
||||||
"memberPortalSortNameAsc": "Name A-Z",
|
|
||||||
"memberPortalSortNameDesc": "Name Z-A",
|
|
||||||
"memberPortalSortDomainAsc": "Domain A-Z",
|
|
||||||
"memberPortalSortDomainDesc": "Domain Z-A",
|
|
||||||
"memberPortalSortEnabledFirst": "Enabled First",
|
|
||||||
"memberPortalSortDisabledFirst": "Disabled First",
|
|
||||||
"memberPortalRefresh": "Refresh",
|
|
||||||
"memberPortalRefreshResources": "Refresh Resources",
|
|
||||||
"memberPortalFailedToLoad": "Failed to load resources",
|
|
||||||
"memberPortalFailedToLoadDescription": "Failed to load resources. Please check your connection and try again.",
|
|
||||||
"memberPortalUnableToLoad": "Unable to Load Resources",
|
|
||||||
"memberPortalTryAgain": "Try Again",
|
|
||||||
"memberPortalNoResourcesFound": "No Resources Found",
|
|
||||||
"memberPortalNoResourcesAvailable": "No Resources Available",
|
|
||||||
"memberPortalNoResourcesMatchSearch": "No resources match \"{query}\". Try adjusting your search terms or clearing the search to see all resources.",
|
|
||||||
"memberPortalNoResourcesAccess": "You don't have access to any resources yet. Contact your administrator to get access to resources you need.",
|
|
||||||
"memberPortalClearSearch": "Clear Search",
|
|
||||||
"memberPortalPublicResources": "Public Resources",
|
|
||||||
"memberPortalPublicResourcesDescription": "Web applications and services accessible via browser",
|
|
||||||
"memberPortalCopiedToClipboard": "Copied to clipboard",
|
|
||||||
"memberPortalCopiedUrlDescription": "Resource URL has been copied to your clipboard.",
|
|
||||||
"memberPortalOpenResource": "Open Resource",
|
|
||||||
"memberPortalPrivateResources": "Private Resources",
|
|
||||||
"memberPortalPrivateResourcesDescription": "Internal network resources accessible via client",
|
|
||||||
"memberPortalResourceDetails": "Resource Details",
|
|
||||||
"memberPortalMode": "Mode",
|
|
||||||
"memberPortalDestination": "Destination",
|
|
||||||
"memberPortalAlias": "Alias",
|
|
||||||
"memberPortalCopiedAliasDescription": "Resource alias has been copied to your clipboard.",
|
|
||||||
"memberPortalCopiedDestinationDescription": "Resource destination has been copied to your clipboard.",
|
|
||||||
"memberPortalRequiresClientConnection": "Requires Client Connection",
|
|
||||||
"memberPortalAuthMethods": "Authentication Methods",
|
|
||||||
"memberPortalSso": "Single Sign-On (SSO)",
|
|
||||||
"memberPortalPasswordProtected": "Password Protected",
|
|
||||||
"memberPortalPinCode": "PIN Code",
|
|
||||||
"memberPortalEmailWhitelist": "Email Whitelist",
|
|
||||||
"memberPortalResourceDisabled": "Resource Disabled",
|
|
||||||
"memberPortalShowingResources": "Showing {start}-{end} of {total} resources",
|
|
||||||
"memberPortalPrevious": "Previous",
|
|
||||||
"memberPortalNext": "Next"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2660,19 +2660,19 @@
|
|||||||
"noMoreAuthMethods": "No Valid Auth",
|
"noMoreAuthMethods": "No Valid Auth",
|
||||||
"ip": "IP",
|
"ip": "IP",
|
||||||
"reason": "Razón",
|
"reason": "Razón",
|
||||||
"requestLogs": "Registros de Solicitud HTTP",
|
"requestLogs": "Registros de Solicitud",
|
||||||
"requestAnalytics": "Analítica de Solicitud",
|
"requestAnalytics": "Analítica de Solicitud",
|
||||||
"host": "Anfitrión",
|
"host": "Anfitrión",
|
||||||
"location": "Ubicación",
|
"location": "Ubicación",
|
||||||
"actionLogs": "Registros de acción",
|
"actionLogs": "Registros de acción",
|
||||||
"sidebarLogsRequest": "Registros de Solicitud HTTP",
|
"sidebarLogsRequest": "Registros de Solicitud",
|
||||||
"sidebarLogsAccess": "Registros de acceso",
|
"sidebarLogsAccess": "Registros de acceso",
|
||||||
"sidebarLogsAction": "Registros de acción",
|
"sidebarLogsAction": "Registros de acción",
|
||||||
"logRetention": "Retención de Log",
|
"logRetention": "Retención de Log",
|
||||||
"logRetentionDescription": "Administrar cuánto tiempo se conservan los diferentes tipos de registros para esta organización o desactivarlos",
|
"logRetentionDescription": "Administrar cuánto tiempo se conservan los diferentes tipos de registros para esta organización o desactivarlos",
|
||||||
"requestLogsDescription": "Ver registros de solicitudes detallados para los recursos de esta organización",
|
"requestLogsDescription": "Ver registros de solicitudes detallados para los recursos de esta organización",
|
||||||
"requestAnalyticsDescription": "Ver análisis de solicitudes detalladas de recursos en esta organización",
|
"requestAnalyticsDescription": "Ver análisis de solicitudes detalladas de recursos en esta organización",
|
||||||
"logRetentionRequestLabel": "Retención de Registro de Solicitud HTTP",
|
"logRetentionRequestLabel": "Retención de Registro de Solicitud",
|
||||||
"logRetentionRequestDescription": "Cuánto tiempo conservar los registros de solicitudes",
|
"logRetentionRequestDescription": "Cuánto tiempo conservar los registros de solicitudes",
|
||||||
"logRetentionAccessLabel": "Retención de Log de Acceso",
|
"logRetentionAccessLabel": "Retención de Log de Acceso",
|
||||||
"logRetentionAccessDescription": "Cuánto tiempo retener los registros de acceso",
|
"logRetentionAccessDescription": "Cuánto tiempo retener los registros de acceso",
|
||||||
@@ -3134,7 +3134,7 @@
|
|||||||
"httpDestActionLogsDescription": "Acciones administrativas realizadas por los usuarios dentro de la organización.",
|
"httpDestActionLogsDescription": "Acciones administrativas realizadas por los usuarios dentro de la organización.",
|
||||||
"httpDestConnectionLogsTitle": "Registros de conexión",
|
"httpDestConnectionLogsTitle": "Registros de conexión",
|
||||||
"httpDestConnectionLogsDescription": "Eventos de conexión de sitios y túneles, incluyendo conexiones y desconexiones.",
|
"httpDestConnectionLogsDescription": "Eventos de conexión de sitios y túneles, incluyendo conexiones y desconexiones.",
|
||||||
"httpDestRequestLogsTitle": "Registros de Solicitud HTTP",
|
"httpDestRequestLogsTitle": "Registros de Solicitud",
|
||||||
"httpDestRequestLogsDescription": "Registros de peticiones HTTP para recursos proxyficados, incluyendo método, ruta y código de respuesta.",
|
"httpDestRequestLogsDescription": "Registros de peticiones HTTP para recursos proxyficados, incluyendo método, ruta y código de respuesta.",
|
||||||
"httpDestSaveChanges": "Guardar Cambios",
|
"httpDestSaveChanges": "Guardar Cambios",
|
||||||
"httpDestCreateDestination": "Crear destino",
|
"httpDestCreateDestination": "Crear destino",
|
||||||
@@ -3208,48 +3208,5 @@
|
|||||||
"domainPickerWildcardCertWarning": "Los recursos comodín pueden requerir configuración adicional para funcionar correctamente.",
|
"domainPickerWildcardCertWarning": "Los recursos comodín pueden requerir configuración adicional para funcionar correctamente.",
|
||||||
"domainPickerWildcardCertWarningLink": "Más información",
|
"domainPickerWildcardCertWarningLink": "Más información",
|
||||||
"health": "Salud",
|
"health": "Salud",
|
||||||
"domainPendingErrorTitle": "Problema de verificación",
|
"domainPendingErrorTitle": "Problema de verificación"
|
||||||
"memberPortalTitle": "Recursos",
|
|
||||||
"memberPortalDescription": "Recursos a los que tiene acceso en esta organización",
|
|
||||||
"memberPortalSortBy": "Ordenar por...",
|
|
||||||
"memberPortalSortNameAsc": "Nombre A-Z",
|
|
||||||
"memberPortalSortNameDesc": "Nombre Z-A",
|
|
||||||
"memberPortalSortDomainAsc": "Dominio A-Z",
|
|
||||||
"memberPortalSortDomainDesc": "Dominio Z-A",
|
|
||||||
"memberPortalSortEnabledFirst": "Habilitado Primero",
|
|
||||||
"memberPortalSortDisabledFirst": "Deshabilitado Primero",
|
|
||||||
"memberPortalRefresh": "Actualizar",
|
|
||||||
"memberPortalRefreshResources": "Actualizar Recursos",
|
|
||||||
"memberPortalFailedToLoad": "No se pudieron cargar los recursos",
|
|
||||||
"memberPortalFailedToLoadDescription": "No se pudieron cargar los recursos. Por favor, revise su conexión e intente de nuevo.",
|
|
||||||
"memberPortalUnableToLoad": "No se pudieron cargar los recursos",
|
|
||||||
"memberPortalTryAgain": "Intentar de Nuevo",
|
|
||||||
"memberPortalNoResourcesFound": "No se encontraron Recursos",
|
|
||||||
"memberPortalNoResourcesAvailable": "No Hay Recursos Disponibles",
|
|
||||||
"memberPortalNoResourcesMatchSearch": "No hay recursos que coincidan con \"{query}\". Intenta ajustar tus términos de búsqueda o limpiar la búsqueda para ver todos los recursos.",
|
|
||||||
"memberPortalNoResourcesAccess": "Aún no tiene acceso a ningún recurso. Comuníquese con su administrador para obtener acceso a los recursos que necesita.",
|
|
||||||
"memberPortalClearSearch": "Limpiar Búsqueda",
|
|
||||||
"memberPortalPublicResources": "Recursos Públicos",
|
|
||||||
"memberPortalPublicResourcesDescription": "Aplicaciones web y servicios accesibles vía navegador",
|
|
||||||
"memberPortalCopiedToClipboard": "Copiado al portapapeles",
|
|
||||||
"memberPortalCopiedUrlDescription": "La URL del recurso ha sido copiada a su portapapeles.",
|
|
||||||
"memberPortalOpenResource": "Abrir Recurso",
|
|
||||||
"memberPortalPrivateResources": "Recursos Privados",
|
|
||||||
"memberPortalPrivateResourcesDescription": "Recursos de red interna accesibles vía cliente",
|
|
||||||
"memberPortalResourceDetails": "Detalles del Recurso",
|
|
||||||
"memberPortalMode": "Modo",
|
|
||||||
"memberPortalDestination": "Destino",
|
|
||||||
"memberPortalAlias": "Alias",
|
|
||||||
"memberPortalCopiedAliasDescription": "El alias del recurso ha sido copiado a su portapapeles.",
|
|
||||||
"memberPortalCopiedDestinationDescription": "El destino del recurso ha sido copiado a su portapapeles.",
|
|
||||||
"memberPortalRequiresClientConnection": "Requiere Conexión de Cliente",
|
|
||||||
"memberPortalAuthMethods": "Métodos de Autenticación",
|
|
||||||
"memberPortalSso": "Inicio de Sesión Único (SSO)",
|
|
||||||
"memberPortalPasswordProtected": "Protegido por Contraseña",
|
|
||||||
"memberPortalPinCode": "Código PIN",
|
|
||||||
"memberPortalEmailWhitelist": "Lista Blanca de Correo",
|
|
||||||
"memberPortalResourceDisabled": "Recurso Deshabilitado",
|
|
||||||
"memberPortalShowingResources": "Mostrando {start}-{end} de {total} recursos",
|
|
||||||
"memberPortalPrevious": "Anterior",
|
|
||||||
"memberPortalNext": "Siguiente"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1356,7 +1356,7 @@
|
|||||||
"sidebarSites": "Nœuds",
|
"sidebarSites": "Nœuds",
|
||||||
"sidebarApprovals": "Demandes d'approbation",
|
"sidebarApprovals": "Demandes d'approbation",
|
||||||
"sidebarResources": "Ressource",
|
"sidebarResources": "Ressource",
|
||||||
"sidebarProxyResources": "Publiques",
|
"sidebarProxyResources": "Publique",
|
||||||
"sidebarClientResources": "Privé",
|
"sidebarClientResources": "Privé",
|
||||||
"sidebarAccessControl": "Contrôle d'accès",
|
"sidebarAccessControl": "Contrôle d'accès",
|
||||||
"sidebarLogsAndAnalytics": "Journaux & Analytiques",
|
"sidebarLogsAndAnalytics": "Journaux & Analytiques",
|
||||||
@@ -2458,8 +2458,8 @@
|
|||||||
"manageUserDevicesDescription": "Voir et gérer les appareils que les utilisateurs utilisent pour se connecter en privé aux ressources",
|
"manageUserDevicesDescription": "Voir et gérer les appareils que les utilisateurs utilisent pour se connecter en privé aux ressources",
|
||||||
"downloadClientBannerTitle": "Télécharger le client Pangolin",
|
"downloadClientBannerTitle": "Télécharger le client Pangolin",
|
||||||
"downloadClientBannerDescription": "Téléchargez le client Pangolin pour votre système afin de vous connecter au réseau Pangolin et accéder aux ressources de manière privée.",
|
"downloadClientBannerDescription": "Téléchargez le client Pangolin pour votre système afin de vous connecter au réseau Pangolin et accéder aux ressources de manière privée.",
|
||||||
"manageMachineClients": "Gérer les machines",
|
"manageMachineClients": "Gérer les clients de la machine",
|
||||||
"manageMachineClientsDescription": "Créer et gérer les clients que les serveurs et systèmes utilisent pour se connecter en privé aux ressources",
|
"manageMachineClientsDescription": "Créer et gérer des clients que les serveurs et les systèmes utilisent pour se connecter en privé aux ressources",
|
||||||
"machineClientsBannerTitle": "Serveurs & Systèmes automatisés",
|
"machineClientsBannerTitle": "Serveurs & Systèmes automatisés",
|
||||||
"machineClientsBannerDescription": "Les clients de machine sont conçus pour les serveurs et les systèmes automatisés qui ne sont pas associés à un utilisateur spécifique. Ils s'authentifient avec un identifiant et une clé secrète, et peuvent être exécutés avec Pangolin CLI, Olm CLI ou Olm en tant que conteneur.",
|
"machineClientsBannerDescription": "Les clients de machine sont conçus pour les serveurs et les systèmes automatisés qui ne sont pas associés à un utilisateur spécifique. Ils s'authentifient avec un identifiant et une clé secrète, et peuvent être exécutés avec Pangolin CLI, Olm CLI ou Olm en tant que conteneur.",
|
||||||
"machineClientsBannerPangolinCLI": "Pangolin CLI",
|
"machineClientsBannerPangolinCLI": "Pangolin CLI",
|
||||||
@@ -2660,19 +2660,19 @@
|
|||||||
"noMoreAuthMethods": "No Valid Auth",
|
"noMoreAuthMethods": "No Valid Auth",
|
||||||
"ip": "IP",
|
"ip": "IP",
|
||||||
"reason": "Raison",
|
"reason": "Raison",
|
||||||
"requestLogs": "Journal des Requêtes HTTP",
|
"requestLogs": "Journal des requêtes",
|
||||||
"requestAnalytics": "Demander des analyses",
|
"requestAnalytics": "Demander des analyses",
|
||||||
"host": "Hôte",
|
"host": "Hôte",
|
||||||
"location": "Localisation",
|
"location": "Localisation",
|
||||||
"actionLogs": "Journaux des actions",
|
"actionLogs": "Journaux des actions",
|
||||||
"sidebarLogsRequest": "Journal des Requêtes HTTP",
|
"sidebarLogsRequest": "Journal des requêtes",
|
||||||
"sidebarLogsAccess": "Journaux d'accès",
|
"sidebarLogsAccess": "Journaux d'accès",
|
||||||
"sidebarLogsAction": "Journaux des actions",
|
"sidebarLogsAction": "Journaux des actions",
|
||||||
"logRetention": "Journaliser la rétention",
|
"logRetention": "Journaliser la rétention",
|
||||||
"logRetentionDescription": "Gérer la durée de conservation des différents types de logs pour cette organisation ou les désactiver",
|
"logRetentionDescription": "Gérer la durée de conservation des différents types de logs pour cette organisation ou les désactiver",
|
||||||
"requestLogsDescription": "Voir les journaux détaillés des requêtes pour les ressources de cette organisation",
|
"requestLogsDescription": "Voir les journaux détaillés des requêtes pour les ressources de cette organisation",
|
||||||
"requestAnalyticsDescription": "Voir les analyses détaillées des demandes pour les ressources de cette organisation",
|
"requestAnalyticsDescription": "Voir les analyses détaillées des demandes pour les ressources de cette organisation",
|
||||||
"logRetentionRequestLabel": "Rétention des Journaux de Requêtes HTTP",
|
"logRetentionRequestLabel": "Demander la rétention des journaux",
|
||||||
"logRetentionRequestDescription": "Durée de conservation des journaux de requêtes",
|
"logRetentionRequestDescription": "Durée de conservation des journaux de requêtes",
|
||||||
"logRetentionAccessLabel": "Rétention du journal d'accès",
|
"logRetentionAccessLabel": "Rétention du journal d'accès",
|
||||||
"logRetentionAccessDescription": "Durée de conservation des journaux d'accès",
|
"logRetentionAccessDescription": "Durée de conservation des journaux d'accès",
|
||||||
@@ -3134,7 +3134,7 @@
|
|||||||
"httpDestActionLogsDescription": "Actions administratives effectuées par les utilisateurs au sein de l'organisation.",
|
"httpDestActionLogsDescription": "Actions administratives effectuées par les utilisateurs au sein de l'organisation.",
|
||||||
"httpDestConnectionLogsTitle": "Journaux de connexion",
|
"httpDestConnectionLogsTitle": "Journaux de connexion",
|
||||||
"httpDestConnectionLogsDescription": "Événements de connexion du site et du tunnel, y compris les connexions et les déconnexions.",
|
"httpDestConnectionLogsDescription": "Événements de connexion du site et du tunnel, y compris les connexions et les déconnexions.",
|
||||||
"httpDestRequestLogsTitle": "Journal des Requêtes HTTP",
|
"httpDestRequestLogsTitle": "Journal des requêtes",
|
||||||
"httpDestRequestLogsDescription": "Journaux des requêtes HTTP pour les ressources proxiées, y compris la méthode, le chemin et le code de réponse.",
|
"httpDestRequestLogsDescription": "Journaux des requêtes HTTP pour les ressources proxiées, y compris la méthode, le chemin et le code de réponse.",
|
||||||
"httpDestSaveChanges": "Enregistrer les modifications",
|
"httpDestSaveChanges": "Enregistrer les modifications",
|
||||||
"httpDestCreateDestination": "Créer une destination",
|
"httpDestCreateDestination": "Créer une destination",
|
||||||
@@ -3154,7 +3154,6 @@
|
|||||||
"healthCheckTabAdvanced": "Avancé",
|
"healthCheckTabAdvanced": "Avancé",
|
||||||
"healthCheckStrategyNotAvailable": "Cette stratégie n'est pas disponible. Veuillez contacter le service commercial pour activer cette fonctionnalité.",
|
"healthCheckStrategyNotAvailable": "Cette stratégie n'est pas disponible. Veuillez contacter le service commercial pour activer cette fonctionnalité.",
|
||||||
"uptime30d": "Disponibilité (30j)",
|
"uptime30d": "Disponibilité (30j)",
|
||||||
"uptimeNoData": "Aucune donnée",
|
|
||||||
"idpAddActionCreateNew": "Créer un nouveau fournisseur d'identité",
|
"idpAddActionCreateNew": "Créer un nouveau fournisseur d'identité",
|
||||||
"idpAddActionImportFromOrg": "Importer d'une autre organisation",
|
"idpAddActionImportFromOrg": "Importer d'une autre organisation",
|
||||||
"idpImportDialogTitle": "Importer le fournisseur d'identité",
|
"idpImportDialogTitle": "Importer le fournisseur d'identité",
|
||||||
@@ -3209,48 +3208,5 @@
|
|||||||
"domainPickerWildcardCertWarning": "Les ressources Joker peuvent nécessiter une configuration supplémentaire pour fonctionner correctement.",
|
"domainPickerWildcardCertWarning": "Les ressources Joker peuvent nécessiter une configuration supplémentaire pour fonctionner correctement.",
|
||||||
"domainPickerWildcardCertWarningLink": "En savoir plus",
|
"domainPickerWildcardCertWarningLink": "En savoir plus",
|
||||||
"health": "Santé",
|
"health": "Santé",
|
||||||
"domainPendingErrorTitle": "Problème de vérification",
|
"domainPendingErrorTitle": "Problème de vérification"
|
||||||
"memberPortalTitle": "Ressources",
|
|
||||||
"memberPortalDescription": "Ressources auxquelles vous avez accès dans cette organisation",
|
|
||||||
"memberPortalSortBy": "Trier par...",
|
|
||||||
"memberPortalSortNameAsc": "Nom A-Z",
|
|
||||||
"memberPortalSortNameDesc": "Nom Z-A",
|
|
||||||
"memberPortalSortDomainAsc": "Domaine A-Z",
|
|
||||||
"memberPortalSortDomainDesc": "Domaine Z-A",
|
|
||||||
"memberPortalSortEnabledFirst": "Activé en premier",
|
|
||||||
"memberPortalSortDisabledFirst": "Désactivé en premier",
|
|
||||||
"memberPortalRefresh": "Actualiser",
|
|
||||||
"memberPortalRefreshResources": "Actualiser les ressources",
|
|
||||||
"memberPortalFailedToLoad": "Échec du chargement des ressources",
|
|
||||||
"memberPortalFailedToLoadDescription": "Échec du chargement des ressources. Veuillez vérifier votre connexion et réessayer.",
|
|
||||||
"memberPortalUnableToLoad": "Impossible de charger les ressources",
|
|
||||||
"memberPortalTryAgain": "Réessayer",
|
|
||||||
"memberPortalNoResourcesFound": "Aucune ressource trouvée",
|
|
||||||
"memberPortalNoResourcesAvailable": "Aucune ressource disponible",
|
|
||||||
"memberPortalNoResourcesMatchSearch": "Aucune ressource ne correspond à \"{query}\". Essayez d'ajuster vos termes de recherche ou de vider la recherche pour voir toutes les ressources.",
|
|
||||||
"memberPortalNoResourcesAccess": "Vous n'avez encore accès à aucune ressource. Contactez votre administrateur pour obtenir l'accès aux ressources dont vous avez besoin.",
|
|
||||||
"memberPortalClearSearch": "Effacer la recherche",
|
|
||||||
"memberPortalPublicResources": "Ressources publiques",
|
|
||||||
"memberPortalPublicResourcesDescription": "Applications et services web accessibles via un navigateur",
|
|
||||||
"memberPortalCopiedToClipboard": "Copié dans le presse-papiers",
|
|
||||||
"memberPortalCopiedUrlDescription": "L'URL de la ressource a été copiée dans votre presse-papiers.",
|
|
||||||
"memberPortalOpenResource": "Ouvrir la ressource",
|
|
||||||
"memberPortalPrivateResources": "Ressources privées",
|
|
||||||
"memberPortalPrivateResourcesDescription": "Ressources réseau internes accessibles via un client",
|
|
||||||
"memberPortalResourceDetails": "Détails de la ressource",
|
|
||||||
"memberPortalMode": "Mode",
|
|
||||||
"memberPortalDestination": "Destination",
|
|
||||||
"memberPortalAlias": "Alias",
|
|
||||||
"memberPortalCopiedAliasDescription": "L'alias de la ressource a été copié dans votre presse-papiers.",
|
|
||||||
"memberPortalCopiedDestinationDescription": "La destination de la ressource a été copiée dans votre presse-papiers.",
|
|
||||||
"memberPortalRequiresClientConnection": "Nécessite une connexion client",
|
|
||||||
"memberPortalAuthMethods": "Méthodes d'authentification",
|
|
||||||
"memberPortalSso": "Authentification unique (SSO)",
|
|
||||||
"memberPortalPasswordProtected": "Protégé par un mot de passe",
|
|
||||||
"memberPortalPinCode": "Code PIN",
|
|
||||||
"memberPortalEmailWhitelist": "Liste blanche des e-mails",
|
|
||||||
"memberPortalResourceDisabled": "Ressource désactivée",
|
|
||||||
"memberPortalShowingResources": "Affichage de {start}-{end} sur {total} ressources",
|
|
||||||
"memberPortalPrevious": "Précédent",
|
|
||||||
"memberPortalNext": "Suivant"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2660,19 +2660,19 @@
|
|||||||
"noMoreAuthMethods": "No Valid Auth",
|
"noMoreAuthMethods": "No Valid Auth",
|
||||||
"ip": "IP",
|
"ip": "IP",
|
||||||
"reason": "Motivo",
|
"reason": "Motivo",
|
||||||
"requestLogs": "Log Richieste HTTP",
|
"requestLogs": "Log Richiesta",
|
||||||
"requestAnalytics": "Richiedi Analisi",
|
"requestAnalytics": "Richiedi Analisi",
|
||||||
"host": "Host",
|
"host": "Host",
|
||||||
"location": "Posizione",
|
"location": "Posizione",
|
||||||
"actionLogs": "Log Azioni",
|
"actionLogs": "Log Azioni",
|
||||||
"sidebarLogsRequest": "Log Richieste HTTP",
|
"sidebarLogsRequest": "Log Richiesta",
|
||||||
"sidebarLogsAccess": "Log Accesso",
|
"sidebarLogsAccess": "Log Accesso",
|
||||||
"sidebarLogsAction": "Log Azioni",
|
"sidebarLogsAction": "Log Azioni",
|
||||||
"logRetention": "Ritenzione Registro",
|
"logRetention": "Ritenzione Registro",
|
||||||
"logRetentionDescription": "Gestisci per quanto tempo i diversi tipi di log sono mantenuti per questa organizzazione o disabilitali",
|
"logRetentionDescription": "Gestisci per quanto tempo i diversi tipi di log sono mantenuti per questa organizzazione o disabilitali",
|
||||||
"requestLogsDescription": "Visualizza i registri di richiesta dettagliati per le risorse in questa organizzazione",
|
"requestLogsDescription": "Visualizza i registri di richiesta dettagliati per le risorse in questa organizzazione",
|
||||||
"requestAnalyticsDescription": "Visualizza le analisi dettagliate della richiesta per le risorse in questa organizzazione",
|
"requestAnalyticsDescription": "Visualizza le analisi dettagliate della richiesta per le risorse in questa organizzazione",
|
||||||
"logRetentionRequestLabel": "Conservazione Log Richieste HTTP",
|
"logRetentionRequestLabel": "Richiedi Ritenzione Log",
|
||||||
"logRetentionRequestDescription": "Per quanto tempo conservare i log delle richieste",
|
"logRetentionRequestDescription": "Per quanto tempo conservare i log delle richieste",
|
||||||
"logRetentionAccessLabel": "Ritenzione Registro Accesso",
|
"logRetentionAccessLabel": "Ritenzione Registro Accesso",
|
||||||
"logRetentionAccessDescription": "Per quanto tempo conservare i log di accesso",
|
"logRetentionAccessDescription": "Per quanto tempo conservare i log di accesso",
|
||||||
@@ -3134,7 +3134,7 @@
|
|||||||
"httpDestActionLogsDescription": "Azioni amministrative eseguite dagli utenti all'interno dell'organizzazione.",
|
"httpDestActionLogsDescription": "Azioni amministrative eseguite dagli utenti all'interno dell'organizzazione.",
|
||||||
"httpDestConnectionLogsTitle": "Log Di Connessione",
|
"httpDestConnectionLogsTitle": "Log Di Connessione",
|
||||||
"httpDestConnectionLogsDescription": "Eventi di connessione al sito e al tunnel, inclusi collegamenti e disconnessioni.",
|
"httpDestConnectionLogsDescription": "Eventi di connessione al sito e al tunnel, inclusi collegamenti e disconnessioni.",
|
||||||
"httpDestRequestLogsTitle": "Log Richieste HTTP",
|
"httpDestRequestLogsTitle": "Log Richiesta",
|
||||||
"httpDestRequestLogsDescription": "Registri di richiesta HTTP per le risorse proxy, inclusi metodo, percorso e codice di risposta.",
|
"httpDestRequestLogsDescription": "Registri di richiesta HTTP per le risorse proxy, inclusi metodo, percorso e codice di risposta.",
|
||||||
"httpDestSaveChanges": "Salva Modifiche",
|
"httpDestSaveChanges": "Salva Modifiche",
|
||||||
"httpDestCreateDestination": "Crea Destinazione",
|
"httpDestCreateDestination": "Crea Destinazione",
|
||||||
@@ -3208,48 +3208,5 @@
|
|||||||
"domainPickerWildcardCertWarning": "Le risorse wildcard potrebbero richiedere configurazioni aggiuntive per funzionare correttamente.",
|
"domainPickerWildcardCertWarning": "Le risorse wildcard potrebbero richiedere configurazioni aggiuntive per funzionare correttamente.",
|
||||||
"domainPickerWildcardCertWarningLink": "Scopri di più",
|
"domainPickerWildcardCertWarningLink": "Scopri di più",
|
||||||
"health": "Salute",
|
"health": "Salute",
|
||||||
"domainPendingErrorTitle": "Problema di Verifica",
|
"domainPendingErrorTitle": "Problema di Verifica"
|
||||||
"memberPortalTitle": "Risorse",
|
|
||||||
"memberPortalDescription": "Risorse a cui hai accesso in questa organizzazione",
|
|
||||||
"memberPortalSortBy": "Ordina per...",
|
|
||||||
"memberPortalSortNameAsc": "Nome A-Z",
|
|
||||||
"memberPortalSortNameDesc": "Nome Z-A",
|
|
||||||
"memberPortalSortDomainAsc": "Dominio A-Z",
|
|
||||||
"memberPortalSortDomainDesc": "Dominio Z-A",
|
|
||||||
"memberPortalSortEnabledFirst": "Abilitati per primi",
|
|
||||||
"memberPortalSortDisabledFirst": "Disabilitati per primi",
|
|
||||||
"memberPortalRefresh": "Aggiorna",
|
|
||||||
"memberPortalRefreshResources": "Aggiorna Risorse",
|
|
||||||
"memberPortalFailedToLoad": "Caricamento delle risorse non riuscito",
|
|
||||||
"memberPortalFailedToLoadDescription": "Caricamento delle risorse non riuscito. Controlla la tua connessione e riprova.",
|
|
||||||
"memberPortalUnableToLoad": "Impossibile caricare le risorse",
|
|
||||||
"memberPortalTryAgain": "Riprova",
|
|
||||||
"memberPortalNoResourcesFound": "Nessuna risorsa trovata",
|
|
||||||
"memberPortalNoResourcesAvailable": "Nessuna risorsa disponibile",
|
|
||||||
"memberPortalNoResourcesMatchSearch": "Nessuna risorsa corrisponde a \"{query}\". Prova ad aggiustare i termini di ricerca o a cancellare la ricerca per vedere tutte le risorse.",
|
|
||||||
"memberPortalNoResourcesAccess": "Non hai ancora accesso a nessuna risorsa. Contatta il tuo amministratore per ottenere l'accesso alle risorse di cui hai bisogno.",
|
|
||||||
"memberPortalClearSearch": "Cancella Ricerca",
|
|
||||||
"memberPortalPublicResources": "Risorse Pubbliche",
|
|
||||||
"memberPortalPublicResourcesDescription": "Applicazioni web e servizi accessibili tramite browser",
|
|
||||||
"memberPortalCopiedToClipboard": "Copiato negli appunti",
|
|
||||||
"memberPortalCopiedUrlDescription": "L'URL della risorsa è stato copiato negli appunti.",
|
|
||||||
"memberPortalOpenResource": "Apri Risorsa",
|
|
||||||
"memberPortalPrivateResources": "Risorse Private",
|
|
||||||
"memberPortalPrivateResourcesDescription": "Risorse di rete interne accessibili tramite client",
|
|
||||||
"memberPortalResourceDetails": "Dettagli della Risorsa",
|
|
||||||
"memberPortalMode": "Modalità",
|
|
||||||
"memberPortalDestination": "Destinazione",
|
|
||||||
"memberPortalAlias": "Alias",
|
|
||||||
"memberPortalCopiedAliasDescription": "L'alias della risorsa è stato copiato negli appunti.",
|
|
||||||
"memberPortalCopiedDestinationDescription": "La destinazione della risorsa è stata copiata negli appunti.",
|
|
||||||
"memberPortalRequiresClientConnection": "Richiede Connessione Client",
|
|
||||||
"memberPortalAuthMethods": "Metodi di Autenticazione",
|
|
||||||
"memberPortalSso": "Accesso unico (Single Sign-On, SSO)",
|
|
||||||
"memberPortalPasswordProtected": "Protetto da password",
|
|
||||||
"memberPortalPinCode": "Codice PIN",
|
|
||||||
"memberPortalEmailWhitelist": "Lista Autorizzazioni Email",
|
|
||||||
"memberPortalResourceDisabled": "Risorsa Disabilitata",
|
|
||||||
"memberPortalShowingResources": "Mostrando {start}-{end} di {total} risorse",
|
|
||||||
"memberPortalPrevious": "Precedente",
|
|
||||||
"memberPortalNext": "Successivo"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2660,19 +2660,19 @@
|
|||||||
"noMoreAuthMethods": "유효한 인증 없음",
|
"noMoreAuthMethods": "유효한 인증 없음",
|
||||||
"ip": "IP",
|
"ip": "IP",
|
||||||
"reason": "이유",
|
"reason": "이유",
|
||||||
"requestLogs": "HTTP 요청 로그",
|
"requestLogs": "요청 로그",
|
||||||
"requestAnalytics": "요청 분석",
|
"requestAnalytics": "요청 분석",
|
||||||
"host": "호스트",
|
"host": "호스트",
|
||||||
"location": "위치",
|
"location": "위치",
|
||||||
"actionLogs": "작업 로그",
|
"actionLogs": "작업 로그",
|
||||||
"sidebarLogsRequest": "HTTP 요청 로그",
|
"sidebarLogsRequest": "요청 로그",
|
||||||
"sidebarLogsAccess": "접근 로그",
|
"sidebarLogsAccess": "접근 로그",
|
||||||
"sidebarLogsAction": "작업 로그",
|
"sidebarLogsAction": "작업 로그",
|
||||||
"logRetention": "로그 보관",
|
"logRetention": "로그 보관",
|
||||||
"logRetentionDescription": "다양한 유형의 로그를 이 조직에 대해 얼마나 오래 보관할지 관리하거나 비활성화합니다",
|
"logRetentionDescription": "다양한 유형의 로그를 이 조직에 대해 얼마나 오래 보관할지 관리하거나 비활성화합니다",
|
||||||
"requestLogsDescription": "이 조직의 자원에 대한 상세한 요청 로그를 봅니다",
|
"requestLogsDescription": "이 조직의 자원에 대한 상세한 요청 로그를 봅니다",
|
||||||
"requestAnalyticsDescription": "이 조직의 리소스에 대한 자세한 요청 분석 보기",
|
"requestAnalyticsDescription": "이 조직의 리소스에 대한 자세한 요청 분석 보기",
|
||||||
"logRetentionRequestLabel": "HTTP 요청 로그 보관",
|
"logRetentionRequestLabel": "요청 로그 보관",
|
||||||
"logRetentionRequestDescription": "요청 로그를 얼마나 오래 보관할지",
|
"logRetentionRequestDescription": "요청 로그를 얼마나 오래 보관할지",
|
||||||
"logRetentionAccessLabel": "접근 로그 보관",
|
"logRetentionAccessLabel": "접근 로그 보관",
|
||||||
"logRetentionAccessDescription": "접근 로그를 얼마나 오래 보관할지",
|
"logRetentionAccessDescription": "접근 로그를 얼마나 오래 보관할지",
|
||||||
@@ -3134,7 +3134,7 @@
|
|||||||
"httpDestActionLogsDescription": "조직 내에서 사용자가 수행한 관리 작업.",
|
"httpDestActionLogsDescription": "조직 내에서 사용자가 수행한 관리 작업.",
|
||||||
"httpDestConnectionLogsTitle": "연결 로그",
|
"httpDestConnectionLogsTitle": "연결 로그",
|
||||||
"httpDestConnectionLogsDescription": "사이트 및 터널 연결 이벤트, 연결 및 연결 끊기를 포함합니다.",
|
"httpDestConnectionLogsDescription": "사이트 및 터널 연결 이벤트, 연결 및 연결 끊기를 포함합니다.",
|
||||||
"httpDestRequestLogsTitle": "HTTP 요청 로그",
|
"httpDestRequestLogsTitle": "요청 로그",
|
||||||
"httpDestRequestLogsDescription": "프록시된 리소스에 대한 HTTP 요청 로그, 메서드, 경로 및 응답 코드를 포함합니다.",
|
"httpDestRequestLogsDescription": "프록시된 리소스에 대한 HTTP 요청 로그, 메서드, 경로 및 응답 코드를 포함합니다.",
|
||||||
"httpDestSaveChanges": "변경 사항 저장",
|
"httpDestSaveChanges": "변경 사항 저장",
|
||||||
"httpDestCreateDestination": "대상지 생성",
|
"httpDestCreateDestination": "대상지 생성",
|
||||||
@@ -3208,48 +3208,5 @@
|
|||||||
"domainPickerWildcardCertWarning": "와일드카드 리소스는 올바르게 작동하려면 추가 구성이 필요할 수 있습니다.",
|
"domainPickerWildcardCertWarning": "와일드카드 리소스는 올바르게 작동하려면 추가 구성이 필요할 수 있습니다.",
|
||||||
"domainPickerWildcardCertWarningLink": "자세히 알아보기",
|
"domainPickerWildcardCertWarningLink": "자세히 알아보기",
|
||||||
"health": "건강",
|
"health": "건강",
|
||||||
"domainPendingErrorTitle": "확인 문제",
|
"domainPendingErrorTitle": "확인 문제"
|
||||||
"memberPortalTitle": "리소스",
|
|
||||||
"memberPortalDescription": "이 조직에서 접근할 수 있는 리소스",
|
|
||||||
"memberPortalSortBy": "정렬 기준...",
|
|
||||||
"memberPortalSortNameAsc": "이름 A-Z",
|
|
||||||
"memberPortalSortNameDesc": "이름 Z-A",
|
|
||||||
"memberPortalSortDomainAsc": "도메인 A-Z",
|
|
||||||
"memberPortalSortDomainDesc": "도메인 Z-A",
|
|
||||||
"memberPortalSortEnabledFirst": "사용 활성화 우선",
|
|
||||||
"memberPortalSortDisabledFirst": "사용 비활성화 우선",
|
|
||||||
"memberPortalRefresh": "새로 고침",
|
|
||||||
"memberPortalRefreshResources": "리소스 새로 고침",
|
|
||||||
"memberPortalFailedToLoad": "리소스를 불러오는 데 실패했습니다",
|
|
||||||
"memberPortalFailedToLoadDescription": "리소스를 불러오는 데 실패했습니다. 연결을 확인하고 다시 시도해 주십시오.",
|
|
||||||
"memberPortalUnableToLoad": "리소스를 가져오는 데 실패했습니다",
|
|
||||||
"memberPortalTryAgain": "다시 시도",
|
|
||||||
"memberPortalNoResourcesFound": "리소스를 발견하지 못했습니다",
|
|
||||||
"memberPortalNoResourcesAvailable": "사용 가능한 리소스가 없습니다",
|
|
||||||
"memberPortalNoResourcesMatchSearch": "\"{query}\"와 일치하는 리소스가 없습니다. 검색어를 수정하거나 검색을 초기화하여 모든 리소스를 확인하십시오.",
|
|
||||||
"memberPortalNoResourcesAccess": "아직 접근할 수 있는 리소스가 없습니다. 필요한 리소스 접근을 위해 관리자에게 문의하세요.",
|
|
||||||
"memberPortalClearSearch": "검색 초기화",
|
|
||||||
"memberPortalPublicResources": "공공 리소스",
|
|
||||||
"memberPortalPublicResourcesDescription": "브라우저를 통해 접근 가능한 웹 애플리케이션 및 서비스",
|
|
||||||
"memberPortalCopiedToClipboard": "클립보드에 복사됨",
|
|
||||||
"memberPortalCopiedUrlDescription": "리소스 URL이 클립보드에 복사되었습니다.",
|
|
||||||
"memberPortalOpenResource": "리소스 열기",
|
|
||||||
"memberPortalPrivateResources": "비공개 리소스",
|
|
||||||
"memberPortalPrivateResourcesDescription": "클라이언트를 통해 접근 가능한 내부 네트워크 리소스",
|
|
||||||
"memberPortalResourceDetails": "리소스 세부 정보",
|
|
||||||
"memberPortalMode": "모드",
|
|
||||||
"memberPortalDestination": "대상지",
|
|
||||||
"memberPortalAlias": "별칭",
|
|
||||||
"memberPortalCopiedAliasDescription": "리소스 별칭이 클립보드에 복사되었습니다.",
|
|
||||||
"memberPortalCopiedDestinationDescription": "리소스 대상지가 클립보드에 복사되었습니다.",
|
|
||||||
"memberPortalRequiresClientConnection": "클라이언트 연결 필요",
|
|
||||||
"memberPortalAuthMethods": "인증 방법",
|
|
||||||
"memberPortalSso": "싱글 사인온 (SSO)",
|
|
||||||
"memberPortalPasswordProtected": "비밀번호 보호",
|
|
||||||
"memberPortalPinCode": "PIN 코드",
|
|
||||||
"memberPortalEmailWhitelist": "이메일 화이트리스트",
|
|
||||||
"memberPortalResourceDisabled": "리소스 비활성화됨",
|
|
||||||
"memberPortalShowingResources": "{start}-{end} 중 {total}개의 리소스를 표시 중",
|
|
||||||
"memberPortalPrevious": "이전",
|
|
||||||
"memberPortalNext": "다음"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2660,19 +2660,19 @@
|
|||||||
"noMoreAuthMethods": "No Valid Auth",
|
"noMoreAuthMethods": "No Valid Auth",
|
||||||
"ip": "IP",
|
"ip": "IP",
|
||||||
"reason": "Grunn",
|
"reason": "Grunn",
|
||||||
"requestLogs": "HTTP-forespørselslogger",
|
"requestLogs": "Forespørselslogger (Automatic Translation)",
|
||||||
"requestAnalytics": "Be om analyser",
|
"requestAnalytics": "Be om analyser",
|
||||||
"host": "Vert",
|
"host": "Vert",
|
||||||
"location": "Sted",
|
"location": "Sted",
|
||||||
"actionLogs": "Handlingslogger",
|
"actionLogs": "Handlingslogger",
|
||||||
"sidebarLogsRequest": "HTTP-forespørselslogger",
|
"sidebarLogsRequest": "Forespørselslogger (Automatic Translation)",
|
||||||
"sidebarLogsAccess": "Tilgangslogger (Automatic Translation)",
|
"sidebarLogsAccess": "Tilgangslogger (Automatic Translation)",
|
||||||
"sidebarLogsAction": "Handlingslogger",
|
"sidebarLogsAction": "Handlingslogger",
|
||||||
"logRetention": "Logg tilbaketrekning",
|
"logRetention": "Logg tilbaketrekning",
|
||||||
"logRetentionDescription": "Håndter hvor lenge ulike typer logger beholdes for denne organisasjonen, eller deaktiver dem",
|
"logRetentionDescription": "Håndter hvor lenge ulike typer logger beholdes for denne organisasjonen, eller deaktiver dem",
|
||||||
"requestLogsDescription": "Se detaljerte forespørselslogger for ressurser i denne organisasjonen",
|
"requestLogsDescription": "Se detaljerte forespørselslogger for ressurser i denne organisasjonen",
|
||||||
"requestAnalyticsDescription": "Se detaljert rekvisisjonsanalyse for ressurser i denne organisasjonen",
|
"requestAnalyticsDescription": "Se detaljert rekvisisjonsanalyse for ressurser i denne organisasjonen",
|
||||||
"logRetentionRequestLabel": "Be om loggbevaring",
|
"logRetentionRequestLabel": "Be om loggoverføring",
|
||||||
"logRetentionRequestDescription": "Hvor lenge du vil beholde forespørselslogger",
|
"logRetentionRequestDescription": "Hvor lenge du vil beholde forespørselslogger",
|
||||||
"logRetentionAccessLabel": "Få tilgang til loggoverføring",
|
"logRetentionAccessLabel": "Få tilgang til loggoverføring",
|
||||||
"logRetentionAccessDescription": "Hvor lenge du vil beholde adgangslogger",
|
"logRetentionAccessDescription": "Hvor lenge du vil beholde adgangslogger",
|
||||||
@@ -3134,7 +3134,7 @@
|
|||||||
"httpDestActionLogsDescription": "Administrative tiltak som utføres av brukere innenfor organisasjonen.",
|
"httpDestActionLogsDescription": "Administrative tiltak som utføres av brukere innenfor organisasjonen.",
|
||||||
"httpDestConnectionLogsTitle": "Loggfiler for tilkobling",
|
"httpDestConnectionLogsTitle": "Loggfiler for tilkobling",
|
||||||
"httpDestConnectionLogsDescription": "Utstyrs- og tunneltilkoblingshendelser, inkludert forbindelser og frakobling.",
|
"httpDestConnectionLogsDescription": "Utstyrs- og tunneltilkoblingshendelser, inkludert forbindelser og frakobling.",
|
||||||
"httpDestRequestLogsTitle": "HTTP-forespørselslogger",
|
"httpDestRequestLogsTitle": "Forespørselslogger (Automatic Translation)",
|
||||||
"httpDestRequestLogsDescription": "HTTP-forespørsel logger for bekreftede ressurser, inkludert metode, bane og responskode.",
|
"httpDestRequestLogsDescription": "HTTP-forespørsel logger for bekreftede ressurser, inkludert metode, bane og responskode.",
|
||||||
"httpDestSaveChanges": "Lagre endringer",
|
"httpDestSaveChanges": "Lagre endringer",
|
||||||
"httpDestCreateDestination": "Opprett mål",
|
"httpDestCreateDestination": "Opprett mål",
|
||||||
@@ -3208,48 +3208,5 @@
|
|||||||
"domainPickerWildcardCertWarning": "Jokertegnressurser kan kreve ekstra konfigurasjon for å fungere skikkelig.",
|
"domainPickerWildcardCertWarning": "Jokertegnressurser kan kreve ekstra konfigurasjon for å fungere skikkelig.",
|
||||||
"domainPickerWildcardCertWarningLink": "Lær mer",
|
"domainPickerWildcardCertWarningLink": "Lær mer",
|
||||||
"health": "Helse",
|
"health": "Helse",
|
||||||
"domainPendingErrorTitle": "Verifiseringsproblem",
|
"domainPendingErrorTitle": "Verifiseringsproblem"
|
||||||
"memberPortalTitle": "Ressurser",
|
|
||||||
"memberPortalDescription": "Ressurser du har tilgang til i denne organisasjonen",
|
|
||||||
"memberPortalSortBy": "Sorter etter...",
|
|
||||||
"memberPortalSortNameAsc": "Navn A-Å",
|
|
||||||
"memberPortalSortNameDesc": "Navn Å-A",
|
|
||||||
"memberPortalSortDomainAsc": "Domene A-Å",
|
|
||||||
"memberPortalSortDomainDesc": "Domene Å-A",
|
|
||||||
"memberPortalSortEnabledFirst": "Aktivert først",
|
|
||||||
"memberPortalSortDisabledFirst": "Deaktivert først",
|
|
||||||
"memberPortalRefresh": "Oppdater",
|
|
||||||
"memberPortalRefreshResources": "Oppdater ressurser",
|
|
||||||
"memberPortalFailedToLoad": "Kunne ikke laste inn ressurser",
|
|
||||||
"memberPortalFailedToLoadDescription": "Kunne ikke laste inn ressurser. Vennligst sjekk tilkoblingen din og prøv igjen.",
|
|
||||||
"memberPortalUnableToLoad": "Kan ikke laste inn ressurser",
|
|
||||||
"memberPortalTryAgain": "Prøv igjen",
|
|
||||||
"memberPortalNoResourcesFound": "Ingen ressurser funnet",
|
|
||||||
"memberPortalNoResourcesAvailable": "Ingen ressurser tilgjengelig",
|
|
||||||
"memberPortalNoResourcesMatchSearch": "Ingen ressurser samsvarer med \"{query}\". Prøv å justere søkeordene dine eller fjern søket for å se alle ressurser.",
|
|
||||||
"memberPortalNoResourcesAccess": "Du har ennå ikke tilgang til noen ressurser. Kontakt administratoren din for å få tilgang til de ressursene du trenger.",
|
|
||||||
"memberPortalClearSearch": "Fjern søk",
|
|
||||||
"memberPortalPublicResources": "Offentlige ressurser",
|
|
||||||
"memberPortalPublicResourcesDescription": "Webapplikasjoner og -tjenester tilgjengelige via nettleser",
|
|
||||||
"memberPortalCopiedToClipboard": "Kopiert til utklippstavlen",
|
|
||||||
"memberPortalCopiedUrlDescription": "Ressurs-URL er kopiert til utklippstavlen din.",
|
|
||||||
"memberPortalOpenResource": "Åpne ressurs",
|
|
||||||
"memberPortalPrivateResources": "Private ressurser",
|
|
||||||
"memberPortalPrivateResourcesDescription": "Interne nettverksressurser tilgjengelige via klient",
|
|
||||||
"memberPortalResourceDetails": "Ressursdetaljer",
|
|
||||||
"memberPortalMode": "Modus",
|
|
||||||
"memberPortalDestination": "Destinasjon",
|
|
||||||
"memberPortalAlias": "Navn",
|
|
||||||
"memberPortalCopiedAliasDescription": "Ressursalias er kopiert til utklippstavlen din.",
|
|
||||||
"memberPortalCopiedDestinationDescription": "Ressursdestinasjon er kopiert til utklippstavlen din.",
|
|
||||||
"memberPortalRequiresClientConnection": "Krever klienttilkobling",
|
|
||||||
"memberPortalAuthMethods": "Autentiseringsmetoder",
|
|
||||||
"memberPortalSso": "Enkeltpålogging (SSO)",
|
|
||||||
"memberPortalPasswordProtected": "Passordbeskyttet",
|
|
||||||
"memberPortalPinCode": "PIN-kode",
|
|
||||||
"memberPortalEmailWhitelist": "E-post-hviteliste",
|
|
||||||
"memberPortalResourceDisabled": "Ressurs deaktivert",
|
|
||||||
"memberPortalShowingResources": "Viser {start}-{end} av {total} ressurser",
|
|
||||||
"memberPortalPrevious": "Forrige",
|
|
||||||
"memberPortalNext": "Neste"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2660,19 +2660,19 @@
|
|||||||
"noMoreAuthMethods": "No Valid Auth",
|
"noMoreAuthMethods": "No Valid Auth",
|
||||||
"ip": "IP-adres",
|
"ip": "IP-adres",
|
||||||
"reason": "Reden",
|
"reason": "Reden",
|
||||||
"requestLogs": "HTTP-aanvraaglogboeken",
|
"requestLogs": "Logboeken aanvragen",
|
||||||
"requestAnalytics": "Analytics opvragen",
|
"requestAnalytics": "Analytics opvragen",
|
||||||
"host": "Hostnaam",
|
"host": "Hostnaam",
|
||||||
"location": "Locatie",
|
"location": "Locatie",
|
||||||
"actionLogs": "Actie logs",
|
"actionLogs": "Actie logs",
|
||||||
"sidebarLogsRequest": "HTTP-aanvraaglogboeken",
|
"sidebarLogsRequest": "Logboeken aanvragen",
|
||||||
"sidebarLogsAccess": "Toegang tot logboek",
|
"sidebarLogsAccess": "Toegang tot logboek",
|
||||||
"sidebarLogsAction": "Actie logs",
|
"sidebarLogsAction": "Actie logs",
|
||||||
"logRetention": "Log bewaring",
|
"logRetention": "Log bewaring",
|
||||||
"logRetentionDescription": "Beheren hoe lang verschillende soorten logs bewaard worden voor deze organisatie of schakel ze uit",
|
"logRetentionDescription": "Beheren hoe lang verschillende soorten logs bewaard worden voor deze organisatie of schakel ze uit",
|
||||||
"requestLogsDescription": "Bekijk gedetailleerde verzoeklogboeken voor resources in deze organisatie",
|
"requestLogsDescription": "Bekijk gedetailleerde verzoeklogboeken voor resources in deze organisatie",
|
||||||
"requestAnalyticsDescription": "Bekijk gedetailleerde request analytics voor resources in deze organisatie",
|
"requestAnalyticsDescription": "Bekijk gedetailleerde request analytics voor resources in deze organisatie",
|
||||||
"logRetentionRequestLabel": "Bewaring van HTTP-aanvraaglogboeken",
|
"logRetentionRequestLabel": "Logboekbewaring aanvragen",
|
||||||
"logRetentionRequestDescription": "Hoe lang de aanvraaglogboeken te behouden",
|
"logRetentionRequestDescription": "Hoe lang de aanvraaglogboeken te behouden",
|
||||||
"logRetentionAccessLabel": "Toegang logboek bewaring",
|
"logRetentionAccessLabel": "Toegang logboek bewaring",
|
||||||
"logRetentionAccessDescription": "Hoe lang de toegangslogboeken behouden blijven",
|
"logRetentionAccessDescription": "Hoe lang de toegangslogboeken behouden blijven",
|
||||||
@@ -3134,7 +3134,7 @@
|
|||||||
"httpDestActionLogsDescription": "Administratieve acties uitgevoerd door gebruikers binnen de organisatie.",
|
"httpDestActionLogsDescription": "Administratieve acties uitgevoerd door gebruikers binnen de organisatie.",
|
||||||
"httpDestConnectionLogsTitle": "Connectie Logs",
|
"httpDestConnectionLogsTitle": "Connectie Logs",
|
||||||
"httpDestConnectionLogsDescription": "Verbinding met de Site en tunnel maken verbroken, inclusief verbindingen en verbindingen.",
|
"httpDestConnectionLogsDescription": "Verbinding met de Site en tunnel maken verbroken, inclusief verbindingen en verbindingen.",
|
||||||
"httpDestRequestLogsTitle": "HTTP-aanvraaglogboeken",
|
"httpDestRequestLogsTitle": "Logboeken aanvragen",
|
||||||
"httpDestRequestLogsDescription": "HTTP request logs voor proxied hulpmiddelen, waaronder methode, pad en response code.",
|
"httpDestRequestLogsDescription": "HTTP request logs voor proxied hulpmiddelen, waaronder methode, pad en response code.",
|
||||||
"httpDestSaveChanges": "Wijzigingen opslaan",
|
"httpDestSaveChanges": "Wijzigingen opslaan",
|
||||||
"httpDestCreateDestination": "Maak bestemming aan",
|
"httpDestCreateDestination": "Maak bestemming aan",
|
||||||
@@ -3208,48 +3208,5 @@
|
|||||||
"domainPickerWildcardCertWarning": "Wildcard-bronnen hebben mogelijk extra configuratie nodig om correct te werken.",
|
"domainPickerWildcardCertWarning": "Wildcard-bronnen hebben mogelijk extra configuratie nodig om correct te werken.",
|
||||||
"domainPickerWildcardCertWarningLink": "Meer informatie",
|
"domainPickerWildcardCertWarningLink": "Meer informatie",
|
||||||
"health": "Gezondheid",
|
"health": "Gezondheid",
|
||||||
"domainPendingErrorTitle": "Verificatieprobleem",
|
"domainPendingErrorTitle": "Verificatieprobleem"
|
||||||
"memberPortalTitle": "Bronnen",
|
|
||||||
"memberPortalDescription": "Bronnen waartoe je toegang hebt binnen deze organisatie",
|
|
||||||
"memberPortalSortBy": "Sorteren op...",
|
|
||||||
"memberPortalSortNameAsc": "Naam A-Z",
|
|
||||||
"memberPortalSortNameDesc": "Naam Z-A",
|
|
||||||
"memberPortalSortDomainAsc": "Domein A-Z",
|
|
||||||
"memberPortalSortDomainDesc": "Domein Z-A",
|
|
||||||
"memberPortalSortEnabledFirst": "Ingeschakeld Eerst",
|
|
||||||
"memberPortalSortDisabledFirst": "Uitgeschakeld Eerst",
|
|
||||||
"memberPortalRefresh": "Vernieuwen",
|
|
||||||
"memberPortalRefreshResources": "Bronnen Vernieuwen",
|
|
||||||
"memberPortalFailedToLoad": "Fout bij het laden van bronnen",
|
|
||||||
"memberPortalFailedToLoadDescription": "Fout bij het laden van bronnen. Controleer uw verbinding en probeer het opnieuw.",
|
|
||||||
"memberPortalUnableToLoad": "Niet in staat om bronnen te laden",
|
|
||||||
"memberPortalTryAgain": "Probeer Opnieuw",
|
|
||||||
"memberPortalNoResourcesFound": "Geen Bronnen Gevonden",
|
|
||||||
"memberPortalNoResourcesAvailable": "Geen Bronnen Beschikbaar",
|
|
||||||
"memberPortalNoResourcesMatchSearch": "Geen bronnen komen overeen met \"{query}\". Probeer uw zoektermen aan te passen of wis de zoekopdracht om alle bronnen te zien.",
|
|
||||||
"memberPortalNoResourcesAccess": "Je hebt nog geen toegang tot bronnen. Neem contact op met je beheerder om toegang te krijgen tot de benodigde bronnen.",
|
|
||||||
"memberPortalClearSearch": "Zoekopdracht Wissen",
|
|
||||||
"memberPortalPublicResources": "Publieke Bronnen",
|
|
||||||
"memberPortalPublicResourcesDescription": "Webapplicaties en services toegankelijk via browser",
|
|
||||||
"memberPortalCopiedToClipboard": "Gekopieerd naar klembord",
|
|
||||||
"memberPortalCopiedUrlDescription": "Bron URL is naar uw klembord gekopieerd.",
|
|
||||||
"memberPortalOpenResource": "Bron Openen",
|
|
||||||
"memberPortalPrivateResources": "Privé Bronnen",
|
|
||||||
"memberPortalPrivateResourcesDescription": "Interne netwerkbronnen toegankelijk via client",
|
|
||||||
"memberPortalResourceDetails": "Bron Details",
|
|
||||||
"memberPortalMode": "Modus",
|
|
||||||
"memberPortalDestination": "Bestemming",
|
|
||||||
"memberPortalAlias": "Alias",
|
|
||||||
"memberPortalCopiedAliasDescription": "Bron alias is naar uw klembord gekopieerd.",
|
|
||||||
"memberPortalCopiedDestinationDescription": "Bron bestemming is naar uw klembord gekopieerd.",
|
|
||||||
"memberPortalRequiresClientConnection": "Clientverbinding Vereist",
|
|
||||||
"memberPortalAuthMethods": "Authenticatiemethoden",
|
|
||||||
"memberPortalSso": "Single Sign-On (SSO)",
|
|
||||||
"memberPortalPasswordProtected": "Wachtwoord Beveiligd",
|
|
||||||
"memberPortalPinCode": "Pincode",
|
|
||||||
"memberPortalEmailWhitelist": "E-mail whitelist",
|
|
||||||
"memberPortalResourceDisabled": "Bron Uitgeschakeld",
|
|
||||||
"memberPortalShowingResources": "Toont {start}-{end} van {total} bronnen",
|
|
||||||
"memberPortalPrevious": "Vorige",
|
|
||||||
"memberPortalNext": "Volgende"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2660,19 +2660,19 @@
|
|||||||
"noMoreAuthMethods": "No Valid Auth",
|
"noMoreAuthMethods": "No Valid Auth",
|
||||||
"ip": "IP",
|
"ip": "IP",
|
||||||
"reason": "Powód",
|
"reason": "Powód",
|
||||||
"requestLogs": "Dzienniki żądań HTTP",
|
"requestLogs": "Dzienniki żądań",
|
||||||
"requestAnalytics": "Żądanie Analityki",
|
"requestAnalytics": "Żądanie Analityki",
|
||||||
"host": "Host",
|
"host": "Host",
|
||||||
"location": "Lokalizacja",
|
"location": "Lokalizacja",
|
||||||
"actionLogs": "Dzienniki działań",
|
"actionLogs": "Dzienniki działań",
|
||||||
"sidebarLogsRequest": "Dzienniki żądań HTTP",
|
"sidebarLogsRequest": "Dzienniki żądań",
|
||||||
"sidebarLogsAccess": "Logi dostępu",
|
"sidebarLogsAccess": "Logi dostępu",
|
||||||
"sidebarLogsAction": "Dzienniki działań",
|
"sidebarLogsAction": "Dzienniki działań",
|
||||||
"logRetention": "Zachowanie dziennika",
|
"logRetention": "Zachowanie dziennika",
|
||||||
"logRetentionDescription": "Zarządzaj jak długo różne typy logów są zachowane dla tej organizacji lub wyłącz je",
|
"logRetentionDescription": "Zarządzaj jak długo różne typy logów są zachowane dla tej organizacji lub wyłącz je",
|
||||||
"requestLogsDescription": "Zobacz szczegółowe dzienniki żądań zasobów w tej organizacji",
|
"requestLogsDescription": "Zobacz szczegółowe dzienniki żądań zasobów w tej organizacji",
|
||||||
"requestAnalyticsDescription": "Zobacz szczegółowe analizy żądań dla zasobów w tej organizacji",
|
"requestAnalyticsDescription": "Zobacz szczegółowe analizy żądań dla zasobów w tej organizacji",
|
||||||
"logRetentionRequestLabel": "Przechowywanie dzienników żądań HTTP",
|
"logRetentionRequestLabel": "Zachowanie dziennika żądań",
|
||||||
"logRetentionRequestDescription": "Jak długo zachować dzienniki żądań",
|
"logRetentionRequestDescription": "Jak długo zachować dzienniki żądań",
|
||||||
"logRetentionAccessLabel": "Zachowanie dziennika dostępu",
|
"logRetentionAccessLabel": "Zachowanie dziennika dostępu",
|
||||||
"logRetentionAccessDescription": "Jak długo zachować dzienniki dostępu",
|
"logRetentionAccessDescription": "Jak długo zachować dzienniki dostępu",
|
||||||
@@ -3134,7 +3134,7 @@
|
|||||||
"httpDestActionLogsDescription": "Działania administracyjne wykonywane przez użytkowników w organizacji.",
|
"httpDestActionLogsDescription": "Działania administracyjne wykonywane przez użytkowników w organizacji.",
|
||||||
"httpDestConnectionLogsTitle": "Dzienniki połączeń",
|
"httpDestConnectionLogsTitle": "Dzienniki połączeń",
|
||||||
"httpDestConnectionLogsDescription": "Zdarzenia związane z miejscem i tunelem, w tym połączenia i rozłączenia.",
|
"httpDestConnectionLogsDescription": "Zdarzenia związane z miejscem i tunelem, w tym połączenia i rozłączenia.",
|
||||||
"httpDestRequestLogsTitle": "Dzienniki żądań HTTP",
|
"httpDestRequestLogsTitle": "Dzienniki żądań",
|
||||||
"httpDestRequestLogsDescription": "Logi żądań HTTP dla zasobów proxy, w tym metody, ścieżki i kodu odpowiedzi.",
|
"httpDestRequestLogsDescription": "Logi żądań HTTP dla zasobów proxy, w tym metody, ścieżki i kodu odpowiedzi.",
|
||||||
"httpDestSaveChanges": "Zapisz zmiany",
|
"httpDestSaveChanges": "Zapisz zmiany",
|
||||||
"httpDestCreateDestination": "Utwórz cel",
|
"httpDestCreateDestination": "Utwórz cel",
|
||||||
@@ -3208,48 +3208,5 @@
|
|||||||
"domainPickerWildcardCertWarning": "Uniwersalne zasoby mogą wymagać dodatkowej konfiguracji, aby działać poprawnie.",
|
"domainPickerWildcardCertWarning": "Uniwersalne zasoby mogą wymagać dodatkowej konfiguracji, aby działać poprawnie.",
|
||||||
"domainPickerWildcardCertWarningLink": "Dowiedz się więcej",
|
"domainPickerWildcardCertWarningLink": "Dowiedz się więcej",
|
||||||
"health": "Zdrowie",
|
"health": "Zdrowie",
|
||||||
"domainPendingErrorTitle": "Problem z weryfikacją",
|
"domainPendingErrorTitle": "Problem z weryfikacją"
|
||||||
"memberPortalTitle": "Zasoby",
|
|
||||||
"memberPortalDescription": "Zasoby, do których masz dostęp w tej organizacji",
|
|
||||||
"memberPortalSortBy": "Sortuj według...",
|
|
||||||
"memberPortalSortNameAsc": "Nazwa A-Z",
|
|
||||||
"memberPortalSortNameDesc": "Nazwa Z-A",
|
|
||||||
"memberPortalSortDomainAsc": "Domena A-Z",
|
|
||||||
"memberPortalSortDomainDesc": "Domena Z-A",
|
|
||||||
"memberPortalSortEnabledFirst": "Włączone najpierw",
|
|
||||||
"memberPortalSortDisabledFirst": "Wyłączone najpierw",
|
|
||||||
"memberPortalRefresh": "Odśwież",
|
|
||||||
"memberPortalRefreshResources": "Odśwież zasoby",
|
|
||||||
"memberPortalFailedToLoad": "Nie udało się załadować zasobów",
|
|
||||||
"memberPortalFailedToLoadDescription": "Nie udało się załadować zasobów. Sprawdź połączenie i spróbuj ponownie.",
|
|
||||||
"memberPortalUnableToLoad": "Nie można załadować zasobów",
|
|
||||||
"memberPortalTryAgain": "Spróbuj ponownie",
|
|
||||||
"memberPortalNoResourcesFound": "Nie znaleziono zasobów",
|
|
||||||
"memberPortalNoResourcesAvailable": "Brak dostępnych zasobów",
|
|
||||||
"memberPortalNoResourcesMatchSearch": "Żadne zasoby nie pasują do „{query}”. Spróbuj dostosować swoje warunki wyszukiwania lub wyczyść wyszukiwanie, aby zobaczyć wszystkie zasoby.",
|
|
||||||
"memberPortalNoResourcesAccess": "Nie masz jeszcze dostępu do żadnych zasobów. Skontaktuj się z administratorem, aby uzyskać dostęp do potrzebnych zasobów.",
|
|
||||||
"memberPortalClearSearch": "Wyczyść wyszukiwanie",
|
|
||||||
"memberPortalPublicResources": "Publiczne zasoby",
|
|
||||||
"memberPortalPublicResourcesDescription": "Aplikacje i usługi internetowe dostępne za pośrednictwem przeglądarki",
|
|
||||||
"memberPortalCopiedToClipboard": "Skopiowano do schowka",
|
|
||||||
"memberPortalCopiedUrlDescription": "URL zasobu został skopiowany do schowka.",
|
|
||||||
"memberPortalOpenResource": "Otwórz zasób",
|
|
||||||
"memberPortalPrivateResources": "Prywatne zasoby",
|
|
||||||
"memberPortalPrivateResourcesDescription": "Zasoby sieci wewnętrznej dostępne za pośrednictwem klienta",
|
|
||||||
"memberPortalResourceDetails": "Szczegóły zasobu",
|
|
||||||
"memberPortalMode": "Tryb",
|
|
||||||
"memberPortalDestination": "Miejsce docelowe",
|
|
||||||
"memberPortalAlias": "Pseudonim",
|
|
||||||
"memberPortalCopiedAliasDescription": "Alias zasobu został skopiowany do schowka.",
|
|
||||||
"memberPortalCopiedDestinationDescription": "Miejsce docelowe zasobu zostało skopiowane do schowka.",
|
|
||||||
"memberPortalRequiresClientConnection": "Wymaga połączenia z klientem",
|
|
||||||
"memberPortalAuthMethods": "Metody uwierzytelniania",
|
|
||||||
"memberPortalSso": "Jednorazowe logowanie (SSO)",
|
|
||||||
"memberPortalPasswordProtected": "Chronione hasłem",
|
|
||||||
"memberPortalPinCode": "Kod PIN",
|
|
||||||
"memberPortalEmailWhitelist": "Biała lista e-mail",
|
|
||||||
"memberPortalResourceDisabled": "Zasób wyłączony",
|
|
||||||
"memberPortalShowingResources": "Wyświetlanie zasobów od {start} do {end} z {total}",
|
|
||||||
"memberPortalPrevious": "Poprzedni",
|
|
||||||
"memberPortalNext": "Następny"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2660,19 +2660,19 @@
|
|||||||
"noMoreAuthMethods": "No Valid Auth",
|
"noMoreAuthMethods": "No Valid Auth",
|
||||||
"ip": "PI",
|
"ip": "PI",
|
||||||
"reason": "Motivo",
|
"reason": "Motivo",
|
||||||
"requestLogs": "Registros de Pedidos HTTP",
|
"requestLogs": "Registro de pedidos",
|
||||||
"requestAnalytics": "Solicitar análise",
|
"requestAnalytics": "Solicitar análise",
|
||||||
"host": "Servidor",
|
"host": "Servidor",
|
||||||
"location": "Local:",
|
"location": "Local:",
|
||||||
"actionLogs": "Logs de Ações",
|
"actionLogs": "Logs de Ações",
|
||||||
"sidebarLogsRequest": "Registros de Pedidos HTTP",
|
"sidebarLogsRequest": "Registro de pedidos",
|
||||||
"sidebarLogsAccess": "Logs de Acesso",
|
"sidebarLogsAccess": "Logs de Acesso",
|
||||||
"sidebarLogsAction": "Logs de Ações",
|
"sidebarLogsAction": "Logs de Ações",
|
||||||
"logRetention": "Retenção de Log",
|
"logRetention": "Retenção de Log",
|
||||||
"logRetentionDescription": "Gerenciar quanto tempo os diferentes tipos de logs são mantidos para esta organização ou desativá-los",
|
"logRetentionDescription": "Gerenciar quanto tempo os diferentes tipos de logs são mantidos para esta organização ou desativá-los",
|
||||||
"requestLogsDescription": "Ver registros de pedidos detalhados de recursos nesta organização",
|
"requestLogsDescription": "Ver registros de pedidos detalhados de recursos nesta organização",
|
||||||
"requestAnalyticsDescription": "Exibir análise detalhada de pedidos para recursos nesta organização",
|
"requestAnalyticsDescription": "Exibir análise detalhada de pedidos para recursos nesta organização",
|
||||||
"logRetentionRequestLabel": "Retenção de Registro de Pedido HTTP",
|
"logRetentionRequestLabel": "Solicitar retenção de registro",
|
||||||
"logRetentionRequestDescription": "Por quanto tempo manter os registros de pedidos",
|
"logRetentionRequestDescription": "Por quanto tempo manter os registros de pedidos",
|
||||||
"logRetentionAccessLabel": "Retenção de Log de Acesso",
|
"logRetentionAccessLabel": "Retenção de Log de Acesso",
|
||||||
"logRetentionAccessDescription": "Por quanto tempo manter os registros de acesso",
|
"logRetentionAccessDescription": "Por quanto tempo manter os registros de acesso",
|
||||||
@@ -3134,7 +3134,7 @@
|
|||||||
"httpDestActionLogsDescription": "Ações administrativas realizadas por usuários dentro da organização.",
|
"httpDestActionLogsDescription": "Ações administrativas realizadas por usuários dentro da organização.",
|
||||||
"httpDestConnectionLogsTitle": "Logs da conexão",
|
"httpDestConnectionLogsTitle": "Logs da conexão",
|
||||||
"httpDestConnectionLogsDescription": "Eventos de conexão de site e túnel, incluindo conexões e desconexões.",
|
"httpDestConnectionLogsDescription": "Eventos de conexão de site e túnel, incluindo conexões e desconexões.",
|
||||||
"httpDestRequestLogsTitle": "Registros de Pedidos HTTP",
|
"httpDestRequestLogsTitle": "Registro de pedidos",
|
||||||
"httpDestRequestLogsDescription": "Logs de solicitação HTTP para recursos proxy incluindo o método, o caminho e o código de resposta.",
|
"httpDestRequestLogsDescription": "Logs de solicitação HTTP para recursos proxy incluindo o método, o caminho e o código de resposta.",
|
||||||
"httpDestSaveChanges": "Salvar as alterações",
|
"httpDestSaveChanges": "Salvar as alterações",
|
||||||
"httpDestCreateDestination": "Criar destino",
|
"httpDestCreateDestination": "Criar destino",
|
||||||
@@ -3208,48 +3208,5 @@
|
|||||||
"domainPickerWildcardCertWarning": "Recursos curinga podem exigir configurações adicionais para funcionarem corretamente.",
|
"domainPickerWildcardCertWarning": "Recursos curinga podem exigir configurações adicionais para funcionarem corretamente.",
|
||||||
"domainPickerWildcardCertWarningLink": "Saiba mais",
|
"domainPickerWildcardCertWarningLink": "Saiba mais",
|
||||||
"health": "Saúde",
|
"health": "Saúde",
|
||||||
"domainPendingErrorTitle": "Problema de Verificação",
|
"domainPendingErrorTitle": "Problema de Verificação"
|
||||||
"memberPortalTitle": "Recursos",
|
|
||||||
"memberPortalDescription": "Recursos aos quais você tem acesso nesta organização",
|
|
||||||
"memberPortalSortBy": "Ordenar por...",
|
|
||||||
"memberPortalSortNameAsc": "Nome A-Z",
|
|
||||||
"memberPortalSortNameDesc": "Nome Z-A",
|
|
||||||
"memberPortalSortDomainAsc": "Domínio A-Z",
|
|
||||||
"memberPortalSortDomainDesc": "Domínio Z-A",
|
|
||||||
"memberPortalSortEnabledFirst": "Habilitados Primeiro",
|
|
||||||
"memberPortalSortDisabledFirst": "Desabilitados Primeiro",
|
|
||||||
"memberPortalRefresh": "Atualizar",
|
|
||||||
"memberPortalRefreshResources": "Atualizar Recursos",
|
|
||||||
"memberPortalFailedToLoad": "Falha ao carregar recursos",
|
|
||||||
"memberPortalFailedToLoadDescription": "Falha ao carregar recursos. Por favor, verifique sua conexão e tente novamente.",
|
|
||||||
"memberPortalUnableToLoad": "Incapaz de Carregar Recursos",
|
|
||||||
"memberPortalTryAgain": "Tentar Novamente",
|
|
||||||
"memberPortalNoResourcesFound": "Nenhum Recurso Encontrado",
|
|
||||||
"memberPortalNoResourcesAvailable": "Nenhum Recurso Disponível",
|
|
||||||
"memberPortalNoResourcesMatchSearch": "Nenhum recurso corresponde a \"{query}\". Tente ajustar seus termos de pesquisa ou limpe a pesquisa para ver todos os recursos.",
|
|
||||||
"memberPortalNoResourcesAccess": "Você ainda não tem acesso a nenhum recurso. Entre em contato com seu administrador para obter acesso aos recursos que precisa.",
|
|
||||||
"memberPortalClearSearch": "Limpar Pesquisa",
|
|
||||||
"memberPortalPublicResources": "Recursos Públicos",
|
|
||||||
"memberPortalPublicResourcesDescription": "Aplicações e serviços web acessíveis via navegador",
|
|
||||||
"memberPortalCopiedToClipboard": "Copiado para a área de transferência",
|
|
||||||
"memberPortalCopiedUrlDescription": "A URL do recurso foi copiada para sua área de transferência.",
|
|
||||||
"memberPortalOpenResource": "Abrir Recurso",
|
|
||||||
"memberPortalPrivateResources": "Recursos Privados",
|
|
||||||
"memberPortalPrivateResourcesDescription": "Recursos da rede interna acessíveis via cliente",
|
|
||||||
"memberPortalResourceDetails": "Detalhes do Recurso",
|
|
||||||
"memberPortalMode": "Modo",
|
|
||||||
"memberPortalDestination": "Destino",
|
|
||||||
"memberPortalAlias": "Apelido",
|
|
||||||
"memberPortalCopiedAliasDescription": "O apelido do recurso foi copiado para sua área de transferência.",
|
|
||||||
"memberPortalCopiedDestinationDescription": "O destino do recurso foi copiado para sua área de transferência.",
|
|
||||||
"memberPortalRequiresClientConnection": "Requer Conexão de Cliente",
|
|
||||||
"memberPortalAuthMethods": "Métodos de Autenticação",
|
|
||||||
"memberPortalSso": "Logon Único (SSO)",
|
|
||||||
"memberPortalPasswordProtected": "Protegido por Senha",
|
|
||||||
"memberPortalPinCode": "Código PIN",
|
|
||||||
"memberPortalEmailWhitelist": "Lista de E-mails Permitidos",
|
|
||||||
"memberPortalResourceDisabled": "Recurso Desativado",
|
|
||||||
"memberPortalShowingResources": "Mostrando {start}-{end} de {total} recursos",
|
|
||||||
"memberPortalPrevious": "Anterior",
|
|
||||||
"memberPortalNext": "Próximo"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2660,19 +2660,19 @@
|
|||||||
"noMoreAuthMethods": "No Valid Auth",
|
"noMoreAuthMethods": "No Valid Auth",
|
||||||
"ip": "IP",
|
"ip": "IP",
|
||||||
"reason": "Причина",
|
"reason": "Причина",
|
||||||
"requestLogs": "HTTP Запросы Логи",
|
"requestLogs": "Запросить журналы",
|
||||||
"requestAnalytics": "Аналитика запроса",
|
"requestAnalytics": "Аналитика запроса",
|
||||||
"host": "Хост",
|
"host": "Хост",
|
||||||
"location": "Местоположение",
|
"location": "Местоположение",
|
||||||
"actionLogs": "Журнал действий",
|
"actionLogs": "Журнал действий",
|
||||||
"sidebarLogsRequest": "HTTP Запросы Логи",
|
"sidebarLogsRequest": "Запросить журналы",
|
||||||
"sidebarLogsAccess": "Журналы доступа",
|
"sidebarLogsAccess": "Журналы доступа",
|
||||||
"sidebarLogsAction": "Журнал действий",
|
"sidebarLogsAction": "Журнал действий",
|
||||||
"logRetention": "Сохранение журнала",
|
"logRetention": "Сохранение журнала",
|
||||||
"logRetentionDescription": "Управление сохранением различных типов журналов для этой организации или отключение их",
|
"logRetentionDescription": "Управление сохранением различных типов журналов для этой организации или отключение их",
|
||||||
"requestLogsDescription": "Просмотреть подробные журналы запроса ресурсов в этой организации",
|
"requestLogsDescription": "Просмотреть подробные журналы запроса ресурсов в этой организации",
|
||||||
"requestAnalyticsDescription": "Просмотреть подробную аналитику запроса для ресурсов в этой организации",
|
"requestAnalyticsDescription": "Просмотреть подробную аналитику запроса для ресурсов в этой организации",
|
||||||
"logRetentionRequestLabel": "Сохранение HTTP Запросов Лога",
|
"logRetentionRequestLabel": "Запросить сохранение журнала",
|
||||||
"logRetentionRequestDescription": "Как долго сохранять журналы запросов",
|
"logRetentionRequestDescription": "Как долго сохранять журналы запросов",
|
||||||
"logRetentionAccessLabel": "Хранение журнала доступа",
|
"logRetentionAccessLabel": "Хранение журнала доступа",
|
||||||
"logRetentionAccessDescription": "Как долго сохранять журналы доступа",
|
"logRetentionAccessDescription": "Как долго сохранять журналы доступа",
|
||||||
@@ -3134,7 +3134,7 @@
|
|||||||
"httpDestActionLogsDescription": "Административные меры, осуществляемые пользователями в рамках организации.",
|
"httpDestActionLogsDescription": "Административные меры, осуществляемые пользователями в рамках организации.",
|
||||||
"httpDestConnectionLogsTitle": "Журнал подключений",
|
"httpDestConnectionLogsTitle": "Журнал подключений",
|
||||||
"httpDestConnectionLogsDescription": "События связи с сайтами и туннелями, включая соединения и отключения.",
|
"httpDestConnectionLogsDescription": "События связи с сайтами и туннелями, включая соединения и отключения.",
|
||||||
"httpDestRequestLogsTitle": "HTTP Запросы Логи",
|
"httpDestRequestLogsTitle": "Запросить журналы",
|
||||||
"httpDestRequestLogsDescription": "Журналы запросов HTTP для проксируемых ресурсов, включая метод, путь и код ответа.",
|
"httpDestRequestLogsDescription": "Журналы запросов HTTP для проксируемых ресурсов, включая метод, путь и код ответа.",
|
||||||
"httpDestSaveChanges": "Сохранить изменения",
|
"httpDestSaveChanges": "Сохранить изменения",
|
||||||
"httpDestCreateDestination": "Создать адрес назначения",
|
"httpDestCreateDestination": "Создать адрес назначения",
|
||||||
@@ -3208,48 +3208,5 @@
|
|||||||
"domainPickerWildcardCertWarning": "Wildcard ресурсы могут потребовать дополнительной настройки для правильной работы.",
|
"domainPickerWildcardCertWarning": "Wildcard ресурсы могут потребовать дополнительной настройки для правильной работы.",
|
||||||
"domainPickerWildcardCertWarningLink": "Узнать больше",
|
"domainPickerWildcardCertWarningLink": "Узнать больше",
|
||||||
"health": "Состояние",
|
"health": "Состояние",
|
||||||
"domainPendingErrorTitle": "Проблема с подтверждением",
|
"domainPendingErrorTitle": "Проблема с подтверждением"
|
||||||
"memberPortalTitle": "Ресурсы",
|
|
||||||
"memberPortalDescription": "Ресурсы, к которым у вас есть доступ в этой организации",
|
|
||||||
"memberPortalSortBy": "Сортировать по...",
|
|
||||||
"memberPortalSortNameAsc": "Имя A-Я",
|
|
||||||
"memberPortalSortNameDesc": "Имя Я-A",
|
|
||||||
"memberPortalSortDomainAsc": "Домен A-Я",
|
|
||||||
"memberPortalSortDomainDesc": "Домен Я-A",
|
|
||||||
"memberPortalSortEnabledFirst": "Включённые сначала",
|
|
||||||
"memberPortalSortDisabledFirst": "Отключённые сначала",
|
|
||||||
"memberPortalRefresh": "Обновить",
|
|
||||||
"memberPortalRefreshResources": "Обновить ресурсы",
|
|
||||||
"memberPortalFailedToLoad": "Не удалось загрузить ресурсы",
|
|
||||||
"memberPortalFailedToLoadDescription": "Не удалось загрузить ресурсы. Пожалуйста, проверьте подключение и попробуйте снова.",
|
|
||||||
"memberPortalUnableToLoad": "Не удалось загрузить ресурсы",
|
|
||||||
"memberPortalTryAgain": "Попробуйте снова",
|
|
||||||
"memberPortalNoResourcesFound": "Ресурсы не найдены",
|
|
||||||
"memberPortalNoResourcesAvailable": "Нет доступных ресурсов",
|
|
||||||
"memberPortalNoResourcesMatchSearch": "Нет ресурсов, соответствующих \"{query}\". Попробуйте изменить условия поиска или очистить поиск, чтобы увидеть все ресурсы.",
|
|
||||||
"memberPortalNoResourcesAccess": "У вас пока нет доступа к ресурсам. Свяжитесь с администратором, чтобы получить доступ к нужным вам ресурсам.",
|
|
||||||
"memberPortalClearSearch": "Очистить поиск",
|
|
||||||
"memberPortalPublicResources": "Публичные ресурсы",
|
|
||||||
"memberPortalPublicResourcesDescription": "Веб-приложения и сервисы, доступные через браузер",
|
|
||||||
"memberPortalCopiedToClipboard": "Скопировано в буфер обмена",
|
|
||||||
"memberPortalCopiedUrlDescription": "URL ресурса был скопирован в ваш буфер обмена.",
|
|
||||||
"memberPortalOpenResource": "Открыть ресурс",
|
|
||||||
"memberPortalPrivateResources": "Приватные ресурсы",
|
|
||||||
"memberPortalPrivateResourcesDescription": "Ресурсы внутренней сети, доступные через клиент",
|
|
||||||
"memberPortalResourceDetails": "Детали ресурса",
|
|
||||||
"memberPortalMode": "Режим",
|
|
||||||
"memberPortalDestination": "Назначение",
|
|
||||||
"memberPortalAlias": "Псевдоним",
|
|
||||||
"memberPortalCopiedAliasDescription": "Псевдоним ресурса был скопирован в ваш буфер обмена.",
|
|
||||||
"memberPortalCopiedDestinationDescription": "Назначение ресурса было скопировано в ваш буфер обмена.",
|
|
||||||
"memberPortalRequiresClientConnection": "Требуется подключение клиента",
|
|
||||||
"memberPortalAuthMethods": "Методы аутентификации",
|
|
||||||
"memberPortalSso": "Единый вход (SSO)",
|
|
||||||
"memberPortalPasswordProtected": "Защищено паролем",
|
|
||||||
"memberPortalPinCode": "PIN-код",
|
|
||||||
"memberPortalEmailWhitelist": "Белый список email",
|
|
||||||
"memberPortalResourceDisabled": "Ресурс отключён",
|
|
||||||
"memberPortalShowingResources": "Показаны {start}-{end} из {total} ресурсов",
|
|
||||||
"memberPortalPrevious": "Предыдущий",
|
|
||||||
"memberPortalNext": "Следующий"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2660,19 +2660,19 @@
|
|||||||
"noMoreAuthMethods": "Daha Fazla Kimlik Doğrulama Yöntemi Yok",
|
"noMoreAuthMethods": "Daha Fazla Kimlik Doğrulama Yöntemi Yok",
|
||||||
"ip": "IP",
|
"ip": "IP",
|
||||||
"reason": "Sebep",
|
"reason": "Sebep",
|
||||||
"requestLogs": "HTTP İstek Günlükleri",
|
"requestLogs": "İstek Günlükleri",
|
||||||
"requestAnalytics": "İstek Analizi",
|
"requestAnalytics": "İstek Analizi",
|
||||||
"host": "Sunucu",
|
"host": "Sunucu",
|
||||||
"location": "Konum",
|
"location": "Konum",
|
||||||
"actionLogs": "Eylem Günlükleri",
|
"actionLogs": "Eylem Günlükleri",
|
||||||
"sidebarLogsRequest": "HTTP İstek Günlükleri",
|
"sidebarLogsRequest": "İstek Günlükleri",
|
||||||
"sidebarLogsAccess": "Erişim Günlükleri",
|
"sidebarLogsAccess": "Erişim Günlükleri",
|
||||||
"sidebarLogsAction": "Eylem Günlükleri",
|
"sidebarLogsAction": "Eylem Günlükleri",
|
||||||
"logRetention": "Kayıt Saklama",
|
"logRetention": "Kayıt Saklama",
|
||||||
"logRetentionDescription": "Bu organizasyon için farklı türdeki günlüklerin ne kadar süre saklanacağını yönetin veya devre dışı bırakın",
|
"logRetentionDescription": "Bu organizasyon için farklı türdeki günlüklerin ne kadar süre saklanacağını yönetin veya devre dışı bırakın",
|
||||||
"requestLogsDescription": "Bu organizasyondaki kaynaklar için ayrıntılı istek günlüklerini görüntüleyin",
|
"requestLogsDescription": "Bu organizasyondaki kaynaklar için ayrıntılı istek günlüklerini görüntüleyin",
|
||||||
"requestAnalyticsDescription": "Bu organizasyondaki kaynaklar için ayrıntılı istek analizlerini görüntüleyin.",
|
"requestAnalyticsDescription": "Bu organizasyondaki kaynaklar için ayrıntılı istek analizlerini görüntüleyin.",
|
||||||
"logRetentionRequestLabel": "HTTP İstek Günlüğü Saklama",
|
"logRetentionRequestLabel": "İstek Günlüğü Saklama",
|
||||||
"logRetentionRequestDescription": "İstek günlüklerini ne kadar süre tutacağını belirle",
|
"logRetentionRequestDescription": "İstek günlüklerini ne kadar süre tutacağını belirle",
|
||||||
"logRetentionAccessLabel": "Erişim Günlüğü Saklama",
|
"logRetentionAccessLabel": "Erişim Günlüğü Saklama",
|
||||||
"logRetentionAccessDescription": "Erişim günlüklerini ne kadar süre tutacağını belirle",
|
"logRetentionAccessDescription": "Erişim günlüklerini ne kadar süre tutacağını belirle",
|
||||||
@@ -3134,7 +3134,7 @@
|
|||||||
"httpDestActionLogsDescription": "Kullanıcılar tarafından organizasyon içerisinde yapılan yönetici eylemleri.",
|
"httpDestActionLogsDescription": "Kullanıcılar tarafından organizasyon içerisinde yapılan yönetici eylemleri.",
|
||||||
"httpDestConnectionLogsTitle": "Bağlantı Kayıtları",
|
"httpDestConnectionLogsTitle": "Bağlantı Kayıtları",
|
||||||
"httpDestConnectionLogsDescription": "Site ve tünel bağlantı olayları, bağlantılar ve bağlantı kesilmeleri dahil.",
|
"httpDestConnectionLogsDescription": "Site ve tünel bağlantı olayları, bağlantılar ve bağlantı kesilmeleri dahil.",
|
||||||
"httpDestRequestLogsTitle": "HTTP İstek Günlükleri",
|
"httpDestRequestLogsTitle": "İstek Kayıtları",
|
||||||
"httpDestRequestLogsDescription": "Yönlendirilmiş kaynaklar için HTTP istek kayıtları, yöntem, yol ve yanıt kodu dahil.",
|
"httpDestRequestLogsDescription": "Yönlendirilmiş kaynaklar için HTTP istek kayıtları, yöntem, yol ve yanıt kodu dahil.",
|
||||||
"httpDestSaveChanges": "Değişiklikleri Kaydet",
|
"httpDestSaveChanges": "Değişiklikleri Kaydet",
|
||||||
"httpDestCreateDestination": "Hedef Oluştur",
|
"httpDestCreateDestination": "Hedef Oluştur",
|
||||||
@@ -3208,48 +3208,5 @@
|
|||||||
"domainPickerWildcardCertWarning": "Genel kaynaklar düzgün çalışmak için ek yapılandırma gerektirebilir.",
|
"domainPickerWildcardCertWarning": "Genel kaynaklar düzgün çalışmak için ek yapılandırma gerektirebilir.",
|
||||||
"domainPickerWildcardCertWarningLink": "Daha fazla bilgi",
|
"domainPickerWildcardCertWarningLink": "Daha fazla bilgi",
|
||||||
"health": "Sağlık",
|
"health": "Sağlık",
|
||||||
"domainPendingErrorTitle": "Doğrulama Sorunu",
|
"domainPendingErrorTitle": "Doğrulama Sorunu"
|
||||||
"memberPortalTitle": "Kaynaklar",
|
|
||||||
"memberPortalDescription": "Bu organizasyondaki erişiminiz olan kaynaklar",
|
|
||||||
"memberPortalSortBy": "Şuna göre sırala...",
|
|
||||||
"memberPortalSortNameAsc": "İsim A-Z",
|
|
||||||
"memberPortalSortNameDesc": "İsim Z-A",
|
|
||||||
"memberPortalSortDomainAsc": "Alan A-Z",
|
|
||||||
"memberPortalSortDomainDesc": "Alan Z-A",
|
|
||||||
"memberPortalSortEnabledFirst": "İlk Etkinleştirilenler",
|
|
||||||
"memberPortalSortDisabledFirst": "İlk Devre Dışı Bırakılanlar",
|
|
||||||
"memberPortalRefresh": "Yenile",
|
|
||||||
"memberPortalRefreshResources": "Kaynakları Yenile",
|
|
||||||
"memberPortalFailedToLoad": "Kaynaklar yüklenemedi",
|
|
||||||
"memberPortalFailedToLoadDescription": "Kaynaklar yüklenemedi. Lütfen bağlantınızı kontrol edin ve tekrar deneyin.",
|
|
||||||
"memberPortalUnableToLoad": "Kaynaklar Yüklenemiyor",
|
|
||||||
"memberPortalTryAgain": "Tekrar Dene",
|
|
||||||
"memberPortalNoResourcesFound": "Hiçbir Kaynak Bulunamadı",
|
|
||||||
"memberPortalNoResourcesAvailable": "Uygun Kaynak Yok",
|
|
||||||
"memberPortalNoResourcesMatchSearch": "Hiçbir kaynak \"{query}\" ile eşleşmiyor. Arama terimlerinizi değiştirerek veya tüm kaynakları görmek için aramayı temizleyerek deneyin.",
|
|
||||||
"memberPortalNoResourcesAccess": "Henüz herhangi bir kaynağa erişiminiz yok. İhtiyacınız olan kaynaklara erişim sağlamak için yöneticinizle iletişime geçin.",
|
|
||||||
"memberPortalClearSearch": "Aramayı Temizle",
|
|
||||||
"memberPortalPublicResources": "Genel Kaynaklar",
|
|
||||||
"memberPortalPublicResourcesDescription": "Tarayıcı üzerinden erişilebilen web uygulamaları ve hizmetler",
|
|
||||||
"memberPortalCopiedToClipboard": "Panoya kopyalandı",
|
|
||||||
"memberPortalCopiedUrlDescription": "Kaynak URL'si panonuza kopyalandı.",
|
|
||||||
"memberPortalOpenResource": "Kaynağı Aç",
|
|
||||||
"memberPortalPrivateResources": "Özel Kaynaklar",
|
|
||||||
"memberPortalPrivateResourcesDescription": "İstemci üzerinden erişilebilen dahili ağ kaynakları",
|
|
||||||
"memberPortalResourceDetails": "Kaynak Detayları",
|
|
||||||
"memberPortalMode": "Mod",
|
|
||||||
"memberPortalDestination": "Hedef",
|
|
||||||
"memberPortalAlias": "Takma İsim",
|
|
||||||
"memberPortalCopiedAliasDescription": "Kaynak takma adı panonuza kopyalandı.",
|
|
||||||
"memberPortalCopiedDestinationDescription": "Kaynak hedefi panonuza kopyalandı.",
|
|
||||||
"memberPortalRequiresClientConnection": "İstemci Bağlantısı Gerektirir",
|
|
||||||
"memberPortalAuthMethods": "Kimlik Doğrulama Yöntemleri",
|
|
||||||
"memberPortalSso": "Tek Oturum Açma (SSO)",
|
|
||||||
"memberPortalPasswordProtected": "Parola ile Korunan",
|
|
||||||
"memberPortalPinCode": "PIN Kodu",
|
|
||||||
"memberPortalEmailWhitelist": "E-posta Beyaz Listesi",
|
|
||||||
"memberPortalResourceDisabled": "Kaynak Devre Dışı",
|
|
||||||
"memberPortalShowingResources": "{total} kaynaktan {start}-{end} gösteriliyor",
|
|
||||||
"memberPortalPrevious": "Önceki",
|
|
||||||
"memberPortalNext": "Sonraki"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2672,7 +2672,7 @@
|
|||||||
"logRetentionDescription": "管理不同类型的日志为这个机构保留多长时间或禁用这些日志",
|
"logRetentionDescription": "管理不同类型的日志为这个机构保留多长时间或禁用这些日志",
|
||||||
"requestLogsDescription": "查看此机构资源的详细请求日志",
|
"requestLogsDescription": "查看此机构资源的详细请求日志",
|
||||||
"requestAnalyticsDescription": "查看此机构资源的详细请求分析",
|
"requestAnalyticsDescription": "查看此机构资源的详细请求分析",
|
||||||
"logRetentionRequestLabel": "HTTP 请求日志保留",
|
"logRetentionRequestLabel": "请求日志保留",
|
||||||
"logRetentionRequestDescription": "保留请求日志的时间",
|
"logRetentionRequestDescription": "保留请求日志的时间",
|
||||||
"logRetentionAccessLabel": "访问日志保留",
|
"logRetentionAccessLabel": "访问日志保留",
|
||||||
"logRetentionAccessDescription": "保留访问日志的时间",
|
"logRetentionAccessDescription": "保留访问日志的时间",
|
||||||
@@ -3208,48 +3208,5 @@
|
|||||||
"domainPickerWildcardCertWarning": "通配符资源可能需要额外配置才能正常工作。",
|
"domainPickerWildcardCertWarning": "通配符资源可能需要额外配置才能正常工作。",
|
||||||
"domainPickerWildcardCertWarningLink": "了解更多",
|
"domainPickerWildcardCertWarningLink": "了解更多",
|
||||||
"health": "健康",
|
"health": "健康",
|
||||||
"domainPendingErrorTitle": "验证问题",
|
"domainPendingErrorTitle": "验证问题"
|
||||||
"memberPortalTitle": "资源",
|
|
||||||
"memberPortalDescription": "您在此组织中可以访问的资源",
|
|
||||||
"memberPortalSortBy": "排序依据……",
|
|
||||||
"memberPortalSortNameAsc": "名称 A-Z",
|
|
||||||
"memberPortalSortNameDesc": "名称 Z-A",
|
|
||||||
"memberPortalSortDomainAsc": "域名 A-Z",
|
|
||||||
"memberPortalSortDomainDesc": "域名 Z-A",
|
|
||||||
"memberPortalSortEnabledFirst": "启用优先",
|
|
||||||
"memberPortalSortDisabledFirst": "禁用优先",
|
|
||||||
"memberPortalRefresh": "刷新",
|
|
||||||
"memberPortalRefreshResources": "刷新资源",
|
|
||||||
"memberPortalFailedToLoad": "加载资源失败",
|
|
||||||
"memberPortalFailedToLoadDescription": "加载资源失败。请检查您的连接并再试一次。",
|
|
||||||
"memberPortalUnableToLoad": "无法加载资源",
|
|
||||||
"memberPortalTryAgain": "再试一次",
|
|
||||||
"memberPortalNoResourcesFound": "找不到资源",
|
|
||||||
"memberPortalNoResourcesAvailable": "无可用资源",
|
|
||||||
"memberPortalNoResourcesMatchSearch": "没有与\"{query}\"匹配的资源。尝试调整您的搜索词或清除搜索以查看所有资源。",
|
|
||||||
"memberPortalNoResourcesAccess": "您尚无访问任何资源的权限。请联系您的管理员获取所需资源的访问权限。",
|
|
||||||
"memberPortalClearSearch": "清除搜索",
|
|
||||||
"memberPortalPublicResources": "公共资源",
|
|
||||||
"memberPortalPublicResourcesDescription": "通过浏览器可访问的网络应用和服务",
|
|
||||||
"memberPortalCopiedToClipboard": "已复制到剪贴板",
|
|
||||||
"memberPortalCopiedUrlDescription": "资源 URL 已复制到您的剪贴板。",
|
|
||||||
"memberPortalOpenResource": "打开资源",
|
|
||||||
"memberPortalPrivateResources": "私有资源",
|
|
||||||
"memberPortalPrivateResourcesDescription": "通过客户端可访问的内部网络资源",
|
|
||||||
"memberPortalResourceDetails": "资源详情",
|
|
||||||
"memberPortalMode": "模式",
|
|
||||||
"memberPortalDestination": "目标",
|
|
||||||
"memberPortalAlias": "别名",
|
|
||||||
"memberPortalCopiedAliasDescription": "资源别名已复制到您的剪贴板。",
|
|
||||||
"memberPortalCopiedDestinationDescription": "资源目的地已复制到您的剪贴板。",
|
|
||||||
"memberPortalRequiresClientConnection": "需要客户端连接",
|
|
||||||
"memberPortalAuthMethods": "身份验证方法",
|
|
||||||
"memberPortalSso": "单一登录 (SSO)",
|
|
||||||
"memberPortalPasswordProtected": "密码保护",
|
|
||||||
"memberPortalPinCode": "PIN 码",
|
|
||||||
"memberPortalEmailWhitelist": "电子邮件白名单",
|
|
||||||
"memberPortalResourceDisabled": "资源已禁用",
|
|
||||||
"memberPortalShowingResources": "显示 {start}-{end} 共 {total} 个资源",
|
|
||||||
"memberPortalPrevious": "上一页",
|
|
||||||
"memberPortalNext": "下一页"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -361,7 +361,7 @@ export async function updateClientResources(
|
|||||||
} else {
|
} else {
|
||||||
let aliasAddress: string | null = null;
|
let aliasAddress: string | null = null;
|
||||||
if (resourceData.mode === "host" || resourceData.mode === "http") {
|
if (resourceData.mode === "host" || resourceData.mode === "http") {
|
||||||
aliasAddress = await getNextAvailableAliasAddress(orgId, trx);
|
aliasAddress = await getNextAvailableAliasAddress(orgId);
|
||||||
}
|
}
|
||||||
|
|
||||||
let domainInfo:
|
let domainInfo:
|
||||||
|
|||||||
@@ -28,159 +28,6 @@ export async function calculateUserClientsForOrgs(
|
|||||||
trx?: Transaction
|
trx?: Transaction
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const execute = async (transaction: Transaction) => {
|
const execute = async (transaction: Transaction) => {
|
||||||
const orgCache = new Map<string, typeof orgs.$inferSelect | null>();
|
|
||||||
const adminRoleCache = new Map<
|
|
||||||
string,
|
|
||||||
typeof roles.$inferSelect | null
|
|
||||||
>();
|
|
||||||
const exitNodesCache = new Map<
|
|
||||||
string,
|
|
||||||
Awaited<ReturnType<typeof listExitNodes>>
|
|
||||||
>();
|
|
||||||
const isOrgLicensedCache = new Map<string, boolean>();
|
|
||||||
const existingClientCache = new Map<
|
|
||||||
string,
|
|
||||||
typeof clients.$inferSelect | null
|
|
||||||
>();
|
|
||||||
const roleClientAccessCache = new Map<string, boolean>();
|
|
||||||
const userClientAccessCache = new Map<string, boolean>();
|
|
||||||
|
|
||||||
const getOrgOlmKey = (orgId: string, olmId: string) =>
|
|
||||||
`${orgId}:${olmId}`;
|
|
||||||
const getRoleClientKey = (roleId: number, clientId: number) =>
|
|
||||||
`${roleId}:${clientId}`;
|
|
||||||
const getUserClientKey = (cachedUserId: string, clientId: number) =>
|
|
||||||
`${cachedUserId}:${clientId}`;
|
|
||||||
|
|
||||||
const getOrg = async (orgId: string) => {
|
|
||||||
if (orgCache.has(orgId)) {
|
|
||||||
return orgCache.get(orgId) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [org] = await transaction
|
|
||||||
.select()
|
|
||||||
.from(orgs)
|
|
||||||
.where(eq(orgs.orgId, orgId));
|
|
||||||
orgCache.set(orgId, org ?? null);
|
|
||||||
|
|
||||||
return org ?? null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAdminRole = async (orgId: string) => {
|
|
||||||
if (adminRoleCache.has(orgId)) {
|
|
||||||
return adminRoleCache.get(orgId) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [adminRole] = await transaction
|
|
||||||
.select()
|
|
||||||
.from(roles)
|
|
||||||
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)))
|
|
||||||
.limit(1);
|
|
||||||
adminRoleCache.set(orgId, adminRole ?? null);
|
|
||||||
|
|
||||||
return adminRole ?? null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getExitNodes = async (orgId: string) => {
|
|
||||||
if (exitNodesCache.has(orgId)) {
|
|
||||||
return exitNodesCache.get(orgId)!;
|
|
||||||
}
|
|
||||||
|
|
||||||
const exitNodes = await listExitNodes(orgId);
|
|
||||||
exitNodesCache.set(orgId, exitNodes);
|
|
||||||
|
|
||||||
return exitNodes;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getIsOrgLicensed = async (orgId: string) => {
|
|
||||||
if (isOrgLicensedCache.has(orgId)) {
|
|
||||||
return isOrgLicensedCache.get(orgId)!;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isOrgLicensed = await isLicensedOrSubscribed(
|
|
||||||
orgId,
|
|
||||||
tierMatrix.deviceApprovals
|
|
||||||
);
|
|
||||||
isOrgLicensedCache.set(orgId, isOrgLicensed);
|
|
||||||
|
|
||||||
return isOrgLicensed;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getExistingClient = async (orgId: string, olmId: string) => {
|
|
||||||
const key = getOrgOlmKey(orgId, olmId);
|
|
||||||
if (existingClientCache.has(key)) {
|
|
||||||
return existingClientCache.get(key) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [existingClient] = await transaction
|
|
||||||
.select()
|
|
||||||
.from(clients)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(clients.userId, userId),
|
|
||||||
eq(clients.orgId, orgId),
|
|
||||||
eq(clients.olmId, olmId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
existingClientCache.set(key, existingClient ?? null);
|
|
||||||
|
|
||||||
return existingClient ?? null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasRoleClientAccess = async (
|
|
||||||
roleId: number,
|
|
||||||
clientId: number
|
|
||||||
) => {
|
|
||||||
const key = getRoleClientKey(roleId, clientId);
|
|
||||||
if (roleClientAccessCache.has(key)) {
|
|
||||||
return roleClientAccessCache.get(key)!;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [existingRoleClient] = await transaction
|
|
||||||
.select()
|
|
||||||
.from(roleClients)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(roleClients.roleId, roleId),
|
|
||||||
eq(roleClients.clientId, clientId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
const hasAccess = Boolean(existingRoleClient);
|
|
||||||
roleClientAccessCache.set(key, hasAccess);
|
|
||||||
|
|
||||||
return hasAccess;
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasUserClientAccess = async (
|
|
||||||
cachedUserId: string,
|
|
||||||
clientId: number
|
|
||||||
) => {
|
|
||||||
const key = getUserClientKey(cachedUserId, clientId);
|
|
||||||
if (userClientAccessCache.has(key)) {
|
|
||||||
return userClientAccessCache.get(key)!;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [existingUserClient] = await transaction
|
|
||||||
.select()
|
|
||||||
.from(userClients)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(userClients.userId, cachedUserId),
|
|
||||||
eq(userClients.clientId, clientId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
const hasAccess = Boolean(existingUserClient);
|
|
||||||
userClientAccessCache.set(key, hasAccess);
|
|
||||||
|
|
||||||
return hasAccess;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get all OLMs for this user
|
// Get all OLMs for this user
|
||||||
const userOlms = await transaction
|
const userOlms = await transaction
|
||||||
.select()
|
.select()
|
||||||
@@ -207,9 +54,7 @@ export async function calculateUserClientsForOrgs(
|
|||||||
.innerJoin(roles, eq(userOrgRoles.roleId, roles.roleId))
|
.innerJoin(roles, eq(userOrgRoles.roleId, roles.roleId))
|
||||||
.where(eq(userOrgs.userId, userId));
|
.where(eq(userOrgs.userId, userId));
|
||||||
|
|
||||||
const userOrgIds = [
|
const userOrgIds = [...new Set(userOrgRoleRows.map((r) => r.userOrgs.orgId))];
|
||||||
...new Set(userOrgRoleRows.map((r) => r.userOrgs.orgId))
|
|
||||||
];
|
|
||||||
const orgIdToRoleRows = new Map<
|
const orgIdToRoleRows = new Map<
|
||||||
string,
|
string,
|
||||||
(typeof userOrgRoleRows)[0][]
|
(typeof userOrgRoleRows)[0][]
|
||||||
@@ -219,13 +64,6 @@ export async function calculateUserClientsForOrgs(
|
|||||||
list.push(r);
|
list.push(r);
|
||||||
orgIdToRoleRows.set(r.userOrgs.orgId, list);
|
orgIdToRoleRows.set(r.userOrgs.orgId, list);
|
||||||
}
|
}
|
||||||
const orgRequiresDeviceApprovalRole = new Map<string, boolean>();
|
|
||||||
for (const [orgId, roleRowsForOrg] of orgIdToRoleRows.entries()) {
|
|
||||||
orgRequiresDeviceApprovalRole.set(
|
|
||||||
orgId,
|
|
||||||
roleRowsForOrg.some((r) => r.roles.requireDeviceApproval)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// For each OLM, ensure there's a client in each org the user is in
|
// For each OLM, ensure there's a client in each org the user is in
|
||||||
for (const olm of userOlms) {
|
for (const olm of userOlms) {
|
||||||
@@ -233,7 +71,10 @@ export async function calculateUserClientsForOrgs(
|
|||||||
const roleRowsForOrg = orgIdToRoleRows.get(orgId)!;
|
const roleRowsForOrg = orgIdToRoleRows.get(orgId)!;
|
||||||
const userOrg = roleRowsForOrg[0].userOrgs;
|
const userOrg = roleRowsForOrg[0].userOrgs;
|
||||||
|
|
||||||
const org = await getOrg(orgId);
|
const [org] = await transaction
|
||||||
|
.select()
|
||||||
|
.from(orgs)
|
||||||
|
.where(eq(orgs.orgId, orgId));
|
||||||
|
|
||||||
if (!org) {
|
if (!org) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
@@ -250,7 +91,11 @@ export async function calculateUserClientsForOrgs(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get admin role for this org (needed for access grants)
|
// Get admin role for this org (needed for access grants)
|
||||||
const adminRole = await getAdminRole(orgId);
|
const [adminRole] = await transaction
|
||||||
|
.select()
|
||||||
|
.from(roles)
|
||||||
|
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
if (!adminRole) {
|
if (!adminRole) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
@@ -260,50 +105,64 @@ export async function calculateUserClientsForOrgs(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if a client already exists for this OLM+user+org combination
|
// Check if a client already exists for this OLM+user+org combination
|
||||||
const existingClient = await getExistingClient(
|
const [existingClient] = await transaction
|
||||||
orgId,
|
.select()
|
||||||
olm.olmId
|
.from(clients)
|
||||||
);
|
.where(
|
||||||
|
and(
|
||||||
|
eq(clients.userId, userId),
|
||||||
|
eq(clients.orgId, orgId),
|
||||||
|
eq(clients.olmId, olm.olmId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
if (existingClient) {
|
if (existingClient) {
|
||||||
// Ensure admin role has access to the client
|
// Ensure admin role has access to the client
|
||||||
const hasRoleAccess = await hasRoleClientAccess(
|
const [existingRoleClient] = await transaction
|
||||||
adminRole.roleId,
|
.select()
|
||||||
existingClient.clientId
|
.from(roleClients)
|
||||||
);
|
.where(
|
||||||
|
and(
|
||||||
|
eq(roleClients.roleId, adminRole.roleId),
|
||||||
|
eq(
|
||||||
|
roleClients.clientId,
|
||||||
|
existingClient.clientId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
if (!hasRoleAccess) {
|
if (!existingRoleClient) {
|
||||||
await transaction.insert(roleClients).values({
|
await transaction.insert(roleClients).values({
|
||||||
roleId: adminRole.roleId,
|
roleId: adminRole.roleId,
|
||||||
clientId: existingClient.clientId
|
clientId: existingClient.clientId
|
||||||
});
|
});
|
||||||
roleClientAccessCache.set(
|
|
||||||
getRoleClientKey(
|
|
||||||
adminRole.roleId,
|
|
||||||
existingClient.clientId
|
|
||||||
),
|
|
||||||
true
|
|
||||||
);
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Granted admin role access to existing client ${existingClient.clientId} for OLM ${olm.olmId} in org ${orgId} (user ${userId})`
|
`Granted admin role access to existing client ${existingClient.clientId} for OLM ${olm.olmId} in org ${orgId} (user ${userId})`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure user has access to the client
|
// Ensure user has access to the client
|
||||||
const hasUserAccess = await hasUserClientAccess(
|
const [existingUserClient] = await transaction
|
||||||
userId,
|
.select()
|
||||||
existingClient.clientId
|
.from(userClients)
|
||||||
);
|
.where(
|
||||||
|
and(
|
||||||
|
eq(userClients.userId, userId),
|
||||||
|
eq(
|
||||||
|
userClients.clientId,
|
||||||
|
existingClient.clientId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
if (!hasUserAccess) {
|
if (!existingUserClient) {
|
||||||
await transaction.insert(userClients).values({
|
await transaction.insert(userClients).values({
|
||||||
userId,
|
userId,
|
||||||
clientId: existingClient.clientId
|
clientId: existingClient.clientId
|
||||||
});
|
});
|
||||||
userClientAccessCache.set(
|
|
||||||
getUserClientKey(userId, existingClient.clientId),
|
|
||||||
true
|
|
||||||
);
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Granted user access to existing client ${existingClient.clientId} for OLM ${olm.olmId} in org ${orgId} (user ${userId})`
|
`Granted user access to existing client ${existingClient.clientId} for OLM ${olm.olmId} in org ${orgId} (user ${userId})`
|
||||||
);
|
);
|
||||||
@@ -316,7 +175,7 @@ export async function calculateUserClientsForOrgs(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get exit nodes for this org
|
// Get exit nodes for this org
|
||||||
const exitNodesList = await getExitNodes(orgId);
|
const exitNodesList = await listExitNodes(orgId);
|
||||||
|
|
||||||
if (exitNodesList.length === 0) {
|
if (exitNodesList.length === 0) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
@@ -347,11 +206,14 @@ export async function calculateUserClientsForOrgs(
|
|||||||
|
|
||||||
const niceId = await getUniqueClientName(orgId);
|
const niceId = await getUniqueClientName(orgId);
|
||||||
|
|
||||||
const isOrgLicensed = await getIsOrgLicensed(userOrg.orgId);
|
const isOrgLicensed = await isLicensedOrSubscribed(
|
||||||
|
userOrg.orgId,
|
||||||
|
tierMatrix.deviceApprovals
|
||||||
|
);
|
||||||
const requireApproval =
|
const requireApproval =
|
||||||
build !== "oss" &&
|
build !== "oss" &&
|
||||||
isOrgLicensed &&
|
isOrgLicensed &&
|
||||||
orgRequiresDeviceApprovalRole.get(orgId) === true;
|
roleRowsForOrg.some((r) => r.roles.requireDeviceApproval);
|
||||||
|
|
||||||
const newClientData: InferInsertModel<typeof clients> = {
|
const newClientData: InferInsertModel<typeof clients> = {
|
||||||
userId,
|
userId,
|
||||||
@@ -370,10 +232,6 @@ export async function calculateUserClientsForOrgs(
|
|||||||
.insert(clients)
|
.insert(clients)
|
||||||
.values(newClientData)
|
.values(newClientData)
|
||||||
.returning();
|
.returning();
|
||||||
existingClientCache.set(
|
|
||||||
getOrgOlmKey(orgId, olm.olmId),
|
|
||||||
newClient
|
|
||||||
);
|
|
||||||
|
|
||||||
// create approval request
|
// create approval request
|
||||||
if (requireApproval) {
|
if (requireApproval) {
|
||||||
@@ -399,20 +257,12 @@ export async function calculateUserClientsForOrgs(
|
|||||||
roleId: adminRole.roleId,
|
roleId: adminRole.roleId,
|
||||||
clientId: newClient.clientId
|
clientId: newClient.clientId
|
||||||
});
|
});
|
||||||
roleClientAccessCache.set(
|
|
||||||
getRoleClientKey(adminRole.roleId, newClient.clientId),
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
// Grant user access to the client
|
// Grant user access to the client
|
||||||
await transaction.insert(userClients).values({
|
await transaction.insert(userClients).values({
|
||||||
userId,
|
userId,
|
||||||
clientId: newClient.clientId
|
clientId: newClient.clientId
|
||||||
});
|
});
|
||||||
userClientAccessCache.set(
|
|
||||||
getUserClientKey(userId, newClient.clientId),
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Created client for OLM ${olm.olmId} in org ${orgId} (user ${userId}) with access granted to admin role and user`
|
`Created client for OLM ${olm.olmId} in org ${orgId} (user ${userId}) with access granted to admin role and user`
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import path from "path";
|
|||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
// This is a placeholder value replaced by the build process
|
// This is a placeholder value replaced by the build process
|
||||||
export const APP_VERSION = "1.18.3";
|
export const APP_VERSION = "1.18.2";
|
||||||
|
|
||||||
export const __FILENAME = fileURLToPath(import.meta.url);
|
export const __FILENAME = fileURLToPath(import.meta.url);
|
||||||
export const __DIRNAME = path.dirname(__FILENAME);
|
export const __DIRNAME = path.dirname(__FILENAME);
|
||||||
|
|||||||
212
server/lib/ip.ts
212
server/lib/ip.ts
@@ -6,7 +6,6 @@ import z from "zod";
|
|||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import semver from "semver";
|
import semver from "semver";
|
||||||
import { getValidCertificatesForDomains } from "#dynamic/lib/certificates";
|
import { getValidCertificatesForDomains } from "#dynamic/lib/certificates";
|
||||||
import { lockManager } from "#dynamic/lib/lock";
|
|
||||||
|
|
||||||
interface IPRange {
|
interface IPRange {
|
||||||
start: bigint;
|
start: bigint;
|
||||||
@@ -328,146 +327,120 @@ export async function getNextAvailableClientSubnet(
|
|||||||
orgId: string,
|
orgId: string,
|
||||||
transaction: Transaction | typeof db = db
|
transaction: Transaction | typeof db = db
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
return await lockManager.withLock(
|
const [org] = await transaction
|
||||||
`client-subnet-allocation:${orgId}`,
|
.select()
|
||||||
async () => {
|
.from(orgs)
|
||||||
const [org] = await transaction
|
.where(eq(orgs.orgId, orgId));
|
||||||
.select()
|
|
||||||
.from(orgs)
|
|
||||||
.where(eq(orgs.orgId, orgId));
|
|
||||||
|
|
||||||
if (!org) {
|
if (!org) {
|
||||||
throw new Error(`Organization with ID ${orgId} not found`);
|
throw new Error(`Organization with ID ${orgId} not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!org.subnet) {
|
if (!org.subnet) {
|
||||||
throw new Error(
|
throw new Error(`Organization with ID ${orgId} has no subnet defined`);
|
||||||
`Organization with ID ${orgId} has no subnet defined`
|
}
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingAddressesSites = await transaction
|
const existingAddressesSites = await transaction
|
||||||
.select({
|
.select({
|
||||||
address: sites.address
|
address: sites.address
|
||||||
})
|
})
|
||||||
.from(sites)
|
.from(sites)
|
||||||
.where(and(isNotNull(sites.address), eq(sites.orgId, orgId)));
|
.where(and(isNotNull(sites.address), eq(sites.orgId, orgId)));
|
||||||
|
|
||||||
const existingAddressesClients = await transaction
|
const existingAddressesClients = await transaction
|
||||||
.select({
|
.select({
|
||||||
address: clients.subnet
|
address: clients.subnet
|
||||||
})
|
})
|
||||||
.from(clients)
|
.from(clients)
|
||||||
.where(
|
.where(and(isNotNull(clients.subnet), eq(clients.orgId, orgId)));
|
||||||
and(isNotNull(clients.subnet), eq(clients.orgId, orgId))
|
|
||||||
);
|
|
||||||
|
|
||||||
const addresses = [
|
const addresses = [
|
||||||
...existingAddressesSites.map(
|
...existingAddressesSites.map(
|
||||||
(site) => `${site.address?.split("/")[0]}/32`
|
(site) => `${site.address?.split("/")[0]}/32`
|
||||||
), // we are overriding the 32 so that we pick individual addresses in the subnet of the org for the site and the client even though they are stored with the /block_size of the org
|
), // we are overriding the 32 so that we pick individual addresses in the subnet of the org for the site and the client even though they are stored with the /block_size of the org
|
||||||
...existingAddressesClients.map(
|
...existingAddressesClients.map(
|
||||||
(client) => `${client.address.split("/")}/32`
|
(client) => `${client.address.split("/")}/32`
|
||||||
)
|
)
|
||||||
].filter((address) => address !== null) as string[];
|
].filter((address) => address !== null) as string[];
|
||||||
|
|
||||||
const subnet = findNextAvailableCidr(addresses, 32, org.subnet); // pick the sites address in the org
|
const subnet = findNextAvailableCidr(addresses, 32, org.subnet); // pick the sites address in the org
|
||||||
if (!subnet) {
|
if (!subnet) {
|
||||||
throw new Error("No available subnets remaining in space");
|
throw new Error("No available subnets remaining in space");
|
||||||
}
|
}
|
||||||
|
|
||||||
return subnet;
|
return subnet;
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getNextAvailableAliasAddress(
|
export async function getNextAvailableAliasAddress(
|
||||||
orgId: string,
|
orgId: string
|
||||||
trx: Transaction | typeof db = db
|
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
return await lockManager.withLock(
|
const [org] = await db.select().from(orgs).where(eq(orgs.orgId, orgId));
|
||||||
`alias-address-allocation:${orgId}`,
|
|
||||||
async () => {
|
|
||||||
const [org] = await trx
|
|
||||||
.select()
|
|
||||||
.from(orgs)
|
|
||||||
.where(eq(orgs.orgId, orgId));
|
|
||||||
|
|
||||||
if (!org) {
|
if (!org) {
|
||||||
throw new Error(`Organization with ID ${orgId} not found`);
|
throw new Error(`Organization with ID ${orgId} not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!org.subnet) {
|
if (!org.subnet) {
|
||||||
throw new Error(
|
throw new Error(`Organization with ID ${orgId} has no subnet defined`);
|
||||||
`Organization with ID ${orgId} has no subnet defined`
|
}
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!org.utilitySubnet) {
|
if (!org.utilitySubnet) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Organization with ID ${orgId} has no utility subnet defined`
|
`Organization with ID ${orgId} has no utility subnet defined`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingAddresses = await trx
|
const existingAddresses = await db
|
||||||
.select({
|
.select({
|
||||||
aliasAddress: siteResources.aliasAddress
|
aliasAddress: siteResources.aliasAddress
|
||||||
})
|
})
|
||||||
.from(siteResources)
|
.from(siteResources)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
isNotNull(siteResources.aliasAddress),
|
isNotNull(siteResources.aliasAddress),
|
||||||
eq(siteResources.orgId, orgId)
|
eq(siteResources.orgId, orgId)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const addresses = [
|
const addresses = [
|
||||||
...existingAddresses.map(
|
...existingAddresses.map(
|
||||||
(site) => `${site.aliasAddress?.split("/")[0]}/32`
|
(site) => `${site.aliasAddress?.split("/")[0]}/32`
|
||||||
),
|
),
|
||||||
// reserve a /29 for the dns server and other stuff
|
// reserve a /29 for the dns server and other stuff
|
||||||
`${org.utilitySubnet.split("/")[0]}/29`
|
`${org.utilitySubnet.split("/")[0]}/29`
|
||||||
].filter((address) => address !== null) as string[];
|
].filter((address) => address !== null) as string[];
|
||||||
|
|
||||||
let subnet = findNextAvailableCidr(
|
let subnet = findNextAvailableCidr(addresses, 32, org.utilitySubnet);
|
||||||
addresses,
|
if (!subnet) {
|
||||||
32,
|
throw new Error("No available subnets remaining in space");
|
||||||
org.utilitySubnet
|
}
|
||||||
);
|
|
||||||
if (!subnet) {
|
|
||||||
throw new Error("No available subnets remaining in space");
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove the cidr
|
// remove the cidr
|
||||||
subnet = subnet.split("/")[0];
|
subnet = subnet.split("/")[0];
|
||||||
|
|
||||||
return subnet;
|
return subnet;
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getNextAvailableOrgSubnet(): Promise<string> {
|
export async function getNextAvailableOrgSubnet(): Promise<string> {
|
||||||
return await lockManager.withLock("org-subnet-allocation", async () => {
|
const existingAddresses = await db
|
||||||
const existingAddresses = await db
|
.select({
|
||||||
.select({
|
subnet: orgs.subnet
|
||||||
subnet: orgs.subnet
|
})
|
||||||
})
|
.from(orgs)
|
||||||
.from(orgs)
|
.where(isNotNull(orgs.subnet));
|
||||||
.where(isNotNull(orgs.subnet));
|
|
||||||
|
|
||||||
const addresses = existingAddresses.map((org) => org.subnet!);
|
const addresses = existingAddresses.map((org) => org.subnet!);
|
||||||
|
|
||||||
const subnet = findNextAvailableCidr(
|
const subnet = findNextAvailableCidr(
|
||||||
addresses,
|
addresses,
|
||||||
config.getRawConfig().orgs.block_size,
|
config.getRawConfig().orgs.block_size,
|
||||||
config.getRawConfig().orgs.subnet_group
|
config.getRawConfig().orgs.subnet_group
|
||||||
);
|
);
|
||||||
if (!subnet) {
|
if (!subnet) {
|
||||||
throw new Error("No available subnets remaining in space");
|
throw new Error("No available subnets remaining in space");
|
||||||
}
|
}
|
||||||
|
|
||||||
return subnet;
|
return subnet;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateRemoteSubnets(
|
export function generateRemoteSubnets(
|
||||||
@@ -505,12 +478,7 @@ export type Alias = { alias: string | null; aliasAddress: string | null };
|
|||||||
|
|
||||||
export function generateAliasConfig(allSiteResources: SiteResource[]): Alias[] {
|
export function generateAliasConfig(allSiteResources: SiteResource[]): Alias[] {
|
||||||
return allSiteResources
|
return allSiteResources
|
||||||
.filter(
|
.filter((sr) => sr.aliasAddress && ((sr.alias && sr.mode == "host") || (sr.fullDomain && sr.mode == "http")))
|
||||||
(sr) =>
|
|
||||||
sr.aliasAddress &&
|
|
||||||
((sr.alias && sr.mode == "host") ||
|
|
||||||
(sr.fullDomain && sr.mode == "http"))
|
|
||||||
)
|
|
||||||
.map((sr) => ({
|
.map((sr) => ({
|
||||||
alias: sr.alias || sr.fullDomain,
|
alias: sr.alias || sr.fullDomain,
|
||||||
aliasAddress: sr.aliasAddress
|
aliasAddress: sr.aliasAddress
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db, logsDb, statusHistory } from "@server/db";
|
import { db, logsDb, statusHistory } from "@server/db";
|
||||||
import { and, eq, gte, asc } from "drizzle-orm";
|
import { and, eq, gte, asc } from "drizzle-orm";
|
||||||
import cache from "@server/lib/cache";
|
import { regionalCache as cache } from "@server/private/lib/cache";
|
||||||
|
|
||||||
const STATUS_HISTORY_CACHE_TTL = 60; // seconds
|
const STATUS_HISTORY_CACHE_TTL = 60; // seconds
|
||||||
|
|
||||||
@@ -24,11 +24,8 @@ export async function getCachedStatusHistory(
|
|||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Anchor to UTC midnight so the query window aligns with stable calendar days
|
const nowSec = Math.floor(Date.now() / 1000);
|
||||||
const utcToday = new Date();
|
const startSec = nowSec - days * 86400;
|
||||||
utcToday.setUTCHours(0, 0, 0, 0);
|
|
||||||
const todayMidnightSec = Math.floor(utcToday.getTime() / 1000);
|
|
||||||
const startSec = todayMidnightSec - days * 86400;
|
|
||||||
|
|
||||||
const events = await logsDb
|
const events = await logsDb
|
||||||
.select()
|
.select()
|
||||||
@@ -66,7 +63,7 @@ export async function invalidateStatusHistoryCache(
|
|||||||
entityId: number
|
entityId: number
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const prefix = `statusHistory:${entityType}:${entityId}:`;
|
const prefix = `statusHistory:${entityType}:${entityId}:`;
|
||||||
const keys = cache.keys().filter((k) => k.startsWith(prefix));
|
const keys = await cache.keysWithPrefix(prefix);
|
||||||
if (keys.length > 0) {
|
if (keys.length > 0) {
|
||||||
await cache.del(keys);
|
await cache.del(keys);
|
||||||
}
|
}
|
||||||
@@ -113,18 +110,11 @@ export function computeBuckets(
|
|||||||
days: number
|
days: number
|
||||||
): { buckets: StatusHistoryDayBucket[]; totalDowntime: number } {
|
): { buckets: StatusHistoryDayBucket[]; totalDowntime: number } {
|
||||||
const nowSec = Math.floor(Date.now() / 1000);
|
const nowSec = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
// Anchor bucket boundaries to UTC midnight so dates are stable calendar days
|
|
||||||
// and don't drift as the cache expires and is recomputed
|
|
||||||
const utcToday = new Date();
|
|
||||||
utcToday.setUTCHours(0, 0, 0, 0);
|
|
||||||
const todayMidnightSec = Math.floor(utcToday.getTime() / 1000);
|
|
||||||
|
|
||||||
const buckets: StatusHistoryDayBucket[] = [];
|
const buckets: StatusHistoryDayBucket[] = [];
|
||||||
let totalDowntime = 0;
|
let totalDowntime = 0;
|
||||||
|
|
||||||
for (let d = 0; d < days; d++) {
|
for (let d = 0; d < days; d++) {
|
||||||
const dayStartSec = todayMidnightSec - (days - d) * 86400;
|
const dayStartSec = nowSec - (days - d) * 86400;
|
||||||
const dayEndSec = dayStartSec + 86400;
|
const dayEndSec = dayStartSec + 86400;
|
||||||
|
|
||||||
const dayEvents = events.filter(
|
const dayEvents = events.filter(
|
||||||
|
|||||||
@@ -29,10 +29,7 @@ import { decrypt } from "@server/lib/crypto";
|
|||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { sendAlertWebhook } from "./sendAlertWebhook";
|
import { sendAlertWebhook } from "./sendAlertWebhook";
|
||||||
import { sendAlertEmail } from "./sendAlertEmail";
|
import { sendAlertEmail } from "./sendAlertEmail";
|
||||||
import {
|
import { AlertContext, WebhookAlertConfig } from "@server/routers/alertRule/types";
|
||||||
AlertContext,
|
|
||||||
WebhookAlertConfig
|
|
||||||
} from "@server/routers/alertRule/types";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Core alert processing pipeline.
|
* Core alert processing pipeline.
|
||||||
@@ -102,10 +99,7 @@ export async function processAlerts(context: AlertContext): Promise<void> {
|
|||||||
baseConditions,
|
baseConditions,
|
||||||
or(
|
or(
|
||||||
eq(alertRules.allHealthChecks, true),
|
eq(alertRules.allHealthChecks, true),
|
||||||
eq(
|
eq(alertHealthChecks.healthCheckId, context.healthCheckId)
|
||||||
alertHealthChecks.healthCheckId,
|
|
||||||
context.healthCheckId
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -214,19 +208,14 @@ async function processRule(
|
|||||||
|
|
||||||
for (const action of emailActions) {
|
for (const action of emailActions) {
|
||||||
try {
|
try {
|
||||||
const recipients = await resolveEmailRecipients(
|
const recipients = await resolveEmailRecipients(action.emailActionId);
|
||||||
action.emailActionId
|
|
||||||
);
|
|
||||||
if (recipients.length > 0) {
|
if (recipients.length > 0) {
|
||||||
await sendAlertEmail(recipients, context);
|
await sendAlertEmail(recipients, context);
|
||||||
await db
|
await db
|
||||||
.update(alertEmailActions)
|
.update(alertEmailActions)
|
||||||
.set({ lastSentAt: now })
|
.set({ lastSentAt: now })
|
||||||
.where(
|
.where(
|
||||||
eq(
|
eq(alertEmailActions.emailActionId, action.emailActionId)
|
||||||
alertEmailActions.emailActionId,
|
|
||||||
action.emailActionId
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -280,7 +269,7 @@ async function processRule(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn(
|
logger.error(
|
||||||
`processAlerts: failed to send alert webhook for action ${action.webhookActionId}`,
|
`processAlerts: failed to send alert webhook for action ${action.webhookActionId}`,
|
||||||
err
|
err
|
||||||
);
|
);
|
||||||
@@ -300,9 +289,7 @@ async function processRule(
|
|||||||
* - All users in a role (by `roleId`, resolved via `userOrgRoles`)
|
* - All users in a role (by `roleId`, resolved via `userOrgRoles`)
|
||||||
* - Direct external email addresses
|
* - Direct external email addresses
|
||||||
*/
|
*/
|
||||||
async function resolveEmailRecipients(
|
async function resolveEmailRecipients(emailActionId: number): Promise<string[]> {
|
||||||
emailActionId: number
|
|
||||||
): Promise<string[]> {
|
|
||||||
const rows = await db
|
const rows = await db
|
||||||
.select()
|
.select()
|
||||||
.from(alertEmailRecipients)
|
.from(alertEmailRecipients)
|
||||||
|
|||||||
@@ -236,43 +236,15 @@ interface TemplateContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render a body template with {{event}}, {{timestamp}}, {{status}}, {{data}},
|
* Render a body template with {{event}}, {{timestamp}}, {{status}}, and
|
||||||
* and individual data-field placeholders (e.g. {{orgId}}, {{siteId}}, …).
|
* {{data}} placeholders, mirroring the logic in HttpLogDestination.
|
||||||
*
|
*
|
||||||
* Replacement order:
|
* {{data}} is replaced first (as raw JSON) so that any literal "{{…}}"
|
||||||
* 1. {{data}} → raw JSON of the full data object (prevents re-expansion of
|
* strings inside data values are not re-expanded.
|
||||||
* nested values that might look like placeholders).
|
|
||||||
* 2. Top-level scalar fields from data (string values are JSON-escaped;
|
|
||||||
* numbers and booleans are rendered as-is). Unknown placeholders are
|
|
||||||
* left untouched.
|
|
||||||
* 3. The fixed top-level keys: event, timestamp, status.
|
|
||||||
*/
|
*/
|
||||||
function renderTemplate(template: string, ctx: TemplateContext): string {
|
function renderTemplate(template: string, ctx: TemplateContext): string {
|
||||||
// Step 1 – expand {{data}} first so its contents are already serialised
|
const rendered = template
|
||||||
// and won't be touched by later passes.
|
.replace(/\{\{data\}\}/g, JSON.stringify(ctx.data))
|
||||||
let rendered = template.replace(/\{\{data\}\}/g, JSON.stringify(ctx.data));
|
|
||||||
|
|
||||||
// Step 2 – expand individual data fields. Only replace placeholders whose
|
|
||||||
// key actually exists in ctx.data; leave everything else as-is.
|
|
||||||
for (const [key, value] of Object.entries(ctx.data)) {
|
|
||||||
if (value === null || value === undefined) continue;
|
|
||||||
const placeholder = new RegExp(
|
|
||||||
`\\{\\{${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\}\\}`,
|
|
||||||
"g"
|
|
||||||
);
|
|
||||||
let serialised: string;
|
|
||||||
if (typeof value === "string") {
|
|
||||||
serialised = escapeJsonString(value);
|
|
||||||
} else if (typeof value === "number" || typeof value === "boolean") {
|
|
||||||
serialised = String(value);
|
|
||||||
} else {
|
|
||||||
serialised = escapeJsonString(JSON.stringify(value));
|
|
||||||
}
|
|
||||||
rendered = rendered.replace(placeholder, serialised);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 3 – expand the fixed top-level keys.
|
|
||||||
rendered = rendered
|
|
||||||
.replace(/\{\{event\}\}/g, escapeJsonString(ctx.event))
|
.replace(/\{\{event\}\}/g, escapeJsonString(ctx.event))
|
||||||
.replace(/\{\{timestamp\}\}/g, escapeJsonString(ctx.timestamp))
|
.replace(/\{\{timestamp\}\}/g, escapeJsonString(ctx.timestamp))
|
||||||
.replace(/\{\{status\}\}/g, escapeJsonString(ctx.status));
|
.replace(/\{\{status\}\}/g, escapeJsonString(ctx.status));
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
import NodeCache from "node-cache";
|
import NodeCache from "node-cache";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { redisManager } from "@server/private/lib/redis";
|
import { redisManager, regionalRedisManager } from "@server/private/lib/redis";
|
||||||
|
|
||||||
// Create local cache with maxKeys limit to prevent memory leaks
|
// Create local cache with maxKeys limit to prevent memory leaks
|
||||||
// With ~10k requests/day and 5min TTL, 10k keys should be more than sufficient
|
// With ~10k requests/day and 5min TTL, 10k keys should be more than sufficient
|
||||||
@@ -298,3 +298,147 @@ class AdaptiveCache {
|
|||||||
// Export singleton instance
|
// Export singleton instance
|
||||||
export const cache = new AdaptiveCache();
|
export const cache = new AdaptiveCache();
|
||||||
export default cache;
|
export default cache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regional adaptive cache backed by the in-cluster Redis instance.
|
||||||
|
* Falls back to a local NodeCache when the regional Redis is unavailable.
|
||||||
|
* Use this for data that is regional in nature (e.g. status history) so
|
||||||
|
* reads are served from the same cluster the user is hitting.
|
||||||
|
*/
|
||||||
|
const regionalLocalCache = new NodeCache({
|
||||||
|
stdTTL: 3600,
|
||||||
|
checkperiod: 120,
|
||||||
|
maxKeys: 10000
|
||||||
|
});
|
||||||
|
|
||||||
|
class RegionalAdaptiveCache {
|
||||||
|
private useRedis(): boolean {
|
||||||
|
return (
|
||||||
|
regionalRedisManager.isRedisEnabled() &&
|
||||||
|
regionalRedisManager.getHealthStatus().isHealthy
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async set(key: string, value: any, ttl?: number): Promise<boolean> {
|
||||||
|
const effectiveTtl = ttl === 0 ? undefined : ttl;
|
||||||
|
const redisTtl = ttl === 0 ? undefined : (ttl ?? 3600);
|
||||||
|
|
||||||
|
if (this.useRedis()) {
|
||||||
|
try {
|
||||||
|
const serialized = JSON.stringify(value);
|
||||||
|
const success = await regionalRedisManager.set(
|
||||||
|
key,
|
||||||
|
serialized,
|
||||||
|
redisTtl
|
||||||
|
);
|
||||||
|
if (success) {
|
||||||
|
logger.debug(`[regional] Set key in Redis: ${key}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`[regional] Redis set error for key ${key}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const success = regionalLocalCache.set(key, value, effectiveTtl || 0);
|
||||||
|
if (success) logger.debug(`[regional] Set key in local cache: ${key}`);
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
async get<T = any>(key: string): Promise<T | undefined> {
|
||||||
|
if (this.useRedis()) {
|
||||||
|
try {
|
||||||
|
const value = await regionalRedisManager.get(key);
|
||||||
|
if (value !== null) {
|
||||||
|
logger.debug(`[regional] Cache hit in Redis: ${key}`);
|
||||||
|
return JSON.parse(value) as T;
|
||||||
|
}
|
||||||
|
logger.debug(`[regional] Cache miss in Redis: ${key}`);
|
||||||
|
return undefined;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`[regional] Redis get error for key ${key}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = regionalLocalCache.get<T>(key);
|
||||||
|
if (value !== undefined) {
|
||||||
|
logger.debug(`[regional] Cache hit in local cache: ${key}`);
|
||||||
|
} else {
|
||||||
|
logger.debug(`[regional] Cache miss in local cache: ${key}`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async del(key: string | string[]): Promise<number> {
|
||||||
|
const keys = Array.isArray(key) ? key : [key];
|
||||||
|
let deletedCount = 0;
|
||||||
|
|
||||||
|
if (this.useRedis()) {
|
||||||
|
try {
|
||||||
|
for (const k of keys) {
|
||||||
|
const success = await regionalRedisManager.del(k);
|
||||||
|
if (success) {
|
||||||
|
deletedCount++;
|
||||||
|
logger.debug(`[regional] Deleted key from Redis: ${k}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (deletedCount === keys.length) return deletedCount;
|
||||||
|
deletedCount = 0;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[regional] Redis del error:`, error);
|
||||||
|
deletedCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const k of keys) {
|
||||||
|
const count = regionalLocalCache.del(k);
|
||||||
|
if (count > 0) {
|
||||||
|
deletedCount++;
|
||||||
|
logger.debug(`[regional] Deleted key from local cache: ${k}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return deletedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
async has(key: string): Promise<boolean> {
|
||||||
|
if (this.useRedis()) {
|
||||||
|
try {
|
||||||
|
const value = await regionalRedisManager.get(key);
|
||||||
|
return value !== null;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`[regional] Redis has error for key ${key}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return regionalLocalCache.has(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns keys matching the given prefix from whichever backend is active.
|
||||||
|
* Redis uses a KEYS scan; local cache filters in-memory keys.
|
||||||
|
*/
|
||||||
|
async keysWithPrefix(prefix: string): Promise<string[]> {
|
||||||
|
if (this.useRedis()) {
|
||||||
|
try {
|
||||||
|
return await regionalRedisManager.keys(`${prefix}*`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[regional] Redis keys error:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return regionalLocalCache.keys().filter((k) => k.startsWith(prefix));
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentBackend(): "redis" | "local" {
|
||||||
|
return this.useRedis() ? "redis" : "local";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const regionalCache = new RegionalAdaptiveCache();
|
||||||
|
|||||||
@@ -73,6 +73,25 @@ export const privateConfigSchema = z
|
|||||||
.object({
|
.object({
|
||||||
rejectUnauthorized: z.boolean().optional().default(true)
|
rejectUnauthorized: z.boolean().optional().default(true)
|
||||||
})
|
})
|
||||||
|
.optional(),
|
||||||
|
regional_redis: z
|
||||||
|
.object({
|
||||||
|
host: z.string(),
|
||||||
|
port: portSchema,
|
||||||
|
password: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform(getEnvOrYaml("REGIONAL_REDIS_PASSWORD")),
|
||||||
|
db: z.int().nonnegative().optional().default(0),
|
||||||
|
tls: z
|
||||||
|
.object({
|
||||||
|
rejectUnauthorized: z
|
||||||
|
.boolean()
|
||||||
|
.optional()
|
||||||
|
.default(true)
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
})
|
||||||
.optional()
|
.optional()
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
|
|||||||
@@ -109,14 +109,14 @@ class RedisManager {
|
|||||||
password: redisConfig.password,
|
password: redisConfig.password,
|
||||||
db: redisConfig.db
|
db: redisConfig.db
|
||||||
};
|
};
|
||||||
|
|
||||||
// Enable TLS if configured (required for AWS ElastiCache in-transit encryption)
|
// Enable TLS if configured (required for AWS ElastiCache in-transit encryption)
|
||||||
if (redisConfig.tls) {
|
if (redisConfig.tls) {
|
||||||
opts.tls = {
|
opts.tls = {
|
||||||
rejectUnauthorized: redisConfig.tls.rejectUnauthorized ?? true
|
rejectUnauthorized: redisConfig.tls.rejectUnauthorized ?? true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return opts;
|
return opts;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,14 +135,14 @@ class RedisManager {
|
|||||||
password: replica.password,
|
password: replica.password,
|
||||||
db: replica.db || redisConfig.db
|
db: replica.db || redisConfig.db
|
||||||
};
|
};
|
||||||
|
|
||||||
// Enable TLS if configured (required for AWS ElastiCache in-transit encryption)
|
// Enable TLS if configured (required for AWS ElastiCache in-transit encryption)
|
||||||
if (redisConfig.tls) {
|
if (redisConfig.tls) {
|
||||||
opts.tls = {
|
opts.tls = {
|
||||||
rejectUnauthorized: redisConfig.tls.rejectUnauthorized ?? true
|
rejectUnauthorized: redisConfig.tls.rejectUnauthorized ?? true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return opts;
|
return opts;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -855,3 +855,163 @@ class RedisManager {
|
|||||||
export const redisManager = new RedisManager();
|
export const redisManager = new RedisManager();
|
||||||
export const redis = redisManager.getClient();
|
export const redis = redisManager.getClient();
|
||||||
export default redisManager;
|
export default redisManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lightweight Redis manager for the regional (in-cluster) Redis instance.
|
||||||
|
* Connects only when `redis.regional_redis` is present in the private config
|
||||||
|
* and `flags.enable_redis` is true. No pub/sub — designed for low-latency
|
||||||
|
* caching of regionally-scoped data.
|
||||||
|
*/
|
||||||
|
class RegionalRedisManager {
|
||||||
|
private writeClient: Redis | null = null;
|
||||||
|
private readClient: Redis | null = null;
|
||||||
|
private isEnabled: boolean = false;
|
||||||
|
private isHealthy: boolean = false;
|
||||||
|
private connectionTimeout: number = 5000;
|
||||||
|
private commandTimeout: number = 5000;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
if (build === "oss") return;
|
||||||
|
|
||||||
|
const cfg = privateConfig.getRawPrivateConfig();
|
||||||
|
if (!cfg.flags.enable_redis || !cfg.redis?.regional_redis) return;
|
||||||
|
|
||||||
|
this.isEnabled = true;
|
||||||
|
this.initializeClients();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getConfig(): RedisOptions {
|
||||||
|
const r = privateConfig.getRawPrivateConfig().redis!.regional_redis!;
|
||||||
|
const opts: RedisOptions = {
|
||||||
|
host: r.host,
|
||||||
|
port: r.port,
|
||||||
|
password: r.password,
|
||||||
|
db: r.db
|
||||||
|
};
|
||||||
|
if (r.tls) {
|
||||||
|
opts.tls = { rejectUnauthorized: r.tls.rejectUnauthorized ?? true };
|
||||||
|
}
|
||||||
|
return opts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializeClients(): void {
|
||||||
|
const cfg = this.getConfig();
|
||||||
|
const baseOpts = {
|
||||||
|
...cfg,
|
||||||
|
enableReadyCheck: false,
|
||||||
|
maxRetriesPerRequest: 3,
|
||||||
|
keepAlive: 10000,
|
||||||
|
connectTimeout: this.connectionTimeout,
|
||||||
|
commandTimeout: this.commandTimeout
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.writeClient = new Redis(baseOpts);
|
||||||
|
// redis-1 (replica) handles reads; fall back to primary if not resolvable
|
||||||
|
this.readClient = new Redis({
|
||||||
|
...baseOpts,
|
||||||
|
host: cfg.host!.replace(/^(.*?)(\.\S+)$/, (_, h, rest) => {
|
||||||
|
// Derive replica hostname from the headless service pattern:
|
||||||
|
// redis.redis.svc.cluster.local -> redis-1.redis-headless.redis.svc.cluster.local
|
||||||
|
// If it doesn't look like a k8s service, just use the same host
|
||||||
|
return h + rest;
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// For simplicity use same host for both; callers can always read from primary
|
||||||
|
// The real replica routing is handled by the StatefulSet headless service
|
||||||
|
this.readClient = this.writeClient;
|
||||||
|
|
||||||
|
this.writeClient.on("ready", () => {
|
||||||
|
logger.info("Regional Redis client ready");
|
||||||
|
this.isHealthy = true;
|
||||||
|
});
|
||||||
|
this.writeClient.on("error", (err) => {
|
||||||
|
logger.error("Regional Redis client error:", err);
|
||||||
|
this.isHealthy = false;
|
||||||
|
});
|
||||||
|
this.writeClient.on("reconnecting", () => {
|
||||||
|
logger.info("Regional Redis client reconnecting...");
|
||||||
|
this.isHealthy = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info("Regional Redis client initialized");
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Failed to initialize regional Redis client:", error);
|
||||||
|
this.isEnabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public isRedisEnabled(): boolean {
|
||||||
|
return this.isEnabled && this.writeClient !== null && this.isHealthy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getHealthStatus() {
|
||||||
|
return { isEnabled: this.isEnabled, isHealthy: this.isHealthy };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async set(
|
||||||
|
key: string,
|
||||||
|
value: string,
|
||||||
|
ttl?: number
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (!this.isRedisEnabled() || !this.writeClient) return false;
|
||||||
|
try {
|
||||||
|
if (ttl) {
|
||||||
|
await this.writeClient.setex(key, ttl, value);
|
||||||
|
} else {
|
||||||
|
await this.writeClient.set(key, value);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Regional Redis SET error:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async get(key: string): Promise<string | null> {
|
||||||
|
if (!this.isRedisEnabled() || !this.readClient) return null;
|
||||||
|
try {
|
||||||
|
return await this.readClient.get(key);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Regional Redis GET error:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async del(key: string): Promise<boolean> {
|
||||||
|
if (!this.isRedisEnabled() || !this.writeClient) return false;
|
||||||
|
try {
|
||||||
|
await this.writeClient.del(key);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Regional Redis DEL error:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async keys(pattern: string): Promise<string[]> {
|
||||||
|
if (!this.isRedisEnabled() || !this.readClient) return [];
|
||||||
|
try {
|
||||||
|
return await this.readClient.keys(pattern);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Regional Redis KEYS error:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async disconnect(): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (this.writeClient) {
|
||||||
|
await this.writeClient.quit();
|
||||||
|
this.writeClient = null;
|
||||||
|
}
|
||||||
|
this.readClient = null;
|
||||||
|
logger.info("Regional Redis client disconnected");
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error disconnecting regional Redis client:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const regionalRedisManager = new RegionalRedisManager();
|
||||||
|
|||||||
@@ -151,8 +151,6 @@ export async function getUserResources(
|
|||||||
destination: string;
|
destination: string;
|
||||||
mode: string;
|
mode: string;
|
||||||
scheme: string | null;
|
scheme: string | null;
|
||||||
ssl: boolean;
|
|
||||||
fullDomain: string | null;
|
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
alias: string | null;
|
alias: string | null;
|
||||||
aliasAddress: string | null;
|
aliasAddress: string | null;
|
||||||
@@ -166,8 +164,6 @@ export async function getUserResources(
|
|||||||
destination: siteResources.destination,
|
destination: siteResources.destination,
|
||||||
mode: siteResources.mode,
|
mode: siteResources.mode,
|
||||||
scheme: siteResources.scheme,
|
scheme: siteResources.scheme,
|
||||||
ssl: siteResources.ssl,
|
|
||||||
fullDomain: siteResources.fullDomain,
|
|
||||||
enabled: siteResources.enabled,
|
enabled: siteResources.enabled,
|
||||||
alias: siteResources.alias,
|
alias: siteResources.alias,
|
||||||
aliasAddress: siteResources.aliasAddress
|
aliasAddress: siteResources.aliasAddress
|
||||||
@@ -255,8 +251,6 @@ export async function getUserResources(
|
|||||||
destination: siteResource.destination,
|
destination: siteResource.destination,
|
||||||
mode: siteResource.mode,
|
mode: siteResource.mode,
|
||||||
protocol: siteResource.scheme,
|
protocol: siteResource.scheme,
|
||||||
ssl: siteResource.ssl,
|
|
||||||
fullDomain: siteResource.fullDomain,
|
|
||||||
enabled: siteResource.enabled,
|
enabled: siteResource.enabled,
|
||||||
alias: siteResource.alias,
|
alias: siteResource.alias,
|
||||||
aliasAddress: siteResource.aliasAddress,
|
aliasAddress: siteResource.aliasAddress,
|
||||||
@@ -302,8 +296,6 @@ export type GetUserResourcesResponse = {
|
|||||||
destination: string;
|
destination: string;
|
||||||
mode: string;
|
mode: string;
|
||||||
protocol: string | null;
|
protocol: string | null;
|
||||||
ssl: boolean;
|
|
||||||
fullDomain: string | null;
|
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
alias: string | null;
|
alias: string | null;
|
||||||
aliasAddress: string | null;
|
aliasAddress: string | null;
|
||||||
|
|||||||
@@ -74,14 +74,16 @@ const createSiteResourceSchema = z
|
|||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
if (data.mode === "host") {
|
if (data.mode === "host") {
|
||||||
// Check if it's a valid IP address using zod (v4 or v6)
|
if (data.mode == "host") {
|
||||||
const isValidIP = z
|
// Check if it's a valid IP address using zod (v4 or v6)
|
||||||
// .union([z.ipv4(), z.ipv6()])
|
const isValidIP = z
|
||||||
.union([z.ipv4()]) // for now lets just do ipv4 until we verify ipv6 works everywhere
|
// .union([z.ipv4(), z.ipv6()])
|
||||||
.safeParse(data.destination).success;
|
.union([z.ipv4()]) // for now lets just do ipv4 until we verify ipv6 works everywhere
|
||||||
|
.safeParse(data.destination).success;
|
||||||
|
|
||||||
if (isValidIP) {
|
if (isValidIP) {
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if it's a valid domain (hostname pattern, TLD not required)
|
// Check if it's a valid domain (hostname pattern, TLD not required)
|
||||||
@@ -94,12 +96,17 @@ const createSiteResourceSchema = z
|
|||||||
data.alias.trim() !== "";
|
data.alias.trim() !== "";
|
||||||
|
|
||||||
return isValidDomain && isValidAlias; // require the alias to be set in the case of domain
|
return isValidDomain && isValidAlias; // require the alias to be set in the case of domain
|
||||||
} else if (data.mode === "http") {
|
}
|
||||||
// we have to have a domainId defined
|
return true;
|
||||||
if (!data.domainId) {
|
},
|
||||||
return false;
|
{
|
||||||
}
|
message:
|
||||||
} else if (data.mode === "cidr") {
|
"Destination must be a valid IPV4 address or valid domain AND alias is required"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.mode === "cidr") {
|
||||||
// Check if it's a valid CIDR (v4 or v6)
|
// Check if it's a valid CIDR (v4 or v6)
|
||||||
const isValidCIDR = z
|
const isValidCIDR = z
|
||||||
.union([z.cidrv4(), z.cidrv6()])
|
.union([z.cidrv4(), z.cidrv6()])
|
||||||
@@ -109,8 +116,7 @@ const createSiteResourceSchema = z
|
|||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message:
|
message: "Destination must be a valid CIDR notation for cidr mode"
|
||||||
"Destination must be a valid IPV4 address or valid domain AND alias is required"
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.refine(
|
.refine(
|
||||||
|
|||||||
@@ -104,17 +104,6 @@ const updateSiteResourceSchema = z
|
|||||||
data.alias.trim() !== "";
|
data.alias.trim() !== "";
|
||||||
|
|
||||||
return isValidDomain && isValidAlias; // require the alias to be set in the case of domain
|
return isValidDomain && isValidAlias; // require the alias to be set in the case of domain
|
||||||
} else if (data.mode === "cidr" && data.destination) {
|
|
||||||
// Check if it's a valid CIDR (v4 or v6)
|
|
||||||
const isValidCIDR = z
|
|
||||||
.union([z.cidrv4(), z.cidrv6()])
|
|
||||||
.safeParse(data.destination).success;
|
|
||||||
return isValidCIDR;
|
|
||||||
} else if (data.mode === "http") {
|
|
||||||
// we have to have a domainId defined
|
|
||||||
if (!data.domainId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
@@ -123,6 +112,21 @@ const updateSiteResourceSchema = z
|
|||||||
"Destination must be a valid IP address or valid domain AND alias is required"
|
"Destination must be a valid IP address or valid domain AND alias is required"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.mode === "cidr" && data.destination) {
|
||||||
|
// Check if it's a valid CIDR (v4 or v6)
|
||||||
|
const isValidCIDR = z
|
||||||
|
.union([z.cidrv4(), z.cidrv6()])
|
||||||
|
.safeParse(data.destination).success;
|
||||||
|
return isValidCIDR;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Destination must be a valid CIDR notation for cidr mode"
|
||||||
|
}
|
||||||
|
)
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
if (data.mode !== "http") return true;
|
if (data.mode !== "http") return true;
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export default async function migration() {
|
|||||||
await db.execute(sql`BEGIN`);
|
await db.execute(sql`BEGIN`);
|
||||||
|
|
||||||
await db.execute(sql`
|
await db.execute(sql`
|
||||||
CREATE TABLE IF NOT EXISTS "trialNotifications" (
|
CREATE TABLE "trialNotifications" (
|
||||||
"notificationId" serial PRIMARY KEY NOT NULL,
|
"notificationId" serial PRIMARY KEY NOT NULL,
|
||||||
"subscriptionId" varchar(255) NOT NULL,
|
"subscriptionId" varchar(255) NOT NULL,
|
||||||
"notificationType" varchar(50) NOT NULL,
|
"notificationType" varchar(50) NOT NULL,
|
||||||
@@ -52,6 +52,10 @@ export default async function migration() {
|
|||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE "trialNotifications" ADD CONSTRAINT "trialNotifications_subscriptionId_subscriptions_subscriptionId_fk" FOREIGN KEY ("subscriptionId") REFERENCES "public"."subscriptions"("subscriptionId") ON DELETE cascade ON UPDATE no action;
|
||||||
|
`);
|
||||||
|
|
||||||
await db.execute(sql`COMMIT`);
|
await db.execute(sql`COMMIT`);
|
||||||
console.log("Migrated database");
|
console.log("Migrated database");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -73,7 +77,7 @@ export default async function migration() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`Updated names for ${existingHealthChecks.length} existing targetHealthCheck row(s)`
|
`Migrated ${existingHealthChecks.length} targetHealthCheck row(s) with corrected IDs`
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error while migrating targetHealthCheck rows:", e);
|
console.error("Error while migrating targetHealthCheck rows:", e);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export default async function migration() {
|
|||||||
db.transaction(() => {
|
db.transaction(() => {
|
||||||
db.prepare(
|
db.prepare(
|
||||||
`
|
`
|
||||||
CREATE TABLE IF NOT EXISTS 'trialNotifications' (
|
CREATE TABLE 'trialNotifications' (
|
||||||
'notificationId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
'notificationId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
'subscriptionId' text NOT NULL,
|
'subscriptionId' text NOT NULL,
|
||||||
'notificationType' text NOT NULL,
|
'notificationType' text NOT NULL,
|
||||||
|
|||||||
@@ -175,6 +175,26 @@ export default function GeneralPage() {
|
|||||||
}, [variant]);
|
}, [variant]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
async function fetchRoles() {
|
||||||
|
const res = await api
|
||||||
|
.get<AxiosResponse<ListRolesResponse>>(`/org/${orgId}/roles`)
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: t("accessRoleErrorFetch"),
|
||||||
|
description: formatAxiosError(
|
||||||
|
e,
|
||||||
|
t("accessRoleErrorFetchDescription")
|
||||||
|
)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res?.status === 200) {
|
||||||
|
setRoles(res.data.data.roles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const loadIdp = async (
|
const loadIdp = async (
|
||||||
availableRoles: { roleId: number; name: string }[]
|
availableRoles: { roleId: number; name: string }[]
|
||||||
) => {
|
) => {
|
||||||
@@ -500,7 +520,6 @@ export default function GeneralPage() {
|
|||||||
onAutoProvisionChange={(checked) => {
|
onAutoProvisionChange={(checked) => {
|
||||||
form.setValue("autoProvision", checked);
|
form.setValue("autoProvision", checked);
|
||||||
}}
|
}}
|
||||||
orgId={orgId as string}
|
|
||||||
roleMappingMode={roleMappingMode}
|
roleMappingMode={roleMappingMode}
|
||||||
onRoleMappingModeChange={(data) => {
|
onRoleMappingModeChange={(data) => {
|
||||||
setRoleMappingMode(data);
|
setRoleMappingMode(data);
|
||||||
|
|||||||
@@ -246,31 +246,123 @@ export default function Page() {
|
|||||||
|
|
||||||
<PaidFeaturesAlert tiers={tierMatrix.orgOidc} />
|
<PaidFeaturesAlert tiers={tierMatrix.orgOidc} />
|
||||||
|
|
||||||
<fieldset
|
<fieldset disabled={disabled} className={disabled ? "opacity-50 pointer-events-none" : ""}>
|
||||||
disabled={disabled}
|
<SettingsContainer>
|
||||||
className={disabled ? "opacity-50 pointer-events-none" : ""}
|
<SettingsSection>
|
||||||
>
|
<SettingsSectionHeader>
|
||||||
<SettingsContainer>
|
<SettingsSectionTitle>
|
||||||
|
{t("idpTitle")}
|
||||||
|
</SettingsSectionTitle>
|
||||||
|
<SettingsSectionDescription>
|
||||||
|
{t("idpCreateSettingsDescription")}
|
||||||
|
</SettingsSectionDescription>
|
||||||
|
</SettingsSectionHeader>
|
||||||
|
<SettingsSectionBody>
|
||||||
|
<OidcIdpProviderTypeSelect
|
||||||
|
value={form.watch("type")}
|
||||||
|
onTypeChange={(next) => {
|
||||||
|
applyOidcIdpProviderType(form.setValue, next);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SettingsSectionForm>
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
className="space-y-4"
|
||||||
|
id="create-idp-form"
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("name")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t("idpDisplayName")}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</SettingsSectionForm>
|
||||||
|
</SettingsSectionBody>
|
||||||
|
</SettingsSection>
|
||||||
|
|
||||||
|
{/* Auto Provision Settings */}
|
||||||
|
<SettingsSection>
|
||||||
|
<SettingsSectionHeader>
|
||||||
|
<SettingsSectionTitle>
|
||||||
|
{t("idpAutoProvisionUsers")}
|
||||||
|
</SettingsSectionTitle>
|
||||||
|
<SettingsSectionDescription>
|
||||||
|
<IdpAutoProvisionUsersDescription />
|
||||||
|
</SettingsSectionDescription>
|
||||||
|
</SettingsSectionHeader>
|
||||||
|
<SettingsSectionBody>
|
||||||
|
<PaidFeaturesAlert
|
||||||
|
tiers={tierMatrix.autoProvisioning}
|
||||||
|
/>
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
className="space-y-4"
|
||||||
|
id="create-idp-form"
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
>
|
||||||
|
<AutoProvisionConfigWidget
|
||||||
|
autoProvision={
|
||||||
|
form.watch("autoProvision") as boolean
|
||||||
|
} // is this right?
|
||||||
|
onAutoProvisionChange={(checked) => {
|
||||||
|
form.setValue("autoProvision", checked);
|
||||||
|
}}
|
||||||
|
roleMappingMode={roleMappingMode}
|
||||||
|
onRoleMappingModeChange={(data) => {
|
||||||
|
setRoleMappingMode(data);
|
||||||
|
}}
|
||||||
|
roles={roles}
|
||||||
|
fixedRoleNames={fixedRoleNames}
|
||||||
|
onFixedRoleNamesChange={setFixedRoleNames}
|
||||||
|
mappingBuilderClaimPath={
|
||||||
|
mappingBuilderClaimPath
|
||||||
|
}
|
||||||
|
onMappingBuilderClaimPathChange={
|
||||||
|
setMappingBuilderClaimPath
|
||||||
|
}
|
||||||
|
mappingBuilderRules={mappingBuilderRules}
|
||||||
|
onMappingBuilderRulesChange={
|
||||||
|
setMappingBuilderRules
|
||||||
|
}
|
||||||
|
rawExpression={rawRoleExpression}
|
||||||
|
onRawExpressionChange={setRawRoleExpression}
|
||||||
|
orgMappingField={{
|
||||||
|
control: form.control,
|
||||||
|
name: "orgMapping"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</SettingsSectionBody>
|
||||||
|
</SettingsSection>
|
||||||
|
|
||||||
|
{form.watch("type") === "google" && (
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
<SettingsSectionHeader>
|
<SettingsSectionHeader>
|
||||||
<SettingsSectionTitle>
|
<SettingsSectionTitle>
|
||||||
{t("idpTitle")}
|
{t("idpGoogleConfigurationTitle")}
|
||||||
</SettingsSectionTitle>
|
</SettingsSectionTitle>
|
||||||
<SettingsSectionDescription>
|
<SettingsSectionDescription>
|
||||||
{t("idpCreateSettingsDescription")}
|
{t("idpGoogleConfigurationDescription")}
|
||||||
</SettingsSectionDescription>
|
</SettingsSectionDescription>
|
||||||
</SettingsSectionHeader>
|
</SettingsSectionHeader>
|
||||||
<SettingsSectionBody>
|
<SettingsSectionBody>
|
||||||
<OidcIdpProviderTypeSelect
|
|
||||||
value={form.watch("type")}
|
|
||||||
onTypeChange={(next) => {
|
|
||||||
applyOidcIdpProviderType(
|
|
||||||
form.setValue,
|
|
||||||
next
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SettingsSectionForm>
|
<SettingsSectionForm>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
@@ -280,17 +372,43 @@ export default function Page() {
|
|||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="name"
|
name="clientId"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{t("name")}
|
{t("idpClientId")}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
{t("idpDisplayName")}
|
{t(
|
||||||
|
"idpGoogleClientIdDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="clientSecret"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("idpClientSecret")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"idpGoogleClientSecretDescription"
|
||||||
|
)}
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -301,504 +419,350 @@ export default function Page() {
|
|||||||
</SettingsSectionForm>
|
</SettingsSectionForm>
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Auto Provision Settings */}
|
{form.watch("type") === "azure" && (
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
<SettingsSectionHeader>
|
<SettingsSectionHeader>
|
||||||
<SettingsSectionTitle>
|
<SettingsSectionTitle>
|
||||||
{t("idpAutoProvisionUsers")}
|
{t("idpAzureConfigurationTitle")}
|
||||||
</SettingsSectionTitle>
|
</SettingsSectionTitle>
|
||||||
<SettingsSectionDescription>
|
<SettingsSectionDescription>
|
||||||
<IdpAutoProvisionUsersDescription />
|
{t("idpAzureConfigurationDescription")}
|
||||||
</SettingsSectionDescription>
|
</SettingsSectionDescription>
|
||||||
</SettingsSectionHeader>
|
</SettingsSectionHeader>
|
||||||
<SettingsSectionBody>
|
<SettingsSectionBody>
|
||||||
<PaidFeaturesAlert
|
<SettingsSectionForm>
|
||||||
tiers={tierMatrix.autoProvisioning}
|
<Form {...form}>
|
||||||
/>
|
<form
|
||||||
<Form {...form}>
|
className="space-y-4"
|
||||||
<form
|
id="create-idp-form"
|
||||||
className="space-y-4"
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
id="create-idp-form"
|
>
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
<FormField
|
||||||
>
|
control={form.control}
|
||||||
<AutoProvisionConfigWidget
|
name="tenantId"
|
||||||
autoProvision={
|
render={({ field }) => (
|
||||||
form.watch(
|
<FormItem>
|
||||||
"autoProvision"
|
<FormLabel>
|
||||||
) as boolean
|
{t("idpTenantIdLabel")}
|
||||||
} // is this right?
|
</FormLabel>
|
||||||
onAutoProvisionChange={(checked) => {
|
<FormControl>
|
||||||
form.setValue(
|
<Input {...field} />
|
||||||
"autoProvision",
|
</FormControl>
|
||||||
checked
|
<FormDescription>
|
||||||
);
|
{t(
|
||||||
}}
|
"idpAzureTenantIdDescription"
|
||||||
orgId={params.orgId as string}
|
)}
|
||||||
roleMappingMode={roleMappingMode}
|
</FormDescription>
|
||||||
onRoleMappingModeChange={(data) => {
|
<FormMessage />
|
||||||
setRoleMappingMode(data);
|
</FormItem>
|
||||||
}}
|
)}
|
||||||
roles={roles}
|
/>
|
||||||
fixedRoleNames={fixedRoleNames}
|
|
||||||
onFixedRoleNamesChange={
|
<FormField
|
||||||
setFixedRoleNames
|
control={form.control}
|
||||||
}
|
name="clientId"
|
||||||
mappingBuilderClaimPath={
|
render={({ field }) => (
|
||||||
mappingBuilderClaimPath
|
<FormItem>
|
||||||
}
|
<FormLabel>
|
||||||
onMappingBuilderClaimPathChange={
|
{t("idpClientId")}
|
||||||
setMappingBuilderClaimPath
|
</FormLabel>
|
||||||
}
|
<FormControl>
|
||||||
mappingBuilderRules={
|
<Input {...field} />
|
||||||
mappingBuilderRules
|
</FormControl>
|
||||||
}
|
<FormDescription>
|
||||||
onMappingBuilderRulesChange={
|
{t(
|
||||||
setMappingBuilderRules
|
"idpAzureClientIdDescription2"
|
||||||
}
|
)}
|
||||||
rawExpression={rawRoleExpression}
|
</FormDescription>
|
||||||
onRawExpressionChange={
|
<FormMessage />
|
||||||
setRawRoleExpression
|
</FormItem>
|
||||||
}
|
)}
|
||||||
orgMappingField={{
|
/>
|
||||||
control: form.control,
|
|
||||||
name: "orgMapping"
|
<FormField
|
||||||
}}
|
control={form.control}
|
||||||
/>
|
name="clientSecret"
|
||||||
</form>
|
render={({ field }) => (
|
||||||
</Form>
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("idpClientSecret")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"idpAzureClientSecretDescription2"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</SettingsSectionForm>
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
)}
|
||||||
|
|
||||||
{form.watch("type") === "google" && (
|
{form.watch("type") === "oidc" && (
|
||||||
|
<SettingsSectionGrid cols={2}>
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
<SettingsSectionHeader>
|
<SettingsSectionHeader>
|
||||||
<SettingsSectionTitle>
|
<SettingsSectionTitle>
|
||||||
{t("idpGoogleConfigurationTitle")}
|
{t("idpOidcConfigure")}
|
||||||
</SettingsSectionTitle>
|
</SettingsSectionTitle>
|
||||||
<SettingsSectionDescription>
|
<SettingsSectionDescription>
|
||||||
{t("idpGoogleConfigurationDescription")}
|
{t("idpOidcConfigureDescription")}
|
||||||
</SettingsSectionDescription>
|
</SettingsSectionDescription>
|
||||||
</SettingsSectionHeader>
|
</SettingsSectionHeader>
|
||||||
<SettingsSectionBody>
|
<SettingsSectionBody>
|
||||||
<SettingsSectionForm>
|
<Form {...form}>
|
||||||
<Form {...form}>
|
<form
|
||||||
<form
|
className="space-y-4"
|
||||||
className="space-y-4"
|
id="create-idp-form"
|
||||||
id="create-idp-form"
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
onSubmit={form.handleSubmit(
|
>
|
||||||
onSubmit
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="clientId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("idpClientId")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"idpClientIdDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
>
|
/>
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="clientId"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
{t("idpClientId")}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
{t(
|
|
||||||
"idpGoogleClientIdDescription"
|
|
||||||
)}
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="clientSecret"
|
name="clientSecret"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{t(
|
{t("idpClientSecret")}
|
||||||
"idpClientSecret"
|
</FormLabel>
|
||||||
)}
|
<FormControl>
|
||||||
</FormLabel>
|
<Input
|
||||||
<FormControl>
|
type="password"
|
||||||
<Input
|
{...field}
|
||||||
type="password"
|
/>
|
||||||
{...field}
|
</FormControl>
|
||||||
/>
|
<FormDescription>
|
||||||
</FormControl>
|
{t(
|
||||||
<FormDescription>
|
"idpClientSecretDescription"
|
||||||
{t(
|
)}
|
||||||
"idpGoogleClientSecretDescription"
|
</FormDescription>
|
||||||
)}
|
<FormMessage />
|
||||||
</FormDescription>
|
</FormItem>
|
||||||
<FormMessage />
|
)}
|
||||||
</FormItem>
|
/>
|
||||||
)}
|
|
||||||
/>
|
<FormField
|
||||||
</form>
|
control={form.control}
|
||||||
</Form>
|
name="authUrl"
|
||||||
</SettingsSectionForm>
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("idpAuthUrl")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="https://your-idp.com/oauth2/authorize"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"idpAuthUrlDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="tokenUrl"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("idpTokenUrl")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="https://your-idp.com/oauth2/token"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"idpTokenUrlDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
)}
|
|
||||||
|
|
||||||
{form.watch("type") === "azure" && (
|
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
<SettingsSectionHeader>
|
<SettingsSectionHeader>
|
||||||
<SettingsSectionTitle>
|
<SettingsSectionTitle>
|
||||||
{t("idpAzureConfigurationTitle")}
|
{t("idpToken")}
|
||||||
</SettingsSectionTitle>
|
</SettingsSectionTitle>
|
||||||
<SettingsSectionDescription>
|
<SettingsSectionDescription>
|
||||||
{t("idpAzureConfigurationDescription")}
|
{t("idpTokenDescription")}
|
||||||
</SettingsSectionDescription>
|
</SettingsSectionDescription>
|
||||||
</SettingsSectionHeader>
|
</SettingsSectionHeader>
|
||||||
<SettingsSectionBody>
|
<SettingsSectionBody>
|
||||||
<SettingsSectionForm>
|
<Form {...form}>
|
||||||
<Form {...form}>
|
<form
|
||||||
<form
|
className="space-y-4"
|
||||||
className="space-y-4"
|
id="create-idp-form"
|
||||||
id="create-idp-form"
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
onSubmit={form.handleSubmit(
|
>
|
||||||
onSubmit
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="identifierPath"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("idpJmespathLabel")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"idpJmespathLabelDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
>
|
/>
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="tenantId"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
{t(
|
|
||||||
"idpTenantIdLabel"
|
|
||||||
)}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
{t(
|
|
||||||
"idpAzureTenantIdDescription"
|
|
||||||
)}
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="clientId"
|
name="emailPath"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{t("idpClientId")}
|
{t(
|
||||||
</FormLabel>
|
"idpJmespathEmailPathOptional"
|
||||||
<FormControl>
|
)}
|
||||||
<Input {...field} />
|
</FormLabel>
|
||||||
</FormControl>
|
<FormControl>
|
||||||
<FormDescription>
|
<Input {...field} />
|
||||||
{t(
|
</FormControl>
|
||||||
"idpAzureClientIdDescription2"
|
<FormDescription>
|
||||||
)}
|
{t(
|
||||||
</FormDescription>
|
"idpJmespathEmailPathOptionalDescription"
|
||||||
<FormMessage />
|
)}
|
||||||
</FormItem>
|
</FormDescription>
|
||||||
)}
|
<FormMessage />
|
||||||
/>
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="clientSecret"
|
name="namePath"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{t(
|
{t(
|
||||||
"idpClientSecret"
|
"idpJmespathNamePathOptional"
|
||||||
)}
|
)}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input {...field} />
|
||||||
type="password"
|
</FormControl>
|
||||||
{...field}
|
<FormDescription>
|
||||||
/>
|
{t(
|
||||||
</FormControl>
|
"idpJmespathNamePathOptionalDescription"
|
||||||
<FormDescription>
|
)}
|
||||||
{t(
|
</FormDescription>
|
||||||
"idpAzureClientSecretDescription2"
|
<FormMessage />
|
||||||
)}
|
</FormItem>
|
||||||
</FormDescription>
|
)}
|
||||||
<FormMessage />
|
/>
|
||||||
</FormItem>
|
|
||||||
)}
|
<FormField
|
||||||
/>
|
control={form.control}
|
||||||
</form>
|
name="scopes"
|
||||||
</Form>
|
render={({ field }) => (
|
||||||
</SettingsSectionForm>
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t(
|
||||||
|
"idpOidcConfigureScopes"
|
||||||
|
)}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"idpOidcConfigureScopesDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
)}
|
</SettingsSectionGrid>
|
||||||
|
)}
|
||||||
|
</SettingsContainer>
|
||||||
|
|
||||||
{form.watch("type") === "oidc" && (
|
<div className="flex justify-end space-x-2 mt-8">
|
||||||
<SettingsSectionGrid cols={2}>
|
<Button
|
||||||
<SettingsSection>
|
type="button"
|
||||||
<SettingsSectionHeader>
|
variant="outline"
|
||||||
<SettingsSectionTitle>
|
onClick={() => {
|
||||||
{t("idpOidcConfigure")}
|
router.push(`/${params.orgId}/settings/idp`);
|
||||||
</SettingsSectionTitle>
|
}}
|
||||||
<SettingsSectionDescription>
|
>
|
||||||
{t("idpOidcConfigureDescription")}
|
{t("cancel")}
|
||||||
</SettingsSectionDescription>
|
</Button>
|
||||||
</SettingsSectionHeader>
|
<Button
|
||||||
<SettingsSectionBody>
|
type="submit"
|
||||||
<Form {...form}>
|
disabled={createLoading || disabled}
|
||||||
<form
|
loading={createLoading}
|
||||||
className="space-y-4"
|
onClick={() => {
|
||||||
id="create-idp-form"
|
if (disabled) return;
|
||||||
onSubmit={form.handleSubmit(
|
// log any issues with the form
|
||||||
onSubmit
|
console.log(form.formState.errors);
|
||||||
)}
|
form.handleSubmit(onSubmit)();
|
||||||
>
|
}}
|
||||||
<FormField
|
>
|
||||||
control={form.control}
|
{t("idpSubmit")}
|
||||||
name="clientId"
|
</Button>
|
||||||
render={({ field }) => (
|
</div>
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
{t("idpClientId")}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
{t(
|
|
||||||
"idpClientIdDescription"
|
|
||||||
)}
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="clientSecret"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
{t(
|
|
||||||
"idpClientSecret"
|
|
||||||
)}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
type="password"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
{t(
|
|
||||||
"idpClientSecretDescription"
|
|
||||||
)}
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="authUrl"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
{t("idpAuthUrl")}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="https://your-idp.com/oauth2/authorize"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
{t(
|
|
||||||
"idpAuthUrlDescription"
|
|
||||||
)}
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="tokenUrl"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
{t("idpTokenUrl")}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="https://your-idp.com/oauth2/token"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
{t(
|
|
||||||
"idpTokenUrlDescription"
|
|
||||||
)}
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</SettingsSectionBody>
|
|
||||||
</SettingsSection>
|
|
||||||
|
|
||||||
<SettingsSection>
|
|
||||||
<SettingsSectionHeader>
|
|
||||||
<SettingsSectionTitle>
|
|
||||||
{t("idpToken")}
|
|
||||||
</SettingsSectionTitle>
|
|
||||||
<SettingsSectionDescription>
|
|
||||||
{t("idpTokenDescription")}
|
|
||||||
</SettingsSectionDescription>
|
|
||||||
</SettingsSectionHeader>
|
|
||||||
<SettingsSectionBody>
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
className="space-y-4"
|
|
||||||
id="create-idp-form"
|
|
||||||
onSubmit={form.handleSubmit(
|
|
||||||
onSubmit
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="identifierPath"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
{t(
|
|
||||||
"idpJmespathLabel"
|
|
||||||
)}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
{t(
|
|
||||||
"idpJmespathLabelDescription"
|
|
||||||
)}
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="emailPath"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
{t(
|
|
||||||
"idpJmespathEmailPathOptional"
|
|
||||||
)}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
{t(
|
|
||||||
"idpJmespathEmailPathOptionalDescription"
|
|
||||||
)}
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="namePath"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
{t(
|
|
||||||
"idpJmespathNamePathOptional"
|
|
||||||
)}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
{t(
|
|
||||||
"idpJmespathNamePathOptionalDescription"
|
|
||||||
)}
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="scopes"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
{t(
|
|
||||||
"idpOidcConfigureScopes"
|
|
||||||
)}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
{t(
|
|
||||||
"idpOidcConfigureScopesDescription"
|
|
||||||
)}
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</SettingsSectionBody>
|
|
||||||
</SettingsSection>
|
|
||||||
</SettingsSectionGrid>
|
|
||||||
)}
|
|
||||||
</SettingsContainer>
|
|
||||||
|
|
||||||
<div className="flex justify-end space-x-2 mt-8">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
router.push(`/${params.orgId}/settings/idp`);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("cancel")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
disabled={createLoading || disabled}
|
|
||||||
loading={createLoading}
|
|
||||||
onClick={() => {
|
|
||||||
if (disabled) return;
|
|
||||||
// log any issues with the form
|
|
||||||
console.log(form.formState.errors);
|
|
||||||
form.handleSubmit(onSubmit)();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("idpSubmit")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,40 +1,44 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import IdpTypeBadge from "@app/components/IdpTypeBadge";
|
|
||||||
import OrgRolesTagField from "@app/components/OrgRolesTagField";
|
|
||||||
import {
|
|
||||||
SettingsContainer,
|
|
||||||
SettingsSection,
|
|
||||||
SettingsSectionBody,
|
|
||||||
SettingsSectionDescription,
|
|
||||||
SettingsSectionFooter,
|
|
||||||
SettingsSectionForm,
|
|
||||||
SettingsSectionHeader,
|
|
||||||
SettingsSectionTitle
|
|
||||||
} from "@app/components/Settings";
|
|
||||||
import { Button } from "@app/components/ui/button";
|
|
||||||
import { Checkbox } from "@app/components/ui/checkbox";
|
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel
|
FormLabel,
|
||||||
|
FormMessage
|
||||||
} from "@app/components/ui/form";
|
} from "@app/components/ui/form";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { Checkbox } from "@app/components/ui/checkbox";
|
||||||
import { userOrgUserContext } from "@app/hooks/useOrgUserContext";
|
import OrgRolesTagField from "@app/components/OrgRolesTagField";
|
||||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { build } from "@server/build";
|
import { AxiosResponse } from "axios";
|
||||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
import { useEffect, useState } from "react";
|
||||||
import { UserType } from "@server/types/UserTypes";
|
|
||||||
import { useTranslations } from "next-intl";
|
|
||||||
import { useParams } from "next/navigation";
|
|
||||||
import { useActionState, useEffect } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { ListRolesResponse } from "@server/routers/role";
|
||||||
|
import { userOrgUserContext } from "@app/hooks/useOrgUserContext";
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
|
import { Button } from "@app/components/ui/button";
|
||||||
|
import {
|
||||||
|
SettingsContainer,
|
||||||
|
SettingsSection,
|
||||||
|
SettingsSectionHeader,
|
||||||
|
SettingsSectionTitle,
|
||||||
|
SettingsSectionDescription,
|
||||||
|
SettingsSectionBody,
|
||||||
|
SettingsSectionForm,
|
||||||
|
SettingsSectionFooter
|
||||||
|
} from "@app/components/Settings";
|
||||||
|
import { formatAxiosError } from "@app/lib/api";
|
||||||
|
import { createApiClient } from "@app/lib/api";
|
||||||
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import IdpTypeBadge from "@app/components/IdpTypeBadge";
|
||||||
|
import { UserType } from "@server/types/UserTypes";
|
||||||
|
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||||
|
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
|
import { build } from "@server/build";
|
||||||
|
|
||||||
const accessControlsFormSchema = z.object({
|
const accessControlsFormSchema = z.object({
|
||||||
username: z.string(),
|
username: z.string(),
|
||||||
@@ -55,6 +59,12 @@ export default function AccessControlsPage() {
|
|||||||
|
|
||||||
const { orgId } = useParams();
|
const { orgId } = useParams();
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [roles, setRoles] = useState<{ roleId: number; name: string }[]>([]);
|
||||||
|
const [activeRoleTagIndex, setActiveRoleTagIndex] = useState<number | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const { isPaidUser } = usePaidStatus();
|
const { isPaidUser } = usePaidStatus();
|
||||||
const isPaid = isPaidUser(tierMatrix.fullRbac);
|
const isPaid = isPaidUser(tierMatrix.fullRbac);
|
||||||
@@ -87,21 +97,44 @@ export default function AccessControlsPage() {
|
|||||||
text: r.name
|
text: r.name
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
}, [user.userId, currentRoleIds.join(",")]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchRoles() {
|
||||||
|
const res = await api
|
||||||
|
.get<AxiosResponse<ListRolesResponse>>(`/org/${orgId}/roles`)
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: t("accessRoleErrorFetch"),
|
||||||
|
description: formatAxiosError(
|
||||||
|
e,
|
||||||
|
t("accessRoleErrorFetchDescription")
|
||||||
|
)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res?.status === 200) {
|
||||||
|
setRoles(res.data.data.roles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchRoles();
|
||||||
form.setValue("autoProvisioned", user.autoProvisioned || false);
|
form.setValue("autoProvisioned", user.autoProvisioned || false);
|
||||||
}, [user.userId, user.autoProvisioned, currentRoleIds.join(",")]);
|
}, []);
|
||||||
|
|
||||||
|
const allRoleOptions = roles.map((role) => ({
|
||||||
|
id: role.roleId.toString(),
|
||||||
|
text: role.name
|
||||||
|
}));
|
||||||
|
|
||||||
const paywallMessage =
|
const paywallMessage =
|
||||||
build === "saas"
|
build === "saas"
|
||||||
? t("singleRolePerUserPlanNotice")
|
? t("singleRolePerUserPlanNotice")
|
||||||
: t("singleRolePerUserEditionNotice");
|
: t("singleRolePerUserEditionNotice");
|
||||||
|
|
||||||
const [, action, isSubmitting] = useActionState(onSubmit, null);
|
async function onSubmit(values: z.infer<typeof accessControlsFormSchema>) {
|
||||||
async function onSubmit() {
|
|
||||||
const isValid = await form.trigger();
|
|
||||||
if (!isValid) return;
|
|
||||||
|
|
||||||
const values = form.getValues();
|
|
||||||
|
|
||||||
if (values.roles.length === 0) {
|
if (values.roles.length === 0) {
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
@@ -111,6 +144,7 @@ export default function AccessControlsPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const roleIds = values.roles.map((r) => parseInt(r.id, 10));
|
const roleIds = values.roles.map((r) => parseInt(r.id, 10));
|
||||||
const updateRoleRequest = supportsMultipleRolesPerUser
|
const updateRoleRequest = supportsMultipleRolesPerUser
|
||||||
@@ -150,6 +184,7 @@ export default function AccessControlsPage() {
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -168,7 +203,7 @@ export default function AccessControlsPage() {
|
|||||||
<SettingsSectionForm>
|
<SettingsSectionForm>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
action={action}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
id="access-controls-form"
|
id="access-controls-form"
|
||||||
>
|
>
|
||||||
@@ -191,7 +226,9 @@ export default function AccessControlsPage() {
|
|||||||
<OrgRolesTagField
|
<OrgRolesTagField
|
||||||
form={form}
|
form={form}
|
||||||
name="roles"
|
name="roles"
|
||||||
orgId={orgId as string}
|
label={t("roles")}
|
||||||
|
placeholder={t("accessRoleSelect2")}
|
||||||
|
allRoleOptions={allRoleOptions}
|
||||||
supportsMultipleRolesPerUser={
|
supportsMultipleRolesPerUser={
|
||||||
supportsMultipleRolesPerUser
|
supportsMultipleRolesPerUser
|
||||||
}
|
}
|
||||||
@@ -199,6 +236,9 @@ export default function AccessControlsPage() {
|
|||||||
showMultiRolePaywallMessage
|
showMultiRolePaywallMessage
|
||||||
}
|
}
|
||||||
paywallMessage={paywallMessage}
|
paywallMessage={paywallMessage}
|
||||||
|
loading={loading}
|
||||||
|
activeTagIndex={activeRoleTagIndex}
|
||||||
|
setActiveTagIndex={setActiveRoleTagIndex}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{user.idpAutoProvision && (
|
{user.idpAutoProvision && (
|
||||||
@@ -237,8 +277,8 @@ export default function AccessControlsPage() {
|
|||||||
<SettingsSectionFooter>
|
<SettingsSectionFooter>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
loading={isSubmitting}
|
loading={loading}
|
||||||
disabled={isSubmitting}
|
disabled={loading}
|
||||||
form="access-controls-form"
|
form="access-controls-form"
|
||||||
>
|
>
|
||||||
{t("accessControlsSubmit")}
|
{t("accessControlsSubmit")}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { StrategyOption, StrategySelect } from "@app/components/StrategySelect";
|
|||||||
import HeaderTitle from "@app/components/SettingsSectionTitle";
|
import HeaderTitle from "@app/components/SettingsSectionTitle";
|
||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import { useActionState, useState } from "react";
|
import { useState } from "react";
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -91,7 +91,7 @@ export default function Page() {
|
|||||||
"internal"
|
"internal"
|
||||||
);
|
);
|
||||||
const [inviteLink, setInviteLink] = useState<string | null>(null);
|
const [inviteLink, setInviteLink] = useState<string | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
const [expiresInDays, setExpiresInDays] = useState(1);
|
const [expiresInDays, setExpiresInDays] = useState(1);
|
||||||
const [roles, setRoles] = useState<{ roleId: number; name: string }[]>([]);
|
const [roles, setRoles] = useState<{ roleId: number; name: string }[]>([]);
|
||||||
const [idps, setIdps] = useState<IdpOption[]>([]);
|
const [idps, setIdps] = useState<IdpOption[]>([]);
|
||||||
@@ -311,29 +311,10 @@ export default function Page() {
|
|||||||
setUserOptions(options);
|
setUserOptions(options);
|
||||||
}, [idps, t]);
|
}, [idps, t]);
|
||||||
|
|
||||||
const [, submitInternalAction, isSubmittingInternal] = useActionState(
|
async function onSubmitInternal(
|
||||||
onSubmitInternal,
|
values: z.infer<typeof internalFormSchema>
|
||||||
null
|
) {
|
||||||
);
|
setLoading(true);
|
||||||
const [, submitGoogleAzureAction, isSubmittingGoogleAzure] = useActionState(
|
|
||||||
onSubmitGoogleAzure,
|
|
||||||
null
|
|
||||||
);
|
|
||||||
const [, submitGenericOidcAction, isSubmittingGenericOidc] = useActionState(
|
|
||||||
onSubmitGenericOidc,
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
const loading =
|
|
||||||
isSubmittingInternal ||
|
|
||||||
isSubmittingGoogleAzure ||
|
|
||||||
isSubmittingGenericOidc;
|
|
||||||
|
|
||||||
async function onSubmitInternal() {
|
|
||||||
const isValid = await internalForm.trigger();
|
|
||||||
if (!isValid) return;
|
|
||||||
|
|
||||||
const values = internalForm.getValues();
|
|
||||||
|
|
||||||
const roleIds = values.roles.map((r) => parseInt(r.id, 10));
|
const roleIds = values.roles.map((r) => parseInt(r.id, 10));
|
||||||
|
|
||||||
@@ -376,24 +357,25 @@ export default function Page() {
|
|||||||
|
|
||||||
setExpiresInDays(parseInt(values.validForHours) / 24);
|
setExpiresInDays(parseInt(values.validForHours) / 24);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onSubmitGoogleAzure() {
|
async function onSubmitGoogleAzure(
|
||||||
const isValid = await googleAzureForm.trigger();
|
values: z.infer<typeof googleAzureFormSchema>
|
||||||
if (!isValid) return;
|
) {
|
||||||
|
|
||||||
const values = googleAzureForm.getValues();
|
|
||||||
|
|
||||||
const selectedUserOption = userOptions.find(
|
const selectedUserOption = userOptions.find(
|
||||||
(opt) => opt.id === selectedOption
|
(opt) => opt.id === selectedOption
|
||||||
);
|
);
|
||||||
if (!selectedUserOption?.idpId) return;
|
if (!selectedUserOption?.idpId) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
const roleIds = values.roles.map((r) => parseInt(r.id, 10));
|
const roleIds = values.roles.map((r) => parseInt(r.id, 10));
|
||||||
|
|
||||||
const res = await api
|
const res = await api
|
||||||
.put(`/org/${orgId}/user`, {
|
.put(`/org/${orgId}/user`, {
|
||||||
username: values.email,
|
username: values.email, // Use email as username for Google/Azure
|
||||||
email: values.email || undefined,
|
email: values.email || undefined,
|
||||||
name: values.name,
|
name: values.name,
|
||||||
type: "oidc",
|
type: "oidc",
|
||||||
@@ -419,19 +401,20 @@ export default function Page() {
|
|||||||
});
|
});
|
||||||
router.push(`/${orgId}/settings/access/users`);
|
router.push(`/${orgId}/settings/access/users`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onSubmitGenericOidc() {
|
async function onSubmitGenericOidc(
|
||||||
const isValid = await genericOidcForm.trigger();
|
values: z.infer<typeof genericOidcFormSchema>
|
||||||
if (!isValid) return;
|
) {
|
||||||
|
|
||||||
const values = genericOidcForm.getValues();
|
|
||||||
|
|
||||||
const selectedUserOption = userOptions.find(
|
const selectedUserOption = userOptions.find(
|
||||||
(opt) => opt.id === selectedOption
|
(opt) => opt.id === selectedOption
|
||||||
);
|
);
|
||||||
if (!selectedUserOption?.idpId) return;
|
if (!selectedUserOption?.idpId) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
const roleIds = values.roles.map((r) => parseInt(r.id, 10));
|
const roleIds = values.roles.map((r) => parseInt(r.id, 10));
|
||||||
|
|
||||||
const res = await api
|
const res = await api
|
||||||
@@ -462,6 +445,8 @@ export default function Page() {
|
|||||||
});
|
});
|
||||||
router.push(`/${orgId}/settings/access/users`);
|
router.push(`/${orgId}/settings/access/users`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -528,9 +513,9 @@ export default function Page() {
|
|||||||
<SettingsSectionForm>
|
<SettingsSectionForm>
|
||||||
<Form {...internalForm}>
|
<Form {...internalForm}>
|
||||||
<form
|
<form
|
||||||
action={
|
onSubmit={internalForm.handleSubmit(
|
||||||
submitInternalAction
|
onSubmitInternal
|
||||||
}
|
)}
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
id="create-user-form"
|
id="create-user-form"
|
||||||
>
|
>
|
||||||
@@ -610,7 +595,13 @@ export default function Page() {
|
|||||||
<OrgRolesTagField
|
<OrgRolesTagField
|
||||||
form={internalForm}
|
form={internalForm}
|
||||||
name="roles"
|
name="roles"
|
||||||
orgId={orgId as string}
|
label={t("roles")}
|
||||||
|
placeholder={t(
|
||||||
|
"accessRoleSelect2"
|
||||||
|
)}
|
||||||
|
allRoleOptions={
|
||||||
|
allRoleOptions
|
||||||
|
}
|
||||||
supportsMultipleRolesPerUser={
|
supportsMultipleRolesPerUser={
|
||||||
supportsMultipleRolesPerUser
|
supportsMultipleRolesPerUser
|
||||||
}
|
}
|
||||||
@@ -620,6 +611,13 @@ export default function Page() {
|
|||||||
paywallMessage={
|
paywallMessage={
|
||||||
invitePaywallMessage
|
invitePaywallMessage
|
||||||
}
|
}
|
||||||
|
loading={loading}
|
||||||
|
activeTagIndex={
|
||||||
|
activeInviteRoleTagIndex
|
||||||
|
}
|
||||||
|
setActiveTagIndex={
|
||||||
|
setActiveInviteRoleTagIndex
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{env.email.emailEnabled && (
|
{env.email.emailEnabled && (
|
||||||
@@ -714,9 +712,9 @@ export default function Page() {
|
|||||||
})() && (
|
})() && (
|
||||||
<Form {...googleAzureForm}>
|
<Form {...googleAzureForm}>
|
||||||
<form
|
<form
|
||||||
action={
|
onSubmit={googleAzureForm.handleSubmit(
|
||||||
submitGoogleAzureAction
|
onSubmitGoogleAzure
|
||||||
}
|
)}
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
id="create-user-form"
|
id="create-user-form"
|
||||||
>
|
>
|
||||||
@@ -765,7 +763,13 @@ export default function Page() {
|
|||||||
<OrgRolesTagField
|
<OrgRolesTagField
|
||||||
form={googleAzureForm}
|
form={googleAzureForm}
|
||||||
name="roles"
|
name="roles"
|
||||||
orgId={orgId as string}
|
label={t("roles")}
|
||||||
|
placeholder={t(
|
||||||
|
"accessRoleSelect2"
|
||||||
|
)}
|
||||||
|
allRoleOptions={
|
||||||
|
allRoleOptions
|
||||||
|
}
|
||||||
supportsMultipleRolesPerUser={
|
supportsMultipleRolesPerUser={
|
||||||
supportsMultipleRolesPerUser
|
supportsMultipleRolesPerUser
|
||||||
}
|
}
|
||||||
@@ -775,6 +779,13 @@ export default function Page() {
|
|||||||
paywallMessage={
|
paywallMessage={
|
||||||
invitePaywallMessage
|
invitePaywallMessage
|
||||||
}
|
}
|
||||||
|
loading={loading}
|
||||||
|
activeTagIndex={
|
||||||
|
activeOidcRoleTagIndex
|
||||||
|
}
|
||||||
|
setActiveTagIndex={
|
||||||
|
setActiveOidcRoleTagIndex
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
@@ -797,9 +808,9 @@ export default function Page() {
|
|||||||
})() && (
|
})() && (
|
||||||
<Form {...genericOidcForm}>
|
<Form {...genericOidcForm}>
|
||||||
<form
|
<form
|
||||||
action={
|
onSubmit={genericOidcForm.handleSubmit(
|
||||||
submitGenericOidcAction
|
onSubmitGenericOidc
|
||||||
}
|
)}
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
id="create-user-form"
|
id="create-user-form"
|
||||||
>
|
>
|
||||||
@@ -877,7 +888,13 @@ export default function Page() {
|
|||||||
<OrgRolesTagField
|
<OrgRolesTagField
|
||||||
form={genericOidcForm}
|
form={genericOidcForm}
|
||||||
name="roles"
|
name="roles"
|
||||||
orgId={orgId as string}
|
label={t("roles")}
|
||||||
|
placeholder={t(
|
||||||
|
"accessRoleSelect2"
|
||||||
|
)}
|
||||||
|
allRoleOptions={
|
||||||
|
allRoleOptions
|
||||||
|
}
|
||||||
supportsMultipleRolesPerUser={
|
supportsMultipleRolesPerUser={
|
||||||
supportsMultipleRolesPerUser
|
supportsMultipleRolesPerUser
|
||||||
}
|
}
|
||||||
@@ -887,6 +904,13 @@ export default function Page() {
|
|||||||
paywallMessage={
|
paywallMessage={
|
||||||
invitePaywallMessage
|
invitePaywallMessage
|
||||||
}
|
}
|
||||||
|
loading={loading}
|
||||||
|
activeTagIndex={
|
||||||
|
activeOidcRoleTagIndex
|
||||||
|
}
|
||||||
|
setActiveTagIndex={
|
||||||
|
setActiveOidcRoleTagIndex
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -3,12 +3,10 @@
|
|||||||
import AlertRuleGraphEditor from "@app/components/alert-rule-editor/AlertRuleGraphEditor";
|
import AlertRuleGraphEditor from "@app/components/alert-rule-editor/AlertRuleGraphEditor";
|
||||||
import HeaderTitle from "@app/components/SettingsSectionTitle";
|
import HeaderTitle from "@app/components/SettingsSectionTitle";
|
||||||
import { defaultFormValues } from "@app/lib/alertRuleForm";
|
import { defaultFormValues } from "@app/lib/alertRuleForm";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
|
||||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useEffect } from "react";
|
|
||||||
|
|
||||||
export default function NewAlertRulePage() {
|
export default function NewAlertRulePage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
@@ -16,19 +14,6 @@ export default function NewAlertRulePage() {
|
|||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const { isPaidUser } = usePaidStatus();
|
const { isPaidUser } = usePaidStatus();
|
||||||
const isPaid = isPaidUser(tierMatrix.alertingRules);
|
const isPaid = isPaidUser(tierMatrix.alertingRules);
|
||||||
const { env } = useEnvContext();
|
|
||||||
const router = useRouter();
|
|
||||||
const disableEnterpriseFeatures = env.flags.disableEnterpriseFeatures;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (disableEnterpriseFeatures) {
|
|
||||||
router.replace(`/${orgId}/settings/alerting/rules`);
|
|
||||||
}
|
|
||||||
}, [disableEnterpriseFeatures, orgId, router]);
|
|
||||||
|
|
||||||
if (disableEnterpriseFeatures) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { RolesSelector } from "@app/components/roles-selector";
|
|
||||||
import SetResourceHeaderAuthForm from "@app/components/SetResourceHeaderAuthForm";
|
import SetResourceHeaderAuthForm from "@app/components/SetResourceHeaderAuthForm";
|
||||||
import SetResourcePincodeForm from "@app/components/SetResourcePincodeForm";
|
import SetResourcePincodeForm from "@app/components/SetResourcePincodeForm";
|
||||||
import {
|
import {
|
||||||
@@ -34,7 +33,6 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue
|
SelectValue
|
||||||
} from "@app/components/ui/select";
|
} from "@app/components/ui/select";
|
||||||
import { UsersSelector } from "@app/components/users-selector";
|
|
||||||
import type { ResourceContextType } from "@app/contexts/resourceContext";
|
import type { ResourceContextType } from "@app/contexts/resourceContext";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||||
@@ -182,6 +180,13 @@ export default function ResourceAuthenticationPage() {
|
|||||||
return [];
|
return [];
|
||||||
}, [orgIdps]);
|
}, [orgIdps]);
|
||||||
|
|
||||||
|
const [activeRolesTagIndex, setActiveRolesTagIndex] = useState<
|
||||||
|
number | null
|
||||||
|
>(null);
|
||||||
|
const [activeUsersTagIndex, setActiveUsersTagIndex] = useState<
|
||||||
|
number | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
const [ssoEnabled, setSsoEnabled] = useState(resource.sso ?? false);
|
const [ssoEnabled, setSsoEnabled] = useState(resource.sso ?? false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -492,27 +497,46 @@ export default function ResourceAuthenticationPage() {
|
|||||||
{t("roles")}
|
{t("roles")}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<RolesSelector
|
<TagInput
|
||||||
selectedRoles={
|
{...field}
|
||||||
field.value ??
|
activeTagIndex={
|
||||||
[]
|
activeRolesTagIndex
|
||||||
}
|
}
|
||||||
restrictAdminRole
|
setActiveTagIndex={
|
||||||
orgId={
|
setActiveRolesTagIndex
|
||||||
org.org
|
|
||||||
.orgId
|
|
||||||
}
|
}
|
||||||
onSelectRoles={(
|
placeholder={t(
|
||||||
newUsers
|
"accessRoleSelect2"
|
||||||
|
)}
|
||||||
|
size="sm"
|
||||||
|
tags={
|
||||||
|
usersRolesForm.getValues()
|
||||||
|
.roles
|
||||||
|
}
|
||||||
|
setTags={(
|
||||||
|
newRoles
|
||||||
) => {
|
) => {
|
||||||
usersRolesForm.setValue(
|
usersRolesForm.setValue(
|
||||||
"roles",
|
"roles",
|
||||||
newUsers as [
|
newRoles as [
|
||||||
Tag,
|
Tag,
|
||||||
...Tag[]
|
...Tag[]
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
enableAutocomplete={
|
||||||
|
true
|
||||||
|
}
|
||||||
|
autocompleteOptions={
|
||||||
|
allRoles
|
||||||
|
}
|
||||||
|
allowDuplicates={
|
||||||
|
false
|
||||||
|
}
|
||||||
|
restrictTagsToAutocompleteOptions={
|
||||||
|
true
|
||||||
|
}
|
||||||
|
sortTags={true}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -533,16 +557,23 @@ export default function ResourceAuthenticationPage() {
|
|||||||
{t("users")}
|
{t("users")}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<UsersSelector
|
<TagInput
|
||||||
selectedUsers={
|
{...field}
|
||||||
field.value ??
|
activeTagIndex={
|
||||||
[]
|
activeUsersTagIndex
|
||||||
}
|
}
|
||||||
orgId={
|
setActiveTagIndex={
|
||||||
org.org
|
setActiveUsersTagIndex
|
||||||
.orgId
|
|
||||||
}
|
}
|
||||||
onSelectUsers={(
|
placeholder={t(
|
||||||
|
"accessUserSelect"
|
||||||
|
)}
|
||||||
|
tags={
|
||||||
|
usersRolesForm.getValues()
|
||||||
|
.users
|
||||||
|
}
|
||||||
|
size="sm"
|
||||||
|
setTags={(
|
||||||
newUsers
|
newUsers
|
||||||
) => {
|
) => {
|
||||||
usersRolesForm.setValue(
|
usersRolesForm.setValue(
|
||||||
@@ -553,6 +584,19 @@ export default function ResourceAuthenticationPage() {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
enableAutocomplete={
|
||||||
|
true
|
||||||
|
}
|
||||||
|
autocompleteOptions={
|
||||||
|
allUsers
|
||||||
|
}
|
||||||
|
allowDuplicates={
|
||||||
|
false
|
||||||
|
}
|
||||||
|
restrictTagsToAutocompleteOptions={
|
||||||
|
true
|
||||||
|
}
|
||||||
|
sortTags={true}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@@ -681,9 +681,6 @@ export default function PoliciesPage() {
|
|||||||
control: form.control,
|
control: form.control,
|
||||||
name: "orgMapping"
|
name: "orgMapping"
|
||||||
}}
|
}}
|
||||||
orgId={
|
|
||||||
editingPolicy?.orgId || policyFormOrgId
|
|
||||||
}
|
|
||||||
roleMappingFieldIdPrefix="admin-idp-policy-role"
|
roleMappingFieldIdPrefix="admin-idp-policy-role"
|
||||||
roleMappingMode={policyRoleMappingMode}
|
roleMappingMode={policyRoleMappingMode}
|
||||||
onRoleMappingModeChange={
|
onRoleMappingModeChange={
|
||||||
|
|||||||
@@ -212,22 +212,16 @@ export const orgNavSections = (
|
|||||||
title: "sidebarManagement",
|
title: "sidebarManagement",
|
||||||
icon: <Building2 className="size-4 flex-none" />,
|
icon: <Building2 className="size-4 flex-none" />,
|
||||||
items: [
|
items: [
|
||||||
...(!env?.flags.disableEnterpriseFeatures
|
{
|
||||||
? [
|
title: "sidebarAlerting",
|
||||||
{
|
href: "/{orgId}/settings/alerting",
|
||||||
title: "sidebarAlerting",
|
icon: <BellRing className="size-4 flex-none" />
|
||||||
href: "/{orgId}/settings/alerting",
|
},
|
||||||
icon: (
|
{
|
||||||
<BellRing className="size-4 flex-none" />
|
title: "sidebarProvisioning",
|
||||||
)
|
href: "/{orgId}/settings/provisioning",
|
||||||
},
|
icon: <Boxes className="size-4 flex-none" />
|
||||||
{
|
},
|
||||||
title: "sidebarProvisioning",
|
|
||||||
href: "/{orgId}/settings/provisioning",
|
|
||||||
icon: <Boxes className="size-4 flex-none" />
|
|
||||||
}
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
{
|
{
|
||||||
title: "sidebarBluePrints",
|
title: "sidebarBluePrints",
|
||||||
href: "/{orgId}/settings/blueprints",
|
href: "/{orgId}/settings/blueprints",
|
||||||
|
|||||||
@@ -134,9 +134,7 @@ export default function AlertingRulesTable({
|
|||||||
}: AlertingRulesTableProps) {
|
}: AlertingRulesTableProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const envContext = useEnvContext();
|
const api = createApiClient(useEnvContext());
|
||||||
const api = createApiClient(envContext);
|
|
||||||
const { env } = envContext;
|
|
||||||
const [isRefreshing, startRefresh] = useTransition();
|
const [isRefreshing, startRefresh] = useTransition();
|
||||||
const { isPaidUser } = usePaidStatus();
|
const { isPaidUser } = usePaidStatus();
|
||||||
const isPaid = isPaidUser(tierMatrix.alertingRules);
|
const isPaid = isPaidUser(tierMatrix.alertingRules);
|
||||||
@@ -428,15 +426,9 @@ export default function AlertingRulesTable({
|
|||||||
searchQuery={query}
|
searchQuery={query}
|
||||||
manualFiltering
|
manualFiltering
|
||||||
manualSorting
|
manualSorting
|
||||||
onAdd={
|
onAdd={() => {
|
||||||
!env.flags.disableEnterpriseFeatures
|
router.push(`/${orgId}/settings/alerting/create`);
|
||||||
? () => {
|
}}
|
||||||
router.push(
|
|
||||||
`/${orgId}/settings/alerting/create`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
onRefresh={refreshList}
|
onRefresh={refreshList}
|
||||||
isRefreshing={isRefreshing || isFiltering}
|
isRefreshing={isRefreshing || isFiltering}
|
||||||
addButtonText={t("alertingAddRule")}
|
addButtonText={t("alertingAddRule")}
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ type AutoProvisionConfigWidgetProps = {
|
|||||||
roleMappingFieldIdPrefix?: string;
|
roleMappingFieldIdPrefix?: string;
|
||||||
showFreeformRoleNamesHint?: boolean;
|
showFreeformRoleNamesHint?: boolean;
|
||||||
autoProvisionSwitchId?: string;
|
autoProvisionSwitchId?: string;
|
||||||
orgId?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function AutoProvisionConfigWidget({
|
export default function AutoProvisionConfigWidget({
|
||||||
@@ -68,8 +67,7 @@ export default function AutoProvisionConfigWidget({
|
|||||||
showAutoProvisionSwitch = true,
|
showAutoProvisionSwitch = true,
|
||||||
roleMappingFieldIdPrefix = "org-idp-auto-provision",
|
roleMappingFieldIdPrefix = "org-idp-auto-provision",
|
||||||
showFreeformRoleNamesHint = false,
|
showFreeformRoleNamesHint = false,
|
||||||
autoProvisionSwitchId = "auto-provision-toggle",
|
autoProvisionSwitchId = "auto-provision-toggle"
|
||||||
orgId
|
|
||||||
}: AutoProvisionConfigWidgetProps) {
|
}: AutoProvisionConfigWidgetProps) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const { isPaidUser } = usePaidStatus();
|
const { isPaidUser } = usePaidStatus();
|
||||||
@@ -108,7 +106,6 @@ export default function AutoProvisionConfigWidget({
|
|||||||
showFreeformRoleNamesHint={
|
showFreeformRoleNamesHint={
|
||||||
showFreeformRoleNamesHint
|
showFreeformRoleNamesHint
|
||||||
}
|
}
|
||||||
orgId={orgId}
|
|
||||||
roleMappingMode={roleMappingMode}
|
roleMappingMode={roleMappingMode}
|
||||||
onRoleMappingModeChange={onRoleMappingModeChange}
|
onRoleMappingModeChange={onRoleMappingModeChange}
|
||||||
roles={roles}
|
roles={roles}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ const CredenzaContent = ({ className, children, ...props }: CredenzaProps) => {
|
|||||||
return (
|
return (
|
||||||
<CredenzaContent
|
<CredenzaContent
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex min-h-0 max-h-[100dvh] flex-col overflow-y-auto md:top-[clamp(1.5rem,12vh,200px)] md:max-h-[calc(100vh-clamp(3rem,24vh,400px))] md:translate-y-0",
|
"flex min-h-0 max-h-[100dvh] flex-col overflow-hidden md:top-[clamp(1.5rem,12vh,200px)] md:max-h-[calc(100vh-clamp(3rem,24vh,400px))] md:translate-y-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -40,12 +40,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
import { UserType } from "@server/types/UserTypes";
|
import { UserType } from "@server/types/UserTypes";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import {
|
import { ChevronsUpDown, ExternalLink } from "lucide-react";
|
||||||
ArrowDownIcon,
|
|
||||||
ChevronDownIcon,
|
|
||||||
ChevronsUpDown,
|
|
||||||
ExternalLink
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
@@ -55,13 +50,11 @@ import {
|
|||||||
formatMultiSitesSelectorLabel
|
formatMultiSitesSelectorLabel
|
||||||
} from "./multi-site-selector";
|
} from "./multi-site-selector";
|
||||||
import type { Selectedsite } from "./site-selector";
|
import type { Selectedsite } from "./site-selector";
|
||||||
|
import { CaretSortIcon } from "@radix-ui/react-icons";
|
||||||
import { MachinesSelector } from "./machines-selector";
|
import { MachinesSelector } from "./machines-selector";
|
||||||
import DomainPicker from "@app/components/DomainPicker";
|
import DomainPicker from "@app/components/DomainPicker";
|
||||||
import { SwitchInput } from "@app/components/SwitchInput";
|
import { SwitchInput } from "@app/components/SwitchInput";
|
||||||
import CertificateStatus from "@app/components/CertificateStatus";
|
import CertificateStatus from "@app/components/CertificateStatus";
|
||||||
import { UsersSelector } from "./users-selector";
|
|
||||||
import { RolesSelector } from "./roles-selector";
|
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
|
|
||||||
// --- Helpers (shared) ---
|
// --- Helpers (shared) ---
|
||||||
@@ -840,16 +833,12 @@ export function InternalResourceForm({
|
|||||||
modeCidrKey
|
modeCidrKey
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
...(!disableEnterpriseFeatures
|
{
|
||||||
? [
|
value: "http",
|
||||||
{
|
label: t(
|
||||||
value: "http" as const,
|
modeHttpKey
|
||||||
label: t(
|
)
|
||||||
modeHttpKey
|
}
|
||||||
)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
: [])
|
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
@@ -1495,22 +1484,40 @@ export function InternalResourceForm({
|
|||||||
<FormItem className="flex flex-col items-start">
|
<FormItem className="flex flex-col items-start">
|
||||||
<FormLabel>{t("roles")}</FormLabel>
|
<FormLabel>{t("roles")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<RolesSelector
|
<TagInput
|
||||||
selectedRoles={
|
{...field}
|
||||||
field.value ?? []
|
activeTagIndex={
|
||||||
|
activeRolesTagIndex
|
||||||
}
|
}
|
||||||
orgId={orgId}
|
setActiveTagIndex={
|
||||||
onSelectRoles={(
|
setActiveRolesTagIndex
|
||||||
newUsers
|
}
|
||||||
) => {
|
placeholder={t(
|
||||||
|
"accessRoleSelect2"
|
||||||
|
)}
|
||||||
|
size="sm"
|
||||||
|
tags={
|
||||||
|
form.getValues()
|
||||||
|
.roles ?? []
|
||||||
|
}
|
||||||
|
setTags={(newRoles) =>
|
||||||
form.setValue(
|
form.setValue(
|
||||||
"roles",
|
"roles",
|
||||||
newUsers as [
|
newRoles as [
|
||||||
Tag,
|
Tag,
|
||||||
...Tag[]
|
...Tag[]
|
||||||
]
|
]
|
||||||
);
|
)
|
||||||
}}
|
}
|
||||||
|
enableAutocomplete
|
||||||
|
autocompleteOptions={
|
||||||
|
allRoles
|
||||||
|
}
|
||||||
|
allowDuplicates={false}
|
||||||
|
restrictTagsToAutocompleteOptions={
|
||||||
|
true
|
||||||
|
}
|
||||||
|
sortTags={true}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -1523,21 +1530,43 @@ export function InternalResourceForm({
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex flex-col items-start">
|
<FormItem className="flex flex-col items-start">
|
||||||
<FormLabel>{t("users")}</FormLabel>
|
<FormLabel>{t("users")}</FormLabel>
|
||||||
<UsersSelector
|
<FormControl>
|
||||||
selectedUsers={
|
<TagInput
|
||||||
field.value ?? []
|
{...field}
|
||||||
}
|
activeTagIndex={
|
||||||
orgId={orgId}
|
activeUsersTagIndex
|
||||||
onSelectUsers={(newUsers) => {
|
}
|
||||||
form.setValue(
|
setActiveTagIndex={
|
||||||
"users",
|
setActiveUsersTagIndex
|
||||||
newUsers as [
|
}
|
||||||
Tag,
|
placeholder={t(
|
||||||
...Tag[]
|
"accessUserSelect"
|
||||||
]
|
)}
|
||||||
);
|
tags={
|
||||||
}}
|
form.getValues()
|
||||||
/>
|
.users ?? []
|
||||||
|
}
|
||||||
|
size="sm"
|
||||||
|
setTags={(newUsers) =>
|
||||||
|
form.setValue(
|
||||||
|
"users",
|
||||||
|
newUsers as [
|
||||||
|
Tag,
|
||||||
|
...Tag[]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
enableAutocomplete={true}
|
||||||
|
autocompleteOptions={
|
||||||
|
allUsers
|
||||||
|
}
|
||||||
|
allowDuplicates={false}
|
||||||
|
restrictTagsToAutocompleteOptions={
|
||||||
|
true
|
||||||
|
}
|
||||||
|
sortTags={true}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@@ -1551,20 +1580,73 @@ export function InternalResourceForm({
|
|||||||
<FormLabel>
|
<FormLabel>
|
||||||
{t("machineClients")}
|
{t("machineClients")}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<MachinesSelector
|
<Popover>
|
||||||
selectedMachines={
|
<PopoverTrigger asChild>
|
||||||
field.value ?? []
|
<FormControl>
|
||||||
}
|
<Button
|
||||||
orgId={orgId}
|
variant="outline"
|
||||||
onSelectMachines={(
|
role="combobox"
|
||||||
machines
|
className={cn(
|
||||||
) => {
|
"justify-between w-full",
|
||||||
form.setValue(
|
"text-muted-foreground pl-1.5"
|
||||||
"clients",
|
)}
|
||||||
machines
|
>
|
||||||
);
|
<span
|
||||||
}}
|
className={cn(
|
||||||
/>
|
"inline-flex items-center gap-1",
|
||||||
|
"overflow-x-auto"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{(
|
||||||
|
field.value ??
|
||||||
|
[]
|
||||||
|
).map(
|
||||||
|
(
|
||||||
|
client
|
||||||
|
) => (
|
||||||
|
<span
|
||||||
|
key={
|
||||||
|
client.clientId
|
||||||
|
}
|
||||||
|
className={cn(
|
||||||
|
"bg-muted-foreground/20 font-normal text-foreground rounded-sm",
|
||||||
|
"py-1 px-1.5 text-xs"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
client.name
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
<span className="pl-1 font-normal">
|
||||||
|
{t(
|
||||||
|
"accessClientSelect"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</FormControl>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="p-0">
|
||||||
|
<MachinesSelector
|
||||||
|
selectedMachines={
|
||||||
|
field.value ??
|
||||||
|
[]
|
||||||
|
}
|
||||||
|
orgId={orgId}
|
||||||
|
onSelectMachines={(
|
||||||
|
machines
|
||||||
|
) => {
|
||||||
|
form.setValue(
|
||||||
|
"clients",
|
||||||
|
machines
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -129,7 +129,9 @@ export function LayoutSidebar({
|
|||||||
user.serverAdmin || Boolean(currentOrg?.isOwner || currentOrg?.isAdmin);
|
user.serverAdmin || Boolean(currentOrg?.isOwner || currentOrg?.isAdmin);
|
||||||
|
|
||||||
const showTrial =
|
const showTrial =
|
||||||
build === "saas" && Boolean(orgId) && subscriptionContext?.isTrial;
|
build === "saas" &&
|
||||||
|
Boolean(orgId) &&
|
||||||
|
subscriptionContext?.isTrial;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -238,16 +240,11 @@ export function LayoutSidebar({
|
|||||||
<div className="px-4">
|
<div className="px-4">
|
||||||
<ProductUpdates isCollapsed={isSidebarCollapsed} />
|
<ProductUpdates isCollapsed={isSidebarCollapsed} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : <div className="mt-0.2"></div>}
|
||||||
<div className="mt-0.2"></div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showTrial && (
|
{showTrial && (
|
||||||
<div className="px-4">
|
<div className="px-4">
|
||||||
<ShowTrialCard
|
<ShowTrialCard isCollapsed={isSidebarCollapsed} />
|
||||||
isCollapsed={isSidebarCollapsed}
|
|
||||||
isOwner={Boolean(currentOrg?.isOwner)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger
|
TooltipTrigger
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import CopyToClipboard from "@app/components/CopyToClipboard";
|
|
||||||
|
|
||||||
// Update Resource type to include site information
|
// Update Resource type to include site information
|
||||||
type Resource = {
|
type Resource = {
|
||||||
@@ -65,8 +64,6 @@ type SiteResource = {
|
|||||||
destination: string;
|
destination: string;
|
||||||
mode: string;
|
mode: string;
|
||||||
protocol: string | null;
|
protocol: string | null;
|
||||||
ssl: boolean;
|
|
||||||
fullDomain: string | null;
|
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
alias: string | null;
|
alias: string | null;
|
||||||
aliasAddress: string | null;
|
aliasAddress: string | null;
|
||||||
@@ -126,7 +123,6 @@ const ResourceFavicon = ({
|
|||||||
|
|
||||||
// Resource Info component
|
// Resource Info component
|
||||||
const ResourceInfo = ({ resource }: { resource: Resource }) => {
|
const ResourceInfo = ({ resource }: { resource: Resource }) => {
|
||||||
const t = useTranslations();
|
|
||||||
const hasAuthMethods =
|
const hasAuthMethods =
|
||||||
resource.sso ||
|
resource.sso ||
|
||||||
resource.password ||
|
resource.password ||
|
||||||
@@ -145,9 +141,7 @@ const ResourceInfo = ({ resource }: { resource: Resource }) => {
|
|||||||
{/* Site Information */}
|
{/* Site Information */}
|
||||||
{resource.siteName && (
|
{resource.siteName && (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs font-medium mb-1.5">
|
<div className="text-xs font-medium mb-1.5">Site</div>
|
||||||
{t("site")}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Combine className="h-4 w-4 text-foreground shrink-0" />
|
<Combine className="h-4 w-4 text-foreground shrink-0" />
|
||||||
<span className="text-sm">{resource.siteName}</span>
|
<span className="text-sm">{resource.siteName}</span>
|
||||||
@@ -163,7 +157,7 @@ const ResourceInfo = ({ resource }: { resource: Resource }) => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="text-xs font-medium mb-1.5">
|
<div className="text-xs font-medium mb-1.5">
|
||||||
{t("memberPortalAuthMethods")}
|
Authentication Methods
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
{resource.sso && (
|
{resource.sso && (
|
||||||
@@ -172,7 +166,7 @@ const ResourceInfo = ({ resource }: { resource: Resource }) => {
|
|||||||
<Key className="h-3 w-3 text-blue-700 dark:text-blue-300" />
|
<Key className="h-3 w-3 text-blue-700 dark:text-blue-300" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm">
|
<span className="text-sm">
|
||||||
{t("memberPortalSso")}
|
Single Sign-On (SSO)
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -182,7 +176,7 @@ const ResourceInfo = ({ resource }: { resource: Resource }) => {
|
|||||||
<KeyRound className="h-3 w-3 text-purple-700 dark:text-purple-300" />
|
<KeyRound className="h-3 w-3 text-purple-700 dark:text-purple-300" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm">
|
<span className="text-sm">
|
||||||
{t("memberPortalPasswordProtected")}
|
Password Protected
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -191,9 +185,7 @@ const ResourceInfo = ({ resource }: { resource: Resource }) => {
|
|||||||
<div className="h-5 w-5 rounded-full flex items-center justify-center bg-emerald-50/50 dark:bg-emerald-950/50">
|
<div className="h-5 w-5 rounded-full flex items-center justify-center bg-emerald-50/50 dark:bg-emerald-950/50">
|
||||||
<Fingerprint className="h-3 w-3 text-emerald-700 dark:text-emerald-300" />
|
<Fingerprint className="h-3 w-3 text-emerald-700 dark:text-emerald-300" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm">
|
<span className="text-sm">PIN Code</span>
|
||||||
{t("memberPortalPinCode")}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{resource.whitelist && (
|
{resource.whitelist && (
|
||||||
@@ -201,9 +193,7 @@ const ResourceInfo = ({ resource }: { resource: Resource }) => {
|
|||||||
<div className="h-5 w-5 rounded-full flex items-center justify-center bg-amber-50/50 dark:bg-amber-950/50">
|
<div className="h-5 w-5 rounded-full flex items-center justify-center bg-amber-50/50 dark:bg-amber-950/50">
|
||||||
<AtSign className="h-3 w-3 text-amber-700 dark:text-amber-300" />
|
<AtSign className="h-3 w-3 text-amber-700 dark:text-amber-300" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm">
|
<span className="text-sm">Email Whitelist</span>
|
||||||
{t("memberPortalEmailWhitelist")}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -218,7 +208,7 @@ const ResourceInfo = ({ resource }: { resource: Resource }) => {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<AlertCircle className="h-4 w-4 text-destructive shrink-0" />
|
<AlertCircle className="h-4 w-4 text-destructive shrink-0" />
|
||||||
<span className="text-sm text-destructive">
|
<span className="text-sm text-destructive">
|
||||||
{t("memberPortalResourceDisabled")}
|
Resource Disabled
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -243,7 +233,6 @@ const PaginationControls = ({
|
|||||||
totalItems: number;
|
totalItems: number;
|
||||||
itemsPerPage: number;
|
itemsPerPage: number;
|
||||||
}) => {
|
}) => {
|
||||||
const t = useTranslations();
|
|
||||||
const startItem = (currentPage - 1) * itemsPerPage + 1;
|
const startItem = (currentPage - 1) * itemsPerPage + 1;
|
||||||
const endItem = Math.min(currentPage * itemsPerPage, totalItems);
|
const endItem = Math.min(currentPage * itemsPerPage, totalItems);
|
||||||
|
|
||||||
@@ -252,11 +241,7 @@ const PaginationControls = ({
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 mt-8">
|
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 mt-8">
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
{t("memberPortalShowingResources", {
|
Showing {startItem}-{endItem} of {totalItems} resources
|
||||||
start: startItem,
|
|
||||||
end: endItem,
|
|
||||||
total: totalItems
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -268,7 +253,7 @@ const PaginationControls = ({
|
|||||||
className="gap-1"
|
className="gap-1"
|
||||||
>
|
>
|
||||||
<ChevronLeft className="h-4 w-4" />
|
<ChevronLeft className="h-4 w-4" />
|
||||||
{t("memberPortalPrevious")}
|
Previous
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
@@ -324,7 +309,7 @@ const PaginationControls = ({
|
|||||||
disabled={currentPage === totalPages}
|
disabled={currentPage === totalPages}
|
||||||
className="gap-1"
|
className="gap-1"
|
||||||
>
|
>
|
||||||
{t("memberPortalNext")}
|
Next
|
||||||
<ChevronRight className="h-4 w-4" />
|
<ChevronRight className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -404,11 +389,13 @@ export default function MemberResourcesPortal({
|
|||||||
response.data.data.siteResources || []
|
response.data.data.siteResources || []
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
setError(t("memberPortalFailedToLoad"));
|
setError("Failed to load resources");
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error fetching user resources:", err);
|
console.error("Error fetching user resources:", err);
|
||||||
setError(t("memberPortalFailedToLoadDescription"));
|
setError(
|
||||||
|
"Failed to load resources. Please check your connection and try again."
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setRefreshing(false);
|
setRefreshing(false);
|
||||||
@@ -539,8 +526,8 @@ export default function MemberResourcesPortal({
|
|||||||
return (
|
return (
|
||||||
<div className="container mx-auto max-w-12xl">
|
<div className="container mx-auto max-w-12xl">
|
||||||
<SettingsSectionTitle
|
<SettingsSectionTitle
|
||||||
title={t("memberPortalTitle")}
|
title="Resources"
|
||||||
description={t("memberPortalDescription")}
|
description="Resources you have access to in this organization"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Search and Sort Controls - Skeleton */}
|
{/* Search and Sort Controls - Skeleton */}
|
||||||
@@ -567,8 +554,8 @@ export default function MemberResourcesPortal({
|
|||||||
return (
|
return (
|
||||||
<div className="container mx-auto max-w-12xl">
|
<div className="container mx-auto max-w-12xl">
|
||||||
<SettingsSectionTitle
|
<SettingsSectionTitle
|
||||||
title={t("memberPortalTitle")}
|
title="Resources"
|
||||||
description={t("memberPortalDescription")}
|
description="Resources you have access to in this organization"
|
||||||
/>
|
/>
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="flex flex-col items-center justify-center py-20 text-center">
|
<CardContent className="flex flex-col items-center justify-center py-20 text-center">
|
||||||
@@ -576,7 +563,7 @@ export default function MemberResourcesPortal({
|
|||||||
<AlertCircle className="h-16 w-16 text-destructive/60" />
|
<AlertCircle className="h-16 w-16 text-destructive/60" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-xl font-semibold text-foreground mb-3">
|
<h3 className="text-xl font-semibold text-foreground mb-3">
|
||||||
{t("memberPortalUnableToLoad")}
|
Unable to Load Resources
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-muted-foreground max-w-lg text-base mb-6">
|
<p className="text-muted-foreground max-w-lg text-base mb-6">
|
||||||
{error}
|
{error}
|
||||||
@@ -587,7 +574,7 @@ export default function MemberResourcesPortal({
|
|||||||
className="gap-2"
|
className="gap-2"
|
||||||
>
|
>
|
||||||
<RefreshCw className="h-4 w-4" />
|
<RefreshCw className="h-4 w-4" />
|
||||||
{t("memberPortalTryAgain")}
|
Try Again
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -598,8 +585,8 @@ export default function MemberResourcesPortal({
|
|||||||
return (
|
return (
|
||||||
<div className="container mx-auto max-w-12xl">
|
<div className="container mx-auto max-w-12xl">
|
||||||
<SettingsSectionTitle
|
<SettingsSectionTitle
|
||||||
title={t("memberPortalTitle")}
|
title="Resources"
|
||||||
description={t("memberPortalDescription")}
|
description="Resources you have access to in this organization"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Search and Sort Controls with Refresh */}
|
{/* Search and Sort Controls with Refresh */}
|
||||||
@@ -608,7 +595,7 @@ export default function MemberResourcesPortal({
|
|||||||
{/* Search */}
|
{/* Search */}
|
||||||
<div className="relative w-full sm:w-80">
|
<div className="relative w-full sm:w-80">
|
||||||
<Input
|
<Input
|
||||||
placeholder={t("resourcesSearch")}
|
placeholder="Search resources..."
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
className="w-full pl-8 bg-card"
|
className="w-full pl-8 bg-card"
|
||||||
@@ -620,28 +607,26 @@ export default function MemberResourcesPortal({
|
|||||||
<div className="w-full sm:w-36">
|
<div className="w-full sm:w-36">
|
||||||
<Select value={sortBy} onValueChange={setSortBy}>
|
<Select value={sortBy} onValueChange={setSortBy}>
|
||||||
<SelectTrigger className="bg-card">
|
<SelectTrigger className="bg-card">
|
||||||
<SelectValue
|
<SelectValue placeholder="Sort by..." />
|
||||||
placeholder={t("memberPortalSortBy")}
|
|
||||||
/>
|
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="name-asc">
|
<SelectItem value="name-asc">
|
||||||
{t("memberPortalSortNameAsc")}
|
Name A-Z
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="name-desc">
|
<SelectItem value="name-desc">
|
||||||
{t("memberPortalSortNameDesc")}
|
Name Z-A
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="domain-asc">
|
<SelectItem value="domain-asc">
|
||||||
{t("memberPortalSortDomainAsc")}
|
Domain A-Z
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="domain-desc">
|
<SelectItem value="domain-desc">
|
||||||
{t("memberPortalSortDomainDesc")}
|
Domain Z-A
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="status-enabled">
|
<SelectItem value="status-enabled">
|
||||||
{t("memberPortalSortEnabledFirst")}
|
Enabled First
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="status-disabled">
|
<SelectItem value="status-disabled">
|
||||||
{t("memberPortalSortDisabledFirst")}
|
Disabled First
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
@@ -659,7 +644,7 @@ export default function MemberResourcesPortal({
|
|||||||
<RefreshCw
|
<RefreshCw
|
||||||
className={`h-4 w-4 ${refreshing ? "animate-spin" : ""}`}
|
className={`h-4 w-4 ${refreshing ? "animate-spin" : ""}`}
|
||||||
/>
|
/>
|
||||||
{t("memberPortalRefresh")}
|
Refresh
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -678,15 +663,13 @@ export default function MemberResourcesPortal({
|
|||||||
</div>
|
</div>
|
||||||
<h3 className="text-2xl font-semibold text-foreground mb-3">
|
<h3 className="text-2xl font-semibold text-foreground mb-3">
|
||||||
{searchQuery
|
{searchQuery
|
||||||
? t("memberPortalNoResourcesFound")
|
? "No Resources Found"
|
||||||
: t("memberPortalNoResourcesAvailable")}
|
: "No Resources Available"}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-muted-foreground max-w-lg text-base mb-6">
|
<p className="text-muted-foreground max-w-lg text-base mb-6">
|
||||||
{searchQuery
|
{searchQuery
|
||||||
? t("memberPortalNoResourcesMatchSearch", {
|
? `No resources match "${searchQuery}". Try adjusting your search terms or clearing the search to see all resources.`
|
||||||
query: searchQuery
|
: "You don't have access to any resources yet. Contact your administrator to get access to resources you need."}
|
||||||
})
|
|
||||||
: t("memberPortalNoResourcesAccess")}
|
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-col sm:flex-row gap-3">
|
<div className="flex flex-col sm:flex-row gap-3">
|
||||||
{searchQuery ? (
|
{searchQuery ? (
|
||||||
@@ -695,7 +678,7 @@ export default function MemberResourcesPortal({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
className="gap-2"
|
className="gap-2"
|
||||||
>
|
>
|
||||||
{t("memberPortalClearSearch")}
|
Clear Search
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
@@ -707,7 +690,7 @@ export default function MemberResourcesPortal({
|
|||||||
<RefreshCw
|
<RefreshCw
|
||||||
className={`h-4 w-4 ${refreshing ? "animate-spin" : ""}`}
|
className={`h-4 w-4 ${refreshing ? "animate-spin" : ""}`}
|
||||||
/>
|
/>
|
||||||
{t("memberPortalRefreshResources")}
|
Refresh Resources
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -721,12 +704,11 @@ export default function MemberResourcesPortal({
|
|||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<h3 className="text-lg font-semibold text-foreground flex items-center gap-2">
|
<h3 className="text-lg font-semibold text-foreground flex items-center gap-2">
|
||||||
<Globe className="h-5 w-5" />
|
<Globe className="h-5 w-5" />
|
||||||
{t("memberPortalPublicResources")}
|
Public Resources
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-muted-foreground mt-1">
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
{t(
|
Web applications and services accessible via
|
||||||
"memberPortalPublicResourcesDescription"
|
browser
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-5 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 2xl:grid-cols-4 auto-cols-fr mb-8">
|
<div className="grid gap-5 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 2xl:grid-cols-4 auto-cols-fr mb-8">
|
||||||
@@ -786,12 +768,9 @@ export default function MemberResourcesPortal({
|
|||||||
resource.domain
|
resource.domain
|
||||||
);
|
);
|
||||||
toast({
|
toast({
|
||||||
title: t(
|
title: "Copied to clipboard",
|
||||||
"memberPortalCopiedToClipboard"
|
description:
|
||||||
),
|
"Resource URL has been copied to your clipboard.",
|
||||||
description: t(
|
|
||||||
"memberPortalCopiedUrlDescription"
|
|
||||||
),
|
|
||||||
duration: 2000
|
duration: 2000
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -812,7 +791,7 @@ export default function MemberResourcesPortal({
|
|||||||
disabled={!resource.enabled}
|
disabled={!resource.enabled}
|
||||||
>
|
>
|
||||||
<ExternalLink className="h-3.5 w-3.5 mr-2" />
|
<ExternalLink className="h-3.5 w-3.5 mr-2" />
|
||||||
{t("memberPortalOpenResource")}
|
Open Resource
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -827,12 +806,11 @@ export default function MemberResourcesPortal({
|
|||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<h3 className="text-lg font-semibold text-foreground flex items-center gap-2">
|
<h3 className="text-lg font-semibold text-foreground flex items-center gap-2">
|
||||||
<Combine className="h-5 w-5" />
|
<Combine className="h-5 w-5" />
|
||||||
{t("memberPortalPrivateResources")}
|
Private Resources
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-muted-foreground mt-1">
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
{t(
|
Internal network resources accessible via
|
||||||
"memberPortalPrivateResourcesDescription"
|
client
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-5 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 2xl:grid-cols-4 auto-cols-fr mb-8">
|
<div className="grid gap-5 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 2xl:grid-cols-4 auto-cols-fr mb-8">
|
||||||
@@ -865,16 +843,11 @@ export default function MemberResourcesPortal({
|
|||||||
<InfoPopup>
|
<InfoPopup>
|
||||||
<div className="space-y-2 text-sm">
|
<div className="space-y-2 text-sm">
|
||||||
<div className="text-xs font-medium mb-1.5">
|
<div className="text-xs font-medium mb-1.5">
|
||||||
{t(
|
Resource Details
|
||||||
"memberPortalResourceDetails"
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{t(
|
Mode:
|
||||||
"memberPortalMode"
|
|
||||||
)}
|
|
||||||
:
|
|
||||||
</span>
|
</span>
|
||||||
<span className="ml-2 text-muted-foreground capitalize">
|
<span className="ml-2 text-muted-foreground capitalize">
|
||||||
{
|
{
|
||||||
@@ -885,10 +858,7 @@ export default function MemberResourcesPortal({
|
|||||||
{siteResource.protocol && (
|
{siteResource.protocol && (
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{t(
|
Protocol:
|
||||||
"protocol"
|
|
||||||
)}
|
|
||||||
:
|
|
||||||
</span>
|
</span>
|
||||||
<span className="ml-2 text-muted-foreground uppercase">
|
<span className="ml-2 text-muted-foreground uppercase">
|
||||||
{
|
{
|
||||||
@@ -899,10 +869,7 @@ export default function MemberResourcesPortal({
|
|||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{t(
|
Destination:
|
||||||
"memberPortalDestination"
|
|
||||||
)}
|
|
||||||
:
|
|
||||||
</span>
|
</span>
|
||||||
<span className="ml-2 text-muted-foreground">
|
<span className="ml-2 text-muted-foreground">
|
||||||
{
|
{
|
||||||
@@ -913,10 +880,7 @@ export default function MemberResourcesPortal({
|
|||||||
{siteResource.alias && (
|
{siteResource.alias && (
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{t(
|
Alias:
|
||||||
"memberPortalAlias"
|
|
||||||
)}
|
|
||||||
:
|
|
||||||
</span>
|
</span>
|
||||||
<span className="ml-2 text-muted-foreground">
|
<span className="ml-2 text-muted-foreground">
|
||||||
{
|
{
|
||||||
@@ -927,21 +891,14 @@ export default function MemberResourcesPortal({
|
|||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{t(
|
Status:
|
||||||
"status"
|
|
||||||
)}
|
|
||||||
:
|
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className={`ml-2 ${siteResource.enabled ? "text-green-600" : "text-red-600"}`}
|
className={`ml-2 ${siteResource.enabled ? "text-green-600" : "text-red-600"}`}
|
||||||
>
|
>
|
||||||
{siteResource.enabled
|
{siteResource.enabled
|
||||||
? t(
|
? "Enabled"
|
||||||
"enabled"
|
: "Disabled"}
|
||||||
)
|
|
||||||
: t(
|
|
||||||
"disabled"
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -950,14 +907,7 @@ export default function MemberResourcesPortal({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
{siteResource.mode === "http" &&
|
{siteResource.alias ? (
|
||||||
siteResource.fullDomain ? (
|
|
||||||
/* HTTP mode - show as clickable link */
|
|
||||||
<CopyToClipboard
|
|
||||||
text={`${siteResource.ssl ? "https" : (siteResource.protocol ?? "http")}://${siteResource.fullDomain}`}
|
|
||||||
isLink={true}
|
|
||||||
/>
|
|
||||||
) : siteResource.alias ? (
|
|
||||||
<>
|
<>
|
||||||
{/* Alias as primary */}
|
{/* Alias as primary */}
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
@@ -975,13 +925,9 @@ export default function MemberResourcesPortal({
|
|||||||
siteResource.alias!
|
siteResource.alias!
|
||||||
);
|
);
|
||||||
toast({
|
toast({
|
||||||
title: t(
|
title: "Copied to clipboard",
|
||||||
"memberPortalCopiedToClipboard"
|
|
||||||
),
|
|
||||||
description:
|
description:
|
||||||
t(
|
"Resource alias has been copied to your clipboard.",
|
||||||
"memberPortalCopiedAliasDescription"
|
|
||||||
),
|
|
||||||
duration: 2000
|
duration: 2000
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -1013,13 +959,9 @@ export default function MemberResourcesPortal({
|
|||||||
siteResource.destination
|
siteResource.destination
|
||||||
);
|
);
|
||||||
toast({
|
toast({
|
||||||
title: t(
|
title: "Copied to clipboard",
|
||||||
"memberPortalCopiedToClipboard"
|
|
||||||
),
|
|
||||||
description:
|
description:
|
||||||
t(
|
"Resource destination has been copied to your clipboard.",
|
||||||
"memberPortalCopiedDestinationDescription"
|
|
||||||
),
|
|
||||||
duration: 2000
|
duration: 2000
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -1031,34 +973,10 @@ export default function MemberResourcesPortal({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-6 pt-0 mt-auto space-y-2">
|
<div className="p-6 pt-0 mt-auto">
|
||||||
{siteResource.mode === "http" &&
|
|
||||||
siteResource.fullDomain ? (
|
|
||||||
<Button
|
|
||||||
onClick={() =>
|
|
||||||
window.open(
|
|
||||||
`${siteResource.ssl ? "https" : (siteResource.protocol ?? "http")}://${siteResource.fullDomain}`,
|
|
||||||
"_blank"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="w-full h-9"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
disabled={
|
|
||||||
!siteResource.enabled
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ExternalLink className="h-3.5 w-3.5 mr-2" />
|
|
||||||
{t(
|
|
||||||
"memberPortalOpenResource"
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
<div className="flex items-center justify-center py-2 px-4 bg-muted/50 rounded text-sm text-muted-foreground">
|
<div className="flex items-center justify-center py-2 px-4 bg-muted/50 rounded text-sm text-muted-foreground">
|
||||||
<Combine className="h-3.5 w-3.5 mr-2" />
|
<Combine className="h-3.5 w-3.5 mr-2" />
|
||||||
{t(
|
Requires Client Connection
|
||||||
"memberPortalRequiresClientConnection"
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -8,42 +8,51 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage
|
FormMessage
|
||||||
} from "@app/components/ui/form";
|
} from "@app/components/ui/form";
|
||||||
|
import { Tag, TagInput } from "@app/components/tags/tag-input";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
import type { FieldValues, Path, UseFormReturn } from "react-hook-form";
|
import type { FieldValues, Path, UseFormReturn } from "react-hook-form";
|
||||||
import { RolesSelector, type SelectedRole } from "./roles-selector";
|
|
||||||
|
export type RoleTag = {
|
||||||
|
id: string;
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
|
||||||
type OrgRolesTagFieldProps<TFieldValues extends FieldValues> = {
|
type OrgRolesTagFieldProps<TFieldValues extends FieldValues> = {
|
||||||
form: Pick<
|
form: Pick<UseFormReturn<TFieldValues>, "control" | "getValues" | "setValue">;
|
||||||
UseFormReturn<TFieldValues>,
|
|
||||||
"control" | "getValues" | "setValue"
|
|
||||||
>;
|
|
||||||
orgId: string;
|
|
||||||
/** Field in the form that holds Tag[] (role tags). Default: `"roles"`. */
|
/** Field in the form that holds Tag[] (role tags). Default: `"roles"`. */
|
||||||
name?: Path<TFieldValues>;
|
name?: Path<TFieldValues>;
|
||||||
label?: string;
|
label: string;
|
||||||
|
placeholder: string;
|
||||||
|
allRoleOptions: Tag[];
|
||||||
supportsMultipleRolesPerUser: boolean;
|
supportsMultipleRolesPerUser: boolean;
|
||||||
showMultiRolePaywallMessage: boolean;
|
showMultiRolePaywallMessage: boolean;
|
||||||
paywallMessage: string;
|
paywallMessage: string;
|
||||||
disabled?: boolean;
|
loading?: boolean;
|
||||||
|
activeTagIndex: number | null;
|
||||||
|
setActiveTagIndex: Dispatch<SetStateAction<number | null>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function OrgRolesTagField<TFieldValues extends FieldValues>({
|
export default function OrgRolesTagField<TFieldValues extends FieldValues>({
|
||||||
form,
|
form,
|
||||||
name = "roles" as Path<TFieldValues>,
|
name = "roles" as Path<TFieldValues>,
|
||||||
label,
|
label,
|
||||||
orgId,
|
placeholder,
|
||||||
|
allRoleOptions,
|
||||||
supportsMultipleRolesPerUser,
|
supportsMultipleRolesPerUser,
|
||||||
showMultiRolePaywallMessage,
|
showMultiRolePaywallMessage,
|
||||||
paywallMessage,
|
paywallMessage,
|
||||||
disabled
|
loading = false,
|
||||||
|
activeTagIndex,
|
||||||
|
setActiveTagIndex
|
||||||
}: OrgRolesTagFieldProps<TFieldValues>) {
|
}: OrgRolesTagFieldProps<TFieldValues>) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
function setRoleTags(nextValue: SelectedRole[]) {
|
function setRoleTags(updater: Tag[] | ((prev: Tag[]) => Tag[])) {
|
||||||
const prev = form.getValues(name) as SelectedRole[];
|
const prev = form.getValues(name) as Tag[];
|
||||||
|
const nextValue =
|
||||||
|
typeof updater === "function" ? updater(prev) : updater;
|
||||||
const next = supportsMultipleRolesPerUser
|
const next = supportsMultipleRolesPerUser
|
||||||
? nextValue
|
? nextValue
|
||||||
: nextValue.length > 1
|
: nextValue.length > 1
|
||||||
@@ -79,13 +88,22 @@ export default function OrgRolesTagField<TFieldValues extends FieldValues>({
|
|||||||
name={name}
|
name={name}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex flex-col items-start">
|
<FormItem className="flex flex-col items-start">
|
||||||
<FormLabel>{label ?? t("roles")}</FormLabel>
|
<FormLabel>{label}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<RolesSelector
|
<TagInput
|
||||||
orgId={orgId}
|
{...field}
|
||||||
selectedRoles={field.value ?? []}
|
activeTagIndex={activeTagIndex}
|
||||||
onSelectRoles={setRoleTags}
|
setActiveTagIndex={setActiveTagIndex}
|
||||||
disabled={disabled}
|
placeholder={placeholder}
|
||||||
|
size="sm"
|
||||||
|
tags={field.value}
|
||||||
|
setTags={setRoleTags}
|
||||||
|
enableAutocomplete={true}
|
||||||
|
autocompleteOptions={allRoleOptions}
|
||||||
|
allowDuplicates={false}
|
||||||
|
restrictTagsToAutocompleteOptions={true}
|
||||||
|
sortTags={true}
|
||||||
|
disabled={loading}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
{showMultiRolePaywallMessage && (
|
{showMultiRolePaywallMessage && (
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
|||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import { RolesSelector } from "./roles-selector";
|
|
||||||
|
|
||||||
export type RoleMappingRoleOption = {
|
export type RoleMappingRoleOption = {
|
||||||
roleId: number;
|
roleId: number;
|
||||||
@@ -39,8 +38,6 @@ export type RoleMappingConfigFieldsProps = {
|
|||||||
fieldIdPrefix?: string;
|
fieldIdPrefix?: string;
|
||||||
/** When true, show extra hint for global default policies (no org role list). */
|
/** When true, show extra hint for global default policies (no org role list). */
|
||||||
showFreeformRoleNamesHint?: boolean;
|
showFreeformRoleNamesHint?: boolean;
|
||||||
/** Org ID to use for role lookup. Falls back to URL params when not provided. */
|
|
||||||
orgId?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RoleMappingConfigFields({
|
export default function RoleMappingConfigFields({
|
||||||
@@ -56,12 +53,14 @@ export default function RoleMappingConfigFields({
|
|||||||
rawExpression,
|
rawExpression,
|
||||||
onRawExpressionChange,
|
onRawExpressionChange,
|
||||||
fieldIdPrefix = "role-mapping",
|
fieldIdPrefix = "role-mapping",
|
||||||
showFreeformRoleNamesHint = false,
|
showFreeformRoleNamesHint = false
|
||||||
orgId
|
|
||||||
}: RoleMappingConfigFieldsProps) {
|
}: RoleMappingConfigFieldsProps) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const { env } = useEnvContext();
|
const { env } = useEnvContext();
|
||||||
const { isPaidUser } = usePaidStatus();
|
const { isPaidUser } = usePaidStatus();
|
||||||
|
const [activeFixedRoleTagIndex, setActiveFixedRoleTagIndex] = useState<
|
||||||
|
number | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
const supportsMultipleRolesPerUser = isPaidUser(tierMatrix.fullRbac);
|
const supportsMultipleRolesPerUser = isPaidUser(tierMatrix.fullRbac);
|
||||||
const showSingleRoleDisclaimer =
|
const showSingleRoleDisclaimer =
|
||||||
@@ -95,10 +94,6 @@ export default function RoleMappingConfigFields({
|
|||||||
}
|
}
|
||||||
}, [supportsMultipleRolesPerUser, fixedRoleNames, onFixedRoleNamesChange]);
|
}, [supportsMultipleRolesPerUser, fixedRoleNames, onFixedRoleNamesChange]);
|
||||||
|
|
||||||
const [fixedRolesActiveTagIndex, setFixedRolesActiveTagIndex] = useState<
|
|
||||||
number | null
|
|
||||||
>(null);
|
|
||||||
|
|
||||||
const fixedRadioId = `${fieldIdPrefix}-fixed-roles-mode`;
|
const fixedRadioId = `${fieldIdPrefix}-fixed-roles-mode`;
|
||||||
const builderRadioId = `${fieldIdPrefix}-mapping-builder-mode`;
|
const builderRadioId = `${fieldIdPrefix}-mapping-builder-mode`;
|
||||||
const rawRadioId = `${fieldIdPrefix}-raw-expression-mode`;
|
const rawRadioId = `${fieldIdPrefix}-raw-expression-mode`;
|
||||||
@@ -165,94 +160,58 @@ export default function RoleMappingConfigFields({
|
|||||||
|
|
||||||
{roleMappingMode === "fixedRoles" && (
|
{roleMappingMode === "fixedRoles" && (
|
||||||
<div className="space-y-2 min-w-0 max-w-full">
|
<div className="space-y-2 min-w-0 max-w-full">
|
||||||
{restrictToOrgRoles ? (
|
<TagInput
|
||||||
<RolesSelector
|
tags={fixedRoleNames.map((name) => ({
|
||||||
selectedRoles={fixedRoleNames.map((name) => ({
|
id: name,
|
||||||
|
text: name
|
||||||
|
}))}
|
||||||
|
setTags={(nextTags) => {
|
||||||
|
const prevTags = fixedRoleNames.map((name) => ({
|
||||||
id: name,
|
id: name,
|
||||||
text: name
|
text: name
|
||||||
}))}
|
}));
|
||||||
mapRolesByName
|
const next =
|
||||||
orgId={orgId as string}
|
typeof nextTags === "function"
|
||||||
onSelectRoles={(nextTags) => {
|
? nextTags(prevTags)
|
||||||
let names = [
|
: nextTags;
|
||||||
...new Set(nextTags.map((tag) => tag.text))
|
|
||||||
];
|
|
||||||
|
|
||||||
if (!supportsMultipleRolesPerUser) {
|
let names = [
|
||||||
if (
|
...new Set(next.map((tag) => tag.text))
|
||||||
names.length === 0 &&
|
];
|
||||||
fixedRoleNames.length > 0
|
|
||||||
) {
|
if (!supportsMultipleRolesPerUser) {
|
||||||
onFixedRoleNamesChange([
|
if (
|
||||||
fixedRoleNames[
|
names.length === 0 &&
|
||||||
fixedRoleNames.length - 1
|
fixedRoleNames.length > 0
|
||||||
]!
|
) {
|
||||||
]);
|
onFixedRoleNamesChange([
|
||||||
return;
|
fixedRoleNames[
|
||||||
}
|
fixedRoleNames.length - 1
|
||||||
if (names.length > 1) {
|
]!
|
||||||
names = [names[names.length - 1]!];
|
]);
|
||||||
}
|
return;
|
||||||
}
|
}
|
||||||
|
if (names.length > 1) {
|
||||||
onFixedRoleNamesChange(names);
|
names = [names[names.length - 1]!];
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<TagInput
|
|
||||||
tags={fixedRoleNames.map((name) => ({
|
|
||||||
id: name,
|
|
||||||
text: name
|
|
||||||
}))}
|
|
||||||
setTags={(nextTags) => {
|
|
||||||
const prev = fixedRoleNames.map((name) => ({
|
|
||||||
id: name,
|
|
||||||
text: name
|
|
||||||
}));
|
|
||||||
const next =
|
|
||||||
typeof nextTags === "function"
|
|
||||||
? nextTags(prev)
|
|
||||||
: nextTags;
|
|
||||||
|
|
||||||
let names = [
|
|
||||||
...new Set(next.map((tag) => tag.text))
|
|
||||||
];
|
|
||||||
|
|
||||||
if (!supportsMultipleRolesPerUser) {
|
|
||||||
if (
|
|
||||||
names.length === 0 &&
|
|
||||||
fixedRoleNames.length > 0
|
|
||||||
) {
|
|
||||||
onFixedRoleNamesChange([
|
|
||||||
fixedRoleNames[
|
|
||||||
fixedRoleNames.length - 1
|
|
||||||
]!
|
|
||||||
]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (names.length > 1) {
|
|
||||||
names = [names[names.length - 1]!];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onFixedRoleNamesChange(names);
|
onFixedRoleNamesChange(names);
|
||||||
}}
|
}}
|
||||||
activeTagIndex={fixedRolesActiveTagIndex}
|
activeTagIndex={activeFixedRoleTagIndex}
|
||||||
setActiveTagIndex={setFixedRolesActiveTagIndex}
|
setActiveTagIndex={setActiveFixedRoleTagIndex}
|
||||||
placeholder={t(
|
placeholder={
|
||||||
"roleMappingAssignRolesPlaceholderFreeform"
|
restrictToOrgRoles
|
||||||
)}
|
? t("roleMappingFixedRolesPlaceholderSelect")
|
||||||
enableAutocomplete={false}
|
: t("roleMappingFixedRolesPlaceholderFreeform")
|
||||||
autocompleteOptions={roleOptions}
|
}
|
||||||
restrictTagsToAutocompleteOptions={false}
|
enableAutocomplete={restrictToOrgRoles}
|
||||||
allowDuplicates={false}
|
autocompleteOptions={roleOptions}
|
||||||
sortTags={true}
|
restrictTagsToAutocompleteOptions={restrictToOrgRoles}
|
||||||
size="sm"
|
allowDuplicates={false}
|
||||||
styleClasses={{
|
sortTags={true}
|
||||||
inlineTagsContainer: "min-w-0 max-w-full"
|
size="sm"
|
||||||
}}
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
{showFreeformRoleNamesHint
|
{showFreeformRoleNamesHint
|
||||||
? t("roleMappingFixedRolesDescriptionDefaultPolicy")
|
? t("roleMappingFixedRolesDescriptionDefaultPolicy")
|
||||||
@@ -302,7 +261,6 @@ export default function RoleMappingConfigFields({
|
|||||||
showFreeformRoleNamesHint={
|
showFreeformRoleNamesHint={
|
||||||
showFreeformRoleNamesHint
|
showFreeformRoleNamesHint
|
||||||
}
|
}
|
||||||
orgId={orgId}
|
|
||||||
supportsMultipleRolesPerUser={
|
supportsMultipleRolesPerUser={
|
||||||
supportsMultipleRolesPerUser
|
supportsMultipleRolesPerUser
|
||||||
}
|
}
|
||||||
@@ -379,8 +337,7 @@ function BuilderRuleRow({
|
|||||||
supportsMultipleRolesPerUser,
|
supportsMultipleRolesPerUser,
|
||||||
showRemoveButton,
|
showRemoveButton,
|
||||||
onChange,
|
onChange,
|
||||||
onRemove,
|
onRemove
|
||||||
orgId
|
|
||||||
}: {
|
}: {
|
||||||
rule: MappingBuilderRule;
|
rule: MappingBuilderRule;
|
||||||
roleOptions: Tag[];
|
roleOptions: Tag[];
|
||||||
@@ -392,7 +349,6 @@ function BuilderRuleRow({
|
|||||||
showRemoveButton: boolean;
|
showRemoveButton: boolean;
|
||||||
onChange: (rule: MappingBuilderRule) => void;
|
onChange: (rule: MappingBuilderRule) => void;
|
||||||
onRemove: () => void;
|
onRemove: () => void;
|
||||||
orgId?: string;
|
|
||||||
}) {
|
}) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const [activeTagIndex, setActiveTagIndex] = useState<number | null>(null);
|
const [activeTagIndex, setActiveTagIndex] = useState<number | null>(null);
|
||||||
@@ -422,109 +378,67 @@ function BuilderRuleRow({
|
|||||||
{t("roleMappingAssignRoles")}
|
{t("roleMappingAssignRoles")}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<div className="min-w-0 max-w-full">
|
<div className="min-w-0 max-w-full">
|
||||||
{restrictToOrgRoles ? (
|
<TagInput
|
||||||
<RolesSelector
|
tags={rule.roleNames.map((name) => ({
|
||||||
selectedRoles={rule.roleNames.map((name) => ({
|
id: name,
|
||||||
|
text: name
|
||||||
|
}))}
|
||||||
|
setTags={(nextTags) => {
|
||||||
|
const prevRoleTags = rule.roleNames.map((name) => ({
|
||||||
id: name,
|
id: name,
|
||||||
text: name
|
text: name
|
||||||
}))}
|
}));
|
||||||
buttonText={t("roleMappingAssignRoles")}
|
const next =
|
||||||
mapRolesByName
|
typeof nextTags === "function"
|
||||||
orgId={orgId as string}
|
? nextTags(prevRoleTags)
|
||||||
onSelectRoles={(nextTags) => {
|
: nextTags;
|
||||||
let names = [
|
|
||||||
...new Set(nextTags.map((tag) => tag.text))
|
|
||||||
];
|
|
||||||
|
|
||||||
if (!supportsMultipleRolesPerUser) {
|
let names = [
|
||||||
if (
|
...new Set(next.map((tag) => tag.text))
|
||||||
names.length === 0 &&
|
];
|
||||||
rule.roleNames.length > 0
|
|
||||||
) {
|
if (!supportsMultipleRolesPerUser) {
|
||||||
onChange({
|
if (
|
||||||
...rule,
|
names.length === 0 &&
|
||||||
roleNames: [
|
rule.roleNames.length > 0
|
||||||
rule.roleNames[
|
) {
|
||||||
rule.roleNames.length - 1
|
onChange({
|
||||||
]!
|
...rule,
|
||||||
]
|
roleNames: [
|
||||||
});
|
rule.roleNames[
|
||||||
return;
|
rule.roleNames.length - 1
|
||||||
}
|
]!
|
||||||
if (names.length > 1) {
|
]
|
||||||
names = [names[names.length - 1]!];
|
});
|
||||||
}
|
return;
|
||||||
}
|
}
|
||||||
|
if (names.length > 1) {
|
||||||
onChange({
|
names = [names[names.length - 1]!];
|
||||||
...rule,
|
|
||||||
roleNames: names
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<TagInput
|
|
||||||
tags={rule.roleNames.map((name) => ({
|
|
||||||
id: name,
|
|
||||||
text: name
|
|
||||||
}))}
|
|
||||||
setTags={(nextTags) => {
|
|
||||||
const prevRoleTags = rule.roleNames.map(
|
|
||||||
(name) => ({
|
|
||||||
id: name,
|
|
||||||
text: name
|
|
||||||
})
|
|
||||||
);
|
|
||||||
const next =
|
|
||||||
typeof nextTags === "function"
|
|
||||||
? nextTags(prevRoleTags)
|
|
||||||
: nextTags;
|
|
||||||
|
|
||||||
let names = [
|
|
||||||
...new Set(next.map((tag) => tag.text))
|
|
||||||
];
|
|
||||||
|
|
||||||
if (!supportsMultipleRolesPerUser) {
|
|
||||||
if (
|
|
||||||
names.length === 0 &&
|
|
||||||
rule.roleNames.length > 0
|
|
||||||
) {
|
|
||||||
onChange({
|
|
||||||
...rule,
|
|
||||||
roleNames: [
|
|
||||||
rule.roleNames[
|
|
||||||
rule.roleNames.length - 1
|
|
||||||
]!
|
|
||||||
]
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (names.length > 1) {
|
|
||||||
names = [names[names.length - 1]!];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onChange({
|
onChange({
|
||||||
...rule,
|
...rule,
|
||||||
roleNames: names
|
roleNames: names
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
activeTagIndex={activeTagIndex}
|
activeTagIndex={activeTagIndex}
|
||||||
setActiveTagIndex={setActiveTagIndex}
|
setActiveTagIndex={setActiveTagIndex}
|
||||||
placeholder={t(
|
placeholder={
|
||||||
"roleMappingAssignRolesPlaceholderFreeform"
|
restrictToOrgRoles
|
||||||
)}
|
? t("roleMappingAssignRoles")
|
||||||
enableAutocomplete={false}
|
: t("roleMappingAssignRolesPlaceholderFreeform")
|
||||||
autocompleteOptions={roleOptions}
|
}
|
||||||
restrictTagsToAutocompleteOptions={false}
|
enableAutocomplete={restrictToOrgRoles}
|
||||||
allowDuplicates={false}
|
autocompleteOptions={roleOptions}
|
||||||
sortTags={true}
|
restrictTagsToAutocompleteOptions={restrictToOrgRoles}
|
||||||
size="sm"
|
allowDuplicates={false}
|
||||||
styleClasses={{
|
sortTags={true}
|
||||||
inlineTagsContainer: "min-w-0 max-w-full"
|
size="sm"
|
||||||
}}
|
styleClasses={{
|
||||||
/>
|
inlineTagsContainer: "min-w-0 max-w-full"
|
||||||
)}
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{showFreeformRoleNamesHint && (
|
{showFreeformRoleNamesHint && (
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
|
|||||||
@@ -17,11 +17,9 @@ import { useTranslations } from "next-intl";
|
|||||||
const TRIAL_DURATION_DAYS = 10;
|
const TRIAL_DURATION_DAYS = 10;
|
||||||
|
|
||||||
export default function ShowTrialCard({
|
export default function ShowTrialCard({
|
||||||
isCollapsed,
|
isCollapsed
|
||||||
isOwner = false
|
|
||||||
}: {
|
}: {
|
||||||
isCollapsed?: boolean;
|
isCollapsed?: boolean;
|
||||||
isOwner?: boolean;
|
|
||||||
}) {
|
}) {
|
||||||
const context = useSubscriptionStatusContext();
|
const context = useSubscriptionStatusContext();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
@@ -34,55 +32,53 @@ export default function ShowTrialCard({
|
|||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const remainingMs = trialExpiresAt - now;
|
const remainingMs = trialExpiresAt - now;
|
||||||
const remainingDays = Math.max(
|
const remainingDays = Math.max(0, Math.ceil(remainingMs / (1000 * 60 * 60 * 24)));
|
||||||
0,
|
|
||||||
Math.ceil(remainingMs / (1000 * 60 * 60 * 24))
|
|
||||||
);
|
|
||||||
const totalMs = TRIAL_DURATION_DAYS * 24 * 60 * 60 * 1000;
|
const totalMs = TRIAL_DURATION_DAYS * 24 * 60 * 60 * 1000;
|
||||||
const progressPct = Math.min(
|
const progressPct = Math.min(100, Math.max(0, ((now - (trialExpiresAt - totalMs)) / totalMs) * 100));
|
||||||
100,
|
|
||||||
Math.max(0, ((now - (trialExpiresAt - totalMs)) / totalMs) * 100)
|
|
||||||
);
|
|
||||||
// Inverted: full bar at start, drains to empty as trial ends
|
// Inverted: full bar at start, drains to empty as trial ends
|
||||||
const displayPct = 100 - progressPct;
|
const displayPct = 100 - progressPct;
|
||||||
|
|
||||||
const billingHref = orgId ? `/${orgId}/settings/billing` : "/";
|
const billingHref = orgId ? `/${orgId}/settings/billing` : "/";
|
||||||
|
|
||||||
if (isCollapsed) {
|
if (isCollapsed) {
|
||||||
const icon = (
|
return (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<span className="flex items-center justify-center rounded-md p-2 text-muted-foreground">
|
<Link
|
||||||
|
href={billingHref}
|
||||||
|
className="flex items-center justify-center rounded-md p-2 text-muted-foreground hover:text-foreground hover:bg-secondary/80 dark:hover:bg-secondary/50 transition-colors"
|
||||||
|
>
|
||||||
<ClockIcon className="h-4 w-4 flex-none" />
|
<ClockIcon className="h-4 w-4 flex-none" />
|
||||||
</span>
|
</Link>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="right" sideOffset={8}>
|
<TooltipContent side="right" sideOffset={8}>
|
||||||
<p>
|
<p>
|
||||||
{remainingDays === 0
|
{remainingDays === 0
|
||||||
? t("trialExpired")
|
? t("trialExpired")
|
||||||
: t("trialDaysLeftShort", {
|
: t("trialDaysLeftShort", { days: remainingDays })}
|
||||||
days: remainingDays
|
|
||||||
})}
|
|
||||||
</p>
|
</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isOwner) {
|
|
||||||
return <Link href={billingHref}>{icon}</Link>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return icon;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const cardContent = (
|
return (
|
||||||
<>
|
<Link
|
||||||
|
href={billingHref}
|
||||||
|
className={cn(
|
||||||
|
"group cursor-pointer block",
|
||||||
|
"rounded-md border bg-secondary p-2 py-3 w-full flex flex-col gap-2 text-sm",
|
||||||
|
"transition duration-200 ease-in-out hover:bg-secondary/80 dark:hover:bg-secondary/60"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<ClockIcon className="flex-none size-4 text-muted-foreground" />
|
<ClockIcon className="flex-none size-4 text-muted-foreground" />
|
||||||
<p className="font-medium flex-1 leading-tight">
|
<p className="font-medium flex-1 leading-tight">
|
||||||
{remainingDays === 0 ? t("trialExpired") : t("trialActive")}
|
{remainingDays === 0
|
||||||
|
? t("trialExpired")
|
||||||
|
: t("trialActive")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
@@ -92,37 +88,11 @@ export default function ShowTrialCard({
|
|||||||
? t("trialHasEnded")
|
? t("trialHasEnded")
|
||||||
: t("trialDaysRemaining", { count: remainingDays })}
|
: t("trialDaysRemaining", { count: remainingDays })}
|
||||||
</small>
|
</small>
|
||||||
{isOwner && (
|
<div className="inline-flex items-center gap-1 text-xs text-muted-foreground group-hover:text-foreground transition-colors">
|
||||||
<div className="inline-flex items-center gap-1 text-xs text-muted-foreground">
|
<span>{t("trialGoToBilling")}</span>
|
||||||
<span>{t("trialGoToBilling")}</span>
|
<ArrowRight className="flex-none size-3" />
|
||||||
<ArrowRight className="flex-none size-3" />
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</Link>
|
||||||
);
|
|
||||||
|
|
||||||
if (isOwner) {
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
href={billingHref}
|
|
||||||
className={cn(
|
|
||||||
"group cursor-pointer block",
|
|
||||||
"rounded-md border bg-secondary p-2 py-3 w-full flex flex-col gap-2 text-sm"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{cardContent}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"rounded-md border bg-secondary p-2 py-3 w-full flex flex-col gap-2 text-sm"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{cardContent}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useMemo } from "react";
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { BellPlus, BellRing } from "lucide-react";
|
||||||
|
import {
|
||||||
|
SettingsSection,
|
||||||
|
SettingsSectionHeader,
|
||||||
|
SettingsSectionTitle,
|
||||||
|
SettingsSectionDescription,
|
||||||
|
SettingsSectionBody
|
||||||
|
} from "@app/components/Settings";
|
||||||
|
import UptimeBar from "@app/components/UptimeBar";
|
||||||
|
import { Button } from "@app/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Credenza,
|
Credenza,
|
||||||
CredenzaBody,
|
CredenzaBody,
|
||||||
@@ -10,32 +23,18 @@ import {
|
|||||||
CredenzaHeader,
|
CredenzaHeader,
|
||||||
CredenzaTitle
|
CredenzaTitle
|
||||||
} from "@app/components/Credenza";
|
} from "@app/components/Credenza";
|
||||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
|
||||||
import {
|
|
||||||
SettingsSection,
|
|
||||||
SettingsSectionBody,
|
|
||||||
SettingsSectionDescription,
|
|
||||||
SettingsSectionHeader,
|
|
||||||
SettingsSectionTitle
|
|
||||||
} from "@app/components/Settings";
|
|
||||||
import UptimeBar from "@app/components/UptimeBar";
|
|
||||||
import { TagInput, type Tag } from "@app/components/tags/tag-input";
|
|
||||||
import { Button } from "@app/components/ui/button";
|
|
||||||
import { Input } from "@app/components/ui/input";
|
import { Input } from "@app/components/ui/input";
|
||||||
import { Label } from "@app/components/ui/label";
|
import { Label } from "@app/components/ui/label";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { TagInput, type Tag } from "@app/components/tags/tag-input";
|
||||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
|
||||||
import { toast } from "@app/hooks/useToast";
|
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { orgQueries } from "@app/lib/queries";
|
import { orgQueries } from "@app/lib/queries";
|
||||||
|
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||||
|
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
||||||
import { BellPlus, BellRing } from "lucide-react";
|
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import Link from "next/link";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { RolesSelector } from "./roles-selector";
|
|
||||||
import { UsersSelector } from "./users-selector";
|
|
||||||
|
|
||||||
interface UptimeAlertSectionProps {
|
interface UptimeAlertSectionProps {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
@@ -53,12 +52,10 @@ export default function UptimeAlertSection({
|
|||||||
days = 90
|
days = 90
|
||||||
}: UptimeAlertSectionProps) {
|
}: UptimeAlertSectionProps) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const envContext = useEnvContext();
|
const api = createApiClient(useEnvContext());
|
||||||
const api = createApiClient(envContext);
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { isPaidUser } = usePaidStatus();
|
const { isPaidUser } = usePaidStatus();
|
||||||
const isPaid = isPaidUser(tierMatrix.alertingRules);
|
const isPaid = isPaidUser(tierMatrix.alertingRules);
|
||||||
const { env } = envContext;
|
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [name, setName] = useState(
|
const [name, setName] = useState(
|
||||||
@@ -67,7 +64,12 @@ export default function UptimeAlertSection({
|
|||||||
const [userTags, setUserTags] = useState<Tag[]>([]);
|
const [userTags, setUserTags] = useState<Tag[]>([]);
|
||||||
const [roleTags, setRoleTags] = useState<Tag[]>([]);
|
const [roleTags, setRoleTags] = useState<Tag[]>([]);
|
||||||
const [emailTags, setEmailTags] = useState<Tag[]>([]);
|
const [emailTags, setEmailTags] = useState<Tag[]>([]);
|
||||||
|
const [activeUserTagIndex, setActiveUserTagIndex] = useState<number | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const [activeRoleTagIndex, setActiveRoleTagIndex] = useState<number | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
const [activeEmailTagIndex, setActiveEmailTagIndex] = useState<
|
const [activeEmailTagIndex, setActiveEmailTagIndex] = useState<
|
||||||
number | null
|
number | null
|
||||||
>(null);
|
>(null);
|
||||||
@@ -78,6 +80,27 @@ export default function UptimeAlertSection({
|
|||||||
enabled: isPaid
|
enabled: isPaid
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data: orgUsers = [] } = useQuery(orgQueries.users({ orgId }));
|
||||||
|
const { data: orgRoles = [] } = useQuery(orgQueries.roles({ orgId }));
|
||||||
|
|
||||||
|
const allUsers = useMemo(
|
||||||
|
() =>
|
||||||
|
orgUsers.map((u) => ({
|
||||||
|
id: String(u.id),
|
||||||
|
text: getUserDisplayName({
|
||||||
|
email: u.email,
|
||||||
|
name: u.name,
|
||||||
|
username: u.username
|
||||||
|
})
|
||||||
|
})),
|
||||||
|
[orgUsers]
|
||||||
|
);
|
||||||
|
|
||||||
|
const allRoles = useMemo(
|
||||||
|
() => orgRoles.map((r) => ({ id: String(r.roleId), text: r.name })),
|
||||||
|
[orgRoles]
|
||||||
|
);
|
||||||
|
|
||||||
const hasRules = (alertRules?.length ?? 0) > 0;
|
const hasRules = (alertRules?.length ?? 0) > 0;
|
||||||
|
|
||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
@@ -178,9 +201,7 @@ export default function UptimeAlertSection({
|
|||||||
{t("uptimeSectionDescription", { days })}
|
{t("uptimeSectionDescription", { days })}
|
||||||
</SettingsSectionDescription>
|
</SettingsSectionDescription>
|
||||||
</div>
|
</div>
|
||||||
{!env.flags.disableEnterpriseFeatures
|
{alertButton}
|
||||||
? alertButton
|
|
||||||
: null}
|
|
||||||
</div>
|
</div>
|
||||||
</SettingsSectionHeader>
|
</SettingsSectionHeader>
|
||||||
<SettingsSectionBody>
|
<SettingsSectionBody>
|
||||||
@@ -206,16 +227,10 @@ export default function UptimeAlertSection({
|
|||||||
</CredenzaHeader>
|
</CredenzaHeader>
|
||||||
<CredenzaBody>
|
<CredenzaBody>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<PaidFeaturesAlert
|
<PaidFeaturesAlert tiers={tierMatrix.alertingRules} />
|
||||||
tiers={tierMatrix.alertingRules}
|
|
||||||
/>
|
|
||||||
<fieldset
|
<fieldset
|
||||||
disabled={!isPaid}
|
disabled={!isPaid}
|
||||||
className={
|
className={!isPaid ? "opacity-50 pointer-events-none" : ""}
|
||||||
!isPaid
|
|
||||||
? "opacity-50 pointer-events-none"
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -225,53 +240,65 @@ export default function UptimeAlertSection({
|
|||||||
<Input
|
<Input
|
||||||
id="alert-name"
|
id="alert-name"
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) =>
|
onChange={(e) => setName(e.target.value)}
|
||||||
setName(e.target.value)
|
placeholder={t("uptimeAlertNamePlaceholder")}
|
||||||
}
|
|
||||||
placeholder={t(
|
|
||||||
"uptimeAlertNamePlaceholder"
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>
|
<Label>{t("alertingNotifyUsers")}</Label>
|
||||||
{t("alertingNotifyUsers")}
|
<TagInput
|
||||||
</Label>
|
activeTagIndex={activeUserTagIndex}
|
||||||
<UsersSelector
|
setActiveTagIndex={setActiveUserTagIndex}
|
||||||
selectedUsers={userTags}
|
placeholder={t("alertingSelectUsers")}
|
||||||
orgId={orgId}
|
size="sm"
|
||||||
onSelectUsers={setUserTags}
|
tags={userTags}
|
||||||
|
setTags={(newTags) => {
|
||||||
|
const next =
|
||||||
|
typeof newTags === "function"
|
||||||
|
? newTags(userTags)
|
||||||
|
: newTags;
|
||||||
|
setUserTags(next as Tag[]);
|
||||||
|
}}
|
||||||
|
enableAutocomplete
|
||||||
|
autocompleteOptions={allUsers}
|
||||||
|
restrictTagsToAutocompleteOptions
|
||||||
|
allowDuplicates={false}
|
||||||
|
sortTags
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>
|
<Label>{t("alertingNotifyRoles")}</Label>
|
||||||
{t("alertingNotifyRoles")}
|
<TagInput
|
||||||
</Label>
|
activeTagIndex={activeRoleTagIndex}
|
||||||
<RolesSelector
|
setActiveTagIndex={setActiveRoleTagIndex}
|
||||||
selectedRoles={roleTags}
|
placeholder={t("alertingSelectRoles")}
|
||||||
restrictAdminRole
|
size="sm"
|
||||||
orgId={orgId}
|
tags={roleTags}
|
||||||
onSelectRoles={setRoleTags}
|
setTags={(newTags) => {
|
||||||
|
const next =
|
||||||
|
typeof newTags === "function"
|
||||||
|
? newTags(roleTags)
|
||||||
|
: newTags;
|
||||||
|
setRoleTags(next as Tag[]);
|
||||||
|
}}
|
||||||
|
enableAutocomplete
|
||||||
|
autocompleteOptions={allRoles}
|
||||||
|
restrictTagsToAutocompleteOptions
|
||||||
|
allowDuplicates={false}
|
||||||
|
sortTags
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>
|
<Label>{t("uptimeAdditionalEmails")}</Label>
|
||||||
{t("uptimeAdditionalEmails")}
|
|
||||||
</Label>
|
|
||||||
<TagInput
|
<TagInput
|
||||||
activeTagIndex={activeEmailTagIndex}
|
activeTagIndex={activeEmailTagIndex}
|
||||||
setActiveTagIndex={
|
setActiveTagIndex={setActiveEmailTagIndex}
|
||||||
setActiveEmailTagIndex
|
placeholder={t("alertingEmailPlaceholder")}
|
||||||
}
|
|
||||||
placeholder={t(
|
|
||||||
"alertingEmailPlaceholder"
|
|
||||||
)}
|
|
||||||
size="sm"
|
size="sm"
|
||||||
tags={emailTags}
|
tags={emailTags}
|
||||||
setTags={(newTags) => {
|
setTags={(newTags) => {
|
||||||
const next =
|
const next =
|
||||||
typeof newTags ===
|
typeof newTags === "function"
|
||||||
"function"
|
|
||||||
? newTags(emailTags)
|
? newTags(emailTags)
|
||||||
: newTags;
|
: newTags;
|
||||||
setEmailTags(next as Tag[]);
|
setEmailTags(next as Tag[]);
|
||||||
@@ -279,9 +306,7 @@ export default function UptimeAlertSection({
|
|||||||
allowDuplicates={false}
|
allowDuplicates={false}
|
||||||
sortTags
|
sortTags
|
||||||
validateTag={(tag) =>
|
validateTag={(tag) =>
|
||||||
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(
|
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(tag)
|
||||||
tag
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
delimiterList={[",", "Enter"]}
|
delimiterList={[",", "Enter"]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ContactSalesBanner } from "@app/components/ContactSalesBanner";
|
|
||||||
import { StrategySelect } from "@app/components/StrategySelect";
|
|
||||||
import { TagInput, type Tag } from "@app/components/tags/tag-input";
|
|
||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
import { Checkbox } from "@app/components/ui/checkbox";
|
import { Checkbox } from "@app/components/ui/checkbox";
|
||||||
import {
|
import {
|
||||||
@@ -24,13 +21,11 @@ import {
|
|||||||
import { Input } from "@app/components/ui/input";
|
import { Input } from "@app/components/ui/input";
|
||||||
import { Switch } from "@app/components/ui/switch";
|
import { Switch } from "@app/components/ui/switch";
|
||||||
import { Textarea } from "@app/components/ui/textarea";
|
import { Textarea } from "@app/components/ui/textarea";
|
||||||
import { Label } from "@app/components/ui/label";
|
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger
|
PopoverTrigger
|
||||||
} from "@app/components/ui/popover";
|
} from "@app/components/ui/popover";
|
||||||
import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group";
|
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -38,21 +33,24 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue
|
SelectValue
|
||||||
} from "@app/components/ui/select";
|
} from "@app/components/ui/select";
|
||||||
|
import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group";
|
||||||
|
import { Label } from "@app/components/ui/label";
|
||||||
|
import { StrategySelect } from "@app/components/StrategySelect";
|
||||||
|
import { TagInput, type Tag } from "@app/components/tags/tag-input";
|
||||||
|
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
|
||||||
import {
|
import {
|
||||||
type AlertRuleFormAction,
|
type AlertRuleFormAction,
|
||||||
type AlertRuleFormValues
|
type AlertRuleFormValues
|
||||||
} from "@app/lib/alertRuleForm";
|
} from "@app/lib/alertRuleForm";
|
||||||
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
|
|
||||||
import { orgQueries } from "@app/lib/queries";
|
import { orgQueries } from "@app/lib/queries";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Bell, ChevronsUpDown, Globe, Plus, Trash2 } from "lucide-react";
|
import { ContactSalesBanner } from "@app/components/ContactSalesBanner";
|
||||||
|
import { Bell, Globe, ChevronsUpDown, Plus, Trash2 } from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import type { Control, UseFormReturn } from "react-hook-form";
|
import type { Control, UseFormReturn } from "react-hook-form";
|
||||||
import { useFormContext, useWatch } from "react-hook-form";
|
import { useFormContext, useWatch } from "react-hook-form";
|
||||||
import { useDebounce } from "use-debounce";
|
import { useDebounce } from "use-debounce";
|
||||||
import { RolesSelector } from "../roles-selector";
|
|
||||||
import { UsersSelector } from "../users-selector";
|
|
||||||
|
|
||||||
export function AddActionPanel({
|
export function AddActionPanel({
|
||||||
onAdd
|
onAdd
|
||||||
@@ -500,6 +498,12 @@ function NotifyActionFields({
|
|||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
const [emailActiveIdx, setEmailActiveIdx] = useState<number | null>(null);
|
const [emailActiveIdx, setEmailActiveIdx] = useState<number | null>(null);
|
||||||
|
const [activeUsersTagIndex, setActiveUsersTagIndex] = useState<
|
||||||
|
number | null
|
||||||
|
>(null);
|
||||||
|
const [activeRolesTagIndex, setActiveRolesTagIndex] = useState<
|
||||||
|
number | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
const { data: orgUsers = [], isLoading: isLoadingUsers } = useQuery(
|
const { data: orgUsers = [], isLoading: isLoadingUsers } = useQuery(
|
||||||
orgQueries.users({ orgId })
|
orgQueries.users({ orgId })
|
||||||
@@ -570,6 +574,14 @@ function NotifyActionFields({
|
|||||||
hasResolvedTagsRef.current = true;
|
hasResolvedTagsRef.current = true;
|
||||||
}, [isLoadingUsers, isLoadingRoles, allUsers, allRoles]);
|
}, [isLoadingUsers, isLoadingRoles, allUsers, allRoles]);
|
||||||
|
|
||||||
|
const userTags = (useWatch({
|
||||||
|
control,
|
||||||
|
name: `actions.${index}.userTags`
|
||||||
|
}) ?? []) as Tag[];
|
||||||
|
const roleTags = (useWatch({
|
||||||
|
control,
|
||||||
|
name: `actions.${index}.roleTags`
|
||||||
|
}) ?? []) as Tag[];
|
||||||
const emailTags = (useWatch({
|
const emailTags = (useWatch({
|
||||||
control,
|
control,
|
||||||
name: `actions.${index}.emailTags`
|
name: `actions.${index}.emailTags`
|
||||||
@@ -584,16 +596,29 @@ function NotifyActionFields({
|
|||||||
<FormItem className="flex flex-col items-start">
|
<FormItem className="flex flex-col items-start">
|
||||||
<FormLabel>{t("alertingNotifyUsers")}</FormLabel>
|
<FormLabel>{t("alertingNotifyUsers")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<UsersSelector
|
<TagInput
|
||||||
selectedUsers={field.value ?? []}
|
{...field}
|
||||||
orgId={orgId}
|
activeTagIndex={activeUsersTagIndex}
|
||||||
onSelectUsers={(newUsers) => {
|
setActiveTagIndex={setActiveUsersTagIndex}
|
||||||
|
placeholder={t("alertingSelectUsers")}
|
||||||
|
size="sm"
|
||||||
|
tags={userTags}
|
||||||
|
setTags={(newTags) => {
|
||||||
|
const next =
|
||||||
|
typeof newTags === "function"
|
||||||
|
? newTags(userTags)
|
||||||
|
: newTags;
|
||||||
form.setValue(
|
form.setValue(
|
||||||
`actions.${index}.userTags`,
|
`actions.${index}.userTags`,
|
||||||
newUsers as [Tag, ...Tag[]],
|
next as Tag[],
|
||||||
{ shouldDirty: true }
|
{ shouldDirty: true }
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
enableAutocomplete={true}
|
||||||
|
autocompleteOptions={allUsers}
|
||||||
|
allowDuplicates={false}
|
||||||
|
restrictTagsToAutocompleteOptions={true}
|
||||||
|
sortTags={true}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -607,17 +632,29 @@ function NotifyActionFields({
|
|||||||
<FormItem className="flex flex-col items-start">
|
<FormItem className="flex flex-col items-start">
|
||||||
<FormLabel>{t("alertingNotifyRoles")}</FormLabel>
|
<FormLabel>{t("alertingNotifyRoles")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<RolesSelector
|
<TagInput
|
||||||
selectedRoles={field.value ?? []}
|
{...field}
|
||||||
restrictAdminRole
|
activeTagIndex={activeRolesTagIndex}
|
||||||
orgId={orgId}
|
setActiveTagIndex={setActiveRolesTagIndex}
|
||||||
onSelectRoles={(newUsers) => {
|
placeholder={t("alertingSelectRoles")}
|
||||||
|
size="sm"
|
||||||
|
tags={roleTags}
|
||||||
|
setTags={(newTags) => {
|
||||||
|
const next =
|
||||||
|
typeof newTags === "function"
|
||||||
|
? newTags(roleTags)
|
||||||
|
: newTags;
|
||||||
form.setValue(
|
form.setValue(
|
||||||
`actions.${index}.roleTags`,
|
`actions.${index}.roleTags`,
|
||||||
newUsers as [Tag, ...Tag[]],
|
next as Tag[],
|
||||||
{ shouldDirty: true }
|
{ shouldDirty: true }
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
enableAutocomplete={true}
|
||||||
|
autocompleteOptions={allRoles}
|
||||||
|
allowDuplicates={false}
|
||||||
|
restrictTagsToAutocompleteOptions={true}
|
||||||
|
sortTags={true}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useMemo, useState } from "react";
|
|||||||
import { useDebounce } from "use-debounce";
|
import { useDebounce } from "use-debounce";
|
||||||
|
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { MultiSelectTagInput } from "./multi-select/multi-select-tag-input";
|
import { MultiSelectTags } from "./multi-select-tags";
|
||||||
|
|
||||||
export type SelectedMachine = Pick<
|
export type SelectedMachine = Pick<
|
||||||
ListClientsResponse["clients"][number],
|
ListClientsResponse["clients"][number],
|
||||||
@@ -28,13 +28,11 @@ export function MachinesSelector({
|
|||||||
|
|
||||||
const [debouncedValue] = useDebounce(machineSearchQuery, 150);
|
const [debouncedValue] = useDebounce(machineSearchQuery, 150);
|
||||||
|
|
||||||
const perPage = 7;
|
|
||||||
|
|
||||||
const { data: machines = [] } = useQuery(
|
const { data: machines = [] } = useQuery(
|
||||||
orgQueries.machineClients({ orgId, perPage, query: debouncedValue })
|
orgQueries.machineClients({ orgId, perPage: 10, query: debouncedValue })
|
||||||
);
|
);
|
||||||
|
|
||||||
// always include the selected machines in the list (if the user isn't searching)
|
// always include the selected machines in the list of machines shown (if the user isn't searching)
|
||||||
const machinesShown = useMemo(() => {
|
const machinesShown = useMemo(() => {
|
||||||
const allMachines: Array<SelectedMachine> = [...machines];
|
const allMachines: Array<SelectedMachine> = [...machines];
|
||||||
if (debouncedValue.trim().length === 0) {
|
if (debouncedValue.trim().length === 0) {
|
||||||
@@ -46,32 +44,75 @@ export function MachinesSelector({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return allMachines;
|
return allMachines;
|
||||||
}, [machines, selectedMachines, debouncedValue]);
|
}, [machines, selectedMachines, debouncedValue]);
|
||||||
|
|
||||||
|
// const selectedMachinesIds = new Set(
|
||||||
|
// selectedMachines.map((m) => m.clientId)
|
||||||
|
// );
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MultiSelectTagInput
|
<MultiSelectTags
|
||||||
buttonText={t("accessClientSelect")}
|
|
||||||
searchPlaceholder={t("search")}
|
|
||||||
emptyPlaceholder={t("machineNotFound")}
|
emptyPlaceholder={t("machineNotFound")}
|
||||||
searchQuery={machineSearchQuery}
|
searchPlaceholder={t("machineSearch")}
|
||||||
onSearch={setMachineSearchQuery}
|
value={selectedMachines.map((m) => ({
|
||||||
options={machinesShown.map((mc) => ({
|
...m,
|
||||||
id: mc.clientId.toString(),
|
text: m.name,
|
||||||
text: mc.name
|
id: m.clientId.toString()
|
||||||
}))}
|
}))}
|
||||||
value={selectedMachines.map((mc) => ({
|
onChange={(values) => {
|
||||||
id: mc.clientId.toString(),
|
onSelectMachines(values);
|
||||||
text: mc.name
|
|
||||||
}))}
|
|
||||||
onChange={(newValues) => {
|
|
||||||
onSelectMachines(
|
|
||||||
newValues.map((v) => ({
|
|
||||||
clientId: Number(v.id),
|
|
||||||
name: v.text
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
|
options={machinesShown.map((m) => ({
|
||||||
|
...m,
|
||||||
|
id: m.clientId.toString(),
|
||||||
|
text: m.name
|
||||||
|
}))}
|
||||||
|
onSearch={setMachineSearchQuery}
|
||||||
|
searchQuery={machineSearchQuery}
|
||||||
/>
|
/>
|
||||||
|
// <Command shouldFilter={false}>
|
||||||
|
// <CommandInput
|
||||||
|
// placeholder={t("machineSearch")}
|
||||||
|
// value={machineSearchQuery}
|
||||||
|
// onValueChange={setMachineSearchQuery}
|
||||||
|
// />
|
||||||
|
// <CommandList>
|
||||||
|
// <CommandEmpty>{t("machineNotFound")}</CommandEmpty>
|
||||||
|
// <CommandGroup>
|
||||||
|
// {machinesShown.map((m) => (
|
||||||
|
// <CommandItem
|
||||||
|
// value={`${m.name}:${m.clientId}`}
|
||||||
|
// key={m.clientId}
|
||||||
|
// onSelect={() => {
|
||||||
|
// let newMachineClients = [];
|
||||||
|
// if (selectedMachinesIds.has(m.clientId)) {
|
||||||
|
// newMachineClients = selectedMachines.filter(
|
||||||
|
// (mc) => mc.clientId !== m.clientId
|
||||||
|
// );
|
||||||
|
// } else {
|
||||||
|
// newMachineClients = [
|
||||||
|
// ...selectedMachines,
|
||||||
|
// m
|
||||||
|
// ];
|
||||||
|
// }
|
||||||
|
// onSelectMachines(newMachineClients);
|
||||||
|
// }}
|
||||||
|
// >
|
||||||
|
// <CheckIcon
|
||||||
|
// className={cn(
|
||||||
|
// "mr-2 h-4 w-4",
|
||||||
|
// selectedMachinesIds.has(m.clientId)
|
||||||
|
// ? "opacity-100"
|
||||||
|
// : "opacity-0"
|
||||||
|
// )}
|
||||||
|
// />
|
||||||
|
// {`${m.name}`}
|
||||||
|
// </CommandItem>
|
||||||
|
// ))}
|
||||||
|
// </CommandGroup>
|
||||||
|
// </CommandList>
|
||||||
|
// </Command>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,26 +6,24 @@ import {
|
|||||||
CommandInput,
|
CommandInput,
|
||||||
CommandItem,
|
CommandItem,
|
||||||
CommandList
|
CommandList
|
||||||
} from "../ui/command";
|
} from "./ui/command";
|
||||||
import { cn } from "@app/lib/cn";
|
import { cn } from "@app/lib/cn";
|
||||||
import { CheckIcon } from "lucide-react";
|
import { CheckIcon } from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
|
||||||
|
|
||||||
export type TagValue = { text: string; id: string };
|
export type TagValue = { text: string; id: string };
|
||||||
|
|
||||||
export type MultiSelectTagsProps<T extends TagValue> = {
|
export type MultiSelectTagsProps<T extends TagValue> = {
|
||||||
emptyPlaceholder?: string;
|
emptyPlaceholder: string;
|
||||||
searchPlaceholder?: string;
|
searchPlaceholder: string;
|
||||||
searchQuery?: string;
|
searchQuery?: string;
|
||||||
options: Array<T>;
|
options: Array<T>;
|
||||||
value: Array<T>;
|
value: Array<T>;
|
||||||
onChange: (newValue: Array<T>) => void;
|
onChange: (newValue: Array<T>) => void;
|
||||||
onSearch: (query: string) => void;
|
onSearch: (query: string) => void;
|
||||||
ref?: Ref<HTMLButtonElement>;
|
ref?: Ref<HTMLButtonElement>;
|
||||||
disabled?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function MultiSelectContent<T extends TagValue>({
|
export function MultiSelectTags<T extends TagValue>({
|
||||||
emptyPlaceholder,
|
emptyPlaceholder,
|
||||||
searchPlaceholder,
|
searchPlaceholder,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
@@ -34,19 +32,16 @@ export function MultiSelectContent<T extends TagValue>({
|
|||||||
onSearch,
|
onSearch,
|
||||||
onChange
|
onChange
|
||||||
}: MultiSelectTagsProps<T>) {
|
}: MultiSelectTagsProps<T>) {
|
||||||
const t = useTranslations();
|
|
||||||
const selectedValues = new Set(value.map((v) => v.id));
|
const selectedValues = new Set(value.map((v) => v.id));
|
||||||
return (
|
return (
|
||||||
<Command shouldFilter={false}>
|
<Command shouldFilter={false}>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
placeholder={searchPlaceholder ?? t("search")}
|
placeholder={searchPlaceholder}
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onValueChange={onSearch}
|
onValueChange={onSearch}
|
||||||
/>
|
/>
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty className="text-muted-foreground">
|
<CommandEmpty>{emptyPlaceholder}</CommandEmpty>
|
||||||
{emptyPlaceholder ?? t("noResults")}
|
|
||||||
</CommandEmpty>
|
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{options.map((option) => (
|
{options.map((option) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
import { buttonVariants } from "@app/components/ui/button";
|
|
||||||
import {
|
|
||||||
Popover,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverTrigger
|
|
||||||
} from "@app/components/ui/popover";
|
|
||||||
import { cn } from "@app/lib/cn";
|
|
||||||
import { ChevronDownIcon, XIcon } from "lucide-react";
|
|
||||||
import {
|
|
||||||
type MultiSelectTagsProps,
|
|
||||||
type TagValue,
|
|
||||||
MultiSelectContent
|
|
||||||
} from "./multi-select-content";
|
|
||||||
|
|
||||||
export interface MultiSelectInputProps<
|
|
||||||
T extends TagValue
|
|
||||||
> extends MultiSelectTagsProps<T> {
|
|
||||||
buttonText?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function MultiSelectTagInput<T extends TagValue>({
|
|
||||||
buttonText,
|
|
||||||
...props
|
|
||||||
}: MultiSelectInputProps<T>) {
|
|
||||||
const selectedValues = new Set(props.value.map((v) => v.id));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Popover
|
|
||||||
onOpenChange={(open) => {
|
|
||||||
if (!open) {
|
|
||||||
// clear input when popover is closed
|
|
||||||
props.onSearch("");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<div
|
|
||||||
role="combobox"
|
|
||||||
className={cn(
|
|
||||||
buttonVariants({
|
|
||||||
variant: "outline"
|
|
||||||
}),
|
|
||||||
"justify-between w-full inline-flex",
|
|
||||||
"text-muted-foreground pl-1.5 cursor-text",
|
|
||||||
"hover:bg-transparent hover:text-muted-foreground",
|
|
||||||
props.disabled && "pointer-events-none opacity-50"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"inline-flex items-center gap-1",
|
|
||||||
"overflow-x-auto"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{props.value.map((option) => (
|
|
||||||
<span
|
|
||||||
key={option.id}
|
|
||||||
className={cn(
|
|
||||||
"bg-muted-foreground/10 font-normal text-foreground rounded-sm",
|
|
||||||
"py-1 pl-1.5 pr-0.5 text-xs inline-flex items-center gap-0.5"
|
|
||||||
)}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
{option.text}
|
|
||||||
<button
|
|
||||||
className="p-0.5 flex-none cursor-pointer"
|
|
||||||
type="button"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
let newValues = [];
|
|
||||||
if (selectedValues.has(option.id)) {
|
|
||||||
newValues = props.value.filter(
|
|
||||||
(v) => v.id !== option.id
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
newValues = [
|
|
||||||
...props.value,
|
|
||||||
option
|
|
||||||
];
|
|
||||||
}
|
|
||||||
props.onChange(newValues);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<XIcon className="size-3.5" />
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
<span className="pl-1 font-normal">{buttonText}</span>
|
|
||||||
</span>
|
|
||||||
<ChevronDownIcon className="ml-2 h-4 w-4 shrink-0 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="p-0">
|
|
||||||
<MultiSelectContent {...props} />
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
import { orgQueries } from "@app/lib/queries";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { useMemo, useState } from "react";
|
|
||||||
import { useDebounce } from "use-debounce";
|
|
||||||
|
|
||||||
import { useTranslations } from "next-intl";
|
|
||||||
import { MultiSelectTagInput } from "./multi-select/multi-select-tag-input";
|
|
||||||
|
|
||||||
export type SelectedRole = { id: string; text: string };
|
|
||||||
|
|
||||||
export type RolesSelectorProps = {
|
|
||||||
orgId: string;
|
|
||||||
selectedRoles?: SelectedRole[];
|
|
||||||
onSelectRoles: (roles: SelectedRole[]) => void;
|
|
||||||
disabled?: boolean;
|
|
||||||
restrictAdminRole?: boolean;
|
|
||||||
mapRolesByName?: boolean;
|
|
||||||
buttonText?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function RolesSelector({
|
|
||||||
orgId,
|
|
||||||
selectedRoles = [],
|
|
||||||
onSelectRoles,
|
|
||||||
disabled,
|
|
||||||
restrictAdminRole,
|
|
||||||
mapRolesByName,
|
|
||||||
buttonText
|
|
||||||
}: RolesSelectorProps) {
|
|
||||||
const t = useTranslations();
|
|
||||||
const [roleSearchQuery, setRoleSearchQuery] = useState("");
|
|
||||||
|
|
||||||
const [debouncedValue] = useDebounce(roleSearchQuery, 150);
|
|
||||||
|
|
||||||
const { data: roles = [] } = useQuery(
|
|
||||||
orgQueries.roles({ orgId, perPage: 10, query: debouncedValue })
|
|
||||||
);
|
|
||||||
|
|
||||||
// always include the selected roles in the list (if the user isn't searching)
|
|
||||||
const rolesShown = useMemo(() => {
|
|
||||||
let allRoles: Array<SelectedRole & { isAdmin?: boolean }> = roles.map(
|
|
||||||
(r) => ({
|
|
||||||
id: mapRolesByName ? r.name : r.roleId.toString(),
|
|
||||||
text: r.name,
|
|
||||||
isAdmin: Boolean(r.isAdmin)
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
if (debouncedValue.trim().length === 0) {
|
|
||||||
for (const role of selectedRoles) {
|
|
||||||
if (!allRoles.find((r) => r.id === role.id)) {
|
|
||||||
allRoles.unshift(role);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (restrictAdminRole) {
|
|
||||||
allRoles = allRoles.filter((role) => !role.isAdmin);
|
|
||||||
}
|
|
||||||
|
|
||||||
return allRoles;
|
|
||||||
}, [
|
|
||||||
roles,
|
|
||||||
selectedRoles,
|
|
||||||
debouncedValue,
|
|
||||||
restrictAdminRole,
|
|
||||||
mapRolesByName
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MultiSelectTagInput
|
|
||||||
buttonText={buttonText ?? t("alertingSelectRoles")}
|
|
||||||
searchQuery={roleSearchQuery}
|
|
||||||
onSearch={setRoleSearchQuery}
|
|
||||||
options={rolesShown}
|
|
||||||
value={selectedRoles}
|
|
||||||
onChange={onSelectRoles}
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,4 @@
|
|||||||
import React, {
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState
|
|
||||||
} from "react";
|
|
||||||
import { TagInputStyleClassesProps, type Tag as TagType } from "./tag-input";
|
import { TagInputStyleClassesProps, type Tag as TagType } from "./tag-input";
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
@@ -226,7 +220,7 @@ export const Autocomplete: React.FC<AutocompleteProps> = ({
|
|||||||
>
|
>
|
||||||
<PopoverAnchor asChild>
|
<PopoverAnchor asChild>
|
||||||
<div
|
<div
|
||||||
className="relative h-full flex items-center rounded-md border border-input bg-transparent pr-1"
|
className="relative h-full flex items-center rounded-md border border-input bg-transparent pr-3"
|
||||||
ref={triggerContainerRef}
|
ref={triggerContainerRef}
|
||||||
>
|
>
|
||||||
{childrenWithProps}
|
{childrenWithProps}
|
||||||
@@ -266,7 +260,10 @@ export const Autocomplete: React.FC<AutocompleteProps> = ({
|
|||||||
side="bottom"
|
side="bottom"
|
||||||
align="start"
|
align="start"
|
||||||
forceMount
|
forceMount
|
||||||
className={cn("p-0", classStyleProps?.popoverContent)}
|
className={cn(
|
||||||
|
"p-0",
|
||||||
|
classStyleProps?.popoverContent
|
||||||
|
)}
|
||||||
style={{
|
style={{
|
||||||
width: `${popoverWidth}px`,
|
width: `${popoverWidth}px`,
|
||||||
minWidth: `${popoverWidth}px`,
|
minWidth: `${popoverWidth}px`,
|
||||||
@@ -303,9 +300,7 @@ export const Autocomplete: React.FC<AutocompleteProps> = ({
|
|||||||
key={option.id}
|
key={option.id}
|
||||||
value={`${option.text} ${option.id}`}
|
value={`${option.text} ${option.id}`}
|
||||||
onSelect={() => toggleTag(option)}
|
onSelect={() => toggleTag(option)}
|
||||||
className={
|
className={classStyleProps?.commandItem}
|
||||||
classStyleProps?.commandItem
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Check
|
<Check
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@@ -85,8 +85,6 @@ export interface TagInputProps
|
|||||||
autocompleteFilter?: (option: string) => boolean;
|
autocompleteFilter?: (option: string) => boolean;
|
||||||
direction?: "row" | "column";
|
direction?: "row" | "column";
|
||||||
onInputChange?: (value: string) => void;
|
onInputChange?: (value: string) => void;
|
||||||
searchQuery?: string;
|
|
||||||
onSearchQueryChange?: (value: string) => void;
|
|
||||||
customTagRenderer?: (tag: Tag, isActiveTag: boolean) => React.ReactNode;
|
customTagRenderer?: (tag: Tag, isActiveTag: boolean) => React.ReactNode;
|
||||||
onFocus?: React.FocusEventHandler<HTMLInputElement>;
|
onFocus?: React.FocusEventHandler<HTMLInputElement>;
|
||||||
onBlur?: React.FocusEventHandler<HTMLInputElement>;
|
onBlur?: React.FocusEventHandler<HTMLInputElement>;
|
||||||
@@ -159,24 +157,10 @@ export function TagInput({ ref, ...props }: TagInputProps) {
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
usePortal = false,
|
usePortal = false,
|
||||||
addOnPaste = false,
|
addOnPaste = false,
|
||||||
generateTagId = uuid,
|
generateTagId = uuid
|
||||||
searchQuery,
|
|
||||||
onSearchQueryChange
|
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [inputValue, setInputValue] = React.useState("");
|
const [inputValue, setInputValue] = React.useState("");
|
||||||
const isControlled = searchQuery !== undefined;
|
|
||||||
const effectiveQuery = isControlled ? searchQuery : inputValue;
|
|
||||||
|
|
||||||
const updateQuery = React.useCallback(
|
|
||||||
(action: React.SetStateAction<string>) => {
|
|
||||||
const resolved =
|
|
||||||
typeof action === "function" ? action(effectiveQuery) : action;
|
|
||||||
if (!isControlled) setInputValue(resolved);
|
|
||||||
onSearchQueryChange?.(resolved);
|
|
||||||
},
|
|
||||||
[isControlled, effectiveQuery, onSearchQueryChange]
|
|
||||||
);
|
|
||||||
const [tagCount, setTagCount] = React.useState(Math.max(0, tags.length));
|
const [tagCount, setTagCount] = React.useState(Math.max(0, tags.length));
|
||||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
@@ -250,9 +234,9 @@ export function TagInput({ ref, ...props }: TagInputProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
updateQuery("");
|
setInputValue("");
|
||||||
} else {
|
} else {
|
||||||
updateQuery(newValue);
|
setInputValue(newValue);
|
||||||
}
|
}
|
||||||
onInputChange?.(newValue);
|
onInputChange?.(newValue);
|
||||||
};
|
};
|
||||||
@@ -263,8 +247,8 @@ export function TagInput({ ref, ...props }: TagInputProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleInputBlur = (event: React.FocusEvent<HTMLInputElement>) => {
|
const handleInputBlur = (event: React.FocusEvent<HTMLInputElement>) => {
|
||||||
if (addTagsOnBlur && effectiveQuery.trim()) {
|
if (addTagsOnBlur && inputValue.trim()) {
|
||||||
const newTagText = effectiveQuery.trim();
|
const newTagText = inputValue.trim();
|
||||||
|
|
||||||
if (validateTag && !validateTag(newTagText)) {
|
if (validateTag && !validateTag(newTagText)) {
|
||||||
return;
|
return;
|
||||||
@@ -289,7 +273,7 @@ export function TagInput({ ref, ...props }: TagInputProps) {
|
|||||||
setTags([...tags, { id: newTagId, text: newTagText }]);
|
setTags([...tags, { id: newTagId, text: newTagText }]);
|
||||||
onTagAdd?.(newTagText);
|
onTagAdd?.(newTagText);
|
||||||
setTagCount((prevTagCount) => prevTagCount + 1);
|
setTagCount((prevTagCount) => prevTagCount + 1);
|
||||||
updateQuery("");
|
setInputValue("");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,7 +287,7 @@ export function TagInput({ ref, ...props }: TagInputProps) {
|
|||||||
: e.key === delimiter || e.key === Delimiter.Enter
|
: e.key === delimiter || e.key === Delimiter.Enter
|
||||||
) {
|
) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const newTagText = effectiveQuery.trim();
|
const newTagText = inputValue.trim();
|
||||||
|
|
||||||
// Check if the tag is in the autocomplete options if restrictTagsToAutocomplete is true
|
// Check if the tag is in the autocomplete options if restrictTagsToAutocomplete is true
|
||||||
if (
|
if (
|
||||||
@@ -345,7 +329,7 @@ export function TagInput({ ref, ...props }: TagInputProps) {
|
|||||||
onTagAdd?.(newTagText);
|
onTagAdd?.(newTagText);
|
||||||
setTagCount((prevTagCount) => prevTagCount + 1);
|
setTagCount((prevTagCount) => prevTagCount + 1);
|
||||||
}
|
}
|
||||||
updateQuery("");
|
setInputValue("");
|
||||||
} else {
|
} else {
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
case "Delete":
|
case "Delete":
|
||||||
@@ -435,6 +419,9 @@ export function TagInput({ ref, ...props }: TagInputProps) {
|
|||||||
onClearAll?.();
|
onClearAll?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// const filteredAutocompleteOptions = autocompleteFilter
|
||||||
|
// ? autocompleteOptions?.filter((option) => autocompleteFilter(option.text))
|
||||||
|
// : autocompleteOptions;
|
||||||
const displayedTags = sortTags ? [...tags].sort() : tags;
|
const displayedTags = sortTags ? [...tags].sort() : tags;
|
||||||
|
|
||||||
const truncatedTags = truncate
|
const truncatedTags = truncate
|
||||||
@@ -449,15 +436,13 @@ export function TagInput({ ref, ...props }: TagInputProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={`w-full flex ${!inlineTags && tags.length > 0 ? "gap-3" : ""} ${
|
||||||
`w-full flex`,
|
|
||||||
!inlineTags && tags.length > 0 && "gap-3",
|
|
||||||
inputFieldPosition === "bottom"
|
inputFieldPosition === "bottom"
|
||||||
? "flex-col"
|
? "flex-col"
|
||||||
: inputFieldPosition === "top"
|
: inputFieldPosition === "top"
|
||||||
? "flex-col-reverse"
|
? "flex-col-reverse"
|
||||||
: "flex-row"
|
: "flex-row"
|
||||||
)}
|
}`}
|
||||||
>
|
>
|
||||||
{!usePopoverForTags &&
|
{!usePopoverForTags &&
|
||||||
(!inlineTags ? (
|
(!inlineTags ? (
|
||||||
@@ -530,14 +515,14 @@ export function TagInput({ ref, ...props }: TagInputProps) {
|
|||||||
? placeholderWhenFull
|
? placeholderWhenFull
|
||||||
: placeholder
|
: placeholder
|
||||||
}
|
}
|
||||||
value={effectiveQuery}
|
value={inputValue}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onFocus={handleInputFocus}
|
onFocus={handleInputFocus}
|
||||||
onBlur={handleInputBlur}
|
onBlur={handleInputBlur}
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-0 px-2 h-6 bg-transparent focus-visible:ring-0 focus-visible:ring-transparent focus-visible:ring-offset-0 flex-1 w-fit shadow-none inset-shadow-none",
|
"border-0 px-0 h-5 bg-transparent focus-visible:ring-0 focus-visible:ring-transparent focus-visible:ring-offset-0 flex-1 w-fit shadow-none inset-shadow-none",
|
||||||
// className,
|
// className,
|
||||||
styleClasses?.input
|
styleClasses?.input
|
||||||
)}
|
)}
|
||||||
@@ -559,17 +544,16 @@ export function TagInput({ ref, ...props }: TagInputProps) {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{enableAutocomplete ? (
|
{enableAutocomplete ? (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
tags={tags}
|
tags={tags}
|
||||||
setTags={setTags}
|
setTags={setTags}
|
||||||
setInputValue={updateQuery}
|
setInputValue={setInputValue}
|
||||||
autocompleteOptions={
|
autocompleteOptions={
|
||||||
(autocompleteOptions || []) as Tag[]
|
(autocompleteOptions || []) as Tag[]
|
||||||
}
|
}
|
||||||
filterQuery={effectiveQuery}
|
filterQuery={inputValue}
|
||||||
setTagCount={setTagCount}
|
setTagCount={setTagCount}
|
||||||
maxTags={maxTags}
|
maxTags={maxTags}
|
||||||
onTagAdd={onTagAdd}
|
onTagAdd={onTagAdd}
|
||||||
@@ -595,7 +579,7 @@ export function TagInput({ ref, ...props }: TagInputProps) {
|
|||||||
// <CommandInput
|
// <CommandInput
|
||||||
// placeholder={maxTags !== undefined && tags.length >= maxTags ? placeholderWhenFull : placeholder}
|
// placeholder={maxTags !== undefined && tags.length >= maxTags ? placeholderWhenFull : placeholder}
|
||||||
// ref={inputRef}
|
// ref={inputRef}
|
||||||
// value={effectiveQuery}
|
// value={inputValue}
|
||||||
// disabled={disabled || (maxTags !== undefined && tags.length >= maxTags)}
|
// disabled={disabled || (maxTags !== undefined && tags.length >= maxTags)}
|
||||||
// onChangeCapture={handleInputChange}
|
// onChangeCapture={handleInputChange}
|
||||||
// onKeyDown={handleKeyDown}
|
// onKeyDown={handleKeyDown}
|
||||||
@@ -617,14 +601,14 @@ export function TagInput({ ref, ...props }: TagInputProps) {
|
|||||||
? placeholderWhenFull
|
? placeholderWhenFull
|
||||||
: placeholder
|
: placeholder
|
||||||
}
|
}
|
||||||
value={effectiveQuery}
|
value={inputValue}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onFocus={handleInputFocus}
|
onFocus={handleInputFocus}
|
||||||
onBlur={handleInputBlur}
|
onBlur={handleInputBlur}
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-0 h-6 bg-transparent focus-visible:ring-0 focus-visible:ring-transparent focus-visible:ring-offset-0 flex-1 w-fit shadow-none inset-shadow-none",
|
"border-0 h-5 bg-transparent focus-visible:ring-0 focus-visible:ring-transparent focus-visible:ring-offset-0 flex-1 w-fit shadow-none inset-shadow-none",
|
||||||
// className,
|
// className,
|
||||||
styleClasses?.input
|
styleClasses?.input
|
||||||
)}
|
)}
|
||||||
@@ -678,7 +662,7 @@ export function TagInput({ ref, ...props }: TagInputProps) {
|
|||||||
{/* <CommandInput
|
{/* <CommandInput
|
||||||
placeholder={maxTags !== undefined && tags.length >= maxTags ? placeholderWhenFull : placeholder}
|
placeholder={maxTags !== undefined && tags.length >= maxTags ? placeholderWhenFull : placeholder}
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
value={effectiveQuery}
|
value={inputValue}
|
||||||
disabled={disabled || (maxTags !== undefined && tags.length >= maxTags)}
|
disabled={disabled || (maxTags !== undefined && tags.length >= maxTags)}
|
||||||
onChangeCapture={handleInputChange}
|
onChangeCapture={handleInputChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
@@ -701,14 +685,14 @@ export function TagInput({ ref, ...props }: TagInputProps) {
|
|||||||
? placeholderWhenFull
|
? placeholderWhenFull
|
||||||
: placeholder
|
: placeholder
|
||||||
}
|
}
|
||||||
value={effectiveQuery}
|
value={inputValue}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onFocus={handleInputFocus}
|
onFocus={handleInputFocus}
|
||||||
onBlur={handleInputBlur}
|
onBlur={handleInputBlur}
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-0 px-2 h-6 bg-transparent focus-visible:ring-0 focus-visible:ring-transparent focus-visible:ring-offset-0 flex-1 w-fit shadow-none inset-shadow-none",
|
"border-0 px-0 h-5 bg-transparent focus-visible:ring-0 focus-visible:ring-transparent focus-visible:ring-offset-0 flex-1 w-fit shadow-none inset-shadow-none",
|
||||||
// className,
|
// className,
|
||||||
styleClasses?.input
|
styleClasses?.input
|
||||||
)}
|
)}
|
||||||
@@ -757,7 +741,7 @@ export function TagInput({ ref, ...props }: TagInputProps) {
|
|||||||
{/* <CommandInput
|
{/* <CommandInput
|
||||||
placeholder={maxTags !== undefined && tags.length >= maxTags ? placeholderWhenFull : placeholder}
|
placeholder={maxTags !== undefined && tags.length >= maxTags ? placeholderWhenFull : placeholder}
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
value={effectiveQuery}
|
value={inputValue}
|
||||||
disabled={disabled || (maxTags !== undefined && tags.length >= maxTags)}
|
disabled={disabled || (maxTags !== undefined && tags.length >= maxTags)}
|
||||||
onChangeCapture={handleInputChange}
|
onChangeCapture={handleInputChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
@@ -779,14 +763,14 @@ export function TagInput({ ref, ...props }: TagInputProps) {
|
|||||||
? placeholderWhenFull
|
? placeholderWhenFull
|
||||||
: placeholder
|
: placeholder
|
||||||
}
|
}
|
||||||
value={effectiveQuery}
|
value={inputValue}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onFocus={handleInputFocus}
|
onFocus={handleInputFocus}
|
||||||
onBlur={handleInputBlur}
|
onBlur={handleInputBlur}
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-0 px-2 h-6 bg-transparent focus-visible:ring-0 focus-visible:ring-transparent focus-visible:ring-offset-0 flex-1 w-fit shadow-none inset-shadow-none",
|
"border-0 px-0 h-5 bg-transparent focus-visible:ring-0 focus-visible:ring-transparent focus-visible:ring-offset-0 flex-1 w-fit shadow-none inset-shadow-none",
|
||||||
// className,
|
// className,
|
||||||
styleClasses?.input
|
styleClasses?.input
|
||||||
)}
|
)}
|
||||||
@@ -822,7 +806,7 @@ export function TagInput({ ref, ...props }: TagInputProps) {
|
|||||||
? placeholderWhenFull
|
? placeholderWhenFull
|
||||||
: placeholder
|
: placeholder
|
||||||
}
|
}
|
||||||
value={effectiveQuery}
|
value={inputValue}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onFocus={handleInputFocus}
|
onFocus={handleInputFocus}
|
||||||
@@ -882,7 +866,7 @@ export function TagInput({ ref, ...props }: TagInputProps) {
|
|||||||
? placeholderWhenFull
|
? placeholderWhenFull
|
||||||
: placeholder
|
: placeholder
|
||||||
}
|
}
|
||||||
value={effectiveQuery}
|
value={inputValue}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onFocus={handleInputFocus}
|
onFocus={handleInputFocus}
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ function CommandList({
|
|||||||
<CommandPrimitive.List
|
<CommandPrimitive.List
|
||||||
data-slot="command-list"
|
data-slot="command-list"
|
||||||
className={cn(
|
className={cn(
|
||||||
"max-h-[300px] scroll-py-1 overflow-x-clip overflow-y-auto",
|
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -96,13 +96,12 @@ function CommandList({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function CommandEmpty({
|
function CommandEmpty({
|
||||||
className,
|
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
||||||
return (
|
return (
|
||||||
<CommandPrimitive.Empty
|
<CommandPrimitive.Empty
|
||||||
data-slot="command-empty"
|
data-slot="command-empty"
|
||||||
className={cn("py-6 text-center text-sm", className)}
|
className="py-6 text-center text-sm"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -116,7 +115,7 @@ function CommandGroup({
|
|||||||
<CommandPrimitive.Group
|
<CommandPrimitive.Group
|
||||||
data-slot="command-group"
|
data-slot="command-group"
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-y-auto p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
|
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -566,7 +566,7 @@ export function ControlledDataTable<TData, TValue>({
|
|||||||
))}
|
))}
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{(table.getRowModel().rows ?? []).length > 0 ? (
|
{table.getRowModel().rows?.length ? (
|
||||||
table.getRowModel().rows.map((row) => (
|
table.getRowModel().rows.map((row) => (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={row.id}
|
key={row.id}
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
import { orgQueries } from "@app/lib/queries";
|
|
||||||
import type { ListUsersResponse } from "@server/routers/user";
|
|
||||||
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { useMemo, useState } from "react";
|
|
||||||
import { useDebounce } from "use-debounce";
|
|
||||||
|
|
||||||
import { useTranslations } from "next-intl";
|
|
||||||
import { MultiSelectTagInput } from "./multi-select/multi-select-tag-input";
|
|
||||||
|
|
||||||
export type SelectedUser = {
|
|
||||||
id: string;
|
|
||||||
text: string;
|
|
||||||
ipdName?: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type UsersSelectorProps = {
|
|
||||||
orgId: string;
|
|
||||||
selectedUsers?: SelectedUser[];
|
|
||||||
onSelectUsers: (users: SelectedUser[]) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function UsersSelector({
|
|
||||||
orgId,
|
|
||||||
selectedUsers = [],
|
|
||||||
onSelectUsers
|
|
||||||
}: UsersSelectorProps) {
|
|
||||||
const t = useTranslations();
|
|
||||||
const [userSearchQuery, setUserSearchQuery] = useState("");
|
|
||||||
|
|
||||||
const [debouncedValue] = useDebounce(userSearchQuery, 150);
|
|
||||||
|
|
||||||
const { data: users = [] } = useQuery(
|
|
||||||
orgQueries.users({ orgId, perPage: 10, query: debouncedValue })
|
|
||||||
);
|
|
||||||
|
|
||||||
// always include the selected users in the list (if the user isn't searching)
|
|
||||||
const usersShown = useMemo(() => {
|
|
||||||
const allUsers: Array<SelectedUser> = users.map((u) => ({
|
|
||||||
id: u.id,
|
|
||||||
text: getUserDisplayName(u)
|
|
||||||
}));
|
|
||||||
if (debouncedValue.trim().length === 0) {
|
|
||||||
for (const user of selectedUsers) {
|
|
||||||
if (!allUsers.find((u) => u.id === user.id)) {
|
|
||||||
allUsers.unshift(user);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return allUsers;
|
|
||||||
}, [users, selectedUsers, debouncedValue]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MultiSelectTagInput
|
|
||||||
buttonText={t("alertingSelectUsers")}
|
|
||||||
searchQuery={userSearchQuery}
|
|
||||||
onSearch={setUserSearchQuery}
|
|
||||||
options={usersShown}
|
|
||||||
value={selectedUsers}
|
|
||||||
onChange={onSelectUsers}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,6 @@ type UserDisplayNameInput =
|
|||||||
email?: string | null;
|
email?: string | null;
|
||||||
name?: string | null;
|
name?: string | null;
|
||||||
username?: string | null;
|
username?: string | null;
|
||||||
idpName?: string | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -22,25 +21,16 @@ export function getUserDisplayName(input: UserDisplayNameInput): string {
|
|||||||
let email: string | null | undefined;
|
let email: string | null | undefined;
|
||||||
let name: string | null | undefined;
|
let name: string | null | undefined;
|
||||||
let username: string | null | undefined;
|
let username: string | null | undefined;
|
||||||
let idpName: string | null | undefined;
|
|
||||||
|
|
||||||
if ("user" in input) {
|
if ("user" in input) {
|
||||||
email = input.user.email;
|
email = input.user.email;
|
||||||
name = input.user.name;
|
name = input.user.name;
|
||||||
username = input.user.username;
|
username = input.user.username;
|
||||||
idpName = input.user.idpName;
|
|
||||||
} else {
|
} else {
|
||||||
email = input.email;
|
email = input.email;
|
||||||
name = input.name;
|
name = input.name;
|
||||||
username = input.username;
|
username = input.username;
|
||||||
idpName = input.idpName;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let nameShown = email || name || username || "";
|
return email || name || username || "";
|
||||||
|
|
||||||
if (idpName) {
|
|
||||||
nameShown = `${nameShown} (${idpName})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return nameShown;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,56 +125,24 @@ export const orgQueries = {
|
|||||||
return res.data.data.clients;
|
return res.data.data.clients;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
users: ({
|
users: ({ orgId }: { orgId: string }) =>
|
||||||
orgId,
|
|
||||||
query,
|
|
||||||
perPage = 10_000
|
|
||||||
}: {
|
|
||||||
orgId: string;
|
|
||||||
query?: string;
|
|
||||||
perPage?: number;
|
|
||||||
}) =>
|
|
||||||
queryOptions({
|
queryOptions({
|
||||||
queryKey: ["ORG", orgId, "USERS", { query, perPage }] as const,
|
queryKey: ["ORG", orgId, "USERS"] as const,
|
||||||
queryFn: async ({ signal, meta }) => {
|
queryFn: async ({ signal, meta }) => {
|
||||||
const sp = new URLSearchParams({
|
|
||||||
pageSize: perPage.toString()
|
|
||||||
});
|
|
||||||
|
|
||||||
if (query?.trim()) {
|
|
||||||
sp.set("query", query);
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await meta!.api.get<
|
const res = await meta!.api.get<
|
||||||
AxiosResponse<ListUsersResponse>
|
AxiosResponse<ListUsersResponse>
|
||||||
>(`/org/${orgId}/users?${sp.toString()}`, { signal });
|
>(`/org/${orgId}/users`, { signal });
|
||||||
|
|
||||||
return res.data.data.users;
|
return res.data.data.users;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
roles: ({
|
roles: ({ orgId }: { orgId: string }) =>
|
||||||
orgId,
|
|
||||||
query,
|
|
||||||
perPage = 10_000
|
|
||||||
}: {
|
|
||||||
orgId: string;
|
|
||||||
query?: string;
|
|
||||||
perPage?: number;
|
|
||||||
}) =>
|
|
||||||
queryOptions({
|
queryOptions({
|
||||||
queryKey: ["ORG", orgId, "ROLES", { query, perPage }] as const,
|
queryKey: ["ORG", orgId, "ROLES"] as const,
|
||||||
queryFn: async ({ signal, meta }) => {
|
queryFn: async ({ signal, meta }) => {
|
||||||
const sp = new URLSearchParams({
|
|
||||||
pageSize: perPage.toString()
|
|
||||||
});
|
|
||||||
|
|
||||||
if (query?.trim()) {
|
|
||||||
sp.set("query", query);
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await meta!.api.get<
|
const res = await meta!.api.get<
|
||||||
AxiosResponse<ListRolesResponse>
|
AxiosResponse<ListRolesResponse>
|
||||||
>(`/org/${orgId}/roles?${sp.toString()}`, { signal });
|
>(`/org/${orgId}/roles`, { signal });
|
||||||
|
|
||||||
return res.data.data.roles;
|
return res.data.data.roles;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user