mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-09 09:49:51 +00:00
Compare commits
49 Commits
1.18.3-s.2
...
crowdin_de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
834e44336c | ||
|
|
f2160ea270 | ||
|
|
c0da2f184d | ||
|
|
3e6eb06bd8 | ||
|
|
fac3f3a8c0 | ||
|
|
425eda8e48 | ||
|
|
aa4860885c | ||
|
|
65605362da | ||
|
|
5d1ce49f63 | ||
|
|
6119c07272 | ||
|
|
9daa95aeef | ||
|
|
3572d92174 | ||
|
|
9dcc423c88 | ||
|
|
e445c2efbd | ||
|
|
9fb677e952 | ||
|
|
88d8414eb8 | ||
|
|
5f3fafb1b0 | ||
|
|
de1338a8cd | ||
|
|
0800aa2a61 | ||
|
|
4959d66ac1 | ||
|
|
9320df8be6 | ||
|
|
13ec6b6620 | ||
|
|
2ca3ef019c | ||
|
|
724e41a54f | ||
|
|
ce5e62d216 | ||
|
|
874dc2b33e | ||
|
|
3b2622d590 | ||
|
|
c81d855741 | ||
|
|
3bce8d3596 | ||
|
|
ee2a1e2bc3 | ||
|
|
a0f3ee74f9 | ||
|
|
82a36fd632 | ||
|
|
c5084137ab | ||
|
|
65ec8da100 | ||
|
|
e76e7581a5 | ||
|
|
a97a4b6ec1 | ||
|
|
e38bbde348 | ||
|
|
026260ddfb | ||
|
|
97be5eb7d5 | ||
|
|
d7b96ba3f5 | ||
|
|
b42672530f | ||
|
|
b6b2dbd8ab | ||
|
|
975f3a01f5 | ||
|
|
4de2dfff85 | ||
|
|
27d230647f | ||
|
|
114486608e | ||
|
|
10fa9274d0 | ||
|
|
cbdc74768f | ||
|
|
451f3d24a8 |
@@ -523,6 +523,12 @@
|
||||
"userMessageOrgRemove": "След като бъде премахнат, този потребител няма да има достъп до организацията. Винаги можете да го поканите отново по-късно, но той ще трябва да приеме отново поканата.",
|
||||
"userRemoveOrgConfirm": "Потвърдете премахването на потребителя",
|
||||
"userRemoveOrg": "Премахване на потребителя от организацията",
|
||||
"userQuestionOrgRemoveSelf": "Are you sure you want to remove yourself from this organization?",
|
||||
"userMessageOrgRemoveSelf": "You will lose access immediately. An administrator can invite you again later, but you will need to accept a new invitation.",
|
||||
"userRemoveOrgConfirmSelf": "Confirm Remove Myself",
|
||||
"userRemoveOrgSelf": "Remove yourself from the organization",
|
||||
"userRemoveOrgSelfWarning": "You will lose access to this organization immediately.",
|
||||
"userRemoveOrgConfirmPhraseSelf": "REMOVE MYSELF FROM ORG",
|
||||
"users": "Потребители",
|
||||
"accessRoleMember": "Член",
|
||||
"accessRoleOwner": "Собственик",
|
||||
@@ -531,6 +537,11 @@
|
||||
"emailInvalid": "Невалиден имейл адрес",
|
||||
"inviteValidityDuration": "Моля, изберете продължителност",
|
||||
"accessRoleSelectPlease": "Моля, изберете роля",
|
||||
"removeOwnAdminRoleConfirmTitle": "Remove your administrator access?",
|
||||
"removeOwnAdminRoleConfirmDescription": "You will no longer have administrator permissions in this organization after saving. Another administrator can restore access if needed.",
|
||||
"removeOwnAdminRoleConfirmButton": "Remove My Administrator Access",
|
||||
"removeOwnAdminRoleConfirmPhrase": "REMOVE MY ADMIN ACCESS",
|
||||
"ownerMustRetainAdminRole": "The organization owner must keep at least one administrator role.",
|
||||
"usernameRequired": "Необходимо е потребителско име",
|
||||
"idpSelectPlease": "Моля, изберете доставчик на идентичност",
|
||||
"idpGenericOidc": "Основен OAuth2/OIDC доставчик.",
|
||||
@@ -3062,7 +3073,7 @@
|
||||
"streamingDatadogTitle": "Datadog",
|
||||
"streamingDatadogDescription": "Пресочвайте събития директно към вашият акаунт в Datadog. Очаквайте скоро.",
|
||||
"streamingTypePickerDescription": "Изберете вид на дестинацията, за да започнете.",
|
||||
"streamingFailedToLoad": "Неуспешно зареждане на дестинации",
|
||||
"streamingLastSyncError": "Възникна грешка при последната синхронизация",
|
||||
"streamingUnexpectedError": "Възникна неочаквана грешка.",
|
||||
"streamingFailedToUpdate": "Неуспешно актуализиране на дестинация",
|
||||
"streamingDeletedSuccess": "Дестинацията беше изтрита успешно",
|
||||
@@ -3079,7 +3090,34 @@
|
||||
"S3DestEditTitle": "Редактиране на дестинацията",
|
||||
"S3DestAddTitle": "Добавете S3 дестинация",
|
||||
"S3DestEditDescription": "Актуализирайте конфигурацията за тази S3 дестинация за предаване на събития.",
|
||||
"S3DestAddDescription": "Конфигурирайте нов крайна точка на S3, за да получавате събития на вашата организация.",
|
||||
"S3DestAddDescription": "Конфигурирайте ново хранилище Amazon S3 (или съвместимо с S3), за да получавате събития на вашата организация.",
|
||||
"s3DestTabSettings": "Настройки",
|
||||
"s3DestTabFormat": "Формат",
|
||||
"s3DestNameLabel": "Име",
|
||||
"s3DestNamePlaceholder": "Моята S3 дестинация",
|
||||
"s3DestAccessKeyIdLabel": "Идентификатор на достъп за AWS Key ID",
|
||||
"s3DestSecretAccessKeyLabel": "Тайният ключ за достъп на AWS",
|
||||
"s3DestSecretAccessKeyPlaceholder": "Вашият таен ключ за достъп за AWS",
|
||||
"s3DestRegionLabel": "AWS Регион",
|
||||
"s3DestBucketLabel": "Име на хранилище",
|
||||
"s3DestPrefixLabel": "Префикс на ключ (по избор)",
|
||||
"s3DestPrefixDescription": "По избор пътеводен префикс, добавен към всеки обектен ключ. Обектите се съхраняват в {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.",
|
||||
"s3DestEndpointLabel": "Потребителски крайна точка (по избор)",
|
||||
"s3DestEndpointDescription": "Заместете крайната точка на S3 за съвместимо с S3 хранилище като MinIO или Cloudflare R2. Оставете празно за стандартното AWS S3.",
|
||||
"s3DestGzipLabel": "Gzip компресия",
|
||||
"s3DestGzipDescription": "Компресирайте всеки качен обект с gzip. Намалява разходите за съхранение и размера на качването.",
|
||||
"s3DestFormatTitle": "Формат на файл",
|
||||
"s3DestFormatDescription": "Как събитията са сериализирани вътре във всеки качен обект.",
|
||||
"s3DestFormatJsonArrayDescription": "Всеки обект е JSON масив от записи на събития. Съвместим с повечето аналитични инструменти.",
|
||||
"s3DestFormatNdjsonDescription": "Всеки обект съдържа един JSON запис на ред (форматиран JSON с нов ред). Съвместим с Athena, BigQuery и Spark.",
|
||||
"s3DestFormatCsvTitle": "CSV",
|
||||
"s3DestFormatCsvDescription": "Всеки обект е RFC-4180 CSV файл с ред заглавие. Имената на колоните са извлечени от полетата на данните за събитията.",
|
||||
"s3DestSaveChanges": "Запази промените",
|
||||
"s3DestCreateDestination": "Създаване на дестинация",
|
||||
"s3DestUpdatedSuccess": "Дестинацията е актуализирана успешно",
|
||||
"s3DestCreatedSuccess": "Дестинацията е създадена успешно",
|
||||
"s3DestUpdateFailed": "Неуспешно актуализиране на дестинацията",
|
||||
"s3DestCreateFailed": "Неуспешно създаване на дестинация",
|
||||
"datadogDestEditTitle": "Редактиране на дестинация",
|
||||
"datadogDestAddTitle": "Добавяне на Datadog дестинация",
|
||||
"datadogDestEditDescription": "Актуализирайте конфигурацията за тази Datadog дестинация за предаване на събития.",
|
||||
|
||||
@@ -523,6 +523,12 @@
|
||||
"userMessageOrgRemove": "Po odstranění tohoto uživatele již nebude mít přístup k organizaci. Vždy je můžete znovu pozvat později, ale budou muset pozvání znovu přijmout.",
|
||||
"userRemoveOrgConfirm": "Potvrdit odebrání uživatele",
|
||||
"userRemoveOrg": "Odebrat uživatele z organizace",
|
||||
"userQuestionOrgRemoveSelf": "Are you sure you want to remove yourself from this organization?",
|
||||
"userMessageOrgRemoveSelf": "You will lose access immediately. An administrator can invite you again later, but you will need to accept a new invitation.",
|
||||
"userRemoveOrgConfirmSelf": "Confirm Remove Myself",
|
||||
"userRemoveOrgSelf": "Remove yourself from the organization",
|
||||
"userRemoveOrgSelfWarning": "You will lose access to this organization immediately.",
|
||||
"userRemoveOrgConfirmPhraseSelf": "REMOVE MYSELF FROM ORG",
|
||||
"users": "Uživatelé",
|
||||
"accessRoleMember": "Člen",
|
||||
"accessRoleOwner": "Vlastník",
|
||||
@@ -531,6 +537,11 @@
|
||||
"emailInvalid": "Neplatná e-mailová adresa",
|
||||
"inviteValidityDuration": "Zvolte prosím dobu trvání",
|
||||
"accessRoleSelectPlease": "Vyberte prosím roli",
|
||||
"removeOwnAdminRoleConfirmTitle": "Remove your administrator access?",
|
||||
"removeOwnAdminRoleConfirmDescription": "You will no longer have administrator permissions in this organization after saving. Another administrator can restore access if needed.",
|
||||
"removeOwnAdminRoleConfirmButton": "Remove My Administrator Access",
|
||||
"removeOwnAdminRoleConfirmPhrase": "REMOVE MY ADMIN ACCESS",
|
||||
"ownerMustRetainAdminRole": "The organization owner must keep at least one administrator role.",
|
||||
"usernameRequired": "Uživatelské jméno je povinné",
|
||||
"idpSelectPlease": "Vyberte poskytovatele identity",
|
||||
"idpGenericOidc": "Generic OAuth2/OIDC provider.",
|
||||
@@ -3062,7 +3073,7 @@
|
||||
"streamingDatadogTitle": "Datadog",
|
||||
"streamingDatadogDescription": "Přeposlat události přímo do vašeho účtu Datadog účtu. Brzy přijde.",
|
||||
"streamingTypePickerDescription": "Vyberte cílový typ pro začátek.",
|
||||
"streamingFailedToLoad": "Nepodařilo se načíst destinace",
|
||||
"streamingLastSyncError": "Došlo k chybě při poslední synchronizaci",
|
||||
"streamingUnexpectedError": "Došlo k neočekávané chybě.",
|
||||
"streamingFailedToUpdate": "Nepodařilo se aktualizovat cíl",
|
||||
"streamingDeletedSuccess": "Cíl byl úspěšně odstraněn",
|
||||
@@ -3079,7 +3090,34 @@
|
||||
"S3DestEditTitle": "Upravit cíl",
|
||||
"S3DestAddTitle": "Přidat S3 cíl",
|
||||
"S3DestEditDescription": "Aktualizujte konfiguraci tohoto S3 cíle pro streamování událostí.",
|
||||
"S3DestAddDescription": "Konfigurujte nový S3 koncový bod pro přijímání událostí vaší organizace.",
|
||||
"S3DestAddDescription": "Nakonfigurujte nový Amazon S3 (nebo S3-kompatibilní) bucket, aby přijímal události vaší organizace.",
|
||||
"s3DestTabSettings": "Nastavení",
|
||||
"s3DestTabFormat": "Formát",
|
||||
"s3DestNameLabel": "Jméno",
|
||||
"s3DestNamePlaceholder": "Moje cílové S3",
|
||||
"s3DestAccessKeyIdLabel": "ID přístupového klíče AWS",
|
||||
"s3DestSecretAccessKeyLabel": "Tajný přístupový klíč AWS",
|
||||
"s3DestSecretAccessKeyPlaceholder": "Váš tajný přístupový klíč AWS",
|
||||
"s3DestRegionLabel": "Oblast AWS",
|
||||
"s3DestBucketLabel": "Název bucketu",
|
||||
"s3DestPrefixLabel": "Předpona klíče (volitelné)",
|
||||
"s3DestPrefixDescription": "Volitelná cesta předpony přidaná ke každému objektovému klíči. Objekty jsou uloženy na {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.",
|
||||
"s3DestEndpointLabel": "Vlastní koncový bod (volitelné)",
|
||||
"s3DestEndpointDescription": "Přepište koncový bod S3 pro S3-kompatibilní úložiště, jako je MinIO nebo Cloudflare R2. Nechte prázdné pro standardní AWS S3.",
|
||||
"s3DestGzipLabel": "Komprese Gzip",
|
||||
"s3DestGzipDescription": "Komprimujte každý nahraný objekt pomocí gzip. Snižuje náklady na uložení a velikost nahrávání.",
|
||||
"s3DestFormatTitle": "Formát souboru",
|
||||
"s3DestFormatDescription": "Jak jsou události serializovány v každém nahraném objektu.",
|
||||
"s3DestFormatJsonArrayDescription": "Každý objekt je pole JSON záznamů událostí. Kompatibilní s většinou analytických nástrojů.",
|
||||
"s3DestFormatNdjsonDescription": "Každý objekt obsahuje jeden JSON záznam na řádku (newline-delimited JSON). Kompatibilní s Athena, BigQuery a Spark.",
|
||||
"s3DestFormatCsvTitle": "CSV",
|
||||
"s3DestFormatCsvDescription": "Každý objekt je soubor CSV podle RFC-4180 s řádkem záhlaví. Názvy sloupců jsou odvozeny z polí dat událostí.",
|
||||
"s3DestSaveChanges": "Uložit změny",
|
||||
"s3DestCreateDestination": "Vytvořit destinaci",
|
||||
"s3DestUpdatedSuccess": "Destinace úspěšně aktualizována",
|
||||
"s3DestCreatedSuccess": "Destinace úspěšně vytvořena",
|
||||
"s3DestUpdateFailed": "Aktualizace destinace se nezdařila",
|
||||
"s3DestCreateFailed": "Vytvoření destinace se nezdařilo",
|
||||
"datadogDestEditTitle": "Upravit cíl",
|
||||
"datadogDestAddTitle": "Přidat Datadog cíl",
|
||||
"datadogDestEditDescription": "Aktualizujte konfiguraci tohoto Datadog cíle pro streamování událostí.",
|
||||
|
||||
@@ -523,6 +523,12 @@
|
||||
"userMessageOrgRemove": "Nach dem Entfernen hat dieser Benutzer keinen Zugriff mehr auf die Organisation. Sie können ihn später jederzeit wieder einladen, aber er muss die Einladung erneut annehmen.",
|
||||
"userRemoveOrgConfirm": "Entfernen des Benutzers bestätigen",
|
||||
"userRemoveOrg": "Benutzer aus der Organisation entfernen",
|
||||
"userQuestionOrgRemoveSelf": "Are you sure you want to remove yourself from this organization?",
|
||||
"userMessageOrgRemoveSelf": "You will lose access immediately. An administrator can invite you again later, but you will need to accept a new invitation.",
|
||||
"userRemoveOrgConfirmSelf": "Confirm Remove Myself",
|
||||
"userRemoveOrgSelf": "Remove yourself from the organization",
|
||||
"userRemoveOrgSelfWarning": "You will lose access to this organization immediately.",
|
||||
"userRemoveOrgConfirmPhraseSelf": "REMOVE MYSELF FROM ORG",
|
||||
"users": "Benutzer",
|
||||
"accessRoleMember": "Mitglied",
|
||||
"accessRoleOwner": "Eigentümer",
|
||||
@@ -531,6 +537,11 @@
|
||||
"emailInvalid": "Ungültige E-Mail-Adresse",
|
||||
"inviteValidityDuration": "Bitte wählen Sie eine Dauer",
|
||||
"accessRoleSelectPlease": "Bitte wählen Sie eine Rolle",
|
||||
"removeOwnAdminRoleConfirmTitle": "Remove your administrator access?",
|
||||
"removeOwnAdminRoleConfirmDescription": "You will no longer have administrator permissions in this organization after saving. Another administrator can restore access if needed.",
|
||||
"removeOwnAdminRoleConfirmButton": "Remove My Administrator Access",
|
||||
"removeOwnAdminRoleConfirmPhrase": "REMOVE MY ADMIN ACCESS",
|
||||
"ownerMustRetainAdminRole": "The organization owner must keep at least one administrator role.",
|
||||
"usernameRequired": "Benutzername ist erforderlich",
|
||||
"idpSelectPlease": "Bitte wählen Sie einen Identitätsanbieter",
|
||||
"idpGenericOidc": "Generischer OAuth2/OIDC-Anbieter.",
|
||||
@@ -3062,7 +3073,7 @@
|
||||
"streamingDatadogTitle": "Datadog",
|
||||
"streamingDatadogDescription": "Events direkt an Ihr Datadog Konto weiterleiten. Kommen Sie bald.",
|
||||
"streamingTypePickerDescription": "Wählen Sie einen Zieltyp aus, um loszulegen.",
|
||||
"streamingFailedToLoad": "Fehler beim Laden der Ziele",
|
||||
"streamingLastSyncError": "Beim letzten Synchronisieren ist ein Fehler aufgetreten.",
|
||||
"streamingUnexpectedError": "Ein unerwarteter Fehler ist aufgetreten.",
|
||||
"streamingFailedToUpdate": "Fehler beim Aktualisieren des Ziels",
|
||||
"streamingDeletedSuccess": "Ziel erfolgreich gelöscht",
|
||||
@@ -3079,7 +3090,34 @@
|
||||
"S3DestEditTitle": "Ziel bearbeiten",
|
||||
"S3DestAddTitle": "S3-Ziel hinzufügen",
|
||||
"S3DestEditDescription": "Konfiguration für dieses S3-Ereignis-Streamingziel aktualisieren.",
|
||||
"S3DestAddDescription": "Neuen S3-Endpunkt konfigurieren, um die Ereignisse Ihrer Organisation zu erhalten.",
|
||||
"S3DestAddDescription": "Konfigurieren Sie einen neuen Amazon S3 (oder S3-kompatiblen) Bucket, um die Ereignisse Ihrer Organisation zu empfangen.",
|
||||
"s3DestTabSettings": "Einstellungen",
|
||||
"s3DestTabFormat": "Format",
|
||||
"s3DestNameLabel": "Name",
|
||||
"s3DestNamePlaceholder": "Mein S3-Ziel",
|
||||
"s3DestAccessKeyIdLabel": "AWS-Zugriffsschlüssel-ID",
|
||||
"s3DestSecretAccessKeyLabel": "AWS-Geheimzugriffsschlüssel",
|
||||
"s3DestSecretAccessKeyPlaceholder": "Ihr AWS-Geheimzugriffsschlüssel",
|
||||
"s3DestRegionLabel": "AWS-Region",
|
||||
"s3DestBucketLabel": "Bucket-Name",
|
||||
"s3DestPrefixLabel": "Schlüssel-Präfix (optional)",
|
||||
"s3DestPrefixDescription": "Optionales Pfadpräfix, das jedem Objektschlüssel vorangestellt wird. Objekte werden unter {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename} gespeichert.",
|
||||
"s3DestEndpointLabel": "Benutzerdefinierter Endpunkt (optional)",
|
||||
"s3DestEndpointDescription": "Überschreiben Sie den S3-Endpunkt für S3-kompatiblen Speicher wie MinIO oder Cloudflare R2. Lassen Sie das Feld leer für standardmäßiges AWS S3.",
|
||||
"s3DestGzipLabel": "Gzip-Komprimierung",
|
||||
"s3DestGzipDescription": "Jedes hochgeladene Objekt mit Gzip komprimieren. Reduziert die Speicherkosten und die Upload-Größe.",
|
||||
"s3DestFormatTitle": "Dateiformat",
|
||||
"s3DestFormatDescription": "Wie Ereignisse in jedem hochgeladenen Objekt serialisiert werden.",
|
||||
"s3DestFormatJsonArrayDescription": "Jedes Objekt ist ein JSON-Array von Ereignisdaten. Kompatibel mit den meisten Analysetools.",
|
||||
"s3DestFormatNdjsonDescription": "Jedes Objekt enthält einen JSON-Datensatz pro Zeile (newline-delimited JSON). Kompatibel mit Athena, BigQuery und Spark.",
|
||||
"s3DestFormatCsvTitle": "CSV",
|
||||
"s3DestFormatCsvDescription": "Jedes Objekt ist eine RFC-4180 CSV-Datei mit einer Kopfzeile. Spaltennamen werden aus den Ereignisdatenfeldern abgeleitet.",
|
||||
"s3DestSaveChanges": "Änderungen speichern",
|
||||
"s3DestCreateDestination": "Ziel erstellen",
|
||||
"s3DestUpdatedSuccess": "Ziel erfolgreich aktualisiert",
|
||||
"s3DestCreatedSuccess": "Ziel erfolgreich erstellt",
|
||||
"s3DestUpdateFailed": "Fehler beim Aktualisieren des Ziels",
|
||||
"s3DestCreateFailed": "Fehler beim Erstellen des Ziels",
|
||||
"datadogDestEditTitle": "Ziel bearbeiten",
|
||||
"datadogDestAddTitle": "Datadog-Ziel hinzufügen",
|
||||
"datadogDestEditDescription": "Konfiguration für dieses Datadog-Ereignis-Streamingziel aktualisieren.",
|
||||
|
||||
@@ -523,6 +523,12 @@
|
||||
"userMessageOrgRemove": "Once removed, this user will no longer have access to the organization. You can always re-invite them later, but they will need to accept the invitation again.",
|
||||
"userRemoveOrgConfirm": "Confirm Remove User",
|
||||
"userRemoveOrg": "Remove User from Organization",
|
||||
"userQuestionOrgRemoveSelf": "Are you sure you want to remove yourself from this organization?",
|
||||
"userMessageOrgRemoveSelf": "You will lose access immediately. An administrator can invite you again later, but you will need to accept a new invitation.",
|
||||
"userRemoveOrgConfirmSelf": "Confirm Remove Myself",
|
||||
"userRemoveOrgSelf": "Remove yourself from the organization",
|
||||
"userRemoveOrgSelfWarning": "You will lose access to this organization immediately.",
|
||||
"userRemoveOrgConfirmPhraseSelf": "REMOVE MYSELF FROM ORG",
|
||||
"users": "Users",
|
||||
"accessRoleMember": "Member",
|
||||
"accessRoleOwner": "Owner",
|
||||
@@ -531,6 +537,11 @@
|
||||
"emailInvalid": "Invalid email address",
|
||||
"inviteValidityDuration": "Please select a duration",
|
||||
"accessRoleSelectPlease": "Please select a role",
|
||||
"removeOwnAdminRoleConfirmTitle": "Remove your administrator access?",
|
||||
"removeOwnAdminRoleConfirmDescription": "You will no longer have administrator permissions in this organization after saving. Another administrator can restore access if needed.",
|
||||
"removeOwnAdminRoleConfirmButton": "Remove My Administrator Access",
|
||||
"removeOwnAdminRoleConfirmPhrase": "REMOVE MY ADMIN ACCESS",
|
||||
"ownerMustRetainAdminRole": "The organization owner must keep at least one administrator role.",
|
||||
"usernameRequired": "Username is required",
|
||||
"idpSelectPlease": "Please select an identity provider",
|
||||
"idpGenericOidc": "Generic OAuth2/OIDC provider.",
|
||||
@@ -3062,7 +3073,7 @@
|
||||
"streamingDatadogTitle": "Datadog",
|
||||
"streamingDatadogDescription": "Forward events directly to your Datadog account.",
|
||||
"streamingTypePickerDescription": "Choose a destination type to get started.",
|
||||
"streamingFailedToLoad": "Failed to load destinations",
|
||||
"streamingLastSyncError": "An error occurred on the last sync",
|
||||
"streamingUnexpectedError": "An unexpected error occurred.",
|
||||
"streamingFailedToUpdate": "Failed to update destination",
|
||||
"streamingDeletedSuccess": "Destination deleted successfully",
|
||||
@@ -3079,7 +3090,34 @@
|
||||
"S3DestEditTitle": "Edit Destination",
|
||||
"S3DestAddTitle": "Add S3 Destination",
|
||||
"S3DestEditDescription": "Update the configuration for this S3 event streaming destination.",
|
||||
"S3DestAddDescription": "Configure a new S3 endpoint to receive your organization's events.",
|
||||
"S3DestAddDescription": "Configure a new Amazon S3 (or S3-compatible) bucket to receive your organization's events.",
|
||||
"s3DestTabSettings": "Settings",
|
||||
"s3DestTabFormat": "Format",
|
||||
"s3DestNameLabel": "Name",
|
||||
"s3DestNamePlaceholder": "My S3 destination",
|
||||
"s3DestAccessKeyIdLabel": "AWS Access Key ID",
|
||||
"s3DestSecretAccessKeyLabel": "AWS Secret Access Key",
|
||||
"s3DestSecretAccessKeyPlaceholder": "Your AWS secret access key",
|
||||
"s3DestRegionLabel": "AWS Region",
|
||||
"s3DestBucketLabel": "Bucket Name",
|
||||
"s3DestPrefixLabel": "Key Prefix (optional)",
|
||||
"s3DestPrefixDescription": "Optional path prefix prepended to every object key. Objects are stored at {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.",
|
||||
"s3DestEndpointLabel": "Custom Endpoint (optional)",
|
||||
"s3DestEndpointDescription": "Override the S3 endpoint for S3-compatible storage such as MinIO or Cloudflare R2. Leave blank for standard AWS S3.",
|
||||
"s3DestGzipLabel": "Gzip compression",
|
||||
"s3DestGzipDescription": "Compress each uploaded object with gzip. Reduces storage costs and upload size.",
|
||||
"s3DestFormatTitle": "File Format",
|
||||
"s3DestFormatDescription": "How events are serialised inside each uploaded object.",
|
||||
"s3DestFormatJsonArrayDescription": "Each object is a JSON array of event records. Compatible with most analytics tools.",
|
||||
"s3DestFormatNdjsonDescription": "Each object contains one JSON record per line (newline-delimited JSON). Compatible with Athena, BigQuery, and Spark.",
|
||||
"s3DestFormatCsvTitle": "CSV",
|
||||
"s3DestFormatCsvDescription": "Each object is an RFC-4180 CSV file with a header row. Column names are derived from the event data fields.",
|
||||
"s3DestSaveChanges": "Save Changes",
|
||||
"s3DestCreateDestination": "Create Destination",
|
||||
"s3DestUpdatedSuccess": "Destination updated successfully",
|
||||
"s3DestCreatedSuccess": "Destination created successfully",
|
||||
"s3DestUpdateFailed": "Failed to update destination",
|
||||
"s3DestCreateFailed": "Failed to create destination",
|
||||
"datadogDestEditTitle": "Edit Destination",
|
||||
"datadogDestAddTitle": "Add Datadog Destination",
|
||||
"datadogDestEditDescription": "Update the configuration for this Datadog event streaming destination.",
|
||||
|
||||
@@ -523,6 +523,12 @@
|
||||
"userMessageOrgRemove": "Una vez eliminado, este usuario ya no tendrá acceso a la organización. Siempre puede volver a invitarlos más tarde, pero tendrán que aceptar la invitación de nuevo.",
|
||||
"userRemoveOrgConfirm": "Confirmar eliminar usuario",
|
||||
"userRemoveOrg": "Eliminar usuario de la organización",
|
||||
"userQuestionOrgRemoveSelf": "Are you sure you want to remove yourself from this organization?",
|
||||
"userMessageOrgRemoveSelf": "You will lose access immediately. An administrator can invite you again later, but you will need to accept a new invitation.",
|
||||
"userRemoveOrgConfirmSelf": "Confirm Remove Myself",
|
||||
"userRemoveOrgSelf": "Remove yourself from the organization",
|
||||
"userRemoveOrgSelfWarning": "You will lose access to this organization immediately.",
|
||||
"userRemoveOrgConfirmPhraseSelf": "REMOVE MYSELF FROM ORG",
|
||||
"users": "Usuarios",
|
||||
"accessRoleMember": "Miembro",
|
||||
"accessRoleOwner": "Propietario",
|
||||
@@ -531,6 +537,11 @@
|
||||
"emailInvalid": "Dirección de correo inválida",
|
||||
"inviteValidityDuration": "Por favor, seleccione una duración",
|
||||
"accessRoleSelectPlease": "Por favor, seleccione un rol",
|
||||
"removeOwnAdminRoleConfirmTitle": "Remove your administrator access?",
|
||||
"removeOwnAdminRoleConfirmDescription": "You will no longer have administrator permissions in this organization after saving. Another administrator can restore access if needed.",
|
||||
"removeOwnAdminRoleConfirmButton": "Remove My Administrator Access",
|
||||
"removeOwnAdminRoleConfirmPhrase": "REMOVE MY ADMIN ACCESS",
|
||||
"ownerMustRetainAdminRole": "The organization owner must keep at least one administrator role.",
|
||||
"usernameRequired": "Nombre de usuario requerido",
|
||||
"idpSelectPlease": "Por favor, seleccione un proveedor de identidad",
|
||||
"idpGenericOidc": "Proveedor OAuth2/OIDC genérico.",
|
||||
@@ -3062,7 +3073,7 @@
|
||||
"streamingDatadogTitle": "Datadog",
|
||||
"streamingDatadogDescription": "Reenviar eventos directamente a tu cuenta de Datadog. Próximamente.",
|
||||
"streamingTypePickerDescription": "Elija un tipo de destino para empezar.",
|
||||
"streamingFailedToLoad": "Error al cargar destinos",
|
||||
"streamingLastSyncError": "Ocurrió un error en la última sincronización.",
|
||||
"streamingUnexpectedError": "Se ha producido un error inesperado.",
|
||||
"streamingFailedToUpdate": "Error al actualizar destino",
|
||||
"streamingDeletedSuccess": "Destino eliminado correctamente",
|
||||
@@ -3079,7 +3090,34 @@
|
||||
"S3DestEditTitle": "Editar destino",
|
||||
"S3DestAddTitle": "Añadir destino S3",
|
||||
"S3DestEditDescription": "Actualice la configuración para este destino de transmisión de eventos S3.",
|
||||
"S3DestAddDescription": "Configure un nuevo punto final S3 para recibir los eventos de su organización.",
|
||||
"S3DestAddDescription": "Configura un nuevo bucket de Amazon S3 (o compatible con S3) para recibir los eventos de tu organización.",
|
||||
"s3DestTabSettings": "Ajustes",
|
||||
"s3DestTabFormat": "Formato",
|
||||
"s3DestNameLabel": "Nombre",
|
||||
"s3DestNamePlaceholder": "Mi destino S3",
|
||||
"s3DestAccessKeyIdLabel": "ID de clave de acceso de AWS",
|
||||
"s3DestSecretAccessKeyLabel": "Clave de acceso secreta de AWS",
|
||||
"s3DestSecretAccessKeyPlaceholder": "Tu clave de acceso secreta de AWS",
|
||||
"s3DestRegionLabel": "Región de AWS",
|
||||
"s3DestBucketLabel": "Nombre del bucket",
|
||||
"s3DestPrefixLabel": "Prefijo clave (opcional)",
|
||||
"s3DestPrefixDescription": "Prefijo de ruta opcional preanexado a cada clave de objeto. Los objetos se almacenan en {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.",
|
||||
"s3DestEndpointLabel": "Punto final personalizado (opcional)",
|
||||
"s3DestEndpointDescription": "Sobrescribe el punto final de S3 para almacenamiento compatible con S3 como MinIO o Cloudflare R2. Deja en blanco para el estándar AWS S3.",
|
||||
"s3DestGzipLabel": "Compresión Gzip",
|
||||
"s3DestGzipDescription": "Comprime cada objeto subido con gzip. Reduce costos de almacenamiento y tamaño de carga.",
|
||||
"s3DestFormatTitle": "Formato de archivo",
|
||||
"s3DestFormatDescription": "Cómo se serializan los eventos dentro de cada objeto cargado.",
|
||||
"s3DestFormatJsonArrayDescription": "Cada objeto es un arreglo JSON de registros de eventos. Compatible con la mayoría de las herramientas de analítica.",
|
||||
"s3DestFormatNdjsonDescription": "Cada objeto contiene un registro JSON por línea (JSON delimitado por nueva línea). Compatible con Athena, BigQuery y Spark.",
|
||||
"s3DestFormatCsvTitle": "CSV",
|
||||
"s3DestFormatCsvDescription": "Cada objeto es un archivo CSV conforme a RFC-4180 con una fila de encabezado. Los nombres de columna se derivan de los campos de datos del evento.",
|
||||
"s3DestSaveChanges": "Guardar cambios",
|
||||
"s3DestCreateDestination": "Crear destino",
|
||||
"s3DestUpdatedSuccess": "Destino actualizado con éxito",
|
||||
"s3DestCreatedSuccess": "Destino creado con éxito",
|
||||
"s3DestUpdateFailed": "No se pudo actualizar el destino",
|
||||
"s3DestCreateFailed": "No se pudo crear el destino",
|
||||
"datadogDestEditTitle": "Editar destino",
|
||||
"datadogDestAddTitle": "Añadir destino Datadog",
|
||||
"datadogDestEditDescription": "Actualice la configuración para este destino de transmisión de eventos Datadog.",
|
||||
|
||||
@@ -523,6 +523,12 @@
|
||||
"userMessageOrgRemove": "Une fois retiré, cet utilisateur n'aura plus accès à l'organisation. Vous pouvez toujours le réinviter plus tard, mais il devra accepter l'invitation à nouveau.",
|
||||
"userRemoveOrgConfirm": "Confirmer la suppression de l'utilisateur",
|
||||
"userRemoveOrg": "Retirer l'utilisateur de l'organisation",
|
||||
"userQuestionOrgRemoveSelf": "Are you sure you want to remove yourself from this organization?",
|
||||
"userMessageOrgRemoveSelf": "You will lose access immediately. An administrator can invite you again later, but you will need to accept a new invitation.",
|
||||
"userRemoveOrgConfirmSelf": "Confirm Remove Myself",
|
||||
"userRemoveOrgSelf": "Remove yourself from the organization",
|
||||
"userRemoveOrgSelfWarning": "You will lose access to this organization immediately.",
|
||||
"userRemoveOrgConfirmPhraseSelf": "REMOVE MYSELF FROM ORG",
|
||||
"users": "Utilisateurs",
|
||||
"accessRoleMember": "Membre",
|
||||
"accessRoleOwner": "Propriétaire",
|
||||
@@ -531,6 +537,11 @@
|
||||
"emailInvalid": "Adresse e-mail invalide",
|
||||
"inviteValidityDuration": "Veuillez sélectionner une durée",
|
||||
"accessRoleSelectPlease": "Veuillez sélectionner un rôle",
|
||||
"removeOwnAdminRoleConfirmTitle": "Remove your administrator access?",
|
||||
"removeOwnAdminRoleConfirmDescription": "You will no longer have administrator permissions in this organization after saving. Another administrator can restore access if needed.",
|
||||
"removeOwnAdminRoleConfirmButton": "Remove My Administrator Access",
|
||||
"removeOwnAdminRoleConfirmPhrase": "REMOVE MY ADMIN ACCESS",
|
||||
"ownerMustRetainAdminRole": "The organization owner must keep at least one administrator role.",
|
||||
"usernameRequired": "Le nom d'utilisateur est requis",
|
||||
"idpSelectPlease": "Veuillez sélectionner un fournisseur d'identité",
|
||||
"idpGenericOidc": "Fournisseur OAuth2/OIDC générique.",
|
||||
@@ -1356,7 +1367,7 @@
|
||||
"sidebarSites": "Nœuds",
|
||||
"sidebarApprovals": "Demandes d'approbation",
|
||||
"sidebarResources": "Ressource",
|
||||
"sidebarProxyResources": "Publiques",
|
||||
"sidebarProxyResources": "Publique",
|
||||
"sidebarClientResources": "Privé",
|
||||
"sidebarAccessControl": "Contrôle d'accès",
|
||||
"sidebarLogsAndAnalytics": "Journaux & Analytiques",
|
||||
@@ -2458,8 +2469,8 @@
|
||||
"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",
|
||||
"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",
|
||||
"manageMachineClientsDescription": "Créer et gérer les clients que les serveurs et systèmes utilisent pour se connecter en privé aux ressources",
|
||||
"manageMachineClients": "Gérer les clients de la machine",
|
||||
"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",
|
||||
"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",
|
||||
@@ -3062,7 +3073,7 @@
|
||||
"streamingDatadogTitle": "Datadog",
|
||||
"streamingDatadogDescription": "Transférer des événements directement sur votre compte Datadog. Prochainement.",
|
||||
"streamingTypePickerDescription": "Choisissez un type de destination pour commencer.",
|
||||
"streamingFailedToLoad": "Impossible de charger les destinations",
|
||||
"streamingLastSyncError": "Une erreur s'est produite lors de la dernière synchronisation",
|
||||
"streamingUnexpectedError": "Une erreur inattendue s'est produite.",
|
||||
"streamingFailedToUpdate": "Impossible de mettre à jour la destination",
|
||||
"streamingDeletedSuccess": "Destination supprimée avec succès",
|
||||
@@ -3079,7 +3090,34 @@
|
||||
"S3DestEditTitle": "Modifier la destination",
|
||||
"S3DestAddTitle": "Ajouter une destination S3",
|
||||
"S3DestEditDescription": "Mettre à jour la configuration de cette destination de diffusion d'événements S3.",
|
||||
"S3DestAddDescription": "Configurer un nouveau point de terminaison S3 pour recevoir les événements de votre organisation.",
|
||||
"S3DestAddDescription": "Configurez un nouveau bucket Amazon S3 (ou compatible S3) pour recevoir les événements de votre organisation.",
|
||||
"s3DestTabSettings": "Réglages",
|
||||
"s3DestTabFormat": "Format",
|
||||
"s3DestNameLabel": "Nom",
|
||||
"s3DestNamePlaceholder": "Ma destination S3",
|
||||
"s3DestAccessKeyIdLabel": "ID de clé d'accès AWS",
|
||||
"s3DestSecretAccessKeyLabel": "Clé d'accès secrète AWS",
|
||||
"s3DestSecretAccessKeyPlaceholder": "Votre clé d'accès secrète AWS",
|
||||
"s3DestRegionLabel": "Région AWS",
|
||||
"s3DestBucketLabel": "Nom du bucket",
|
||||
"s3DestPrefixLabel": "Préfixe clé (facultatif)",
|
||||
"s3DestPrefixDescription": "Préfixe de chemin facultatif préfixé à chaque clé d'objet. Les objets sont stockés à {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.",
|
||||
"s3DestEndpointLabel": "Point de terminaison personnalisé (facultatif)",
|
||||
"s3DestEndpointDescription": "Modifiez le point de terminaison S3 pour un stockage compatible S3 tel que MinIO ou Cloudflare R2. Laissez vide pour l'AWS S3 standard.",
|
||||
"s3DestGzipLabel": "Compression Gzip",
|
||||
"s3DestGzipDescription": "Compressez chaque objet téléchargé avec gzip. Réduit les coûts de stockage et la taille de téléchargement.",
|
||||
"s3DestFormatTitle": "Format de fichier",
|
||||
"s3DestFormatDescription": "Comment les événements sont sérialisés dans chaque objet téléchargé.",
|
||||
"s3DestFormatJsonArrayDescription": "Chaque objet est un tableau JSON des enregistrements d'événements. Compatible avec la plupart des outils d'analyse.",
|
||||
"s3DestFormatNdjsonDescription": "Chaque objet contient un enregistrement JSON par ligne (JSON délimité par saut de ligne). Compatible avec Athena, BigQuery et Spark.",
|
||||
"s3DestFormatCsvTitle": "CSV",
|
||||
"s3DestFormatCsvDescription": "Chaque objet est un fichier CSV RFC-4180 avec une ligne d'en-tête. Les noms de colonne sont dérivés des champs de données de l'événement.",
|
||||
"s3DestSaveChanges": "Enregistrer les modifications",
|
||||
"s3DestCreateDestination": "Créer une destination",
|
||||
"s3DestUpdatedSuccess": "Destination mise à jour avec succès",
|
||||
"s3DestCreatedSuccess": "Destination créée avec succès",
|
||||
"s3DestUpdateFailed": "Échec de la mise à jour de la destination",
|
||||
"s3DestCreateFailed": "Échec de la création de la destination",
|
||||
"datadogDestEditTitle": "Modifier la destination",
|
||||
"datadogDestAddTitle": "Ajouter une destination Datadog",
|
||||
"datadogDestEditDescription": "Mettre à jour la configuration de cette destination de diffusion d'événements Datadog.",
|
||||
@@ -3154,7 +3192,6 @@
|
||||
"healthCheckTabAdvanced": "Avancé",
|
||||
"healthCheckStrategyNotAvailable": "Cette stratégie n'est pas disponible. Veuillez contacter le service commercial pour activer cette fonctionnalité.",
|
||||
"uptime30d": "Disponibilité (30j)",
|
||||
"uptimeNoData": "Aucune donnée",
|
||||
"idpAddActionCreateNew": "Créer un nouveau fournisseur d'identité",
|
||||
"idpAddActionImportFromOrg": "Importer d'une autre organisation",
|
||||
"idpImportDialogTitle": "Importer le fournisseur d'identité",
|
||||
|
||||
@@ -523,6 +523,12 @@
|
||||
"userMessageOrgRemove": "Una volta rimosso questo utente non avrà più accesso all'organizzazione. Puoi sempre reinvitarlo in seguito, ma dovrà accettare nuovamente l'invito.",
|
||||
"userRemoveOrgConfirm": "Conferma Rimozione Utente",
|
||||
"userRemoveOrg": "Rimuovi Utente dall'Organizzazione",
|
||||
"userQuestionOrgRemoveSelf": "Are you sure you want to remove yourself from this organization?",
|
||||
"userMessageOrgRemoveSelf": "You will lose access immediately. An administrator can invite you again later, but you will need to accept a new invitation.",
|
||||
"userRemoveOrgConfirmSelf": "Confirm Remove Myself",
|
||||
"userRemoveOrgSelf": "Remove yourself from the organization",
|
||||
"userRemoveOrgSelfWarning": "You will lose access to this organization immediately.",
|
||||
"userRemoveOrgConfirmPhraseSelf": "REMOVE MYSELF FROM ORG",
|
||||
"users": "Utenti",
|
||||
"accessRoleMember": "Membro",
|
||||
"accessRoleOwner": "Proprietario",
|
||||
@@ -531,6 +537,11 @@
|
||||
"emailInvalid": "Indirizzo email non valido",
|
||||
"inviteValidityDuration": "Seleziona una durata",
|
||||
"accessRoleSelectPlease": "Seleziona un ruolo",
|
||||
"removeOwnAdminRoleConfirmTitle": "Remove your administrator access?",
|
||||
"removeOwnAdminRoleConfirmDescription": "You will no longer have administrator permissions in this organization after saving. Another administrator can restore access if needed.",
|
||||
"removeOwnAdminRoleConfirmButton": "Remove My Administrator Access",
|
||||
"removeOwnAdminRoleConfirmPhrase": "REMOVE MY ADMIN ACCESS",
|
||||
"ownerMustRetainAdminRole": "The organization owner must keep at least one administrator role.",
|
||||
"usernameRequired": "Username richiesto",
|
||||
"idpSelectPlease": "Seleziona un provider di identità",
|
||||
"idpGenericOidc": "Provider OAuth2/OIDC generico.",
|
||||
@@ -3062,7 +3073,7 @@
|
||||
"streamingDatadogTitle": "Datadog",
|
||||
"streamingDatadogDescription": "Inoltra gli eventi direttamente al tuo account Datadog. In arrivo.",
|
||||
"streamingTypePickerDescription": "Scegli un tipo di destinazione per iniziare.",
|
||||
"streamingFailedToLoad": "Impossibile caricare le destinazioni",
|
||||
"streamingLastSyncError": "Si è verificato un errore durante l'ultima sincronizzazione",
|
||||
"streamingUnexpectedError": "Si è verificato un errore imprevisto.",
|
||||
"streamingFailedToUpdate": "Impossibile aggiornare la destinazione",
|
||||
"streamingDeletedSuccess": "Destinazione eliminata con successo",
|
||||
@@ -3079,7 +3090,34 @@
|
||||
"S3DestEditTitle": "Modifica Destinazione",
|
||||
"S3DestAddTitle": "Aggiungi Destinazione S3",
|
||||
"S3DestEditDescription": "Aggiorna la configurazione per questa destinazione di streaming eventi S3.",
|
||||
"S3DestAddDescription": "Configura un nuovo endpoint S3 per ricevere gli eventi della tua organizzazione.",
|
||||
"S3DestAddDescription": "Configura un nuovo bucket Amazon S3 (o compatibile con S3) per ricevere gli eventi della tua organizzazione.",
|
||||
"s3DestTabSettings": "Impostazioni",
|
||||
"s3DestTabFormat": "Formato",
|
||||
"s3DestNameLabel": "Nome",
|
||||
"s3DestNamePlaceholder": "La mia destinazione S3",
|
||||
"s3DestAccessKeyIdLabel": "ID Chiave Accesso AWS",
|
||||
"s3DestSecretAccessKeyLabel": "Chiave Segreta Accesso AWS",
|
||||
"s3DestSecretAccessKeyPlaceholder": "La tua chiave segreta di accesso AWS",
|
||||
"s3DestRegionLabel": "Regione AWS",
|
||||
"s3DestBucketLabel": "Nome Bucket",
|
||||
"s3DestPrefixLabel": "Prefisso Chiave (facoltativo)",
|
||||
"s3DestPrefixDescription": "Prefisso percorso facoltativo anteposto a ogni chiave oggetto. Gli oggetti vengono archiviati in {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.",
|
||||
"s3DestEndpointLabel": "Endpoint personalizzato (facoltativo)",
|
||||
"s3DestEndpointDescription": "Sostituisci l'endpoint S3 per lo storage compatibile con S3 come MinIO o Cloudflare R2. Lasciare vuoto per l'AWS S3 standard.",
|
||||
"s3DestGzipLabel": "Compressione Gzip",
|
||||
"s3DestGzipDescription": "Comprimi ogni oggetto caricato con gzip. Riduce i costi di archiviazione e la dimensione di caricamento.",
|
||||
"s3DestFormatTitle": "Formato del File",
|
||||
"s3DestFormatDescription": "Come gli eventi sono serializzati all'interno di ciascun oggetto caricato.",
|
||||
"s3DestFormatJsonArrayDescription": "Ogni oggetto è un array JSON di record di eventi. Compatibile con la maggior parte degli strumenti analitici.",
|
||||
"s3DestFormatNdjsonDescription": "Ogni oggetto contiene un record JSON per linea (JSON delimitato da newline). Compatibile con Athena, BigQuery e Spark.",
|
||||
"s3DestFormatCsvTitle": "\"CSV\"",
|
||||
"s3DestFormatCsvDescription": "Ogni oggetto è un file CSV RFC-4180 con una riga di intestazione. I nomi delle colonne sono derivati dai campi dei dati degli eventi.",
|
||||
"s3DestSaveChanges": "Salva modifiche",
|
||||
"s3DestCreateDestination": "Crea destinazione",
|
||||
"s3DestUpdatedSuccess": "Destinazione aggiornata con successo",
|
||||
"s3DestCreatedSuccess": "Destinazione creata con successo",
|
||||
"s3DestUpdateFailed": "Aggiornamento della destinazione fallito",
|
||||
"s3DestCreateFailed": "Creazione della destinazione fallita",
|
||||
"datadogDestEditTitle": "Modifica Destinazione",
|
||||
"datadogDestAddTitle": "Aggiungi Destinazione Datadog",
|
||||
"datadogDestEditDescription": "Aggiorna la configurazione per questa destinazione di streaming eventi Datadog.",
|
||||
|
||||
@@ -523,6 +523,12 @@
|
||||
"userMessageOrgRemove": "이 사용자가 제거되면 더 이상 조직에 접근할 수 없습니다. 나중에 다시 초대할 수 있지만, 초대를 다시 수락해야 합니다.",
|
||||
"userRemoveOrgConfirm": "사용자 제거 확인",
|
||||
"userRemoveOrg": "조직에서 사용자 제거",
|
||||
"userQuestionOrgRemoveSelf": "Are you sure you want to remove yourself from this organization?",
|
||||
"userMessageOrgRemoveSelf": "You will lose access immediately. An administrator can invite you again later, but you will need to accept a new invitation.",
|
||||
"userRemoveOrgConfirmSelf": "Confirm Remove Myself",
|
||||
"userRemoveOrgSelf": "Remove yourself from the organization",
|
||||
"userRemoveOrgSelfWarning": "You will lose access to this organization immediately.",
|
||||
"userRemoveOrgConfirmPhraseSelf": "REMOVE MYSELF FROM ORG",
|
||||
"users": "사용자",
|
||||
"accessRoleMember": "회원",
|
||||
"accessRoleOwner": "소유자",
|
||||
@@ -531,6 +537,11 @@
|
||||
"emailInvalid": "유효하지 않은 이메일 주소입니다.",
|
||||
"inviteValidityDuration": "지속 시간을 선택하십시오.",
|
||||
"accessRoleSelectPlease": "역할을 선택하세요",
|
||||
"removeOwnAdminRoleConfirmTitle": "Remove your administrator access?",
|
||||
"removeOwnAdminRoleConfirmDescription": "You will no longer have administrator permissions in this organization after saving. Another administrator can restore access if needed.",
|
||||
"removeOwnAdminRoleConfirmButton": "Remove My Administrator Access",
|
||||
"removeOwnAdminRoleConfirmPhrase": "REMOVE MY ADMIN ACCESS",
|
||||
"ownerMustRetainAdminRole": "The organization owner must keep at least one administrator role.",
|
||||
"usernameRequired": "사용자 이름은 필수입니다.",
|
||||
"idpSelectPlease": "신원 제공자를 선택하십시오",
|
||||
"idpGenericOidc": "일반 OAuth2/OIDC 공급자.",
|
||||
@@ -3062,7 +3073,7 @@
|
||||
"streamingDatadogTitle": "데이터독",
|
||||
"streamingDatadogDescription": "이벤트를 직접 Datadog 계정으로 전달합니다. 곧 제공됩니다.",
|
||||
"streamingTypePickerDescription": "목표 유형을 선택하여 시작합니다.",
|
||||
"streamingFailedToLoad": "대상 로드에 실패했습니다",
|
||||
"streamingLastSyncError": "마지막 동기화에서 오류가 발생했습니다.",
|
||||
"streamingUnexpectedError": "예기치 않은 오류가 발생했습니다.",
|
||||
"streamingFailedToUpdate": "대상지를 업데이트하는 데 실패했습니다",
|
||||
"streamingDeletedSuccess": "대상지가 성공적으로 삭제되었습니다",
|
||||
@@ -3079,7 +3090,34 @@
|
||||
"S3DestEditTitle": "대상지 수정",
|
||||
"S3DestAddTitle": "S3 대상지 추가",
|
||||
"S3DestEditDescription": "이 S3 이벤트 스트리밍 대상지의 구성을 업데이트하세요.",
|
||||
"S3DestAddDescription": "조직의 이벤트를 받기 위한 새로운 S3 엔드포인트를 구성하세요.",
|
||||
"S3DestAddDescription": "조직의 이벤트를 수신할 새로운 Amazon S3(또는 S3 호환) 버킷을 구성하세요.",
|
||||
"s3DestTabSettings": "설정",
|
||||
"s3DestTabFormat": "형식",
|
||||
"s3DestNameLabel": "이름",
|
||||
"s3DestNamePlaceholder": "내 S3 대상",
|
||||
"s3DestAccessKeyIdLabel": "AWS 액세스 키 ID",
|
||||
"s3DestSecretAccessKeyLabel": "AWS 비밀 액세스 키",
|
||||
"s3DestSecretAccessKeyPlaceholder": "귀하의 AWS 비밀 액세스 키",
|
||||
"s3DestRegionLabel": "AWS 지역",
|
||||
"s3DestBucketLabel": "버킷 이름",
|
||||
"s3DestPrefixLabel": "키 접두사(선택 사항)",
|
||||
"s3DestPrefixDescription": "하나의 객체 키 앞에 붙이는 선택적 경로 접두사입니다. 객체는 {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}에 저장됩니다.",
|
||||
"s3DestEndpointLabel": "사용자 정의 엔드포인트(선택 사항)",
|
||||
"s3DestEndpointDescription": "MinIO 또는 Cloudflare R2와 같은 S3 호환 저장소에 대한 S3 엔드포인트를 재정의합니다. 표준 AWS S3의 경우 비워 두십시오.",
|
||||
"s3DestGzipLabel": "Gzip 압축",
|
||||
"s3DestGzipDescription": "각 업로드된 객체를 gzip으로 압축합니다. 저장 비용과 업로드 크기를 줄입니다.",
|
||||
"s3DestFormatTitle": "파일 형식",
|
||||
"s3DestFormatDescription": "업로드된 각 객체 내에서 이벤트가 직렬화되는 방식입니다.",
|
||||
"s3DestFormatJsonArrayDescription": "각 객체는 이벤트 기록의 JSON 배열입니다. 대부분의 분석 도구와 호환됩니다.",
|
||||
"s3DestFormatNdjsonDescription": "각 객체는 한 줄당 하나의 JSON 레코드를 포함합니다(새 줄로 구분된 JSON). Athena, BigQuery, Spark와 호환됩니다.",
|
||||
"s3DestFormatCsvTitle": "CSV",
|
||||
"s3DestFormatCsvDescription": "각 객체는 헤더 행이 있는 RFC-4180 CSV 파일입니다. 열 이름은 이벤트 데이터 필드에서 파생됩니다.",
|
||||
"s3DestSaveChanges": "변경 사항 저장",
|
||||
"s3DestCreateDestination": "대상 생성",
|
||||
"s3DestUpdatedSuccess": "대상이 성공적으로 업데이트되었습니다",
|
||||
"s3DestCreatedSuccess": "대상이 성공적으로 생성되었습니다",
|
||||
"s3DestUpdateFailed": "대상 업데이트에 실패했습니다",
|
||||
"s3DestCreateFailed": "대상 생성에 실패했습니다",
|
||||
"datadogDestEditTitle": "대상지 수정",
|
||||
"datadogDestAddTitle": "Datadog 대상지 추가",
|
||||
"datadogDestEditDescription": "이 Datadog 이벤트 스트리밍 대상지의 구성을 업데이트하세요.",
|
||||
|
||||
@@ -523,6 +523,12 @@
|
||||
"userMessageOrgRemove": "Når denne brukeren er fjernet, vil de ikke lenger ha tilgang til organisasjonen. Du kan alltid invitere dem på nytt senere, men de vil måtte godta invitasjonen på nytt.",
|
||||
"userRemoveOrgConfirm": "Bekreft fjerning av bruker",
|
||||
"userRemoveOrg": "Fjern bruker fra organisasjon",
|
||||
"userQuestionOrgRemoveSelf": "Are you sure you want to remove yourself from this organization?",
|
||||
"userMessageOrgRemoveSelf": "You will lose access immediately. An administrator can invite you again later, but you will need to accept a new invitation.",
|
||||
"userRemoveOrgConfirmSelf": "Confirm Remove Myself",
|
||||
"userRemoveOrgSelf": "Remove yourself from the organization",
|
||||
"userRemoveOrgSelfWarning": "You will lose access to this organization immediately.",
|
||||
"userRemoveOrgConfirmPhraseSelf": "REMOVE MYSELF FROM ORG",
|
||||
"users": "Brukere",
|
||||
"accessRoleMember": "Medlem",
|
||||
"accessRoleOwner": "Eier",
|
||||
@@ -531,6 +537,11 @@
|
||||
"emailInvalid": "Ugyldig e-postadresse",
|
||||
"inviteValidityDuration": "Vennligst velg en varighet",
|
||||
"accessRoleSelectPlease": "Vennligst velg en rolle",
|
||||
"removeOwnAdminRoleConfirmTitle": "Remove your administrator access?",
|
||||
"removeOwnAdminRoleConfirmDescription": "You will no longer have administrator permissions in this organization after saving. Another administrator can restore access if needed.",
|
||||
"removeOwnAdminRoleConfirmButton": "Remove My Administrator Access",
|
||||
"removeOwnAdminRoleConfirmPhrase": "REMOVE MY ADMIN ACCESS",
|
||||
"ownerMustRetainAdminRole": "The organization owner must keep at least one administrator role.",
|
||||
"usernameRequired": "Brukernavn er påkrevd",
|
||||
"idpSelectPlease": "Vennligst velg en identitetsleverandør",
|
||||
"idpGenericOidc": "Generisk OAuth2/OIDC-leverandør.",
|
||||
@@ -3062,7 +3073,7 @@
|
||||
"streamingDatadogTitle": "Datadog",
|
||||
"streamingDatadogDescription": "Videresend arrangementer direkte til din Datadog-konto. Kommer snart.",
|
||||
"streamingTypePickerDescription": "Velg en måltype for å komme i gang.",
|
||||
"streamingFailedToLoad": "Kan ikke laste inn destinasjoner",
|
||||
"streamingLastSyncError": "Det oppstod en feil under siste synkronisering",
|
||||
"streamingUnexpectedError": "En uventet feil oppstod.",
|
||||
"streamingFailedToUpdate": "Kunne ikke oppdatere destinasjon",
|
||||
"streamingDeletedSuccess": "Målet ble slettet",
|
||||
@@ -3079,7 +3090,34 @@
|
||||
"S3DestEditTitle": "Rediger destinasjon",
|
||||
"S3DestAddTitle": "Legg til S3 destinasjon",
|
||||
"S3DestEditDescription": "Oppdatere konfigurasjonen for denne S3-hendelsesstrømmingsdestinasjonen.",
|
||||
"S3DestAddDescription": "Konfigurer et nytt S3-endepunkt for å motta organisasjonens hendelser.",
|
||||
"S3DestAddDescription": "Konfigurer en ny Amazon S3 (eller S3-kompatibel) bucket for å motta din organisasjons hendelser.",
|
||||
"s3DestTabSettings": "Innstillinger",
|
||||
"s3DestTabFormat": "Format",
|
||||
"s3DestNameLabel": "Navn",
|
||||
"s3DestNamePlaceholder": "Min S3-destinasjon",
|
||||
"s3DestAccessKeyIdLabel": "AWS tilgangsnøkkel-ID",
|
||||
"s3DestSecretAccessKeyLabel": "AWS hemmelige tilgangsnøkkel",
|
||||
"s3DestSecretAccessKeyPlaceholder": "Din AWS secret access key",
|
||||
"s3DestRegionLabel": "AWS-region",
|
||||
"s3DestBucketLabel": "Bucket-navn",
|
||||
"s3DestPrefixLabel": "Nøkkelprefiks (valgfritt)",
|
||||
"s3DestPrefixDescription": "Valgfritt bane-prefiks lagt til hver objektnøkkel. Objekter er lagret på {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.",
|
||||
"s3DestEndpointLabel": "Egendefinert endepunkt (valgfritt)",
|
||||
"s3DestEndpointDescription": "Overstyr S3-endepunktet for S3-kompatibel lagring som MinIO eller Cloudflare R2. La stå tomt for standard AWS S3.",
|
||||
"s3DestGzipLabel": "Gzip-komprimering",
|
||||
"s3DestGzipDescription": "Komprimer hvert opplastede objekt med gzip. Reduserer lagringskostnader og opplastingsstørrelse.",
|
||||
"s3DestFormatTitle": "Filformat",
|
||||
"s3DestFormatDescription": "Hvordan hendelser er serialisert inni hvert opplastede objekt.",
|
||||
"s3DestFormatJsonArrayDescription": "Hvert objekt er et JSON-array av hendelsesposter. Kompatibel med de fleste analyseverktøy.",
|
||||
"s3DestFormatNdjsonDescription": "Hvert objekt inneholder en JSON-post per linje (nylinje-delt JSON). Kompatibel med Athena, BigQuery, og Spark.",
|
||||
"s3DestFormatCsvTitle": "CSV",
|
||||
"s3DestFormatCsvDescription": "Hvert objekt er en RFC-4180 CSV-fil med en overskriftsrad. Kolonnenavn er avledet fra hendelsesdatafeltene.",
|
||||
"s3DestSaveChanges": "Lagre endringer",
|
||||
"s3DestCreateDestination": "Opprett destinasjon",
|
||||
"s3DestUpdatedSuccess": "Destinasjon oppdatert vellykket",
|
||||
"s3DestCreatedSuccess": "Destinasjon opprettet vellykket",
|
||||
"s3DestUpdateFailed": "Kunne ikke oppdatere destinasjon",
|
||||
"s3DestCreateFailed": "Kunne ikke opprette destinasjon",
|
||||
"datadogDestEditTitle": "Rediger destinasjon",
|
||||
"datadogDestAddTitle": "Legg til Datadog destinasjon",
|
||||
"datadogDestEditDescription": "Oppdatere konfigurasjonen for denne Datadog-hendelsesstrømmingsdestinasjonen.",
|
||||
@@ -3174,7 +3212,7 @@
|
||||
"publicIpEndpoint": "Endepunkt",
|
||||
"lastTriggeredAt": "Siste utløste",
|
||||
"reject": "Avvis",
|
||||
"uptimeDaysAgo": "{count} days ago",
|
||||
"uptimeDaysAgo": "{count} dager siden",
|
||||
"uptimeToday": "I dag",
|
||||
"uptimeNoDataAvailable": "Ingen data tilgjengelig",
|
||||
"uptimeSuffix": "oppetid",
|
||||
|
||||
@@ -523,6 +523,12 @@
|
||||
"userMessageOrgRemove": "Eenmaal verwijderd, heeft deze gebruiker geen toegang meer tot de organisatie. Je kunt ze later altijd opnieuw uitnodigen, maar ze zullen de uitnodiging opnieuw moeten accepteren.",
|
||||
"userRemoveOrgConfirm": "Bevestig verwijderen gebruiker",
|
||||
"userRemoveOrg": "Gebruiker uit organisatie verwijderen",
|
||||
"userQuestionOrgRemoveSelf": "Are you sure you want to remove yourself from this organization?",
|
||||
"userMessageOrgRemoveSelf": "You will lose access immediately. An administrator can invite you again later, but you will need to accept a new invitation.",
|
||||
"userRemoveOrgConfirmSelf": "Confirm Remove Myself",
|
||||
"userRemoveOrgSelf": "Remove yourself from the organization",
|
||||
"userRemoveOrgSelfWarning": "You will lose access to this organization immediately.",
|
||||
"userRemoveOrgConfirmPhraseSelf": "REMOVE MYSELF FROM ORG",
|
||||
"users": "Gebruikers",
|
||||
"accessRoleMember": "Lid",
|
||||
"accessRoleOwner": "Eigenaar",
|
||||
@@ -531,6 +537,11 @@
|
||||
"emailInvalid": "Ongeldig e-mailadres",
|
||||
"inviteValidityDuration": "Selecteer een tijdsduur",
|
||||
"accessRoleSelectPlease": "Selecteer een rol",
|
||||
"removeOwnAdminRoleConfirmTitle": "Remove your administrator access?",
|
||||
"removeOwnAdminRoleConfirmDescription": "You will no longer have administrator permissions in this organization after saving. Another administrator can restore access if needed.",
|
||||
"removeOwnAdminRoleConfirmButton": "Remove My Administrator Access",
|
||||
"removeOwnAdminRoleConfirmPhrase": "REMOVE MY ADMIN ACCESS",
|
||||
"ownerMustRetainAdminRole": "The organization owner must keep at least one administrator role.",
|
||||
"usernameRequired": "Gebruikersnaam is verplicht",
|
||||
"idpSelectPlease": "Selecteer een identiteitsprovider",
|
||||
"idpGenericOidc": "Algemene OAuth2/OIDC provider.",
|
||||
@@ -3062,7 +3073,7 @@
|
||||
"streamingDatadogTitle": "Datadog",
|
||||
"streamingDatadogDescription": "Stuur gebeurtenissen rechtstreeks door naar je Datadog account. Binnenkort beschikbaar.",
|
||||
"streamingTypePickerDescription": "Kies een bestemmingstype om te beginnen.",
|
||||
"streamingFailedToLoad": "Laden van bestemmingen mislukt",
|
||||
"streamingLastSyncError": "Er is een fout opgetreden bij de laatste synchronisatie",
|
||||
"streamingUnexpectedError": "Er is een onverwachte fout opgetreden.",
|
||||
"streamingFailedToUpdate": "Bijwerken bestemming mislukt",
|
||||
"streamingDeletedSuccess": "Bestemming succesvol verwijderd",
|
||||
@@ -3079,7 +3090,34 @@
|
||||
"S3DestEditTitle": "Bestemming bewerken",
|
||||
"S3DestAddTitle": "S3-bestemming toevoegen",
|
||||
"S3DestEditDescription": "Werk de configuratie bij voor deze S3-gebeurtenisstreamingbestemming.",
|
||||
"S3DestAddDescription": "Configureer een nieuw S3-eindpunt om de gebeurtenissen van uw organisatie te ontvangen.",
|
||||
"S3DestAddDescription": "Configureer een nieuwe Amazon S3 (of S3-compatibele) bucket om de gebeurtenissen van uw organisatie te ontvangen.",
|
||||
"s3DestTabSettings": "Instellingen",
|
||||
"s3DestTabFormat": "Formaat",
|
||||
"s3DestNameLabel": "Naam",
|
||||
"s3DestNamePlaceholder": "Mijn S3-bestemming",
|
||||
"s3DestAccessKeyIdLabel": "AWS-toegangssleutel-ID",
|
||||
"s3DestSecretAccessKeyLabel": "AWS Geheime Toegangssleutel",
|
||||
"s3DestSecretAccessKeyPlaceholder": "Uw AWS geheime toegangssleutel",
|
||||
"s3DestRegionLabel": "AWS-regio",
|
||||
"s3DestBucketLabel": "Bucketnaam",
|
||||
"s3DestPrefixLabel": "Sleutelvoorvoegsel (optioneel)",
|
||||
"s3DestPrefixDescription": "Optioneel padvoorvoegsel dat aan elke object sleutel wordt toegevoegd. Objecten worden opgeslagen op {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.",
|
||||
"s3DestEndpointLabel": "Aangepast Eindpunt (optioneel)",
|
||||
"s3DestEndpointDescription": "Overschrijf het S3-eindpunt voor S3-compatibele opslag zoals MinIO of Cloudflare R2. Laat leeg voor standaard AWS S3.",
|
||||
"s3DestGzipLabel": "Gzip-compressie",
|
||||
"s3DestGzipDescription": "Comprimeer elk geüpload object met gzip. Verlaagt opslagkosten en uploadgrootte.",
|
||||
"s3DestFormatTitle": "Bestandsformaat",
|
||||
"s3DestFormatDescription": "Hoe gebeurtenissen binnen elk geüpload object worden geserialiseerd.",
|
||||
"s3DestFormatJsonArrayDescription": "Elk object is een JSON-array van gebeurtenisrecords. Compatibel met de meeste analysetools.",
|
||||
"s3DestFormatNdjsonDescription": "Elk object bevat één JSON-record per regel (nieuwregel-gescheiden JSON). Compatibel met Athena, BigQuery en Spark.",
|
||||
"s3DestFormatCsvTitle": "CSV",
|
||||
"s3DestFormatCsvDescription": "Elk object is een RFC-4180 CSV-bestand met een kopregel. Kolomnamen zijn afgeleid van de gebeurtenis gegevensvelden.",
|
||||
"s3DestSaveChanges": "Wijzigingen opslaan",
|
||||
"s3DestCreateDestination": "Bestemming maken",
|
||||
"s3DestUpdatedSuccess": "Bestemming succesvol bijgewerkt",
|
||||
"s3DestCreatedSuccess": "Bestemming succesvol gecreëerd",
|
||||
"s3DestUpdateFailed": "Bijwerken bestemming mislukt",
|
||||
"s3DestCreateFailed": "Aanmaken bestemming mislukt",
|
||||
"datadogDestEditTitle": "Bestemming bewerken",
|
||||
"datadogDestAddTitle": "Datadog-bestemming toevoegen",
|
||||
"datadogDestEditDescription": "Werk de configuratie bij voor deze Datadog-gebeurtenisstreamingbestemming.",
|
||||
|
||||
@@ -523,6 +523,12 @@
|
||||
"userMessageOrgRemove": "Po usunięciu ten użytkownik nie będzie miał już dostępu do organizacji. Zawsze możesz ponownie go zaprosić później, ale będzie musiał ponownie zaakceptować zaproszenie.",
|
||||
"userRemoveOrgConfirm": "Potwierdź usunięcie użytkownika",
|
||||
"userRemoveOrg": "Usuń użytkownika z organizacji",
|
||||
"userQuestionOrgRemoveSelf": "Are you sure you want to remove yourself from this organization?",
|
||||
"userMessageOrgRemoveSelf": "You will lose access immediately. An administrator can invite you again later, but you will need to accept a new invitation.",
|
||||
"userRemoveOrgConfirmSelf": "Confirm Remove Myself",
|
||||
"userRemoveOrgSelf": "Remove yourself from the organization",
|
||||
"userRemoveOrgSelfWarning": "You will lose access to this organization immediately.",
|
||||
"userRemoveOrgConfirmPhraseSelf": "REMOVE MYSELF FROM ORG",
|
||||
"users": "Użytkownicy",
|
||||
"accessRoleMember": "Członek",
|
||||
"accessRoleOwner": "Właściciel",
|
||||
@@ -531,6 +537,11 @@
|
||||
"emailInvalid": "Nieprawidłowy adres e-mail",
|
||||
"inviteValidityDuration": "Proszę wybrać okres ważności",
|
||||
"accessRoleSelectPlease": "Proszę wybrać rolę",
|
||||
"removeOwnAdminRoleConfirmTitle": "Remove your administrator access?",
|
||||
"removeOwnAdminRoleConfirmDescription": "You will no longer have administrator permissions in this organization after saving. Another administrator can restore access if needed.",
|
||||
"removeOwnAdminRoleConfirmButton": "Remove My Administrator Access",
|
||||
"removeOwnAdminRoleConfirmPhrase": "REMOVE MY ADMIN ACCESS",
|
||||
"ownerMustRetainAdminRole": "The organization owner must keep at least one administrator role.",
|
||||
"usernameRequired": "Nazwa użytkownika jest wymagana",
|
||||
"idpSelectPlease": "Proszę wybrać dostawcę tożsamości",
|
||||
"idpGenericOidc": "Ogólny dostawca OAuth2/OIDC.",
|
||||
@@ -3062,7 +3073,7 @@
|
||||
"streamingDatadogTitle": "Datadog",
|
||||
"streamingDatadogDescription": "Przekaż wydarzenia bezpośrednio do Twojego konta Datadog. Już wkrótce.",
|
||||
"streamingTypePickerDescription": "Wybierz typ docelowy, aby rozpocząć.",
|
||||
"streamingFailedToLoad": "Nie udało się załadować miejsc docelowych",
|
||||
"streamingLastSyncError": "Wystąpił błąd podczas ostatniej synchronizacji",
|
||||
"streamingUnexpectedError": "Wystąpił nieoczekiwany błąd.",
|
||||
"streamingFailedToUpdate": "Nie udało się zaktualizować miejsca docelowego",
|
||||
"streamingDeletedSuccess": "Cel usunięty pomyślnie",
|
||||
@@ -3079,7 +3090,34 @@
|
||||
"S3DestEditTitle": "Edytuj Miejsce Docelowe",
|
||||
"S3DestAddTitle": "Dodaj Miejsce Docelowe S3",
|
||||
"S3DestEditDescription": "Zaktualizuj konfigurację dla tego miejsca docelowego strumieniowego zdarzeń S3.",
|
||||
"S3DestAddDescription": "Skonfiguruj nowy punkt końcowy S3, aby odbierać zdarzenia Twojej organizacji.",
|
||||
"S3DestAddDescription": "Skonfiguruj nowy zasobnik Amazon S3 (lub zgodny z S3), aby otrzymywać zdarzenia twojej organizacji.",
|
||||
"s3DestTabSettings": "Ustawienia",
|
||||
"s3DestTabFormat": "Format",
|
||||
"s3DestNameLabel": "Nazwa",
|
||||
"s3DestNamePlaceholder": "Moje miejsce docelowe S3",
|
||||
"s3DestAccessKeyIdLabel": "AWS Access Key ID",
|
||||
"s3DestSecretAccessKeyLabel": "AWS Secret Access Key",
|
||||
"s3DestSecretAccessKeyPlaceholder": "Twój AWS Secret Access Key",
|
||||
"s3DestRegionLabel": "Region AWS",
|
||||
"s3DestBucketLabel": "Nazwa kubła",
|
||||
"s3DestPrefixLabel": "Prefiks klucza (opcjonalnie)",
|
||||
"s3DestPrefixDescription": "Opcjonalny prefiks ścieżki dołączony do każdego klucza obiektu. Obiekty są przechowywane w {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.",
|
||||
"s3DestEndpointLabel": "Niestandardowy punkt końcowy (opcjonalnie)",
|
||||
"s3DestEndpointDescription": "Nadpisz punkt końcowy S3 dla zgodnego przechowywania danych, takiego jak MinIO lub Cloudflare R2. Pozostaw puste dla standardowego AWS S3.",
|
||||
"s3DestGzipLabel": "Kompresja Gzip",
|
||||
"s3DestGzipDescription": "Skompresuj każdy przesłany obiekt za pomocą gzip. Zmniejsza koszty przechowywania i rozmiar przesyłu.",
|
||||
"s3DestFormatTitle": "Format pliku",
|
||||
"s3DestFormatDescription": "Jak zdarzenia są serializowane w każdym przesłanym obiekcie.",
|
||||
"s3DestFormatJsonArrayDescription": "Każdy obiekt to tablica JSON z rekordami zdarzeń. Zgodne z większością narzędzi analitycznych.",
|
||||
"s3DestFormatNdjsonDescription": "Każdy obiekt zawiera jeden rekord JSON na linię (nowa linia-dzielone JSON). Zgodne z Athena, BigQuery i Spark.",
|
||||
"s3DestFormatCsvTitle": "CSV",
|
||||
"s3DestFormatCsvDescription": "Każdy obiekt to plik CSV zgodny z RFC-4180 z wierszem nagłówka. Nazwy kolumn pochodzą z pól danych zdarzeń.",
|
||||
"s3DestSaveChanges": "Zapisz zmiany",
|
||||
"s3DestCreateDestination": "Utwórz miejsce docelowe",
|
||||
"s3DestUpdatedSuccess": "Miejsce docelowe zaktualizowane pomyślnie",
|
||||
"s3DestCreatedSuccess": "Miejsce docelowe utworzone pomyślnie",
|
||||
"s3DestUpdateFailed": "Nie udało się zaktualizować miejsca docelowego",
|
||||
"s3DestCreateFailed": "Nie udało się utworzyć miejsca docelowego",
|
||||
"datadogDestEditTitle": "Edytuj Miejsce Docelowe",
|
||||
"datadogDestAddTitle": "Dodaj Miejsce Docelowe Datadog",
|
||||
"datadogDestEditDescription": "Zaktualizuj konfigurację dla tego miejsca docelowego strumieniowego zdarzeń Datadog.",
|
||||
|
||||
@@ -523,6 +523,12 @@
|
||||
"userMessageOrgRemove": "Uma vez removido, este utilizador não terá mais acesso à organização. Você sempre pode reconvidá-lo depois, mas eles precisarão aceitar o convite novamente.",
|
||||
"userRemoveOrgConfirm": "Confirmar Remoção do Usuário",
|
||||
"userRemoveOrg": "Remover Usuário da Organização",
|
||||
"userQuestionOrgRemoveSelf": "Are you sure you want to remove yourself from this organization?",
|
||||
"userMessageOrgRemoveSelf": "You will lose access immediately. An administrator can invite you again later, but you will need to accept a new invitation.",
|
||||
"userRemoveOrgConfirmSelf": "Confirm Remove Myself",
|
||||
"userRemoveOrgSelf": "Remove yourself from the organization",
|
||||
"userRemoveOrgSelfWarning": "You will lose access to this organization immediately.",
|
||||
"userRemoveOrgConfirmPhraseSelf": "REMOVE MYSELF FROM ORG",
|
||||
"users": "Utilizadores",
|
||||
"accessRoleMember": "Membro",
|
||||
"accessRoleOwner": "Proprietário",
|
||||
@@ -531,6 +537,11 @@
|
||||
"emailInvalid": "Endereço de email inválido",
|
||||
"inviteValidityDuration": "Por favor, selecione uma duração",
|
||||
"accessRoleSelectPlease": "Por favor, selecione uma função",
|
||||
"removeOwnAdminRoleConfirmTitle": "Remove your administrator access?",
|
||||
"removeOwnAdminRoleConfirmDescription": "You will no longer have administrator permissions in this organization after saving. Another administrator can restore access if needed.",
|
||||
"removeOwnAdminRoleConfirmButton": "Remove My Administrator Access",
|
||||
"removeOwnAdminRoleConfirmPhrase": "REMOVE MY ADMIN ACCESS",
|
||||
"ownerMustRetainAdminRole": "The organization owner must keep at least one administrator role.",
|
||||
"usernameRequired": "Nome de utilizador é obrigatório",
|
||||
"idpSelectPlease": "Por favor, selecione um provedor de identidade",
|
||||
"idpGenericOidc": "Provedor genérico OAuth2/OIDC.",
|
||||
@@ -3062,7 +3073,7 @@
|
||||
"streamingDatadogTitle": "Datadog",
|
||||
"streamingDatadogDescription": "Encaminha eventos diretamente para a sua conta no Datadog. Em breve.",
|
||||
"streamingTypePickerDescription": "Escolha um tipo de destino para começar.",
|
||||
"streamingFailedToLoad": "Falha ao carregar destinos",
|
||||
"streamingLastSyncError": "Ocorreu um erro na última sincronização",
|
||||
"streamingUnexpectedError": "Ocorreu um erro inesperado.",
|
||||
"streamingFailedToUpdate": "Falha ao atualizar destino",
|
||||
"streamingDeletedSuccess": "Destino apagado com sucesso",
|
||||
@@ -3079,7 +3090,34 @@
|
||||
"S3DestEditTitle": "Editar Destino",
|
||||
"S3DestAddTitle": "Adicionar Destino S3",
|
||||
"S3DestEditDescription": "Atualize a configuração para este destino de streaming de eventos S3.",
|
||||
"S3DestAddDescription": "Configure um novo endpoint S3 para receber os eventos da sua organização.",
|
||||
"S3DestAddDescription": "Configure um novo bucket Amazon S3 (ou compatível com S3) para receber os eventos da sua organização.",
|
||||
"s3DestTabSettings": "Configurações",
|
||||
"s3DestTabFormat": "Formato",
|
||||
"s3DestNameLabel": "Nome",
|
||||
"s3DestNamePlaceholder": "Meu destino S3",
|
||||
"s3DestAccessKeyIdLabel": "ID da Chave de Acesso AWS",
|
||||
"s3DestSecretAccessKeyLabel": "Chave de Acesso Secreta AWS",
|
||||
"s3DestSecretAccessKeyPlaceholder": "Sua chave de acesso secreta AWS",
|
||||
"s3DestRegionLabel": "Região AWS",
|
||||
"s3DestBucketLabel": "Nome do Bucket",
|
||||
"s3DestPrefixLabel": "Prefixo da Chave (opcional)",
|
||||
"s3DestPrefixDescription": "Prefixo de caminho opcional adicionado a cada chave de objeto. Os objetos são armazenados em {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.",
|
||||
"s3DestEndpointLabel": "Endpoint Personalizado (opcional)",
|
||||
"s3DestEndpointDescription": "Substitua o endpoint S3 por armazenamento compatível com S3, como MinIO ou Cloudflare R2. Deixe em branco para o padrão AWS S3.",
|
||||
"s3DestGzipLabel": "Compressão Gzip",
|
||||
"s3DestGzipDescription": "Comprime cada objeto carregado com gzip. Reduz custos de armazenamento e tamanho de upload.",
|
||||
"s3DestFormatTitle": "Formato de Arquivo",
|
||||
"s3DestFormatDescription": "Como os eventos são serializados dentro de cada objeto carregado.",
|
||||
"s3DestFormatJsonArrayDescription": "Cada objeto é um array JSON de registros de eventos. Compatível com a maioria das ferramentas de análise.",
|
||||
"s3DestFormatNdjsonDescription": "Cada objeto contém um registro JSON por linha (JSON delimitado por nova linha). Compatível com Athena, BigQuery e Spark.",
|
||||
"s3DestFormatCsvTitle": "CSV",
|
||||
"s3DestFormatCsvDescription": "Cada objeto é um arquivo CSV RFC-4180 com uma linha de cabeçalho. Nomes de colunas são derivados dos campos de dados do evento.",
|
||||
"s3DestSaveChanges": "Salvar Alterações",
|
||||
"s3DestCreateDestination": "Criar Destino",
|
||||
"s3DestUpdatedSuccess": "Destino atualizado com sucesso",
|
||||
"s3DestCreatedSuccess": "Destino criado com sucesso",
|
||||
"s3DestUpdateFailed": "Falha ao atualizar destino",
|
||||
"s3DestCreateFailed": "Falha ao criar destino",
|
||||
"datadogDestEditTitle": "Editar Destino",
|
||||
"datadogDestAddTitle": "Adicionar Destino Datadog",
|
||||
"datadogDestEditDescription": "Atualize a configuração para este destino de streaming de eventos Datadog.",
|
||||
|
||||
@@ -523,6 +523,12 @@
|
||||
"userMessageOrgRemove": "После удаления этот пользователь больше не будет иметь доступ к организации. Вы всегда можете пригласить его заново, но ему нужно будет снова принять приглашение.",
|
||||
"userRemoveOrgConfirm": "Подтвердить удаление пользователя",
|
||||
"userRemoveOrg": "Удалить пользователя из организации",
|
||||
"userQuestionOrgRemoveSelf": "Are you sure you want to remove yourself from this organization?",
|
||||
"userMessageOrgRemoveSelf": "You will lose access immediately. An administrator can invite you again later, but you will need to accept a new invitation.",
|
||||
"userRemoveOrgConfirmSelf": "Confirm Remove Myself",
|
||||
"userRemoveOrgSelf": "Remove yourself from the organization",
|
||||
"userRemoveOrgSelfWarning": "You will lose access to this organization immediately.",
|
||||
"userRemoveOrgConfirmPhraseSelf": "REMOVE MYSELF FROM ORG",
|
||||
"users": "Пользователи",
|
||||
"accessRoleMember": "Участник",
|
||||
"accessRoleOwner": "Владелец",
|
||||
@@ -531,6 +537,11 @@
|
||||
"emailInvalid": "Неверный адрес Email",
|
||||
"inviteValidityDuration": "Пожалуйста, выберите продолжительность",
|
||||
"accessRoleSelectPlease": "Пожалуйста, выберите роль",
|
||||
"removeOwnAdminRoleConfirmTitle": "Remove your administrator access?",
|
||||
"removeOwnAdminRoleConfirmDescription": "You will no longer have administrator permissions in this organization after saving. Another administrator can restore access if needed.",
|
||||
"removeOwnAdminRoleConfirmButton": "Remove My Administrator Access",
|
||||
"removeOwnAdminRoleConfirmPhrase": "REMOVE MY ADMIN ACCESS",
|
||||
"ownerMustRetainAdminRole": "The organization owner must keep at least one administrator role.",
|
||||
"usernameRequired": "Имя пользователя обязательно",
|
||||
"idpSelectPlease": "Пожалуйста, выберите Identity Provider",
|
||||
"idpGenericOidc": "Обычный OAuth2/OIDC provider.",
|
||||
@@ -3062,7 +3073,7 @@
|
||||
"streamingDatadogTitle": "Datadog",
|
||||
"streamingDatadogDescription": "Перенаправлять события непосредственно на ваш аккаунт в Datadog. Скоро будет доступно.",
|
||||
"streamingTypePickerDescription": "Выберите тип назначения, чтобы начать.",
|
||||
"streamingFailedToLoad": "Не удалось загрузить места назначения",
|
||||
"streamingLastSyncError": "Во время последней синхронизации произошла ошибка",
|
||||
"streamingUnexpectedError": "Произошла непредвиденная ошибка.",
|
||||
"streamingFailedToUpdate": "Не удалось обновить место назначения",
|
||||
"streamingDeletedSuccess": "Адрес назначения успешно удален",
|
||||
@@ -3079,7 +3090,34 @@
|
||||
"S3DestEditTitle": "Редактировать пункт назначения",
|
||||
"S3DestAddTitle": "Добавить S3 пункт назначения",
|
||||
"S3DestEditDescription": "Обновите конфигурацию для этого S3 пункта назначения потоковых событий.",
|
||||
"S3DestAddDescription": "Настройте новую S3 конечную точку для получения событий вашей организации.",
|
||||
"S3DestAddDescription": "Настройте новый Amazon S3 (или совместимое S3) хранилище для получения событий вашей организации.",
|
||||
"s3DestTabSettings": "Настройки",
|
||||
"s3DestTabFormat": "Формат",
|
||||
"s3DestNameLabel": "Имя",
|
||||
"s3DestNamePlaceholder": "Моя S3 конечная точка",
|
||||
"s3DestAccessKeyIdLabel": "Идентификатор ключа доступа AWS",
|
||||
"s3DestSecretAccessKeyLabel": "Секретный ключ доступа AWS",
|
||||
"s3DestSecretAccessKeyPlaceholder": "Ваш секретный ключ доступа AWS",
|
||||
"s3DestRegionLabel": "Регион AWS",
|
||||
"s3DestBucketLabel": "Имя хранилища",
|
||||
"s3DestPrefixLabel": "Префикс ключа (по желанию)",
|
||||
"s3DestPrefixDescription": "Необязательный префикс пути, добавляется к каждому ключу объекта. Объекты хранятся в {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.",
|
||||
"s3DestEndpointLabel": "Пользовательская конечная точка (по желанию)",
|
||||
"s3DestEndpointDescription": "Переопределите конечную точку S3 для совместимого хранилища, такого как MinIO или Cloudflare R2. Оставьте пустым для стандартного AWS S3.",
|
||||
"s3DestGzipLabel": "Сжатие Gzip",
|
||||
"s3DestGzipDescription": "Сжимайте каждый загруженный объект с помощью gzip. Уменьшает стоимость хранения и размер загрузки.",
|
||||
"s3DestFormatTitle": "Формат файла",
|
||||
"s3DestFormatDescription": "Как события сериализуются внутри каждого загруженного объекта.",
|
||||
"s3DestFormatJsonArrayDescription": "Каждый объект — это JSON массив записей событий. Совместим с большинством аналитических инструментов.",
|
||||
"s3DestFormatNdjsonDescription": "Каждый объект содержит одну запись JSON на строку (JSON, разделённый новой строкой). Совместим с Athena, BigQuery и Spark.",
|
||||
"s3DestFormatCsvTitle": "CSV",
|
||||
"s3DestFormatCsvDescription": "Каждый объект представляет собой CSV файл по стандарту RFC-4180 с заголовочной строкой. Имена столбцов выведены из полей данных событий.",
|
||||
"s3DestSaveChanges": "Сохранить изменения",
|
||||
"s3DestCreateDestination": "Создать конечную точку",
|
||||
"s3DestUpdatedSuccess": "Конечная точка успешно обновлена",
|
||||
"s3DestCreatedSuccess": "Конечная точка успешно создана",
|
||||
"s3DestUpdateFailed": "Не удалось обновить конечную точку",
|
||||
"s3DestCreateFailed": "Не удалось создать конечную точку",
|
||||
"datadogDestEditTitle": "Редактировать пункт назначения",
|
||||
"datadogDestAddTitle": "Добавить пункт назначения Datadog",
|
||||
"datadogDestEditDescription": "Обновите конфигурацию для этого пункта назначения потоковых событий Datadog.",
|
||||
|
||||
@@ -523,6 +523,12 @@
|
||||
"userMessageOrgRemove": "Kaldırıldığında, bu kullanıcı organizasyona artık erişim sağlayamayacak. Kullanıcı tekrar davet edilebilir, ancak daveti kabul etmesi gerekecek.",
|
||||
"userRemoveOrgConfirm": "Kullanıcıyı Kaldırmayı Onayla",
|
||||
"userRemoveOrg": "Kullanıcıyı Organizasyondan Kaldır",
|
||||
"userQuestionOrgRemoveSelf": "Are you sure you want to remove yourself from this organization?",
|
||||
"userMessageOrgRemoveSelf": "You will lose access immediately. An administrator can invite you again later, but you will need to accept a new invitation.",
|
||||
"userRemoveOrgConfirmSelf": "Confirm Remove Myself",
|
||||
"userRemoveOrgSelf": "Remove yourself from the organization",
|
||||
"userRemoveOrgSelfWarning": "You will lose access to this organization immediately.",
|
||||
"userRemoveOrgConfirmPhraseSelf": "REMOVE MYSELF FROM ORG",
|
||||
"users": "Kullanıcılar",
|
||||
"accessRoleMember": "Üye",
|
||||
"accessRoleOwner": "Sahip",
|
||||
@@ -531,6 +537,11 @@
|
||||
"emailInvalid": "Geçersiz e-posta adresi",
|
||||
"inviteValidityDuration": "Lütfen bir süre seçin",
|
||||
"accessRoleSelectPlease": "Lütfen bir rol seçin",
|
||||
"removeOwnAdminRoleConfirmTitle": "Remove your administrator access?",
|
||||
"removeOwnAdminRoleConfirmDescription": "You will no longer have administrator permissions in this organization after saving. Another administrator can restore access if needed.",
|
||||
"removeOwnAdminRoleConfirmButton": "Remove My Administrator Access",
|
||||
"removeOwnAdminRoleConfirmPhrase": "REMOVE MY ADMIN ACCESS",
|
||||
"ownerMustRetainAdminRole": "The organization owner must keep at least one administrator role.",
|
||||
"usernameRequired": "Kullanıcı adı gereklidir",
|
||||
"idpSelectPlease": "Lütfen bir kimlik sağlayıcı seçin",
|
||||
"idpGenericOidc": "Genel OAuth2/OIDC sağlayıcısı.",
|
||||
@@ -3062,7 +3073,7 @@
|
||||
"streamingDatadogTitle": "Datadog",
|
||||
"streamingDatadogDescription": "Olayları doğrudan Datadog hesabınıza iletin. Yakında gelicek.",
|
||||
"streamingTypePickerDescription": "Başlamak için bir hedef türü seçin.",
|
||||
"streamingFailedToLoad": "Hedefler yüklenemedi",
|
||||
"streamingLastSyncError": "Son senkronizasyonda bir hata oluştu",
|
||||
"streamingUnexpectedError": "Beklenmeyen bir hata oluştu.",
|
||||
"streamingFailedToUpdate": "Hedef güncellenemedi",
|
||||
"streamingDeletedSuccess": "Hedef başarıyla silindi",
|
||||
@@ -3079,7 +3090,34 @@
|
||||
"S3DestEditTitle": "Hedefi Düzenle",
|
||||
"S3DestAddTitle": "S3 Hedefi Ekle",
|
||||
"S3DestEditDescription": "Bu S3 olay akışı hedefi için yapılandırmayı güncelleyin.",
|
||||
"S3DestAddDescription": "Kuruluşunuzun olaylarını almak için yeni bir S3 uç noktası yapılandırın.",
|
||||
"S3DestAddDescription": "Kuruluşunuzun etkinliklerini almak için yeni bir Amazon S3 (veya S3-uyumlu) kovası yapılandırın.",
|
||||
"s3DestTabSettings": "Ayarlar",
|
||||
"s3DestTabFormat": "Biçim",
|
||||
"s3DestNameLabel": "Ad",
|
||||
"s3DestNamePlaceholder": "Benim S3 hedefim",
|
||||
"s3DestAccessKeyIdLabel": "AWS Erişim Anahtar Kimliği",
|
||||
"s3DestSecretAccessKeyLabel": "AWS Gizli Erişim Anahtarı",
|
||||
"s3DestSecretAccessKeyPlaceholder": "AWS gizli erişim anahtarınız",
|
||||
"s3DestRegionLabel": "AWS Bölgesi",
|
||||
"s3DestBucketLabel": "Kova Adı",
|
||||
"s3DestPrefixLabel": "Anahtar Ön Eki (isteğe bağlı)",
|
||||
"s3DestPrefixDescription": "Her nesne anahtarının önüne eklenen isteğe bağlı yol öneki. Nesneler {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename} konumunda saklanır.",
|
||||
"s3DestEndpointLabel": "Özel Uç Nokta (isteğe bağlı)",
|
||||
"s3DestEndpointDescription": "MinIO veya Cloudflare R2 gibi S3-uyumlu depolama için S3 uç noktasını geçersiz kılın. Standart AWS S3 için boş bırakın.",
|
||||
"s3DestGzipLabel": "Gzip sıkıştırması",
|
||||
"s3DestGzipDescription": "Her yüklü nesneyi gzip ile sıkıştırın. Depolama maliyetlerini ve yükleme boyutunu azaltır.",
|
||||
"s3DestFormatTitle": "Dosya Biçimi",
|
||||
"s3DestFormatDescription": "Etkinliklerin her yüklendiği nesne içinde nasıl serileştirildiği.",
|
||||
"s3DestFormatJsonArrayDescription": "Her nesne bir olay kayıtlarının JSON dizisidir. Çoğu analiz aracıyla uyumludur.",
|
||||
"s3DestFormatNdjsonDescription": "Her nesne satır başına bir JSON kaydı içerir (yeni satır ile ayrılmış JSON). Athena, BigQuery ve Spark ile uyumludur.",
|
||||
"s3DestFormatCsvTitle": "CSV",
|
||||
"s3DestFormatCsvDescription": "Her nesne, bir başlık satırı ile birlikte RFC-4180 CSV dosyasıdır. Sütun isimleri olay verileri alanlarından türetilmiştir.",
|
||||
"s3DestSaveChanges": "Değişiklikleri Kaydet",
|
||||
"s3DestCreateDestination": "Hedef Oluştur",
|
||||
"s3DestUpdatedSuccess": "Hedef başarıyla güncellendi",
|
||||
"s3DestCreatedSuccess": "Hedef başarıyla oluşturuldu",
|
||||
"s3DestUpdateFailed": "Hedef güncellenemedi",
|
||||
"s3DestCreateFailed": "Hedef oluşturulamadı",
|
||||
"datadogDestEditTitle": "Hedefi Düzenle",
|
||||
"datadogDestAddTitle": "Datadog Hedefi Ekle",
|
||||
"datadogDestEditDescription": "Bu Datadog olay akışı hedefi için yapılandırmayı güncelleyin.",
|
||||
|
||||
@@ -523,6 +523,12 @@
|
||||
"userMessageOrgRemove": "一旦删除,这个用户将不再能够访问组织。 你总是可以稍后重新邀请他们,但他们需要再次接受邀请。",
|
||||
"userRemoveOrgConfirm": "确认删除用户",
|
||||
"userRemoveOrg": "从组织中删除用户",
|
||||
"userQuestionOrgRemoveSelf": "Are you sure you want to remove yourself from this organization?",
|
||||
"userMessageOrgRemoveSelf": "You will lose access immediately. An administrator can invite you again later, but you will need to accept a new invitation.",
|
||||
"userRemoveOrgConfirmSelf": "Confirm Remove Myself",
|
||||
"userRemoveOrgSelf": "Remove yourself from the organization",
|
||||
"userRemoveOrgSelfWarning": "You will lose access to this organization immediately.",
|
||||
"userRemoveOrgConfirmPhraseSelf": "REMOVE MYSELF FROM ORG",
|
||||
"users": "用户",
|
||||
"accessRoleMember": "成员",
|
||||
"accessRoleOwner": "所有者",
|
||||
@@ -531,6 +537,11 @@
|
||||
"emailInvalid": "无效的电子邮件地址",
|
||||
"inviteValidityDuration": "请选择持续时间",
|
||||
"accessRoleSelectPlease": "请选择一个角色",
|
||||
"removeOwnAdminRoleConfirmTitle": "Remove your administrator access?",
|
||||
"removeOwnAdminRoleConfirmDescription": "You will no longer have administrator permissions in this organization after saving. Another administrator can restore access if needed.",
|
||||
"removeOwnAdminRoleConfirmButton": "Remove My Administrator Access",
|
||||
"removeOwnAdminRoleConfirmPhrase": "REMOVE MY ADMIN ACCESS",
|
||||
"ownerMustRetainAdminRole": "The organization owner must keep at least one administrator role.",
|
||||
"usernameRequired": "必须输入用户名",
|
||||
"idpSelectPlease": "请选择身份提供商",
|
||||
"idpGenericOidc": "通用的 OAuth2/OIDC 提供商。",
|
||||
@@ -3062,7 +3073,7 @@
|
||||
"streamingDatadogTitle": "Datadog",
|
||||
"streamingDatadogDescription": "直接转发事件到您的Datadog 帐户。即将推出。",
|
||||
"streamingTypePickerDescription": "选择要开始的目标类型。",
|
||||
"streamingFailedToLoad": "加载目的地失败",
|
||||
"streamingLastSyncError": "最后一次同步时发生错误",
|
||||
"streamingUnexpectedError": "发生意外错误.",
|
||||
"streamingFailedToUpdate": "更新目标失败",
|
||||
"streamingDeletedSuccess": "目标删除成功",
|
||||
@@ -3079,7 +3090,34 @@
|
||||
"S3DestEditTitle": "编辑目的地",
|
||||
"S3DestAddTitle": "添加 S3 目的地",
|
||||
"S3DestEditDescription": "更新此 S3 事件流目的地的配置。",
|
||||
"S3DestAddDescription": "配置新的 S3 终端以接收您的组织事件。",
|
||||
"S3DestAddDescription": "配置一个新的 Amazon S3(或兼容 S3 的)存储桶以接收您的组织事件。",
|
||||
"s3DestTabSettings": "设置",
|
||||
"s3DestTabFormat": "格式",
|
||||
"s3DestNameLabel": "名称",
|
||||
"s3DestNamePlaceholder": "我的 S3 目的地",
|
||||
"s3DestAccessKeyIdLabel": "AWS 访问密钥 ID",
|
||||
"s3DestSecretAccessKeyLabel": "AWS 秘密访问密钥",
|
||||
"s3DestSecretAccessKeyPlaceholder": "您的 AWS 密钥",
|
||||
"s3DestRegionLabel": "AWS 地区",
|
||||
"s3DestBucketLabel": "存储桶名称",
|
||||
"s3DestPrefixLabel": "密钥前缀(可选)",
|
||||
"s3DestPrefixDescription": "每个对象密钥前加的可选路径前缀。对象存储在 {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}。",
|
||||
"s3DestEndpointLabel": "自定义端点(可选)",
|
||||
"s3DestEndpointDescription": "替代 S3 端点用于 MinIO 或 Cloudflare R2 等兼容 S3 的存储。标准 AWS S3 留空。",
|
||||
"s3DestGzipLabel": "Gzip 压缩",
|
||||
"s3DestGzipDescription": "使用 gzip 压缩每个上传的对象。减少存储成本和上传大小。",
|
||||
"s3DestFormatTitle": "文件格式",
|
||||
"s3DestFormatDescription": "事件在每个上传对象内的序列化方式。",
|
||||
"s3DestFormatJsonArrayDescription": "每个对象是事件记录的 JSON 数组。兼容大多数分析工具。",
|
||||
"s3DestFormatNdjsonDescription": "每个对象每行包含一个 JSON 记录(换行分隔的 JSON)。兼容 Athena、BigQuery 和 Spark。",
|
||||
"s3DestFormatCsvTitle": "CSV",
|
||||
"s3DestFormatCsvDescription": "每个对象是带有标题行的 RFC-4180 CSV 文件。列名来自事件数据字段。",
|
||||
"s3DestSaveChanges": "保存更改",
|
||||
"s3DestCreateDestination": "创建目的地",
|
||||
"s3DestUpdatedSuccess": "目的地更新成功",
|
||||
"s3DestCreatedSuccess": "目的地创建成功",
|
||||
"s3DestUpdateFailed": "更新目的地失败",
|
||||
"s3DestCreateFailed": "创建目的地失败",
|
||||
"datadogDestEditTitle": "编辑目的地",
|
||||
"datadogDestAddTitle": "添加 Datadog 目的地",
|
||||
"datadogDestEditDescription": "更新此 Datadog 事件流目的地的配置。",
|
||||
|
||||
@@ -332,6 +332,7 @@ export const connectionAuditLog = pgTable(
|
||||
clientId: integer("clientId").references(() => clients.clientId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
clientEndpoint: text("clientEndpoint"),
|
||||
userId: text("userId").references(() => users.userId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
@@ -439,6 +440,8 @@ export const eventStreamingDestinations = pgTable(
|
||||
type: varchar("type", { length: 50 }).notNull(), // e.g. "http", "kafka", etc.
|
||||
config: text("config").notNull(), // JSON string with the configuration for the destination
|
||||
enabled: boolean("enabled").notNull().default(true),
|
||||
lastError: text("lastError"), // last send error message, null if healthy
|
||||
lastErrorAt: bigint("lastErrorAt", { mode: "number" }), // epoch ms of last error, null if healthy
|
||||
createdAt: bigint("createdAt", { mode: "number" }).notNull(),
|
||||
updatedAt: bigint("updatedAt", { mode: "number" }).notNull()
|
||||
}
|
||||
|
||||
@@ -332,6 +332,7 @@ export const connectionAuditLog = sqliteTable(
|
||||
clientId: integer("clientId").references(() => clients.clientId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
clientEndpoint: text("clientEndpoint"),
|
||||
userId: text("userId").references(() => users.userId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
@@ -445,6 +446,8 @@ export const eventStreamingDestinations = sqliteTable(
|
||||
enabled: integer("enabled", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(true),
|
||||
lastError: text("lastError"), // last send error message, null if healthy
|
||||
lastErrorAt: integer("lastErrorAt"), // epoch ms of last error, null if healthy
|
||||
createdAt: integer("createdAt").notNull(),
|
||||
updatedAt: integer("updatedAt").notNull()
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ export interface ConnectionLogRecord {
|
||||
orgId: string;
|
||||
siteId: number;
|
||||
clientId: number | null;
|
||||
clientEndpoint: string | null;
|
||||
userId: string | null;
|
||||
sourceAddr: string;
|
||||
destAddr: string;
|
||||
|
||||
@@ -30,10 +30,12 @@ import {
|
||||
LOG_TYPES,
|
||||
LogEvent,
|
||||
DestinationFailureState,
|
||||
HttpConfig
|
||||
HttpConfig,
|
||||
S3Config
|
||||
} from "./types";
|
||||
import { LogDestinationProvider } from "./providers/LogDestinationProvider";
|
||||
import { HttpLogDestination } from "./providers/HttpLogDestination";
|
||||
import { S3LogDestination } from "./providers/S3LogDestination";
|
||||
import type { EventStreamingDestination } from "@server/db";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -72,11 +74,11 @@ const MAX_CATCHUP_BATCHES = 20;
|
||||
* After the last entry the max value is re-used.
|
||||
*/
|
||||
const BACKOFF_SCHEDULE_MS = [
|
||||
60_000, // 1 min (failure 1)
|
||||
2 * 60_000, // 2 min (failure 2)
|
||||
5 * 60_000, // 5 min (failure 3)
|
||||
10 * 60_000, // 10 min (failure 4)
|
||||
30 * 60_000 // 30 min (failure 5+)
|
||||
60_000, // 1 min (failure 1)
|
||||
2 * 60_000, // 2 min (failure 2)
|
||||
5 * 60_000, // 5 min (failure 3)
|
||||
10 * 60_000, // 10 min (failure 4)
|
||||
30 * 60_000 // 30 min (failure 5+)
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -204,7 +206,10 @@ export class LogStreamingManager {
|
||||
this.pollTimer = null;
|
||||
this.runPoll()
|
||||
.catch((err) =>
|
||||
logger.error("LogStreamingManager: unexpected poll error", err)
|
||||
logger.error(
|
||||
"LogStreamingManager: unexpected poll error",
|
||||
err
|
||||
)
|
||||
)
|
||||
.finally(() => {
|
||||
if (this.isRunning) {
|
||||
@@ -275,10 +280,13 @@ export class LogStreamingManager {
|
||||
}
|
||||
|
||||
// Decrypt and parse config – skip destination if either step fails
|
||||
let configFromDb: HttpConfig;
|
||||
let configFromDb: unknown;
|
||||
try {
|
||||
const decryptedConfig = decrypt(dest.config, config.getRawConfig().server.secret!);
|
||||
configFromDb = JSON.parse(decryptedConfig) as HttpConfig;
|
||||
const decryptedConfig = decrypt(
|
||||
dest.config,
|
||||
config.getRawConfig().server.secret!
|
||||
);
|
||||
configFromDb = JSON.parse(decryptedConfig);
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`LogStreamingManager: destination ${dest.destinationId} has invalid or undecryptable config`,
|
||||
@@ -305,6 +313,7 @@ export class LogStreamingManager {
|
||||
if (enabledTypes.length === 0) return;
|
||||
|
||||
let anyFailure = false;
|
||||
let firstError: string | null = null;
|
||||
|
||||
for (const logType of enabledTypes) {
|
||||
if (!this.isRunning) break;
|
||||
@@ -312,6 +321,10 @@ export class LogStreamingManager {
|
||||
await this.processLogType(dest, provider, logType);
|
||||
} catch (err) {
|
||||
anyFailure = true;
|
||||
if (firstError === null) {
|
||||
firstError =
|
||||
err instanceof Error ? err.message : String(err);
|
||||
}
|
||||
logger.error(
|
||||
`LogStreamingManager: failed to process "${logType}" logs ` +
|
||||
`for destination ${dest.destinationId}`,
|
||||
@@ -322,6 +335,10 @@ export class LogStreamingManager {
|
||||
|
||||
if (anyFailure) {
|
||||
this.recordFailure(dest.destinationId);
|
||||
await this.setDestinationError(
|
||||
dest.destinationId,
|
||||
firstError ?? "Unknown error"
|
||||
);
|
||||
} else {
|
||||
// Any success resets the failure/back-off state
|
||||
if (this.failures.has(dest.destinationId)) {
|
||||
@@ -330,6 +347,7 @@ export class LogStreamingManager {
|
||||
`LogStreamingManager: destination ${dest.destinationId} recovered`
|
||||
);
|
||||
}
|
||||
await this.clearDestinationError(dest.destinationId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,7 +380,10 @@ export class LogStreamingManager {
|
||||
.from(eventStreamingCursors)
|
||||
.where(
|
||||
and(
|
||||
eq(eventStreamingCursors.destinationId, dest.destinationId),
|
||||
eq(
|
||||
eventStreamingCursors.destinationId,
|
||||
dest.destinationId
|
||||
),
|
||||
eq(eventStreamingCursors.logType, logType)
|
||||
)
|
||||
)
|
||||
@@ -431,9 +452,7 @@ export class LogStreamingManager {
|
||||
|
||||
if (rows.length === 0) break;
|
||||
|
||||
const events = rows.map((row) =>
|
||||
this.rowToLogEvent(logType, row)
|
||||
);
|
||||
const events = rows.map((row) => this.rowToLogEvent(logType, row));
|
||||
|
||||
// Throws on failure – caught by the caller which applies back-off
|
||||
await provider.send(events);
|
||||
@@ -677,8 +696,7 @@ export class LogStreamingManager {
|
||||
break;
|
||||
}
|
||||
|
||||
const orgId =
|
||||
typeof row.orgId === "string" ? row.orgId : "";
|
||||
const orgId = typeof row.orgId === "string" ? row.orgId : "";
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
@@ -708,6 +726,8 @@ export class LogStreamingManager {
|
||||
switch (type) {
|
||||
case "http":
|
||||
return new HttpLogDestination(config as HttpConfig);
|
||||
case "s3":
|
||||
return new S3LogDestination(config as S3Config);
|
||||
// Future providers:
|
||||
// case "datadog": return new DatadogLogDestination(config as DatadogConfig);
|
||||
default:
|
||||
@@ -749,6 +769,45 @@ export class LogStreamingManager {
|
||||
// DB helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private async setDestinationError(
|
||||
destinationId: number,
|
||||
errorMessage: string
|
||||
): Promise<void> {
|
||||
// Truncate to 1000 chars so it fits comfortably in the text column.
|
||||
const truncated = errorMessage.slice(0, 1000);
|
||||
try {
|
||||
await db
|
||||
.update(eventStreamingDestinations)
|
||||
.set({ lastError: truncated, lastErrorAt: Date.now() })
|
||||
.where(
|
||||
eq(eventStreamingDestinations.destinationId, destinationId)
|
||||
);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
`LogStreamingManager: could not persist error status for destination ${destinationId}`,
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async clearDestinationError(destinationId: number): Promise<void> {
|
||||
try {
|
||||
// Only update if there is actually an error stored, to avoid
|
||||
// unnecessary writes on every successful poll cycle.
|
||||
await db
|
||||
.update(eventStreamingDestinations)
|
||||
.set({ lastError: null, lastErrorAt: null })
|
||||
.where(
|
||||
eq(eventStreamingDestinations.destinationId, destinationId)
|
||||
);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
`LogStreamingManager: could not clear error status for destination ${destinationId}`,
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async loadEnabledDestinations(): Promise<
|
||||
EventStreamingDestination[]
|
||||
> {
|
||||
|
||||
279
server/private/lib/logStreaming/providers/S3LogDestination.ts
Normal file
279
server/private/lib/logStreaming/providers/S3LogDestination.ts
Normal file
@@ -0,0 +1,279 @@
|
||||
/*
|
||||
* This file is part of a proprietary work.
|
||||
*
|
||||
* Copyright (c) 2025-2026 Fossorial, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is licensed under the Fossorial Commercial License.
|
||||
* You may not use this file except in compliance with the License.
|
||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
*
|
||||
* This file is not licensed under the AGPLv3.
|
||||
*/
|
||||
|
||||
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
|
||||
import { gzip as gzipCallback } from "zlib";
|
||||
import { promisify } from "util";
|
||||
import { randomUUID } from "crypto";
|
||||
import logger from "@server/logger";
|
||||
import { LogEvent, S3Config, S3PayloadFormat } from "../types";
|
||||
import { LogDestinationProvider } from "./LogDestinationProvider";
|
||||
|
||||
const gzipAsync = promisify(gzipCallback);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Maximum time (ms) to wait for a single S3 PutObject response. */
|
||||
const REQUEST_TIMEOUT_MS = 60_000;
|
||||
|
||||
/** Default payload format when none is specified in the config. */
|
||||
const DEFAULT_FORMAT: S3PayloadFormat = "json_array";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// S3LogDestination
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Forwards a batch of log events to an S3-compatible object store by
|
||||
* uploading a single object per `send()` call.
|
||||
*
|
||||
* **Object key layout**
|
||||
* ```
|
||||
* {prefix}/{logType}/{YYYY}/{MM}/{DD}/{HH}-{mm}-{ss}-{uuid}.{ext}[.gz]
|
||||
* ```
|
||||
* - `prefix` – from `config.prefix` (default: empty – key starts at logType)
|
||||
* - `logType` – one of "request", "action", "access", "connection"
|
||||
* - Date components are derived from the upload time (UTC)
|
||||
* - `ext` – `json` | `ndjson` | `csv`
|
||||
* - `.gz` – appended when `config.gzip` is true
|
||||
*
|
||||
* **Payload formats** (controlled by `config.format`):
|
||||
* - `json_array` (default) – body is a JSON array of event objects.
|
||||
* - `ndjson` – one JSON object per line (newline-delimited).
|
||||
* - `csv` – RFC-4180 CSV with a header row; columns are the
|
||||
* union of all field names in the batch's event data.
|
||||
*
|
||||
* **Compression**: when `config.gzip` is `true` the body is gzip-compressed
|
||||
* before upload and `Content-Encoding: gzip` is set on the object.
|
||||
*
|
||||
* **Custom endpoint**: set `config.endpoint` to target any S3-compatible
|
||||
* storage service (e.g. MinIO, Cloudflare R2).
|
||||
*/
|
||||
export class S3LogDestination implements LogDestinationProvider {
|
||||
readonly type = "s3";
|
||||
|
||||
private readonly config: S3Config;
|
||||
|
||||
constructor(config: S3Config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// LogDestinationProvider implementation
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
async send(events: LogEvent[]): Promise<void> {
|
||||
if (events.length === 0) return;
|
||||
|
||||
const format = this.config.format ?? DEFAULT_FORMAT;
|
||||
const useGzip = this.config.gzip ?? false;
|
||||
const logType = events[0].logType;
|
||||
|
||||
const rawBody = this.serialize(events, format);
|
||||
const bodyBuffer = Buffer.from(rawBody, "utf-8");
|
||||
|
||||
let uploadBody: Buffer;
|
||||
let contentEncoding: string | undefined;
|
||||
|
||||
if (useGzip) {
|
||||
uploadBody = (await gzipAsync(bodyBuffer)) as Buffer;
|
||||
contentEncoding = "gzip";
|
||||
} else {
|
||||
uploadBody = bodyBuffer;
|
||||
}
|
||||
|
||||
const key = this.buildObjectKey(logType, format, useGzip);
|
||||
const contentType = this.contentType(format);
|
||||
|
||||
const clientConfig: ConstructorParameters<typeof S3Client>[0] = {
|
||||
region: this.config.region,
|
||||
credentials: {
|
||||
accessKeyId: this.config.accessKeyId,
|
||||
secretAccessKey: this.config.secretAccessKey
|
||||
},
|
||||
requestHandler: {
|
||||
requestTimeout: REQUEST_TIMEOUT_MS
|
||||
}
|
||||
};
|
||||
|
||||
if (this.config.endpoint?.trim()) {
|
||||
clientConfig.endpoint = this.config.endpoint.trim();
|
||||
}
|
||||
|
||||
const client = new S3Client(clientConfig);
|
||||
|
||||
try {
|
||||
await client.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: this.config.bucket,
|
||||
Key: key,
|
||||
Body: uploadBody,
|
||||
ContentType: contentType,
|
||||
...(contentEncoding
|
||||
? { ContentEncoding: contentEncoding }
|
||||
: {})
|
||||
})
|
||||
);
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
throw new Error(
|
||||
`S3LogDestination: failed to upload object "${key}" ` +
|
||||
`to bucket "${this.config.bucket}" – ${msg}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Internal helpers
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Construct a unique S3 object key for the given log type and format.
|
||||
* Keys are partitioned by logType and date so they can be queried or
|
||||
* lifecycle-managed independently.
|
||||
*/
|
||||
private buildObjectKey(
|
||||
logType: string,
|
||||
format: S3PayloadFormat,
|
||||
gzip: boolean
|
||||
): string {
|
||||
const now = new Date();
|
||||
const year = now.getUTCFullYear();
|
||||
const month = String(now.getUTCMonth() + 1).padStart(2, "0");
|
||||
const day = String(now.getUTCDate()).padStart(2, "0");
|
||||
const hh = String(now.getUTCHours()).padStart(2, "0");
|
||||
const mm = String(now.getUTCMinutes()).padStart(2, "0");
|
||||
const ss = String(now.getUTCSeconds()).padStart(2, "0");
|
||||
const uid = randomUUID();
|
||||
|
||||
const ext =
|
||||
format === "csv" ? "csv" : format === "ndjson" ? "ndjson" : "json";
|
||||
const fileName = `${hh}-${mm}-${ss}-${uid}.${ext}${gzip ? ".gz" : ""}`;
|
||||
|
||||
const rawPrefix = (this.config.prefix ?? "").trim().replace(/\/+$/, "");
|
||||
const parts = [
|
||||
rawPrefix,
|
||||
logType,
|
||||
`${year}/${month}/${day}`,
|
||||
fileName
|
||||
].filter((p) => p !== "");
|
||||
|
||||
return parts.join("/");
|
||||
}
|
||||
|
||||
private contentType(format: S3PayloadFormat): string {
|
||||
switch (format) {
|
||||
case "csv":
|
||||
return "text/csv; charset=utf-8";
|
||||
case "ndjson":
|
||||
return "application/x-ndjson";
|
||||
default:
|
||||
return "application/json";
|
||||
}
|
||||
}
|
||||
|
||||
private serialize(events: LogEvent[], format: S3PayloadFormat): string {
|
||||
switch (format) {
|
||||
case "json_array":
|
||||
return JSON.stringify(events.map(toPayload));
|
||||
case "ndjson":
|
||||
return events
|
||||
.map((e) => JSON.stringify(toPayload(e)))
|
||||
.join("\n");
|
||||
case "csv":
|
||||
return toCsv(events);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Payload helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function toPayload(event: LogEvent): unknown {
|
||||
return {
|
||||
event: event.logType,
|
||||
timestamp: new Date(event.timestamp * 1000).toISOString(),
|
||||
data: event.data
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a batch of events to RFC-4180 CSV.
|
||||
*
|
||||
* The column set is the union of `event`, `timestamp`, and all keys present in
|
||||
* `event.data` across the batch, preserving insertion order. Values that
|
||||
* contain commas, double-quotes, or newlines are quoted and escaped.
|
||||
*/
|
||||
function toCsv(events: LogEvent[]): string {
|
||||
if (events.length === 0) return "";
|
||||
|
||||
// Collect all unique data keys in stable order
|
||||
const keySet = new LinkedSet<string>();
|
||||
keySet.add("event");
|
||||
keySet.add("timestamp");
|
||||
for (const e of events) {
|
||||
for (const k of Object.keys(e.data)) {
|
||||
keySet.add(k);
|
||||
}
|
||||
}
|
||||
const headers = keySet.toArray();
|
||||
|
||||
const rows: string[] = [headers.map(csvEscape).join(",")];
|
||||
|
||||
for (const e of events) {
|
||||
const flat: Record<string, unknown> = {
|
||||
event: e.logType,
|
||||
timestamp: new Date(e.timestamp * 1000).toISOString(),
|
||||
...e.data
|
||||
};
|
||||
rows.push(
|
||||
headers.map((h) => csvEscape(flattenValue(flat[h]))).join(",")
|
||||
);
|
||||
}
|
||||
|
||||
return rows.join("\n");
|
||||
}
|
||||
|
||||
/** Flatten a value to a plain string suitable for a CSV cell. */
|
||||
function flattenValue(value: unknown): string {
|
||||
if (value === null || value === undefined) return "";
|
||||
if (typeof value === "object") return JSON.stringify(value);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
/** RFC-4180 CSV escaping. */
|
||||
function csvEscape(value: string): string {
|
||||
if (/[",\n\r]/.test(value)) {
|
||||
return `"${value.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Minimal ordered set (preserves insertion order, deduplicates)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class LinkedSet<T> {
|
||||
private readonly map = new Map<T, true>();
|
||||
|
||||
add(value: T): void {
|
||||
this.map.set(value, true);
|
||||
}
|
||||
|
||||
toArray(): T[] {
|
||||
return Array.from(this.map.keys());
|
||||
}
|
||||
}
|
||||
@@ -107,6 +107,40 @@ export interface HttpConfig {
|
||||
bodyTemplate?: string;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// S3 destination configuration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Controls how the batch of events is serialised into each S3 object.
|
||||
*
|
||||
* - `json_array` – `[{…}, {…}]` – default; each object is a JSON array.
|
||||
* - `ndjson` – `{…}\n{…}` – newline-delimited JSON, one object per line.
|
||||
* - `csv` – RFC-4180 CSV with a header row derived from the event fields.
|
||||
*/
|
||||
export type S3PayloadFormat = "json_array" | "ndjson" | "csv";
|
||||
|
||||
export interface S3Config {
|
||||
/** Human-readable label for the destination */
|
||||
name: string;
|
||||
/** AWS Access Key ID */
|
||||
accessKeyId: string;
|
||||
/** AWS Secret Access Key */
|
||||
secretAccessKey: string;
|
||||
/** AWS region (e.g. "us-east-1") */
|
||||
region: string;
|
||||
/** Target S3 bucket name */
|
||||
bucket: string;
|
||||
/** Optional key prefix – appended before the auto-generated path */
|
||||
prefix?: string;
|
||||
/** Override the S3 endpoint for S3-compatible storage (e.g. MinIO, R2) */
|
||||
endpoint?: string;
|
||||
/** How events are serialised into each object. Defaults to "json_array". */
|
||||
format: S3PayloadFormat;
|
||||
/** Whether to gzip-compress the object before upload. */
|
||||
gzip: boolean;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Per-destination per-log-type cursor (reflects the DB table)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -124,15 +124,11 @@ function getWhere(data: Q) {
|
||||
data.clientId
|
||||
? eq(connectionAuditLog.clientId, data.clientId)
|
||||
: undefined,
|
||||
data.siteId
|
||||
? eq(connectionAuditLog.siteId, data.siteId)
|
||||
: undefined,
|
||||
data.siteId ? eq(connectionAuditLog.siteId, data.siteId) : undefined,
|
||||
data.siteResourceId
|
||||
? eq(connectionAuditLog.siteResourceId, data.siteResourceId)
|
||||
: undefined,
|
||||
data.userId
|
||||
? eq(connectionAuditLog.userId, data.userId)
|
||||
: undefined
|
||||
data.userId ? eq(connectionAuditLog.userId, data.userId) : undefined
|
||||
);
|
||||
}
|
||||
|
||||
@@ -144,6 +140,7 @@ export function queryConnection(data: Q) {
|
||||
orgId: connectionAuditLog.orgId,
|
||||
siteId: connectionAuditLog.siteId,
|
||||
clientId: connectionAuditLog.clientId,
|
||||
clientEndpoint: connectionAuditLog.clientEndpoint,
|
||||
userId: connectionAuditLog.userId,
|
||||
sourceAddr: connectionAuditLog.sourceAddr,
|
||||
destAddr: connectionAuditLog.destAddr,
|
||||
@@ -203,10 +200,7 @@ async function enrichWithDetails(
|
||||
];
|
||||
|
||||
// Fetch resource details from main database
|
||||
const resourceMap = new Map<
|
||||
number,
|
||||
{ name: string; niceId: string }
|
||||
>();
|
||||
const resourceMap = new Map<number, { name: string; niceId: string }>();
|
||||
if (siteResourceIds.length > 0) {
|
||||
const resourceDetails = await primaryDb
|
||||
.select({
|
||||
@@ -268,10 +262,7 @@ async function enrichWithDetails(
|
||||
}
|
||||
|
||||
// Fetch user details from main database
|
||||
const userMap = new Map<
|
||||
string,
|
||||
{ email: string | null }
|
||||
>();
|
||||
const userMap = new Map<string, { email: string | null }>();
|
||||
if (userIds.length > 0) {
|
||||
const userDetails = await primaryDb
|
||||
.select({
|
||||
@@ -290,29 +281,25 @@ async function enrichWithDetails(
|
||||
return logs.map((log) => ({
|
||||
...log,
|
||||
resourceName: log.siteResourceId
|
||||
? resourceMap.get(log.siteResourceId)?.name ?? null
|
||||
? (resourceMap.get(log.siteResourceId)?.name ?? null)
|
||||
: null,
|
||||
resourceNiceId: log.siteResourceId
|
||||
? resourceMap.get(log.siteResourceId)?.niceId ?? null
|
||||
: null,
|
||||
siteName: log.siteId
|
||||
? siteMap.get(log.siteId)?.name ?? null
|
||||
? (resourceMap.get(log.siteResourceId)?.niceId ?? null)
|
||||
: null,
|
||||
siteName: log.siteId ? (siteMap.get(log.siteId)?.name ?? null) : null,
|
||||
siteNiceId: log.siteId
|
||||
? siteMap.get(log.siteId)?.niceId ?? null
|
||||
? (siteMap.get(log.siteId)?.niceId ?? null)
|
||||
: null,
|
||||
clientName: log.clientId
|
||||
? clientMap.get(log.clientId)?.name ?? null
|
||||
? (clientMap.get(log.clientId)?.name ?? null)
|
||||
: null,
|
||||
clientNiceId: log.clientId
|
||||
? clientMap.get(log.clientId)?.niceId ?? null
|
||||
? (clientMap.get(log.clientId)?.niceId ?? null)
|
||||
: null,
|
||||
clientType: log.clientId
|
||||
? clientMap.get(log.clientId)?.type ?? null
|
||||
? (clientMap.get(log.clientId)?.type ?? null)
|
||||
: null,
|
||||
userEmail: log.userId
|
||||
? userMap.get(log.userId)?.email ?? null
|
||||
: null
|
||||
userEmail: log.userId ? (userMap.get(log.userId)?.email ?? null) : null
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -521,4 +508,4 @@ export async function queryConnectionAuditLogs(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,8 @@ export type ListEventStreamingDestinationsResponse = {
|
||||
type: string;
|
||||
config: string;
|
||||
enabled: boolean;
|
||||
lastError: string | null;
|
||||
lastErrorAt: number | null;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
sendConnectionLogs: boolean;
|
||||
@@ -79,7 +81,8 @@ async function query(orgId: string, limit: number, offset: number) {
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/event-streaming-destination",
|
||||
description: "List all event streaming destinations for a specific organization.",
|
||||
description:
|
||||
"List all event streaming destinations for a specific organization.",
|
||||
tags: [OpenAPITags.Org],
|
||||
request: {
|
||||
query: querySchema,
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
* This file is not licensed under the AGPLv3.
|
||||
*/
|
||||
|
||||
import { db } from "@server/db";
|
||||
import { clientSitesAssociationsCache, db } from "@server/db";
|
||||
import { MessageHandler } from "@server/routers/ws";
|
||||
import { sites, Newt, clients, orgs } from "@server/db";
|
||||
import { and, eq, inArray } from "drizzle-orm";
|
||||
@@ -146,7 +146,11 @@ export const handleConnectionLogMessage: MessageHandler = async (context) => {
|
||||
// each unique sourceAddr + the org's CIDR suffix and do a targeted IN query.
|
||||
const ipToClient = new Map<
|
||||
string,
|
||||
{ clientId: number; userId: string | null }
|
||||
{
|
||||
clientId: number;
|
||||
userId: string | null;
|
||||
clientEndpoint: string | null;
|
||||
}
|
||||
>();
|
||||
|
||||
if (cidrSuffix) {
|
||||
@@ -172,9 +176,21 @@ export const handleConnectionLogMessage: MessageHandler = async (context) => {
|
||||
.select({
|
||||
clientId: clients.clientId,
|
||||
userId: clients.userId,
|
||||
subnet: clients.subnet
|
||||
subnet: clients.subnet,
|
||||
clientEndpoint: clientSitesAssociationsCache.endpoint
|
||||
})
|
||||
.from(clients)
|
||||
.leftJoin(
|
||||
// this should be one to one
|
||||
clientSitesAssociationsCache,
|
||||
and(
|
||||
eq(
|
||||
clients.clientId,
|
||||
clientSitesAssociationsCache.clientId
|
||||
),
|
||||
eq(clientSitesAssociationsCache.siteId, newt.siteId)
|
||||
)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(clients.orgId, orgId),
|
||||
@@ -189,7 +205,8 @@ export const handleConnectionLogMessage: MessageHandler = async (context) => {
|
||||
);
|
||||
ipToClient.set(ip, {
|
||||
clientId: c.clientId,
|
||||
userId: c.userId
|
||||
userId: c.userId,
|
||||
clientEndpoint: c.clientEndpoint
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -234,6 +251,7 @@ export const handleConnectionLogMessage: MessageHandler = async (context) => {
|
||||
orgId,
|
||||
siteId: newt.siteId,
|
||||
clientId: clientInfo?.clientId ?? null,
|
||||
clientEndpoint: clientInfo?.clientEndpoint ?? null,
|
||||
userId: clientInfo?.userId ?? null,
|
||||
sourceAddr: session.sourceAddr,
|
||||
destAddr: session.destAddr,
|
||||
|
||||
@@ -98,15 +98,6 @@ export async function addUserRole(
|
||||
);
|
||||
}
|
||||
|
||||
if (existingUser[0].isOwner) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Cannot change the role of the owner of the organization"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const roleExists = await db
|
||||
.select()
|
||||
.from(roles)
|
||||
|
||||
@@ -98,11 +98,11 @@ export async function removeUserRole(
|
||||
);
|
||||
}
|
||||
|
||||
if (existingUser.isOwner) {
|
||||
if (existingUser.isOwner && role.isAdmin === true) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Cannot change the roles of the owner of the organization"
|
||||
"Cannot remove the administrator role from the organization owner"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -87,17 +87,8 @@ export async function setUserOrgRoles(
|
||||
);
|
||||
}
|
||||
|
||||
if (existingUser.isOwner) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Cannot change the roles of the owner of the organization"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const orgRoles = await db
|
||||
.select({ roleId: roles.roleId })
|
||||
.select({ roleId: roles.roleId, isAdmin: roles.isAdmin })
|
||||
.from(roles)
|
||||
.where(
|
||||
and(
|
||||
@@ -115,6 +106,18 @@ export async function setUserOrgRoles(
|
||||
);
|
||||
}
|
||||
|
||||
if (existingUser.isOwner) {
|
||||
const hasAdminRole = orgRoles.some((r) => r.isAdmin === true);
|
||||
if (!hasAdminRole) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"The organization owner must retain an administrator role"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let orgClientsToRebuild: Client[] = [];
|
||||
await db.transaction(async (trx) => {
|
||||
await trx
|
||||
|
||||
@@ -100,6 +100,7 @@ export type QueryConnectionAuditLogResponse = {
|
||||
orgId: string | null;
|
||||
siteId: number | null;
|
||||
clientId: number | null;
|
||||
clientEndpoint: string | null;
|
||||
userId: string | null;
|
||||
sourceAddr: string;
|
||||
destAddr: string;
|
||||
|
||||
@@ -88,11 +88,11 @@ export async function addUserRoleLegacy(
|
||||
);
|
||||
}
|
||||
|
||||
if (existingUser.isOwner) {
|
||||
if (existingUser.isOwner && role.isAdmin !== true) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Cannot change the role of the owner of the organization"
|
||||
"The organization owner must retain an administrator role"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -47,10 +47,7 @@ export async function queryUser(orgId: string, userId: string) {
|
||||
.from(userOrgRoles)
|
||||
.leftJoin(roles, eq(userOrgRoles.roleId, roles.roleId))
|
||||
.where(
|
||||
and(
|
||||
eq(userOrgRoles.userId, userId),
|
||||
eq(userOrgRoles.orgId, orgId)
|
||||
)
|
||||
and(eq(userOrgRoles.userId, userId), eq(userOrgRoles.orgId, orgId))
|
||||
);
|
||||
|
||||
const isAdmin = roleRows.some((r) => r.isAdmin);
|
||||
@@ -61,7 +58,8 @@ export async function queryUser(orgId: string, userId: string) {
|
||||
roleIds: roleRows.map((r) => r.roleId),
|
||||
roles: roleRows.map((r) => ({
|
||||
roleId: r.roleId,
|
||||
name: r.roleName ?? ""
|
||||
name: r.roleName ?? "",
|
||||
isAdmin: r.isAdmin === true
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||
import IdpTypeBadge from "@app/components/IdpTypeBadge";
|
||||
import OrgRolesTagField from "@app/components/OrgRolesTagField";
|
||||
import {
|
||||
@@ -25,6 +26,7 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { userOrgUserContext } from "@app/hooks/useOrgUserContext";
|
||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { useUserContext } from "@app/hooks/useUserContext";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { build } from "@server/build";
|
||||
@@ -32,7 +34,7 @@ import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import { UserType } from "@server/types/UserTypes";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useActionState, useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -42,13 +44,15 @@ const accessControlsFormSchema = z.object({
|
||||
roles: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
text: z.string()
|
||||
text: z.string(),
|
||||
isAdmin: z.boolean().optional()
|
||||
})
|
||||
)
|
||||
});
|
||||
|
||||
export default function AccessControlsPage() {
|
||||
const { orgUser: user, updateOrgUser } = userOrgUserContext();
|
||||
const { user: sessionUser } = useUserContext();
|
||||
const { env } = useEnvContext();
|
||||
|
||||
const api = createApiClient({ env });
|
||||
@@ -72,7 +76,8 @@ export default function AccessControlsPage() {
|
||||
autoProvisioned: user.autoProvisioned || false,
|
||||
roles: (user.roles ?? []).map((r) => ({
|
||||
id: r.roleId.toString(),
|
||||
text: r.name
|
||||
text: r.name,
|
||||
isAdmin: r.isAdmin === true
|
||||
}))
|
||||
}
|
||||
});
|
||||
@@ -84,7 +89,8 @@ export default function AccessControlsPage() {
|
||||
"roles",
|
||||
(user.roles ?? []).map((r) => ({
|
||||
id: r.roleId.toString(),
|
||||
text: r.name
|
||||
text: r.name,
|
||||
isAdmin: r.isAdmin === true
|
||||
}))
|
||||
);
|
||||
form.setValue("autoProvisioned", user.autoProvisioned || false);
|
||||
@@ -95,11 +101,11 @@ export default function AccessControlsPage() {
|
||||
? t("singleRolePerUserPlanNotice")
|
||||
: t("singleRolePerUserEditionNotice");
|
||||
|
||||
const [, action, isSubmitting] = useActionState(onSubmit, null);
|
||||
async function onSubmit() {
|
||||
const isValid = await form.trigger();
|
||||
if (!isValid) return;
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [confirmRemoveOwnAdminOpen, setConfirmRemoveOwnAdminOpen] =
|
||||
useState(false);
|
||||
|
||||
async function executeSave() {
|
||||
const values = form.getValues();
|
||||
|
||||
if (values.roles.length === 0) {
|
||||
@@ -111,6 +117,7 @@ export default function AccessControlsPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const roleIds = values.roles.map((r) => parseInt(r.id, 10));
|
||||
const updateRoleRequest = supportsMultipleRolesPerUser
|
||||
@@ -130,7 +137,8 @@ export default function AccessControlsPage() {
|
||||
roleIds,
|
||||
roles: values.roles.map((r) => ({
|
||||
roleId: parseInt(r.id, 10),
|
||||
name: r.text
|
||||
name: r.text,
|
||||
isAdmin: r.isAdmin === true
|
||||
})),
|
||||
autoProvisioned: values.autoProvisioned
|
||||
});
|
||||
@@ -149,11 +157,61 @@ export default function AccessControlsPage() {
|
||||
t("accessRoleErrorAddDescription")
|
||||
)
|
||||
});
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAccessControlsSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
|
||||
const isValid = await form.trigger();
|
||||
if (!isValid) return;
|
||||
|
||||
const values = form.getValues();
|
||||
|
||||
if (values.roles.length === 0) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t("accessRoleErrorAdd"),
|
||||
description: t("accessRoleSelectPlease")
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const willHaveAdminRole = values.roles.some(
|
||||
(r) => r.isAdmin === true
|
||||
);
|
||||
|
||||
const isRemovingOwnAdmin =
|
||||
sessionUser.userId === user.userId &&
|
||||
user.isAdmin &&
|
||||
!willHaveAdminRole;
|
||||
|
||||
if (isRemovingOwnAdmin) {
|
||||
setConfirmRemoveOwnAdminOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
await executeSave();
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsContainer>
|
||||
<ConfirmDeleteDialog
|
||||
open={confirmRemoveOwnAdminOpen}
|
||||
setOpen={setConfirmRemoveOwnAdminOpen}
|
||||
title={t("removeOwnAdminRoleConfirmTitle")}
|
||||
dialog={
|
||||
<div className="space-y-2">
|
||||
<p>{t("removeOwnAdminRoleConfirmDescription")}</p>
|
||||
</div>
|
||||
}
|
||||
buttonText={t("removeOwnAdminRoleConfirmButton")}
|
||||
string={t("removeOwnAdminRoleConfirmPhrase")}
|
||||
onConfirm={executeSave}
|
||||
/>
|
||||
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
@@ -168,7 +226,7 @@ export default function AccessControlsPage() {
|
||||
<SettingsSectionForm>
|
||||
<Form {...form}>
|
||||
<form
|
||||
action={action}
|
||||
onSubmit={(e) => void handleAccessControlsSubmit(e)}
|
||||
className="space-y-4"
|
||||
id="access-controls-form"
|
||||
>
|
||||
@@ -237,8 +295,8 @@ export default function AccessControlsPage() {
|
||||
<SettingsSectionFooter>
|
||||
<Button
|
||||
type="submit"
|
||||
loading={isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
loading={isSaving}
|
||||
disabled={isSaving}
|
||||
form="access-controls-form"
|
||||
>
|
||||
{t("accessControlsSubmit")}
|
||||
|
||||
@@ -645,6 +645,12 @@ export default function ConnectionLogsPage() {
|
||||
</span>
|
||||
)}
|
||||
</div>*/}
|
||||
<div>
|
||||
<strong>Client Endpoint:</strong>{" "}
|
||||
<span className="font-mono">
|
||||
{row.clientEndpoint ?? "-"}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Site:</strong> {row.siteName ?? "-"}
|
||||
{row.siteNiceId && (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
@@ -22,7 +22,18 @@ import {
|
||||
} from "@app/components/Credenza";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { Switch } from "@app/components/ui/switch";
|
||||
import { Globe, MoreHorizontal, Plus } from "lucide-react";
|
||||
import {
|
||||
Globe,
|
||||
MoreHorizontal,
|
||||
Plus,
|
||||
AlertCircle,
|
||||
ChevronDown
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger
|
||||
} from "@app/components/ui/popover";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { build } from "@server/build";
|
||||
import Image from "next/image";
|
||||
@@ -38,7 +49,10 @@ import {
|
||||
HttpDestinationCredenza,
|
||||
parseHttpConfig
|
||||
} from "@app/components/HttpDestinationCredenza";
|
||||
import { S3DestinationCredenza } from "@app/components/S3DestinationCredenza";
|
||||
import {
|
||||
S3DestinationCredenza,
|
||||
parseS3Config
|
||||
} from "@app/components/S3DestinationCredenza";
|
||||
import { DatadogDestinationCredenza } from "@app/components/DatadogDestinationCredenza";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
@@ -64,6 +78,42 @@ interface DestinationCardProps {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
function getDestinationDisplay(destination: Destination): {
|
||||
name: string;
|
||||
typeLabel: string;
|
||||
detail: string;
|
||||
icon: React.ReactNode;
|
||||
} {
|
||||
if (destination.type === "s3") {
|
||||
const cfg = parseS3Config(destination.config);
|
||||
const detail = cfg.bucket
|
||||
? `s3://${cfg.bucket}${cfg.prefix ? `/${cfg.prefix.replace(/^\/+/, "")}` : ""}`
|
||||
: "";
|
||||
return {
|
||||
name: cfg.name,
|
||||
typeLabel: "Amazon S3",
|
||||
detail,
|
||||
icon: (
|
||||
<Image
|
||||
src="/third-party/s3.png"
|
||||
alt="Amazon S3"
|
||||
width={16}
|
||||
height={16}
|
||||
className="rounded-sm"
|
||||
/>
|
||||
)
|
||||
};
|
||||
}
|
||||
// Default: HTTP
|
||||
const cfg = parseHttpConfig(destination.config);
|
||||
return {
|
||||
name: cfg.name,
|
||||
typeLabel: "HTTP",
|
||||
detail: cfg.url,
|
||||
icon: <Globe className="h-3.5 w-3.5 text-black" />
|
||||
};
|
||||
}
|
||||
|
||||
function DestinationCard({
|
||||
destination,
|
||||
onToggle,
|
||||
@@ -73,25 +123,25 @@ function DestinationCard({
|
||||
disabled = false
|
||||
}: DestinationCardProps) {
|
||||
const t = useTranslations();
|
||||
const cfg = parseHttpConfig(destination.config);
|
||||
const { name, typeLabel, detail, icon } =
|
||||
getDestinationDisplay(destination);
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col rounded-lg border bg-card text-card-foreground p-5 gap-3">
|
||||
{/* Top row: icon + name/type + toggle */}
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
{/* Squirkle icon: gray outer → white inner → black globe */}
|
||||
<div className="shrink-0 flex items-center justify-center w-10 h-10 rounded-2xl bg-muted">
|
||||
<div className="flex items-center justify-center w-6 h-6 rounded-xl bg-white shadow-sm">
|
||||
<Globe className="h-3.5 w-3.5 text-black" />
|
||||
{icon}
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="font-semibold text-sm leading-tight truncate">
|
||||
{cfg.name || t("streamingUnnamedDestination")}
|
||||
{name || t("streamingUnnamedDestination")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground truncate mt-0.5">
|
||||
HTTP
|
||||
{typeLabel}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -105,15 +155,40 @@ function DestinationCard({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* URL preview */}
|
||||
{/* Detail preview (URL for HTTP, s3:// path for S3) */}
|
||||
<p className="text-xs text-muted-foreground truncate">
|
||||
{cfg.url || (
|
||||
{detail || (
|
||||
<span className="italic">
|
||||
{t("streamingNoUrlConfigured")}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
|
||||
{/* Error indicator */}
|
||||
{destination.lastError && (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1.5 text-left cursor-pointer rounded px-0 hover:opacity-75 transition-opacity"
|
||||
>
|
||||
<AlertCircle className="h-3.5 w-3.5 text-destructive shrink-0" />
|
||||
<p className="text-xs text-destructive">
|
||||
{t("streamingLastSyncError")}
|
||||
</p>
|
||||
<ChevronDown className="h-3 w-3 text-destructive shrink-0 ml-auto" />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
side="bottom"
|
||||
align="end"
|
||||
className="w-80 text-xs break-words"
|
||||
>
|
||||
{destination.lastError}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
|
||||
{/* Footer: edit button + three-dots menu */}
|
||||
<div className="mt-auto pt-5 flex gap-2">
|
||||
<Button
|
||||
@@ -485,7 +560,7 @@ export default function StreamingDestinationsPage() {
|
||||
if (!v) setDeleteTarget(null);
|
||||
}}
|
||||
string={
|
||||
parseHttpConfig(deleteTarget.config).name ||
|
||||
getDestinationDisplay(deleteTarget).name ||
|
||||
t("streamingDeleteDialogThisDestination")
|
||||
}
|
||||
title={t("streamingDeleteTitle")}
|
||||
@@ -493,7 +568,7 @@ export default function StreamingDestinationsPage() {
|
||||
<p>
|
||||
{t("streamingDeleteDialogAreYouSure")}{" "}
|
||||
<span>
|
||||
{parseHttpConfig(deleteTarget.config).name ||
|
||||
{getDestinationDisplay(deleteTarget).name ||
|
||||
t("streamingDeleteDialogThisDestination")}
|
||||
</span>
|
||||
{t("streamingDeleteDialogPermanentlyRemoved")}
|
||||
|
||||
@@ -19,7 +19,8 @@ import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
||||
import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group";
|
||||
import { Textarea } from "@app/components/ui/textarea";
|
||||
import { Checkbox } from "@app/components/ui/checkbox";
|
||||
import { Plus, X } from "lucide-react";
|
||||
import { Plus, X, AlertCircle } from "lucide-react";
|
||||
import { Alert, AlertDescription } from "@app/components/ui/alert";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
@@ -56,6 +57,8 @@ export interface Destination {
|
||||
sendActionLogs: boolean;
|
||||
sendConnectionLogs: boolean;
|
||||
sendRequestLogs: boolean;
|
||||
lastError: string | null;
|
||||
lastErrorAt: number | null;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
@@ -122,9 +125,7 @@ function HeadersEditor({ headers, onChange }: HeadersEditorProps) {
|
||||
/>
|
||||
<Input
|
||||
value={h.value}
|
||||
onChange={(e) =>
|
||||
updateRow(i, "value", e.target.value)
|
||||
}
|
||||
onChange={(e) => updateRow(i, "value", e.target.value)}
|
||||
placeholder={t("httpDestHeaderValuePlaceholder")}
|
||||
className="flex-1"
|
||||
/>
|
||||
@@ -200,10 +201,7 @@ export function HttpDestinationCredenza({
|
||||
if (!raw) return null;
|
||||
try {
|
||||
const parsed = new URL(raw);
|
||||
if (
|
||||
parsed.protocol !== "http:" &&
|
||||
parsed.protocol !== "https:"
|
||||
) {
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
||||
return t("httpDestUrlErrorHttpRequired");
|
||||
}
|
||||
if (build === "saas" && parsed.protocol !== "https:") {
|
||||
@@ -216,9 +214,7 @@ export function HttpDestinationCredenza({
|
||||
})();
|
||||
|
||||
const isValid =
|
||||
cfg.name.trim() !== "" &&
|
||||
cfg.url.trim() !== "" &&
|
||||
urlError === null;
|
||||
cfg.name.trim() !== "" && cfg.url.trim() !== "" && urlError === null;
|
||||
|
||||
async function handleSave() {
|
||||
if (!isValid) return;
|
||||
@@ -253,10 +249,7 @@ export function HttpDestinationCredenza({
|
||||
title: editing
|
||||
? t("httpDestUpdateFailed")
|
||||
: t("httpDestCreateFailed"),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
t("streamingUnexpectedError")
|
||||
)
|
||||
description: formatAxiosError(e, t("streamingUnexpectedError"))
|
||||
});
|
||||
} finally {
|
||||
setSaving(false);
|
||||
@@ -280,6 +273,14 @@ export function HttpDestinationCredenza({
|
||||
</CredenzaHeader>
|
||||
|
||||
<CredenzaBody>
|
||||
{editing?.lastError && (
|
||||
<Alert variant="destructive" className="mb-4">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription className="break-words">
|
||||
{editing.lastError}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<HorizontalTabs
|
||||
clientSide
|
||||
items={[
|
||||
@@ -357,7 +358,9 @@ export function HttpDestinationCredenza({
|
||||
{t("httpDestAuthNoneTitle")}
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{t("httpDestAuthNoneDescription")}
|
||||
{t(
|
||||
"httpDestAuthNoneDescription"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -375,15 +378,21 @@ export function HttpDestinationCredenza({
|
||||
htmlFor="auth-bearer"
|
||||
className="cursor-pointer font-medium"
|
||||
>
|
||||
{t("httpDestAuthBearerTitle")}
|
||||
{t(
|
||||
"httpDestAuthBearerTitle"
|
||||
)}
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{t("httpDestAuthBearerDescription")}
|
||||
{t(
|
||||
"httpDestAuthBearerDescription"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
{cfg.authType === "bearer" && (
|
||||
<Input
|
||||
placeholder={t("httpDestAuthBearerPlaceholder")}
|
||||
placeholder={t(
|
||||
"httpDestAuthBearerPlaceholder"
|
||||
)}
|
||||
value={
|
||||
cfg.bearerToken ?? ""
|
||||
}
|
||||
@@ -411,15 +420,21 @@ export function HttpDestinationCredenza({
|
||||
htmlFor="auth-basic"
|
||||
className="cursor-pointer font-medium"
|
||||
>
|
||||
{t("httpDestAuthBasicTitle")}
|
||||
{t(
|
||||
"httpDestAuthBasicTitle"
|
||||
)}
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{t("httpDestAuthBasicDescription")}
|
||||
{t(
|
||||
"httpDestAuthBasicDescription"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
{cfg.authType === "basic" && (
|
||||
<Input
|
||||
placeholder={t("httpDestAuthBasicPlaceholder")}
|
||||
placeholder={t(
|
||||
"httpDestAuthBasicPlaceholder"
|
||||
)}
|
||||
value={
|
||||
cfg.basicCredentials ??
|
||||
""
|
||||
@@ -448,16 +463,22 @@ export function HttpDestinationCredenza({
|
||||
htmlFor="auth-custom"
|
||||
className="cursor-pointer font-medium"
|
||||
>
|
||||
{t("httpDestAuthCustomTitle")}
|
||||
{t(
|
||||
"httpDestAuthCustomTitle"
|
||||
)}
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{t("httpDestAuthCustomDescription")}
|
||||
{t(
|
||||
"httpDestAuthCustomDescription"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
{cfg.authType === "custom" && (
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder={t("httpDestAuthCustomHeaderNamePlaceholder")}
|
||||
placeholder={t(
|
||||
"httpDestAuthCustomHeaderNamePlaceholder"
|
||||
)}
|
||||
value={
|
||||
cfg.customHeaderName ??
|
||||
""
|
||||
@@ -472,7 +493,9 @@ export function HttpDestinationCredenza({
|
||||
className="flex-1"
|
||||
/>
|
||||
<Input
|
||||
placeholder={t("httpDestAuthCustomHeaderValuePlaceholder")}
|
||||
placeholder={t(
|
||||
"httpDestAuthCustomHeaderValuePlaceholder"
|
||||
)}
|
||||
value={
|
||||
cfg.customHeaderValue ??
|
||||
""
|
||||
@@ -593,10 +616,14 @@ export function HttpDestinationCredenza({
|
||||
htmlFor="fmt-json-array"
|
||||
className="cursor-pointer font-medium"
|
||||
>
|
||||
{t("httpDestFormatJsonArrayTitle")}
|
||||
{t(
|
||||
"httpDestFormatJsonArrayTitle"
|
||||
)}
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{t("httpDestFormatJsonArrayDescription")}
|
||||
{t(
|
||||
"httpDestFormatJsonArrayDescription"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -616,7 +643,9 @@ export function HttpDestinationCredenza({
|
||||
{t("httpDestFormatNdjsonTitle")}
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{t("httpDestFormatNdjsonDescription")}
|
||||
{t(
|
||||
"httpDestFormatNdjsonDescription"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -636,7 +665,9 @@ export function HttpDestinationCredenza({
|
||||
{t("httpDestFormatSingleTitle")}
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{t("httpDestFormatSingleDescription")}
|
||||
{t(
|
||||
"httpDestFormatSingleDescription"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -717,7 +748,9 @@ export function HttpDestinationCredenza({
|
||||
{t("httpDestConnectionLogsTitle")}
|
||||
</label>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{t("httpDestConnectionLogsDescription")}
|
||||
{t(
|
||||
"httpDestConnectionLogsDescription"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -739,7 +772,9 @@ export function HttpDestinationCredenza({
|
||||
{t("httpDestRequestLogsTitle")}
|
||||
</label>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{t("httpDestRequestLogsDescription")}
|
||||
{t(
|
||||
"httpDestRequestLogsDescription"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -764,10 +799,12 @@ export function HttpDestinationCredenza({
|
||||
loading={saving}
|
||||
disabled={!isValid || saving}
|
||||
>
|
||||
{editing ? t("httpDestSaveChanges") : t("httpDestCreateDestination")}
|
||||
{editing
|
||||
? t("httpDestSaveChanges")
|
||||
: t("httpDestCreateDestination")}
|
||||
</Button>
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
</Credenza>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
Credenza,
|
||||
CredenzaBody,
|
||||
@@ -12,13 +12,64 @@ import {
|
||||
CredenzaTitle
|
||||
} from "@app/components/Credenza";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { ContactSalesBanner } from "@app/components/ContactSalesBanner";
|
||||
import { Input } from "@app/components/ui/input";
|
||||
import { Label } from "@app/components/ui/label";
|
||||
import { Switch } from "@app/components/ui/switch";
|
||||
import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
||||
import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group";
|
||||
import { Checkbox } from "@app/components/ui/checkbox";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import { Alert, AlertDescription } from "@app/components/ui/alert";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Destination } from "@app/components/HttpDestinationCredenza";
|
||||
|
||||
// ── Types ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
export type S3PayloadFormat = "json_array" | "ndjson" | "csv";
|
||||
|
||||
export interface S3Config {
|
||||
name: string;
|
||||
accessKeyId: string;
|
||||
secretAccessKey: string;
|
||||
region: string;
|
||||
bucket: string;
|
||||
prefix: string;
|
||||
endpoint: string;
|
||||
format: S3PayloadFormat;
|
||||
gzip: boolean;
|
||||
}
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export const defaultS3Config = (): S3Config => ({
|
||||
name: "",
|
||||
accessKeyId: "",
|
||||
secretAccessKey: "",
|
||||
region: "us-east-1",
|
||||
bucket: "",
|
||||
prefix: "",
|
||||
endpoint: "",
|
||||
format: "json_array",
|
||||
gzip: false
|
||||
});
|
||||
|
||||
export function parseS3Config(raw: string): S3Config {
|
||||
try {
|
||||
return { ...defaultS3Config(), ...JSON.parse(raw) };
|
||||
} catch {
|
||||
return defaultS3Config();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Component ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface S3DestinationCredenzaProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
editing: any;
|
||||
editing: Destination | null;
|
||||
orgId: string;
|
||||
onSaved: () => void;
|
||||
}
|
||||
@@ -28,18 +79,84 @@ export function S3DestinationCredenza({
|
||||
onOpenChange,
|
||||
editing,
|
||||
orgId,
|
||||
onSaved,
|
||||
onSaved
|
||||
}: S3DestinationCredenzaProps) {
|
||||
const api = createApiClient(useEnvContext());
|
||||
const t = useTranslations();
|
||||
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [cfg, setCfg] = useState<S3Config>(defaultS3Config());
|
||||
const [sendAccessLogs, setSendAccessLogs] = useState(false);
|
||||
const [sendActionLogs, setSendActionLogs] = useState(false);
|
||||
const [sendConnectionLogs, setSendConnectionLogs] = useState(false);
|
||||
const [sendRequestLogs, setSendRequestLogs] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setCfg(editing ? parseS3Config(editing.config) : defaultS3Config());
|
||||
setSendAccessLogs(editing?.sendAccessLogs ?? false);
|
||||
setSendActionLogs(editing?.sendActionLogs ?? false);
|
||||
setSendConnectionLogs(editing?.sendConnectionLogs ?? false);
|
||||
setSendRequestLogs(editing?.sendRequestLogs ?? false);
|
||||
}
|
||||
}, [open, editing]);
|
||||
|
||||
const update = (patch: Partial<S3Config>) =>
|
||||
setCfg((prev) => ({ ...prev, ...patch }));
|
||||
|
||||
const isValid =
|
||||
cfg.name.trim() !== "" &&
|
||||
cfg.accessKeyId.trim() !== "" &&
|
||||
cfg.secretAccessKey.trim() !== "" &&
|
||||
cfg.region.trim() !== "" &&
|
||||
cfg.bucket.trim() !== "";
|
||||
|
||||
async function handleSave() {
|
||||
if (!isValid) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
const payload = {
|
||||
type: "s3",
|
||||
config: JSON.stringify(cfg),
|
||||
sendAccessLogs,
|
||||
sendActionLogs,
|
||||
sendConnectionLogs,
|
||||
sendRequestLogs
|
||||
};
|
||||
if (editing) {
|
||||
await api.post(
|
||||
`/org/${orgId}/event-streaming-destination/${editing.destinationId}`,
|
||||
payload
|
||||
);
|
||||
toast({ title: t("s3DestUpdatedSuccess") });
|
||||
} else {
|
||||
await api.put(
|
||||
`/org/${orgId}/event-streaming-destination`,
|
||||
payload
|
||||
);
|
||||
toast({ title: t("s3DestCreatedSuccess") });
|
||||
}
|
||||
onSaved();
|
||||
onOpenChange(false);
|
||||
} catch (e) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: editing
|
||||
? t("s3DestUpdateFailed")
|
||||
: t("s3DestCreateFailed"),
|
||||
description: formatAxiosError(e, t("streamingUnexpectedError"))
|
||||
});
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Credenza open={open} onOpenChange={onOpenChange}>
|
||||
<CredenzaContent className="sm:max-w-2xl">
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle>
|
||||
{editing
|
||||
? t("S3DestEditTitle")
|
||||
: t("S3DestAddTitle")}
|
||||
{editing ? t("S3DestEditTitle") : t("S3DestAddTitle")}
|
||||
</CredenzaTitle>
|
||||
<CredenzaDescription>
|
||||
{editing
|
||||
@@ -49,13 +166,375 @@ export function S3DestinationCredenza({
|
||||
</CredenzaHeader>
|
||||
|
||||
<CredenzaBody>
|
||||
<ContactSalesBanner />
|
||||
{editing?.lastError && (
|
||||
<Alert variant="destructive" className="mb-4">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription className="break-words">
|
||||
{editing.lastError}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<HorizontalTabs
|
||||
clientSide
|
||||
items={[
|
||||
{ title: t("s3DestTabSettings"), href: "" },
|
||||
{ title: t("s3DestTabFormat"), href: "" },
|
||||
{ title: t("httpDestTabLogs"), href: "" }
|
||||
]}
|
||||
>
|
||||
{/* ── Settings tab ────────────────────────────── */}
|
||||
<div className="space-y-6 mt-4 p-1">
|
||||
{/* Name */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="s3-name">
|
||||
{t("s3DestNameLabel")}
|
||||
</Label>
|
||||
<Input
|
||||
id="s3-name"
|
||||
placeholder={t("s3DestNamePlaceholder")}
|
||||
value={cfg.name}
|
||||
onChange={(e) =>
|
||||
update({ name: e.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* AWS Access Key ID */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="s3-access-key-id">
|
||||
{t("s3DestAccessKeyIdLabel")}
|
||||
</Label>
|
||||
<Input
|
||||
id="s3-access-key-id"
|
||||
placeholder="AKIAIOSFODNN7EXAMPLE"
|
||||
value={cfg.accessKeyId}
|
||||
onChange={(e) =>
|
||||
update({
|
||||
accessKeyId: e.target.value
|
||||
})
|
||||
}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* AWS Secret Access Key */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="s3-secret-key">
|
||||
{t("s3DestSecretAccessKeyLabel")}
|
||||
</Label>
|
||||
<Input
|
||||
id="s3-secret-key"
|
||||
type="password"
|
||||
placeholder={t(
|
||||
"s3DestSecretAccessKeyPlaceholder"
|
||||
)}
|
||||
value={cfg.secretAccessKey}
|
||||
onChange={(e) =>
|
||||
update({
|
||||
secretAccessKey: e.target.value
|
||||
})
|
||||
}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Region */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="s3-region">
|
||||
{t("s3DestRegionLabel")}
|
||||
</Label>
|
||||
<Input
|
||||
id="s3-region"
|
||||
placeholder="us-east-1"
|
||||
value={cfg.region}
|
||||
onChange={(e) =>
|
||||
update({ region: e.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Bucket */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="s3-bucket">
|
||||
{t("s3DestBucketLabel")}
|
||||
</Label>
|
||||
<Input
|
||||
id="s3-bucket"
|
||||
placeholder="my-logs-bucket"
|
||||
value={cfg.bucket}
|
||||
onChange={(e) =>
|
||||
update({ bucket: e.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Prefix */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="s3-prefix">
|
||||
{t("s3DestPrefixLabel")}
|
||||
</Label>
|
||||
<Input
|
||||
id="s3-prefix"
|
||||
placeholder="pangolin/logs"
|
||||
value={cfg.prefix}
|
||||
onChange={(e) =>
|
||||
update({ prefix: e.target.value })
|
||||
}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("s3DestPrefixDescription")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Custom endpoint (optional – for S3-compatible storage) */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="s3-endpoint">
|
||||
{t("s3DestEndpointLabel")}
|
||||
</Label>
|
||||
<Input
|
||||
id="s3-endpoint"
|
||||
placeholder="https://s3.example.com"
|
||||
value={cfg.endpoint}
|
||||
onChange={(e) =>
|
||||
update({ endpoint: e.target.value })
|
||||
}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("s3DestEndpointDescription")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Format tab ───────────────────────────────── */}
|
||||
<div className="space-y-6 mt-4 p-1">
|
||||
{/* Gzip compression toggle */}
|
||||
<div className="flex items-start gap-3 rounded-md border p-3">
|
||||
<Switch
|
||||
id="s3-gzip"
|
||||
checked={cfg.gzip}
|
||||
onCheckedChange={(v) => update({ gzip: v })}
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<Label
|
||||
htmlFor="s3-gzip"
|
||||
className="cursor-pointer font-medium"
|
||||
>
|
||||
{t("s3DestGzipLabel")}
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{t("s3DestGzipDescription")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Payload format selector */}
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="font-medium block">
|
||||
{t("s3DestFormatTitle")}
|
||||
</label>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">
|
||||
{t("s3DestFormatDescription")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<RadioGroup
|
||||
value={cfg.format}
|
||||
onValueChange={(v) =>
|
||||
update({
|
||||
format: v as S3PayloadFormat
|
||||
})
|
||||
}
|
||||
className="gap-2"
|
||||
>
|
||||
{/* JSON Array */}
|
||||
<label className="flex items-start gap-3 rounded-md border p-3 cursor-pointer has-[:checked]:border-primary has-[:checked]:bg-primary/5">
|
||||
<RadioGroupItem
|
||||
value="json_array"
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<p className="text-sm font-medium leading-none">
|
||||
{t(
|
||||
"httpDestFormatJsonArrayTitle"
|
||||
)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{t(
|
||||
"s3DestFormatJsonArrayDescription"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
{/* NDJSON */}
|
||||
<label className="flex items-start gap-3 rounded-md border p-3 cursor-pointer has-[:checked]:border-primary has-[:checked]:bg-primary/5">
|
||||
<RadioGroupItem
|
||||
value="ndjson"
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<p className="text-sm font-medium leading-none">
|
||||
{t("httpDestFormatNdjsonTitle")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{t(
|
||||
"s3DestFormatNdjsonDescription"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
{/* CSV */}
|
||||
<label className="flex items-start gap-3 rounded-md border p-3 cursor-pointer has-[:checked]:border-primary has-[:checked]:bg-primary/5">
|
||||
<RadioGroupItem
|
||||
value="csv"
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<p className="text-sm font-medium leading-none">
|
||||
{t("s3DestFormatCsvTitle")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{t(
|
||||
"s3DestFormatCsvDescription"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Logs tab ──────────────────────────────────── */}
|
||||
<div className="space-y-6 mt-4 p-1">
|
||||
<div>
|
||||
<label className="font-medium block">
|
||||
{t("httpDestLogTypesTitle")}
|
||||
</label>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">
|
||||
{t("httpDestLogTypesDescription")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-start gap-3 rounded-md border p-3">
|
||||
<Checkbox
|
||||
id="s3-log-access"
|
||||
checked={sendAccessLogs}
|
||||
onCheckedChange={(v) =>
|
||||
setSendAccessLogs(v === true)
|
||||
}
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<Label
|
||||
htmlFor="s3-log-access"
|
||||
className="cursor-pointer font-medium"
|
||||
>
|
||||
{t("httpDestAccessLogsTitle")}
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{t("httpDestAccessLogsDescription")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3 rounded-md border p-3">
|
||||
<Checkbox
|
||||
id="s3-log-action"
|
||||
checked={sendActionLogs}
|
||||
onCheckedChange={(v) =>
|
||||
setSendActionLogs(v === true)
|
||||
}
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<Label
|
||||
htmlFor="s3-log-action"
|
||||
className="cursor-pointer font-medium"
|
||||
>
|
||||
{t("httpDestActionLogsTitle")}
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{t("httpDestActionLogsDescription")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3 rounded-md border p-3">
|
||||
<Checkbox
|
||||
id="s3-log-connection"
|
||||
checked={sendConnectionLogs}
|
||||
onCheckedChange={(v) =>
|
||||
setSendConnectionLogs(v === true)
|
||||
}
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<Label
|
||||
htmlFor="s3-log-connection"
|
||||
className="cursor-pointer font-medium"
|
||||
>
|
||||
{t("httpDestConnectionLogsTitle")}
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{t(
|
||||
"httpDestConnectionLogsDescription"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3 rounded-md border p-3">
|
||||
<Checkbox
|
||||
id="s3-log-request"
|
||||
checked={sendRequestLogs}
|
||||
onCheckedChange={(v) =>
|
||||
setSendRequestLogs(v === true)
|
||||
}
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<Label
|
||||
htmlFor="s3-log-request"
|
||||
className="cursor-pointer font-medium"
|
||||
>
|
||||
{t("httpDestRequestLogsTitle")}
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{t(
|
||||
"httpDestRequestLogsDescription"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</HorizontalTabs>
|
||||
</CredenzaBody>
|
||||
|
||||
<CredenzaFooter>
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline">{t("cancel")}</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
disabled={saving}
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</CredenzaClose>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleSave}
|
||||
loading={saving}
|
||||
disabled={!isValid || saving}
|
||||
>
|
||||
{editing
|
||||
? t("s3DestSaveChanges")
|
||||
: t("s3DestCreateDestination")}
|
||||
</Button>
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
</Credenza>
|
||||
|
||||
@@ -99,6 +99,14 @@ export default function UsersTable({
|
||||
];
|
||||
}, [searchParams.toString()]);
|
||||
|
||||
const isRemovingSelf = useMemo(() => {
|
||||
if (!selectedUser || !user) return false;
|
||||
return (
|
||||
`${selectedUser.username}-${selectedUser.idpId}` ===
|
||||
`${user.username}-${user.idpId}`
|
||||
);
|
||||
}, [selectedUser, user]);
|
||||
|
||||
function handleFilterChange(
|
||||
column: string,
|
||||
value: string | undefined | null
|
||||
@@ -223,10 +231,7 @@ export default function UsersTable({
|
||||
header: () => <span className="p-3"></span>,
|
||||
cell: ({ row }) => {
|
||||
const userRow = row.original;
|
||||
const isCurrentUser =
|
||||
`${userRow.username}-${userRow.idpId}` ===
|
||||
`${user?.username}-${user?.idpId}`;
|
||||
const isDisabled = userRow.isOwner || isCurrentUser;
|
||||
const canRemoveFromOrg = !userRow.isOwner;
|
||||
return (
|
||||
<div className="flex items-center justify-end">
|
||||
<div>
|
||||
@@ -235,7 +240,6 @@ export default function UsersTable({
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-8 w-8 p-0"
|
||||
disabled={isDisabled}
|
||||
>
|
||||
<span className="sr-only">
|
||||
{t("openMenu")}
|
||||
@@ -247,16 +251,12 @@ export default function UsersTable({
|
||||
<Link
|
||||
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
|
||||
className="block w-full"
|
||||
aria-disabled={isDisabled}
|
||||
onClick={(e) =>
|
||||
isDisabled && e.preventDefault()
|
||||
}
|
||||
>
|
||||
<DropdownMenuItem disabled={isDisabled}>
|
||||
<DropdownMenuItem>
|
||||
{t("accessUserManage")}
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
{!isDisabled && (
|
||||
{canRemoveFromOrg && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
@@ -271,25 +271,14 @@ export default function UsersTable({
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
{isDisabled ? (
|
||||
<Button
|
||||
variant={"outline"}
|
||||
className="ml-2"
|
||||
disabled
|
||||
>
|
||||
<Link
|
||||
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
|
||||
>
|
||||
<Button variant={"outline"} className="ml-2">
|
||||
{t("manage")}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
) : (
|
||||
<Link
|
||||
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
|
||||
>
|
||||
<Button variant={"outline"} className="ml-2">
|
||||
{t("manage")}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -359,22 +348,45 @@ export default function UsersTable({
|
||||
}}
|
||||
dialog={
|
||||
<div className="space-y-2">
|
||||
<p>{t("userQuestionOrgRemove")}</p>
|
||||
<p>{t("userMessageOrgRemove")}</p>
|
||||
<p>
|
||||
{t(
|
||||
isRemovingSelf
|
||||
? "userQuestionOrgRemoveSelf"
|
||||
: "userQuestionOrgRemove"
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{t(
|
||||
isRemovingSelf
|
||||
? "userMessageOrgRemoveSelf"
|
||||
: "userMessageOrgRemove"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
buttonText={t("userRemoveOrgConfirm")}
|
||||
buttonText={t(
|
||||
isRemovingSelf
|
||||
? "userRemoveOrgConfirmSelf"
|
||||
: "userRemoveOrgConfirm"
|
||||
)}
|
||||
warningText={
|
||||
isRemovingSelf ? t("userRemoveOrgSelfWarning") : undefined
|
||||
}
|
||||
onConfirm={async () => startTransition(removeUser)}
|
||||
string={
|
||||
selectedUser
|
||||
? getUserDisplayName({
|
||||
email: selectedUser.email,
|
||||
name: selectedUser.name,
|
||||
username: selectedUser.username
|
||||
})
|
||||
: ""
|
||||
isRemovingSelf
|
||||
? t("userRemoveOrgConfirmPhraseSelf")
|
||||
: selectedUser
|
||||
? getUserDisplayName({
|
||||
email: selectedUser.email,
|
||||
name: selectedUser.name,
|
||||
username: selectedUser.username
|
||||
})
|
||||
: ""
|
||||
}
|
||||
title={t("userRemoveOrg")}
|
||||
title={t(
|
||||
isRemovingSelf ? "userRemoveOrgSelf" : "userRemoveOrg"
|
||||
)}
|
||||
/>
|
||||
|
||||
<ControlledDataTable
|
||||
|
||||
@@ -11,7 +11,7 @@ import { cn } from "@app/lib/cn";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export type TagValue = { text: string; id: string };
|
||||
export type TagValue = { text: string; id: string; isAdmin?: boolean };
|
||||
|
||||
export type MultiSelectTagsProps<T extends TagValue> = {
|
||||
emptyPlaceholder?: string;
|
||||
|
||||
@@ -6,7 +6,7 @@ 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 SelectedRole = { id: string; text: string; isAdmin?: boolean };
|
||||
|
||||
export type RolesSelectorProps = {
|
||||
orgId: string;
|
||||
|
||||
Reference in New Issue
Block a user