diff --git a/apps/web/app/(app)/environments/[environmentId]/analysis/charts/page.tsx b/apps/web/app/(app)/environments/[environmentId]/analysis/charts/page.tsx
index 30137fba40..3de42e9d04 100644
--- a/apps/web/app/(app)/environments/[environmentId]/analysis/charts/page.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/analysis/charts/page.tsx
@@ -1,9 +1,8 @@
-const ChartsPage = () => {
- return (
-
- Charts will appear here.
-
- );
+import { ChartsListPage } from "@/modules/ee/analysis/charts/components/charts-list-page";
+
+const ChartsPage = async (props: Readonly<{ params: Promise<{ environmentId: string }> }>) => {
+ const { environmentId } = await props.params;
+ return ;
};
export default ChartsPage;
diff --git a/apps/web/app/(app)/environments/[environmentId]/analysis/dashboards/[dashboardId]/page.tsx b/apps/web/app/(app)/environments/[environmentId]/analysis/dashboards/[dashboardId]/page.tsx
index f0aa426ff1..05bc0fd75e 100644
--- a/apps/web/app/(app)/environments/[environmentId]/analysis/dashboards/[dashboardId]/page.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/analysis/dashboards/[dashboardId]/page.tsx
@@ -1,4 +1,4 @@
-const DashboardDetailPage = async (props: { params: Promise<{ dashboardId: string }> }) => {
+const DashboardDetailPage = async (props: Readonly<{ params: Promise<{ dashboardId: string }> }>) => {
const { dashboardId } = await props.params;
return (
diff --git a/apps/web/app/(app)/environments/[environmentId]/analysis/dashboards/page.tsx b/apps/web/app/(app)/environments/[environmentId]/analysis/dashboards/page.tsx
index d21f752e70..2ab40d21a4 100644
--- a/apps/web/app/(app)/environments/[environmentId]/analysis/dashboards/page.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/analysis/dashboards/page.tsx
@@ -1 +1,8 @@
-export { DashboardsListPage as default } from "@/modules/ee/analysis/dashboards/pages/dashboards-list-page";
+import { DashboardsListPage } from "@/modules/ee/analysis/dashboards/pages/dashboards-list-page";
+
+const DashboardsPage = async (props: Readonly<{ params: Promise<{ environmentId: string }> }>) => {
+ const { environmentId } = await props.params;
+ return ;
+};
+
+export default DashboardsPage;
diff --git a/apps/web/app/(app)/environments/[environmentId]/analysis/page.tsx b/apps/web/app/(app)/environments/[environmentId]/analysis/page.tsx
index 0de9ee1a64..c2ab125626 100644
--- a/apps/web/app/(app)/environments/[environmentId]/analysis/page.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/analysis/page.tsx
@@ -1,6 +1,6 @@
import { redirect } from "next/navigation";
-const AnalysisPage = async (props: { params: Promise<{ environmentId: string }> }) => {
+const AnalysisPage = async (props: Readonly<{ params: Promise<{ environmentId: string }> }>) => {
const { environmentId } = await props.params;
return redirect(`/environments/${environmentId}/analysis/dashboards`);
};
diff --git a/apps/web/i18n.lock b/apps/web/i18n.lock
index d94a6a253d..426ad21f60 100644
--- a/apps/web/i18n.lock
+++ b/apps/web/i18n.lock
@@ -123,6 +123,7 @@ checksums:
common/bottom_right: aaef9a70ef795affc806c6d1853d8373
common/cancel: 2e2a849c2223911717de8caa2c71bade
common/centered_modal: 982ff411cb7e91e30300c2ed56b7e507
+ common/chart: 6f4d9c56e45ceb8fc22d2f74454cd813
common/charts: 1da4564d89264c89de4ed28d7451b43e
common/choices: 8a7a77a71ec6eebc363c5dc0f8490a4d
common/choose_environment: 5762cd499529815fc3e6a7feea39f90b
@@ -203,6 +204,7 @@ checksums:
common/failed_to_copy_to_clipboard: de836a7d628d36c832809252f188f784
common/failed_to_load_organizations: 512808a2b674c7c28bca73f8f91fd87e
common/failed_to_load_workspaces: 6ee3448097394517dc605074cd4e6ea4
+ common/filter: 626325a05e4c8800f7ede7012b0cadaf
common/finish: ffa7a10f71182b48fefed7135bee24fa
common/first_name: cf040a5d6a9fd696be400380cc99f54b
common/follow_these: 3a730b242bb17a3f95e01bf0dae86885
@@ -585,6 +587,20 @@ checksums:
environments/actions/you_can_track_code_action_anywhere_in_your_app_using: 3c0bbf160b8ddbeef142403103b70554
environments/actions/your_survey_would_be_shown_on_this_url: 766fdeeb52d170c156af5d035a1f8c37
environments/actions/your_survey_would_not_be_shown: af44fe160f449ff9557ebe5d3686832d
+ environments/analysis/charts/action_coming_soon: ee2b0671e00972773210c5be5a9ccb89
+ environments/analysis/charts/chart_deleted_successfully: 79148f471cd9acc2c8d0d033fb85437e
+ environments/analysis/charts/chart_deletion_error: 267eb65c168e726075d7cea678dd32e0
+ environments/analysis/charts/chart_duplicated_successfully: 755c4ce5bf533764d549a53c33e32165
+ environments/analysis/charts/chart_duplication_error: 90d7166c85188b52f821c9d9f53ff8c4
+ environments/analysis/charts/chart_type_area: 535754c6425f045f17e1dcb551840c93
+ environments/analysis/charts/chart_type_bar: c11d460595d3ddfe8efd67ac068574c5
+ environments/analysis/charts/chart_type_big_number: 9d17fb96241507c955dca25e143ae67a
+ environments/analysis/charts/chart_type_line: f42dd53238ed4d44def306a61d47d5c4
+ environments/analysis/charts/chart_type_pie: 068a797404233ccf68d07ad63af7b50c
+ environments/analysis/charts/create_chart: ca7fdcc964e01f42ea9709924221edba
+ environments/analysis/charts/delete_chart_confirmation: f7fd7b0a08e81c9b392b08c9c1ad2147
+ environments/analysis/charts/no_charts_found: d4a27d5b56e49ebdd38bf28791dbcc42
+ environments/analysis/charts/open_options: 2c6a35fec9b9d008e41728594bcd07d7
environments/analysis/dashboards/create_dashboard: 9396aec1ea4a9b05ada94483655d1373
environments/analysis/dashboards/create_dashboard_description: d29f60615f6d8c96cc4265541e75ec26
environments/analysis/dashboards/create_failed: 7b58f15568047a35220b3a47cc3b0f71
diff --git a/apps/web/locales/de-DE.json b/apps/web/locales/de-DE.json
index 724aea290e..e8982b84f7 100644
--- a/apps/web/locales/de-DE.json
+++ b/apps/web/locales/de-DE.json
@@ -150,6 +150,7 @@
"bottom_right": "Unten rechts",
"cancel": "Abbrechen",
"centered_modal": "Zentriertes Modalfenster",
+ "chart": "Diagramm",
"charts": "Diagramme",
"choices": "Entscheidungen",
"choose_environment": "Umgebung auswählen",
@@ -620,6 +621,22 @@
"your_survey_would_not_be_shown": "Ihre Umfrage wäre nicht angezeigt."
},
"analysis": {
+ "charts": {
+ "action_coming_soon": "Kommt bald",
+ "chart_deleted_successfully": "Diagramm erfolgreich gelöscht",
+ "chart_deletion_error": "Diagramm konnte nicht gelöscht werden",
+ "chart_duplicated_successfully": "Diagramm erfolgreich dupliziert",
+ "chart_duplication_error": "Diagramm konnte nicht dupliziert werden",
+ "chart_type_area": "Flächendiagramm",
+ "chart_type_bar": "Balkendiagramm",
+ "chart_type_big_number": "Große Zahl",
+ "chart_type_line": "Liniendiagramm",
+ "chart_type_pie": "Kreisdiagramm",
+ "create_chart": "Diagramm erstellen",
+ "delete_chart_confirmation": "Bist du sicher, dass du dieses Diagramm löschen möchtest?",
+ "no_charts_found": "Keine Diagramme gefunden.",
+ "open_options": "Diagrammoptionen öffnen"
+ },
"dashboards": {
"create_dashboard": "Dashboard erstellen",
"create_dashboard_description": "Gib einen Namen für dein neues Dashboard ein.",
diff --git a/apps/web/locales/en-US.json b/apps/web/locales/en-US.json
index a87117513e..0151452ab6 100644
--- a/apps/web/locales/en-US.json
+++ b/apps/web/locales/en-US.json
@@ -150,6 +150,7 @@
"bottom_right": "Bottom Right",
"cancel": "Cancel",
"centered_modal": "Centered Modal",
+ "chart": "Chart",
"charts": "Charts",
"choices": "Choices",
"choose_environment": "Choose environment",
@@ -620,6 +621,22 @@
"your_survey_would_not_be_shown": "Your survey would not be shown."
},
"analysis": {
+ "charts": {
+ "action_coming_soon": "Coming soon",
+ "chart_deleted_successfully": "Chart deleted successfully",
+ "chart_deletion_error": "Failed to delete chart",
+ "chart_duplicated_successfully": "Chart duplicated successfully",
+ "chart_duplication_error": "Failed to duplicate chart",
+ "chart_type_area": "Area Chart",
+ "chart_type_bar": "Bar Chart",
+ "chart_type_big_number": "Big Number",
+ "chart_type_line": "Line Chart",
+ "chart_type_pie": "Pie Chart",
+ "create_chart": "Create Chart",
+ "delete_chart_confirmation": "Are you sure you want to delete this chart?",
+ "no_charts_found": "No charts found.",
+ "open_options": "Open chart options"
+ },
"dashboards": {
"create_dashboard": "Create Dashboard",
"create_dashboard_description": "Enter a name for your new dashboard.",
diff --git a/apps/web/locales/es-ES.json b/apps/web/locales/es-ES.json
index ad879ee5d9..fddc819817 100644
--- a/apps/web/locales/es-ES.json
+++ b/apps/web/locales/es-ES.json
@@ -150,6 +150,7 @@
"bottom_right": "Inferior derecha",
"cancel": "Cancelar",
"centered_modal": "Modal centrado",
+ "chart": "Gráfico",
"charts": "Gráficos",
"choices": "Opciones",
"choose_environment": "Elegir entorno",
@@ -620,6 +621,22 @@
"your_survey_would_not_be_shown": "Tu encuesta no se mostraría."
},
"analysis": {
+ "charts": {
+ "action_coming_soon": "Próximamente",
+ "chart_deleted_successfully": "Gráfico eliminado correctamente",
+ "chart_deletion_error": "Error al eliminar el gráfico",
+ "chart_duplicated_successfully": "Gráfico duplicado correctamente",
+ "chart_duplication_error": "Error al duplicar el gráfico",
+ "chart_type_area": "Gráfico de área",
+ "chart_type_bar": "Gráfico de barras",
+ "chart_type_big_number": "Número grande",
+ "chart_type_line": "Gráfico de líneas",
+ "chart_type_pie": "Gráfico circular",
+ "create_chart": "Crear gráfico",
+ "delete_chart_confirmation": "¿Estás seguro de que quieres eliminar este gráfico?",
+ "no_charts_found": "No se encontraron gráficos.",
+ "open_options": "Abrir opciones del gráfico"
+ },
"dashboards": {
"create_dashboard": "Crear panel de control",
"create_dashboard_description": "Introduce un nombre para tu panel de control nuevo.",
diff --git a/apps/web/locales/fr-FR.json b/apps/web/locales/fr-FR.json
index ff23cacc8e..4c33a92266 100644
--- a/apps/web/locales/fr-FR.json
+++ b/apps/web/locales/fr-FR.json
@@ -112,7 +112,6 @@
"link_expired_description": "Le lien que vous avez utilisé n'est plus valide."
},
"common": {
- "Filter": "Filtrer",
"accepted": "Accepté",
"account": "Compte",
"account_settings": "Paramètres du compte",
@@ -151,6 +150,7 @@
"bottom_right": "En bas à droite",
"cancel": "Annuler",
"centered_modal": "Au centre",
+ "chart": "Graphique",
"charts": "Graphiques",
"choices": "Choix",
"choose_environment": "Choisir l'environnement",
@@ -621,6 +621,22 @@
"your_survey_would_not_be_shown": "Votre enquête ne serait pas affichée."
},
"analysis": {
+ "charts": {
+ "action_coming_soon": "À venir bientôt",
+ "chart_deleted_successfully": "Graphique supprimé avec succès",
+ "chart_deletion_error": "Échec de la suppression du graphique",
+ "chart_duplicated_successfully": "Graphique dupliqué avec succès",
+ "chart_duplication_error": "Échec de la duplication du graphique",
+ "chart_type_area": "Graphique en aires",
+ "chart_type_bar": "Graphique à barres",
+ "chart_type_big_number": "Grand nombre",
+ "chart_type_line": "Graphique linéaire",
+ "chart_type_pie": "Graphique circulaire",
+ "create_chart": "Créer un graphique",
+ "delete_chart_confirmation": "Êtes-vous sûr de vouloir supprimer ce graphique ?",
+ "no_charts_found": "Aucun graphique trouvé.",
+ "open_options": "Ouvrir les options du graphique"
+ },
"dashboards": {
"create_dashboard": "Créer un tableau de bord",
"create_dashboard_description": "Saisissez un nom pour votre nouveau tableau de bord.",
diff --git a/apps/web/locales/hu-HU.json b/apps/web/locales/hu-HU.json
index 61e5ffe719..3e0b4b66cc 100644
--- a/apps/web/locales/hu-HU.json
+++ b/apps/web/locales/hu-HU.json
@@ -150,6 +150,7 @@
"bottom_right": "Jobbra lent",
"cancel": "Mégse",
"centered_modal": "Középre helyezett kizárólagos",
+ "chart": "Diagram",
"charts": "Diagramok",
"choices": "Választási lehetőségek",
"choose_environment": "Környezet kiválasztása",
@@ -620,6 +621,22 @@
"your_survey_would_not_be_shown": "A kérdőív nem jelenne meg."
},
"analysis": {
+ "charts": {
+ "action_coming_soon": "Hamarosan",
+ "chart_deleted_successfully": "A diagram sikeresen törölve",
+ "chart_deletion_error": "A diagram törlése sikertelen",
+ "chart_duplicated_successfully": "A diagram sikeresen duplikálva",
+ "chart_duplication_error": "A diagram duplikálása sikertelen",
+ "chart_type_area": "Területdiagram",
+ "chart_type_bar": "Oszlopdiagram",
+ "chart_type_big_number": "Nagy szám",
+ "chart_type_line": "Vonaldiagram",
+ "chart_type_pie": "Kördiagram",
+ "create_chart": "Diagram létrehozása",
+ "delete_chart_confirmation": "Biztosan törölni szeretnéd ezt a diagramot?",
+ "no_charts_found": "Nem található diagram.",
+ "open_options": "Diagram beállításainak megnyitása"
+ },
"dashboards": {
"create_dashboard": "Vezérlőpult létrehozása",
"create_dashboard_description": "Adjon nevet az új vezérlőpultnak.",
diff --git a/apps/web/locales/ja-JP.json b/apps/web/locales/ja-JP.json
index 91e4697aa3..494fedc67c 100644
--- a/apps/web/locales/ja-JP.json
+++ b/apps/web/locales/ja-JP.json
@@ -150,6 +150,7 @@
"bottom_right": "右下",
"cancel": "キャンセル",
"centered_modal": "中央モーダル",
+ "chart": "チャート",
"charts": "チャート",
"choices": "選択肢",
"choose_environment": "環境を選択",
@@ -620,6 +621,22 @@
"your_survey_would_not_be_shown": "あなたのフォームは表示されません。"
},
"analysis": {
+ "charts": {
+ "action_coming_soon": "近日公開",
+ "chart_deleted_successfully": "チャートを削除しました",
+ "chart_deletion_error": "チャートの削除に失敗しました",
+ "chart_duplicated_successfully": "チャートを複製しました",
+ "chart_duplication_error": "チャートの複製に失敗しました",
+ "chart_type_area": "エリアチャート",
+ "chart_type_bar": "棒グラフ",
+ "chart_type_big_number": "大きな数値",
+ "chart_type_line": "折れ線グラフ",
+ "chart_type_pie": "円グラフ",
+ "create_chart": "チャートを作成",
+ "delete_chart_confirmation": "このチャートを削除してもよろしいですか?",
+ "no_charts_found": "チャートが見つかりません。",
+ "open_options": "チャートオプションを開く"
+ },
"dashboards": {
"create_dashboard": "ダッシュボードを作成",
"create_dashboard_description": "新しいダッシュボードの名前を入力してください。",
diff --git a/apps/web/locales/nl-NL.json b/apps/web/locales/nl-NL.json
index 38b02e0094..afb4cee5a7 100644
--- a/apps/web/locales/nl-NL.json
+++ b/apps/web/locales/nl-NL.json
@@ -150,6 +150,7 @@
"bottom_right": "Rechtsonder",
"cancel": "Annuleren",
"centered_modal": "Gecentreerd modaal",
+ "chart": "Grafiek",
"charts": "Grafieken",
"choices": "Keuzes",
"choose_environment": "Kies omgeving",
@@ -620,6 +621,22 @@
"your_survey_would_not_be_shown": "Uw enquête wordt niet getoond."
},
"analysis": {
+ "charts": {
+ "action_coming_soon": "Binnenkort beschikbaar",
+ "chart_deleted_successfully": "Grafiek succesvol verwijderd",
+ "chart_deletion_error": "Verwijderen van grafiek mislukt",
+ "chart_duplicated_successfully": "Grafiek succesvol gedupliceerd",
+ "chart_duplication_error": "Dupliceren van grafiek mislukt",
+ "chart_type_area": "Vlakdiagram",
+ "chart_type_bar": "Staafdiagram",
+ "chart_type_big_number": "Groot getal",
+ "chart_type_line": "Lijndiagram",
+ "chart_type_pie": "Cirkeldiagram",
+ "create_chart": "Diagram maken",
+ "delete_chart_confirmation": "Weet je zeker dat je deze grafiek wilt verwijderen?",
+ "no_charts_found": "Geen diagrammen gevonden.",
+ "open_options": "Open diagramopties"
+ },
"dashboards": {
"create_dashboard": "Dashboard creëren",
"create_dashboard_description": "Voer een naam in voor je nieuwe dashboard.",
diff --git a/apps/web/locales/pt-BR.json b/apps/web/locales/pt-BR.json
index a5e0a02aef..4d58ddd62b 100644
--- a/apps/web/locales/pt-BR.json
+++ b/apps/web/locales/pt-BR.json
@@ -150,6 +150,7 @@
"bottom_right": "Canto Inferior Direito",
"cancel": "Cancelar",
"centered_modal": "Modal Centralizado",
+ "chart": "Gráfico",
"charts": "Gráficos",
"choices": "Escolhas",
"choose_environment": "Escolher ambiente",
@@ -620,6 +621,22 @@
"your_survey_would_not_be_shown": "Sua pesquisa não seria exibida."
},
"analysis": {
+ "charts": {
+ "action_coming_soon": "Em breve",
+ "chart_deleted_successfully": "Gráfico excluído com sucesso",
+ "chart_deletion_error": "Falha ao excluir gráfico",
+ "chart_duplicated_successfully": "Gráfico duplicado com sucesso",
+ "chart_duplication_error": "Falha ao duplicar gráfico",
+ "chart_type_area": "Gráfico de área",
+ "chart_type_bar": "Gráfico de barras",
+ "chart_type_big_number": "Número grande",
+ "chart_type_line": "Gráfico de linhas",
+ "chart_type_pie": "Gráfico de pizza",
+ "create_chart": "Criar gráfico",
+ "delete_chart_confirmation": "Tem certeza de que deseja excluir este gráfico?",
+ "no_charts_found": "Nenhum gráfico encontrado.",
+ "open_options": "Abrir opções do gráfico"
+ },
"dashboards": {
"create_dashboard": "Criar painel",
"create_dashboard_description": "Digite um nome para o seu novo painel.",
diff --git a/apps/web/locales/pt-PT.json b/apps/web/locales/pt-PT.json
index 0ade50ac37..8354adae0b 100644
--- a/apps/web/locales/pt-PT.json
+++ b/apps/web/locales/pt-PT.json
@@ -150,6 +150,7 @@
"bottom_right": "Inferior Direito",
"cancel": "Cancelar",
"centered_modal": "Modal Centralizado",
+ "chart": "Gráfico",
"charts": "Gráficos",
"choices": "Escolhas",
"choose_environment": "Escolha o ambiente",
@@ -620,6 +621,22 @@
"your_survey_would_not_be_shown": "O seu inquérito não seria mostrado."
},
"analysis": {
+ "charts": {
+ "action_coming_soon": "Em breve",
+ "chart_deleted_successfully": "Gráfico eliminado com sucesso",
+ "chart_deletion_error": "Falha ao eliminar gráfico",
+ "chart_duplicated_successfully": "Gráfico duplicado com sucesso",
+ "chart_duplication_error": "Falha ao duplicar gráfico",
+ "chart_type_area": "Gráfico de área",
+ "chart_type_bar": "Gráfico de barras",
+ "chart_type_big_number": "Número grande",
+ "chart_type_line": "Gráfico de linhas",
+ "chart_type_pie": "Gráfico circular",
+ "create_chart": "Criar gráfico",
+ "delete_chart_confirmation": "Tens a certeza de que queres eliminar este gráfico?",
+ "no_charts_found": "Nenhum gráfico encontrado.",
+ "open_options": "Abrir opções do gráfico"
+ },
"dashboards": {
"create_dashboard": "Criar painel",
"create_dashboard_description": "Introduza um nome para o seu novo painel.",
diff --git a/apps/web/locales/ro-RO.json b/apps/web/locales/ro-RO.json
index 8284dabc9a..7499bdcf7e 100644
--- a/apps/web/locales/ro-RO.json
+++ b/apps/web/locales/ro-RO.json
@@ -150,6 +150,7 @@
"bottom_right": "Dreapta Jos",
"cancel": "Anulare",
"centered_modal": "Modală centralizată",
+ "chart": "Grafic",
"charts": "Grafice",
"choices": "Alegeri",
"choose_environment": "Alege mediul",
@@ -620,6 +621,22 @@
"your_survey_would_not_be_shown": "Sondajul dumneavoastră nu va fi afișat."
},
"analysis": {
+ "charts": {
+ "action_coming_soon": "În curând",
+ "chart_deleted_successfully": "Graficul a fost șters cu succes",
+ "chart_deletion_error": "Nu s-a putut șterge graficul",
+ "chart_duplicated_successfully": "Graficul a fost duplicat cu succes",
+ "chart_duplication_error": "Nu s-a putut duplica graficul",
+ "chart_type_area": "Grafic de tip arie",
+ "chart_type_bar": "Grafic de tip bară",
+ "chart_type_big_number": "Număr mare",
+ "chart_type_line": "Grafic de tip linie",
+ "chart_type_pie": "Grafic de tip plăcintă",
+ "create_chart": "Creează grafic",
+ "delete_chart_confirmation": "Ești sigur că vrei să ștergi acest grafic?",
+ "no_charts_found": "Nu s-au găsit grafice.",
+ "open_options": "Deschide opțiunile graficului"
+ },
"dashboards": {
"create_dashboard": "Creează tablou de bord",
"create_dashboard_description": "Introdu un nume pentru noul tău tablou de bord.",
diff --git a/apps/web/locales/ru-RU.json b/apps/web/locales/ru-RU.json
index e647dcbd23..63e7f11d05 100644
--- a/apps/web/locales/ru-RU.json
+++ b/apps/web/locales/ru-RU.json
@@ -112,7 +112,6 @@
"link_expired_description": "Ссылка, которой вы воспользовались, больше не действительна."
},
"common": {
- "Filter": "Фильтр",
"accepted": "Принято",
"account": "Аккаунт",
"account_settings": "Настройки аккаунта",
@@ -151,6 +150,7 @@
"bottom_right": "Внизу справа",
"cancel": "Отмена",
"centered_modal": "Центрированное модальное окно",
+ "chart": "График",
"charts": "Графики",
"choices": "Варианты",
"choose_environment": "Выберите среду",
@@ -621,6 +621,22 @@
"your_survey_would_not_be_shown": "Ваш опрос не будет отображаться."
},
"analysis": {
+ "charts": {
+ "action_coming_soon": "Скоро будет",
+ "chart_deleted_successfully": "График успешно удалён",
+ "chart_deletion_error": "Не удалось удалить график",
+ "chart_duplicated_successfully": "График успешно дублирован",
+ "chart_duplication_error": "Не удалось дублировать график",
+ "chart_type_area": "График областью",
+ "chart_type_bar": "Столбчатая диаграмма",
+ "chart_type_big_number": "Большое число",
+ "chart_type_line": "Линейный график",
+ "chart_type_pie": "Круговая диаграмма",
+ "create_chart": "Создать график",
+ "delete_chart_confirmation": "Ты уверен, что хочешь удалить этот график?",
+ "no_charts_found": "Графики не найдены.",
+ "open_options": "Открыть настройки графика"
+ },
"dashboards": {
"create_dashboard": "Создать панель управления",
"create_dashboard_description": "Введите название для новой панели управления.",
diff --git a/apps/web/locales/sv-SE.json b/apps/web/locales/sv-SE.json
index 09eaf5c732..4aae9a5a92 100644
--- a/apps/web/locales/sv-SE.json
+++ b/apps/web/locales/sv-SE.json
@@ -150,6 +150,7 @@
"bottom_right": "Nedre höger",
"cancel": "Avbryt",
"centered_modal": "Centrerad modal",
+ "chart": "Diagram",
"charts": "Diagram",
"choices": "Val",
"choose_environment": "Välj miljö",
@@ -620,6 +621,22 @@
"your_survey_would_not_be_shown": "Din enkät skulle inte visas."
},
"analysis": {
+ "charts": {
+ "action_coming_soon": "Kommer snart",
+ "chart_deleted_successfully": "Diagrammet har tagits bort",
+ "chart_deletion_error": "Det gick inte att ta bort diagrammet",
+ "chart_duplicated_successfully": "Diagrammet har duplicerats",
+ "chart_duplication_error": "Det gick inte att duplicera diagrammet",
+ "chart_type_area": "Ytdiagram",
+ "chart_type_bar": "Stapeldiagram",
+ "chart_type_big_number": "Stort tal",
+ "chart_type_line": "Linjediagram",
+ "chart_type_pie": "Cirkeldiagram",
+ "create_chart": "Skapa diagram",
+ "delete_chart_confirmation": "Är du säker på att du vill ta bort det här diagrammet?",
+ "no_charts_found": "Inga diagram hittades.",
+ "open_options": "Öppna diagramalternativ"
+ },
"dashboards": {
"create_dashboard": "Skapa instrumentpanel",
"create_dashboard_description": "Ange ett namn för din nya instrumentpanel.",
diff --git a/apps/web/locales/zh-Hans-CN.json b/apps/web/locales/zh-Hans-CN.json
index 768550b189..6b4e56fa05 100644
--- a/apps/web/locales/zh-Hans-CN.json
+++ b/apps/web/locales/zh-Hans-CN.json
@@ -150,6 +150,7 @@
"bottom_right": "右下",
"cancel": "取消",
"centered_modal": "居中 模态",
+ "chart": "图表",
"charts": "图表",
"choices": "选项",
"choose_environment": "选择 环境",
@@ -620,6 +621,22 @@
"your_survey_would_not_be_shown": "您的 调查 不会 显示。"
},
"analysis": {
+ "charts": {
+ "action_coming_soon": "即将推出",
+ "chart_deleted_successfully": "图表删除成功",
+ "chart_deletion_error": "图表删除失败",
+ "chart_duplicated_successfully": "图表复制成功",
+ "chart_duplication_error": "图表复制失败",
+ "chart_type_area": "面积图",
+ "chart_type_bar": "柱状图",
+ "chart_type_big_number": "大数字",
+ "chart_type_line": "折线图",
+ "chart_type_pie": "饼图",
+ "create_chart": "创建图表",
+ "delete_chart_confirmation": "你确定要删除这个图表吗?",
+ "no_charts_found": "未找到图表。",
+ "open_options": "打开图表选项"
+ },
"dashboards": {
"create_dashboard": "创建 Dashboard",
"create_dashboard_description": "请输入新 Dashboard 的名称。",
diff --git a/apps/web/locales/zh-Hant-TW.json b/apps/web/locales/zh-Hant-TW.json
index 6fcb352159..6a96fa577a 100644
--- a/apps/web/locales/zh-Hant-TW.json
+++ b/apps/web/locales/zh-Hant-TW.json
@@ -150,6 +150,7 @@
"bottom_right": "右下",
"cancel": "取消",
"centered_modal": "置中彈窗",
+ "chart": "圖表",
"charts": "圖表",
"choices": "選項",
"choose_environment": "選擇環境",
@@ -620,6 +621,22 @@
"your_survey_would_not_be_shown": "您的問卷將不會顯示。"
},
"analysis": {
+ "charts": {
+ "action_coming_soon": "即將推出",
+ "chart_deleted_successfully": "圖表已成功刪除",
+ "chart_deletion_error": "刪除圖表失敗",
+ "chart_duplicated_successfully": "圖表已成功複製",
+ "chart_duplication_error": "圖表複製失敗",
+ "chart_type_area": "區域圖",
+ "chart_type_bar": "長條圖",
+ "chart_type_big_number": "大數字",
+ "chart_type_line": "折線圖",
+ "chart_type_pie": "圓餅圖",
+ "create_chart": "建立圖表",
+ "delete_chart_confirmation": "你確定要刪除此圖表嗎?",
+ "no_charts_found": "找不到圖表。",
+ "open_options": "開啟圖表選項"
+ },
"dashboards": {
"create_dashboard": "建立儀表板",
"create_dashboard_description": "請輸入新儀表板的名稱。",
diff --git a/apps/web/modules/ee/analysis/charts/components/chart-dropdown-menu.tsx b/apps/web/modules/ee/analysis/charts/components/chart-dropdown-menu.tsx
new file mode 100644
index 0000000000..95482ec660
--- /dev/null
+++ b/apps/web/modules/ee/analysis/charts/components/chart-dropdown-menu.tsx
@@ -0,0 +1,125 @@
+"use client";
+
+import { CopyIcon, MoreVertical, SquarePenIcon, TrashIcon } from "lucide-react";
+import { useRouter } from "next/navigation";
+import { useState } from "react";
+import toast from "react-hot-toast";
+import { useTranslation } from "react-i18next";
+import { getFormattedErrorMessage } from "@/lib/utils/helper";
+import { deleteChartAction, duplicateChartAction } from "@/modules/ee/analysis/charts/actions";
+import type { TChartWithCreator } from "@/modules/ee/analysis/types/analysis";
+import { Button } from "@/modules/ui/components/button";
+import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuGroup,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@/modules/ui/components/dropdown-menu";
+
+interface ChartDropdownMenuProps {
+ environmentId: string;
+ chart: TChartWithCreator;
+}
+
+export function ChartDropdownMenu({ environmentId, chart }: Readonly) {
+ const { t } = useTranslation();
+ const router = useRouter();
+ const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
+ const [isDeleting, setIsDeleting] = useState(false);
+ const [isDuplicating, setIsDuplicating] = useState(false);
+ const [isDropDownOpen, setIsDropDownOpen] = useState(false);
+
+ const handleDuplicate = async () => {
+ setIsDuplicating(true);
+ try {
+ const result = await duplicateChartAction({ environmentId, chartId: chart.id });
+ if (result?.data) {
+ toast.success(t("environments.analysis.charts.chart_duplicated_successfully"));
+ router.refresh();
+ } else {
+ toast.error(
+ getFormattedErrorMessage(result) || t("environments.analysis.charts.chart_duplication_error")
+ );
+ }
+ } catch {
+ toast.error(t("environments.analysis.charts.chart_duplication_error"));
+ } finally {
+ setIsDuplicating(false);
+ }
+ };
+
+ const handleDelete = async () => {
+ setIsDeleting(true);
+ try {
+ const result = await deleteChartAction({ environmentId, chartId: chart.id });
+ if (result?.data) {
+ toast.success(t("environments.analysis.charts.chart_deleted_successfully"));
+ setDeleteDialogOpen(false);
+ router.refresh();
+ } else {
+ const msg =
+ getFormattedErrorMessage(result) || t("environments.analysis.charts.chart_deletion_error");
+ toast.error(msg);
+ }
+ } catch {
+ toast.error(t("common.something_went_wrong_please_try_again"));
+ } finally {
+ setIsDeleting(false);
+ }
+ };
+
+ const handleEdit = () => {
+ toast(t("environments.analysis.charts.action_coming_soon"));
+ };
+
+ return (
+
+
+
+
+
+
+
+ } onClick={handleEdit}>
+ {t("common.edit")}
+
+
+ }
+ onClick={() => {
+ setIsDropDownOpen(false);
+ handleDuplicate();
+ }}
+ disabled={isDuplicating}>
+ {t("common.duplicate")}
+
+
+ }
+ onClick={() => {
+ setIsDropDownOpen(false);
+ setDeleteDialogOpen(true);
+ }}
+ disabled={isDeleting}>
+ {t("common.delete")}
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/modules/ee/analysis/charts/components/chart-row.tsx b/apps/web/modules/ee/analysis/charts/components/chart-row.tsx
new file mode 100644
index 0000000000..e6a29a3ff4
--- /dev/null
+++ b/apps/web/modules/ee/analysis/charts/components/chart-row.tsx
@@ -0,0 +1,48 @@
+"use client";
+
+import { BarChart3Icon } from "lucide-react";
+import { convertDateString, timeSinceDate } from "@/lib/time";
+import { ChartDropdownMenu } from "@/modules/ee/analysis/charts/components/chart-dropdown-menu";
+import { CHART_TYPE_ICONS } from "@/modules/ee/analysis/charts/lib/chart-types";
+import type { TChartWithCreator } from "@/modules/ee/analysis/types/analysis";
+
+interface ChartRowProps {
+ chart: TChartWithCreator;
+ environmentId: string;
+ isReadOnly: boolean;
+}
+
+export function ChartRow({ chart, environmentId, isReadOnly }: Readonly) {
+ const IconComponent = CHART_TYPE_ICONS[chart.type as keyof typeof CHART_TYPE_ICONS] ?? BarChart3Icon;
+
+ return (
+
+
+
+
+
{chart.creator?.name ?? "-"}
+
+
+
+ {convertDateString(chart.createdAt.toISOString())}
+
+
+
+
{timeSinceDate(new Date(chart.updatedAt))}
+
+
+
+ {!isReadOnly && }
+
+
+ );
+}
diff --git a/apps/web/modules/ee/analysis/charts/components/charts-list-page.tsx b/apps/web/modules/ee/analysis/charts/components/charts-list-page.tsx
new file mode 100644
index 0000000000..7d5be19338
--- /dev/null
+++ b/apps/web/modules/ee/analysis/charts/components/charts-list-page.tsx
@@ -0,0 +1,63 @@
+import { Delay } from "@suspensive/react";
+import { Suspense, use } from "react";
+import { getTranslate } from "@/lingodotdev/server";
+import { ChartsList } from "@/modules/ee/analysis/charts/components/charts-list";
+import { ChartsListSkeleton } from "@/modules/ee/analysis/charts/components/charts-list-skeleton";
+import { CreateChartButton } from "@/modules/ee/analysis/charts/components/create-chart-button";
+import { getChartsWithCreator } from "@/modules/ee/analysis/charts/lib/charts";
+import { AnalysisPageLayout } from "@/modules/ee/analysis/components/analysis-page-layout";
+import type { TChartWithCreator } from "@/modules/ee/analysis/types/analysis";
+import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
+
+interface ChartsListContentProps {
+ chartsPromise: Promise;
+ environmentId: string;
+ isReadOnly: boolean;
+}
+
+const ChartsListContent = ({
+ chartsPromise,
+ environmentId,
+ isReadOnly,
+}: Readonly) => {
+ const charts = use(chartsPromise);
+
+ return ;
+};
+
+interface ChartsListPageProps {
+ environmentId: string;
+}
+
+export async function ChartsListPage({ environmentId }: Readonly) {
+ const t = await getTranslate();
+ const { project, isReadOnly } = await getEnvironmentAuth(environmentId);
+ const chartsPromise = getChartsWithCreator(project.id);
+
+ return (
+ }>
+
+
+
+ }>
+
+
+
+ );
+}
diff --git a/apps/web/modules/ee/analysis/charts/components/charts-list-skeleton.tsx b/apps/web/modules/ee/analysis/charts/components/charts-list-skeleton.tsx
new file mode 100644
index 0000000000..8fb2dfa25f
--- /dev/null
+++ b/apps/web/modules/ee/analysis/charts/components/charts-list-skeleton.tsx
@@ -0,0 +1,43 @@
+const SKELETON_ROWS = 3;
+
+const SkeletonRow = () => {
+ return (
+
+ );
+};
+
+interface ChartsListSkeletonProps {
+ columnHeaders: [string, string, string, string];
+}
+
+export const ChartsListSkeleton = ({ columnHeaders }: Readonly) => {
+ return (
+
+
+
{columnHeaders[0]}
+
{columnHeaders[1]}
+
{columnHeaders[2]}
+
{columnHeaders[3]}
+
+
+ {Array.from({ length: SKELETON_ROWS }).map((_, i) => (
+
+ ))}
+
+ );
+};
diff --git a/apps/web/modules/ee/analysis/charts/components/charts-list.tsx b/apps/web/modules/ee/analysis/charts/components/charts-list.tsx
new file mode 100644
index 0000000000..a142defff6
--- /dev/null
+++ b/apps/web/modules/ee/analysis/charts/components/charts-list.tsx
@@ -0,0 +1,34 @@
+import { getTranslate } from "@/lingodotdev/server";
+import { ChartRow } from "@/modules/ee/analysis/charts/components/chart-row";
+import type { TChartWithCreator } from "@/modules/ee/analysis/types/analysis";
+
+interface ChartsListProps {
+ charts: TChartWithCreator[];
+ environmentId: string;
+ isReadOnly: boolean;
+}
+
+export const ChartsList = async ({ charts, environmentId, isReadOnly }: Readonly) => {
+ const t = await getTranslate();
+
+ return (
+
+
+
{t("common.title")}
+
{t("common.created_by")}
+
{t("common.created_at")}
+
{t("common.updated_at")}
+
+
+ {charts.length === 0 ? (
+
+ {t("environments.analysis.charts.no_charts_found")}
+
+ ) : (
+ charts.map((chart) => (
+
+ ))
+ )}
+
+ );
+};
diff --git a/apps/web/modules/ee/analysis/charts/components/create-chart-button.tsx b/apps/web/modules/ee/analysis/charts/components/create-chart-button.tsx
new file mode 100644
index 0000000000..1235210e44
--- /dev/null
+++ b/apps/web/modules/ee/analysis/charts/components/create-chart-button.tsx
@@ -0,0 +1,21 @@
+"use client";
+
+import { PlusIcon } from "lucide-react";
+import toast from "react-hot-toast";
+import { useTranslation } from "react-i18next";
+import { Button } from "@/modules/ui/components/button";
+
+export function CreateChartButton() {
+ const { t } = useTranslation();
+
+ const handleClick = () => {
+ toast(t("environments.analysis.charts.action_coming_soon"));
+ };
+
+ return (
+
+ );
+}
diff --git a/apps/web/modules/ee/analysis/charts/lib/chart-types.test.ts b/apps/web/modules/ee/analysis/charts/lib/chart-types.test.ts
new file mode 100644
index 0000000000..0eca1f99d9
--- /dev/null
+++ b/apps/web/modules/ee/analysis/charts/lib/chart-types.test.ts
@@ -0,0 +1,18 @@
+import { describe, expect, test, vi } from "vitest";
+import { CHART_TYPE_ICONS, getChartTypes } from "./chart-types";
+
+describe("chart-types", () => {
+ test("CHART_TYPE_ICONS has all chart types", () => {
+ expect(Object.keys(CHART_TYPE_ICONS)).toEqual(["area", "bar", "line", "pie", "big_number"]);
+ });
+
+ test("getChartTypes returns chart types with translated labels", () => {
+ const t = vi.fn((key: string) => key);
+ const result = getChartTypes(t);
+
+ expect(result).toHaveLength(5);
+ expect(result.map((r) => r.id)).toEqual(["area", "bar", "line", "pie", "big_number"]);
+ expect(t).toHaveBeenCalledWith("environments.analysis.charts.chart_type_area");
+ expect(result[0].label).toBe("environments.analysis.charts.chart_type_area");
+ });
+});
diff --git a/apps/web/modules/ee/analysis/charts/lib/chart-types.ts b/apps/web/modules/ee/analysis/charts/lib/chart-types.ts
new file mode 100644
index 0000000000..ea5300560e
--- /dev/null
+++ b/apps/web/modules/ee/analysis/charts/lib/chart-types.ts
@@ -0,0 +1,35 @@
+import type { TFunction } from "i18next";
+import { ActivityIcon, AreaChartIcon, BarChart3Icon, LineChartIcon, PieChartIcon } from "lucide-react";
+import type React from "react";
+import type { TChartType } from "@/modules/ee/analysis/types/analysis";
+
+export const CHART_TYPE_ICONS: Record<
+ TChartType,
+ React.ComponentType<{ className?: string; strokeWidth?: number }>
+> = {
+ area: AreaChartIcon,
+ bar: BarChart3Icon,
+ line: LineChartIcon,
+ pie: PieChartIcon,
+ big_number: ActivityIcon,
+};
+
+export function getChartTypes(
+ t: TFunction
+): readonly {
+ id: TChartType;
+ icon: React.ComponentType<{ className?: string; strokeWidth?: number }>;
+ label: string;
+}[] {
+ return [
+ { id: "area", icon: CHART_TYPE_ICONS.area, label: t("environments.analysis.charts.chart_type_area") },
+ { id: "bar", icon: CHART_TYPE_ICONS.bar, label: t("environments.analysis.charts.chart_type_bar") },
+ { id: "line", icon: CHART_TYPE_ICONS.line, label: t("environments.analysis.charts.chart_type_line") },
+ { id: "pie", icon: CHART_TYPE_ICONS.pie, label: t("environments.analysis.charts.chart_type_pie") },
+ {
+ id: "big_number",
+ icon: CHART_TYPE_ICONS.big_number,
+ label: t("environments.analysis.charts.chart_type_big_number"),
+ },
+ ];
+}
diff --git a/apps/web/modules/ee/analysis/charts/lib/charts.test.ts b/apps/web/modules/ee/analysis/charts/lib/charts.test.ts
index 3ce9d748f9..90df49e381 100644
--- a/apps/web/modules/ee/analysis/charts/lib/charts.test.ts
+++ b/apps/web/modules/ee/analysis/charts/lib/charts.test.ts
@@ -380,4 +380,35 @@ describe("Chart Service", () => {
});
});
});
+
+ describe("getChartsWithCreator", () => {
+ test("returns charts with creator info", async () => {
+ const chartsWithCreator = [
+ { ...mockChart, creator: { name: "Alice" } },
+ { ...mockChart, id: "chart-2", name: "Chart 2", creator: null },
+ ];
+ vi.mocked(prisma.chart.findMany).mockResolvedValue(chartsWithCreator as any);
+ const { getChartsWithCreator } = await import("./charts");
+
+ const result = await getChartsWithCreator(mockProjectId);
+
+ expect(result).toEqual(chartsWithCreator);
+ expect(prisma.chart.findMany).toHaveBeenCalledWith({
+ where: { projectId: mockProjectId },
+ orderBy: { createdAt: "desc" },
+ select: expect.objectContaining({
+ creator: { select: { name: true } },
+ }),
+ });
+ });
+
+ test("throws DatabaseError on Prisma errors", async () => {
+ vi.mocked(prisma.chart.findMany).mockRejectedValue(makePrismaError("P9999"));
+ const { getChartsWithCreator } = await import("./charts");
+
+ await expect(getChartsWithCreator(mockProjectId)).rejects.toMatchObject({
+ name: "DatabaseError",
+ });
+ });
+ });
});
diff --git a/apps/web/modules/ee/analysis/charts/lib/charts.ts b/apps/web/modules/ee/analysis/charts/lib/charts.ts
index 2f77318c3b..ef1d3f88c0 100644
--- a/apps/web/modules/ee/analysis/charts/lib/charts.ts
+++ b/apps/web/modules/ee/analysis/charts/lib/charts.ts
@@ -10,6 +10,7 @@ import {
TChart,
TChartCreateInput,
TChartUpdateInput,
+ TChartWithCreator,
TChartWithWidgets,
ZChartCreateInput,
ZChartType,
@@ -244,3 +245,25 @@ export const getCharts = async (projectId: string): Promise
throw error;
}
};
+
+export const getChartsWithCreator = async (projectId: string): Promise => {
+ validateInputs([projectId, ZId]);
+
+ try {
+ return await prisma.chart.findMany({
+ where: { projectId },
+ orderBy: { createdAt: "desc" },
+ select: {
+ ...selectChart,
+ creator: {
+ select: { name: true },
+ },
+ },
+ });
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ throw new DatabaseError(error.message);
+ }
+ throw error;
+ }
+};
diff --git a/apps/web/modules/ee/analysis/dashboards/pages/dashboards-list-page.tsx b/apps/web/modules/ee/analysis/dashboards/pages/dashboards-list-page.tsx
index 93157f3922..0dc71353ed 100644
--- a/apps/web/modules/ee/analysis/dashboards/pages/dashboards-list-page.tsx
+++ b/apps/web/modules/ee/analysis/dashboards/pages/dashboards-list-page.tsx
@@ -25,8 +25,11 @@ const DashboardsListContent = ({
return ;
};
-export const DashboardsListPage = async (props: { params: Promise<{ environmentId: string }> }) => {
- const { environmentId } = await props.params;
+interface DashboardsListPageProps {
+ environmentId: string;
+}
+
+export const DashboardsListPage = async ({ environmentId }: Readonly) => {
const t = await getTranslate();
const { project, isReadOnly } = await getEnvironmentAuth(environmentId);
diff --git a/apps/web/modules/ee/analysis/types/analysis.ts b/apps/web/modules/ee/analysis/types/analysis.ts
index c9af94688f..bb3759adcc 100644
--- a/apps/web/modules/ee/analysis/types/analysis.ts
+++ b/apps/web/modules/ee/analysis/types/analysis.ts
@@ -41,6 +41,10 @@ export type TChartWithWidgets = TChart & {
widgets: { dashboardId: string }[];
};
+export type TChartWithCreator = TChart & {
+ creator: { name: string } | null;
+};
+
// ── Dashboard input schemas ─────────────────────────────────────────────────
export const ZDashboardCreateInput = z.object({