mirror of
https://github.com/gosticks/plane.git
synced 2025-10-16 12:45:33 +00:00
[WEB-4246] Analytics minor improvements (#7194)
* chore: updated label for epics
* chore: improved export logic
* refactor: move csvConfig to export.ts and clean up export logic
* refactor: remove unused CSV export logic from WorkItemsInsightTable component
* refactor: streamline data handling in InsightTable component for improved rendering
* feat: add translation for "No. of {entity}" and update priority chart y-axis label to use new translation
* refactor: cleaned up some component and added utilitites
* feat: add "at_risk" translation to multiple languages in translations.json files
* refactor: update TrendPiece component to use new status variants for analytics
* fix: adjust TrendPiece component logic for on-track and off-track status
* refactor: use nullish coalescing operator for yAxis.dx in line and scatter charts
* feat: add "at_risk" translation to various languages in translations.json files
* feat: add "no_of" translation to various languages in translations.json files
* feat: update "at_risk" translation in Ukrainian, Vietnamese, and Chinese locales in translations.json files
This commit is contained in:
parent
ad11a34efc
commit
c1a078ef3f
@ -1,9 +1,16 @@
|
||||
// types
|
||||
import {
|
||||
TModuleLayoutOptions,
|
||||
TModuleOrderByOptions,
|
||||
TModuleStatus,
|
||||
} from "@plane/types";
|
||||
import { TModuleLayoutOptions, TModuleOrderByOptions, TModuleStatus } from "@plane/types";
|
||||
|
||||
export const MODULE_STATUS_COLORS: {
|
||||
[key in TModuleStatus]: string;
|
||||
} = {
|
||||
backlog: "#a3a3a2",
|
||||
planned: "#3f76ff",
|
||||
paused: "#525252",
|
||||
completed: "#16a34a",
|
||||
cancelled: "#ef4444",
|
||||
"in-progress": "#f39e1f",
|
||||
};
|
||||
|
||||
export const MODULE_STATUS: {
|
||||
i18n_label: string;
|
||||
@ -15,42 +22,42 @@ export const MODULE_STATUS: {
|
||||
{
|
||||
i18n_label: "project_modules.status.backlog",
|
||||
value: "backlog",
|
||||
color: "#a3a3a2",
|
||||
color: MODULE_STATUS_COLORS.backlog,
|
||||
textColor: "text-custom-text-400",
|
||||
bgColor: "bg-custom-background-80",
|
||||
},
|
||||
{
|
||||
i18n_label: "project_modules.status.planned",
|
||||
value: "planned",
|
||||
color: "#3f76ff",
|
||||
color: MODULE_STATUS_COLORS.planned,
|
||||
textColor: "text-blue-500",
|
||||
bgColor: "bg-indigo-50",
|
||||
},
|
||||
{
|
||||
i18n_label: "project_modules.status.in_progress",
|
||||
value: "in-progress",
|
||||
color: "#f39e1f",
|
||||
color: MODULE_STATUS_COLORS["in-progress"],
|
||||
textColor: "text-amber-500",
|
||||
bgColor: "bg-amber-50",
|
||||
},
|
||||
{
|
||||
i18n_label: "project_modules.status.paused",
|
||||
value: "paused",
|
||||
color: "#525252",
|
||||
color: MODULE_STATUS_COLORS.paused,
|
||||
textColor: "text-custom-text-300",
|
||||
bgColor: "bg-custom-background-90",
|
||||
},
|
||||
{
|
||||
i18n_label: "project_modules.status.completed",
|
||||
value: "completed",
|
||||
color: "#16a34a",
|
||||
color: MODULE_STATUS_COLORS.completed,
|
||||
textColor: "text-green-600",
|
||||
bgColor: "bg-green-100",
|
||||
},
|
||||
{
|
||||
i18n_label: "project_modules.status.cancelled",
|
||||
value: "cancelled",
|
||||
color: "#ef4444",
|
||||
color: MODULE_STATUS_COLORS.cancelled,
|
||||
textColor: "text-red-500",
|
||||
bgColor: "bg-red-50",
|
||||
},
|
||||
|
||||
@ -872,13 +872,15 @@
|
||||
"guests": "Hosté",
|
||||
"on_track": "Na správné cestě",
|
||||
"off_track": "Mimo plán",
|
||||
"at_risk": "V ohrožení",
|
||||
"timeline": "Časová osa",
|
||||
"completion": "Dokončení",
|
||||
"upcoming": "Nadcházející",
|
||||
"completed": "Dokončeno",
|
||||
"in_progress": "Probíhá",
|
||||
"planned": "Plánováno",
|
||||
"paused": "Pozastaveno"
|
||||
"paused": "Pozastaveno",
|
||||
"no_of": "Počet {entity}"
|
||||
},
|
||||
"chart": {
|
||||
"x_axis": "Osa X",
|
||||
@ -2467,4 +2469,4 @@
|
||||
"plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane se nespustil. To může být způsobeno tím, že se jeden nebo více služeb Plane nepodařilo spustit.",
|
||||
"choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Vyberte View Logs z setup.sh a Docker logů, abyste si byli jisti."
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -872,13 +872,15 @@
|
||||
"guests": "Gäste",
|
||||
"on_track": "Im Plan",
|
||||
"off_track": "Außer Plan",
|
||||
"at_risk": "Gefährdet",
|
||||
"timeline": "Zeitleiste",
|
||||
"completion": "Fertigstellung",
|
||||
"upcoming": "Bevorstehend",
|
||||
"completed": "Abgeschlossen",
|
||||
"in_progress": "In Bearbeitung",
|
||||
"planned": "Geplant",
|
||||
"paused": "Pausiert"
|
||||
"paused": "Pausiert",
|
||||
"no_of": "Anzahl {entity}"
|
||||
},
|
||||
"chart": {
|
||||
"x_axis": "X-Achse",
|
||||
|
||||
@ -617,6 +617,7 @@
|
||||
"click_to_add_description": "Click to add description",
|
||||
"on_track": "On-Track",
|
||||
"off_track": "Off-Track",
|
||||
"at_risk": "At risk",
|
||||
"timeline": "Timeline",
|
||||
"completion": "Completion",
|
||||
"upcoming": "Upcoming",
|
||||
@ -721,7 +722,8 @@
|
||||
"deactivated_user": "Deactivated user",
|
||||
"apply": "Apply",
|
||||
"applying": "Applying",
|
||||
"overview": "Overview"
|
||||
"overview": "Overview",
|
||||
"no_of": "No. of {entity}"
|
||||
},
|
||||
"chart": {
|
||||
"x_axis": "X-axis",
|
||||
@ -2343,4 +2345,4 @@
|
||||
"plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane didn't start up. This could be because one or more Plane services failed to start.",
|
||||
"choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Choose View Logs from setup.sh and Docker logs to be sure."
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -875,13 +875,15 @@
|
||||
"guests": "Invitados",
|
||||
"on_track": "En camino",
|
||||
"off_track": "Fuera de camino",
|
||||
"at_risk": "En riesgo",
|
||||
"timeline": "Cronograma",
|
||||
"completion": "Finalización",
|
||||
"upcoming": "Próximo",
|
||||
"completed": "Completado",
|
||||
"in_progress": "En progreso",
|
||||
"planned": "Planificado",
|
||||
"paused": "Pausado"
|
||||
"paused": "Pausado",
|
||||
"no_of": "N.º de {entity}"
|
||||
},
|
||||
"chart": {
|
||||
"x_axis": "Eje X",
|
||||
@ -2469,4 +2471,4 @@
|
||||
"plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane no se inició. Esto podría deberse a que uno o más servicios de Plane fallaron al iniciar.",
|
||||
"choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Selecciona View Logs desde setup.sh y los logs de Docker para estar seguro."
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -873,13 +873,15 @@
|
||||
"guests": "Invités",
|
||||
"on_track": "Sur la bonne voie",
|
||||
"off_track": "Hors de la bonne voie",
|
||||
"at_risk": "À risque",
|
||||
"timeline": "Chronologie",
|
||||
"completion": "Achèvement",
|
||||
"upcoming": "À venir",
|
||||
"completed": "Terminé",
|
||||
"in_progress": "En cours",
|
||||
"planned": "Planifié",
|
||||
"paused": "En pause"
|
||||
"paused": "En pause",
|
||||
"no_of": "Nº de {entity}"
|
||||
},
|
||||
"chart": {
|
||||
"x_axis": "Axe X",
|
||||
@ -2467,4 +2469,4 @@
|
||||
"plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane n'a pas démarré. Cela pourrait être dû au fait qu'un ou plusieurs services Plane ont échoué à démarrer.",
|
||||
"choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Choisissez View Logs depuis setup.sh et les logs Docker pour en être sûr."
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -872,13 +872,15 @@
|
||||
"guests": "Tamu",
|
||||
"on_track": "Sesuai Jalur",
|
||||
"off_track": "Menyimpang",
|
||||
"at_risk": "Dalam risiko",
|
||||
"timeline": "Linimasa",
|
||||
"completion": "Penyelesaian",
|
||||
"upcoming": "Mendatang",
|
||||
"completed": "Selesai",
|
||||
"in_progress": "Sedang berlangsung",
|
||||
"planned": "Direncanakan",
|
||||
"paused": "Dijedaikan"
|
||||
"paused": "Dijedaikan",
|
||||
"no_of": "Jumlah {entity}"
|
||||
},
|
||||
"chart": {
|
||||
"x_axis": "Sumbu-X",
|
||||
@ -2460,5 +2462,6 @@
|
||||
"self_hosted_maintenance_message": {
|
||||
"plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane tidak berhasil dimulai. Ini bisa karena satu atau lebih layanan Plane gagal untuk dimulai.",
|
||||
"choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Pilih View Logs dari setup.sh dan log Docker untuk memastikan."
|
||||
}
|
||||
}
|
||||
},
|
||||
"no_of": "Jumlah {entity}"
|
||||
}
|
||||
@ -871,13 +871,15 @@
|
||||
"guests": "Ospiti",
|
||||
"on_track": "In linea",
|
||||
"off_track": "Fuori rotta",
|
||||
"at_risk": "A rischio",
|
||||
"timeline": "Cronologia",
|
||||
"completion": "Completamento",
|
||||
"upcoming": "In arrivo",
|
||||
"completed": "Completato",
|
||||
"in_progress": "In corso",
|
||||
"planned": "Pianificato",
|
||||
"paused": "In pausa"
|
||||
"paused": "In pausa",
|
||||
"no_of": "N. di {entity}"
|
||||
},
|
||||
"chart": {
|
||||
"x_axis": "Asse X",
|
||||
@ -2466,4 +2468,4 @@
|
||||
"plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane non si è avviato. Questo potrebbe essere dovuto al fatto che uno o più servizi Plane non sono riusciti ad avviarsi.",
|
||||
"choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Scegli View Logs da setup.sh e dai log Docker per essere sicuro."
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -873,13 +873,15 @@
|
||||
"guests": "ゲスト",
|
||||
"on_track": "順調",
|
||||
"off_track": "遅れ",
|
||||
"at_risk": "リスクあり",
|
||||
"timeline": "タイムライン",
|
||||
"completion": "完了",
|
||||
"upcoming": "今後の予定",
|
||||
"completed": "完了",
|
||||
"in_progress": "進行中",
|
||||
"planned": "計画済み",
|
||||
"paused": "一時停止"
|
||||
"paused": "一時停止",
|
||||
"no_of": "{entity} の数"
|
||||
},
|
||||
"chart": {
|
||||
"x_axis": "エックス アクシス",
|
||||
@ -2467,4 +2469,4 @@
|
||||
"plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Planeが起動しませんでした。これは1つまたは複数のPlaneサービスの起動に失敗したことが原因である可能性があります。",
|
||||
"choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "setup.shとDockerログからView Logsを選択して確認してください。"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -874,13 +874,15 @@
|
||||
"guests": "게스트",
|
||||
"on_track": "계획대로 진행 중",
|
||||
"off_track": "계획 이탈",
|
||||
"at_risk": "위험",
|
||||
"timeline": "타임라인",
|
||||
"completion": "완료",
|
||||
"upcoming": "예정된",
|
||||
"completed": "완료됨",
|
||||
"in_progress": "진행 중",
|
||||
"planned": "계획된",
|
||||
"paused": "일시 중지됨"
|
||||
"paused": "일시 중지됨",
|
||||
"no_of": "{entity} 수"
|
||||
},
|
||||
"chart": {
|
||||
"x_axis": "X축",
|
||||
@ -2469,4 +2471,4 @@
|
||||
"plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane이 시작되지 않았습니다. 이는 하나 이상의 Plane 서비스가 시작에 실패했기 때문일 수 있습니다.",
|
||||
"choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "확실히 하려면 setup.sh와 Docker 로그에서 View Logs를 선택하세요."
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -874,13 +874,15 @@
|
||||
"guests": "Goście",
|
||||
"on_track": "Na dobrej drodze",
|
||||
"off_track": "Poza planem",
|
||||
"at_risk": "W zagrożeniu",
|
||||
"timeline": "Oś czasu",
|
||||
"completion": "Zakończenie",
|
||||
"upcoming": "Nadchodzące",
|
||||
"completed": "Zakończone",
|
||||
"in_progress": "W trakcie",
|
||||
"planned": "Zaplanowane",
|
||||
"paused": "Wstrzymane"
|
||||
"paused": "Wstrzymane",
|
||||
"no_of": "Liczba {entity}"
|
||||
},
|
||||
"chart": {
|
||||
"x_axis": "Oś X",
|
||||
@ -2468,4 +2470,4 @@
|
||||
"plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane nie uruchomił się. Może to być spowodowane tym, że jedna lub więcej usług Plane nie mogła się uruchomić.",
|
||||
"choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Wybierz View Logs z setup.sh i logów Docker, aby mieć pewność."
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -874,13 +874,15 @@
|
||||
"guests": "Convidados",
|
||||
"on_track": "No caminho certo",
|
||||
"off_track": "Fora do caminho",
|
||||
"at_risk": "Em risco",
|
||||
"timeline": "Linha do tempo",
|
||||
"completion": "Conclusão",
|
||||
"upcoming": "Próximo",
|
||||
"completed": "Concluído",
|
||||
"in_progress": "Em andamento",
|
||||
"planned": "Planejado",
|
||||
"paused": "Pausado"
|
||||
"paused": "Pausado",
|
||||
"no_of": "Nº de {entity}"
|
||||
},
|
||||
"chart": {
|
||||
"x_axis": "Eixo X",
|
||||
@ -2463,4 +2465,4 @@
|
||||
"plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "O Plane não inicializou. Isso pode ser porque um ou mais serviços do Plane falharam ao iniciar.",
|
||||
"choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Escolha View Logs do setup.sh e logs do Docker para ter certeza."
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -872,13 +872,15 @@
|
||||
"guests": "Invitați",
|
||||
"on_track": "Pe drumul cel bun",
|
||||
"off_track": "În afara traiectoriei",
|
||||
"at_risk": "În pericol",
|
||||
"timeline": "Cronologie",
|
||||
"completion": "Finalizare",
|
||||
"upcoming": "Viitor",
|
||||
"completed": "Finalizat",
|
||||
"in_progress": "În desfășurare",
|
||||
"planned": "Planificat",
|
||||
"paused": "Pauzat"
|
||||
"paused": "Pauzat",
|
||||
"no_of": "Nr. de {entity}"
|
||||
},
|
||||
"chart": {
|
||||
"x_axis": "axa-X",
|
||||
@ -2461,4 +2463,4 @@
|
||||
"plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane nu a pornit. Aceasta ar putea fi din cauza că unul sau mai multe servicii Plane au eșuat să pornească.",
|
||||
"choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Alegeți View Logs din setup.sh și logurile Docker pentru a fi siguri."
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -874,6 +874,7 @@
|
||||
"guests": "Гости",
|
||||
"on_track": "По плану",
|
||||
"off_track": "Отклонение от плана",
|
||||
"at_risk": "Под угрозой",
|
||||
"timeline": "Хронология",
|
||||
"completion": "Завершение",
|
||||
"upcoming": "Предстоящие",
|
||||
@ -2468,5 +2469,6 @@
|
||||
"self_hosted_maintenance_message": {
|
||||
"plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane не запустился. Это может быть из-за того, что один или несколько сервисов Plane не смогли запуститься.",
|
||||
"choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Выберите View Logs из setup.sh и логов Docker, чтобы убедиться."
|
||||
}
|
||||
}
|
||||
},
|
||||
"no_of": "Количество {entity}"
|
||||
}
|
||||
@ -874,13 +874,15 @@
|
||||
"guests": "Hostia",
|
||||
"on_track": "Na správnej ceste",
|
||||
"off_track": "Mimo plán",
|
||||
"at_risk": "V ohrození",
|
||||
"timeline": "Časová os",
|
||||
"completion": "Dokončenie",
|
||||
"upcoming": "Nadchádzajúce",
|
||||
"completed": "Dokončené",
|
||||
"in_progress": "Prebieha",
|
||||
"planned": "Plánované",
|
||||
"paused": "Pozastavené"
|
||||
"paused": "Pozastavené",
|
||||
"no_of": "Počet {entity}"
|
||||
},
|
||||
"chart": {
|
||||
"x_axis": "Os X",
|
||||
@ -2468,4 +2470,4 @@
|
||||
"plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane sa nespustil. Toto môže byť spôsobené tým, že sa jedna alebo viac služieb Plane nepodarilo spustiť.",
|
||||
"choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Vyberte View Logs z setup.sh a Docker logov, aby ste si boli istí."
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -875,13 +875,15 @@
|
||||
"guests": "Misafirler",
|
||||
"on_track": "Yolunda",
|
||||
"off_track": "Yolunda değil",
|
||||
"at_risk": "Risk altında",
|
||||
"timeline": "Zaman çizelgesi",
|
||||
"completion": "Tamamlama",
|
||||
"upcoming": "Yaklaşan",
|
||||
"completed": "Tamamlandı",
|
||||
"in_progress": "Devam ediyor",
|
||||
"planned": "Planlandı",
|
||||
"paused": "Durduruldu"
|
||||
"paused": "Durduruldu",
|
||||
"no_of": "{entity} sayısı"
|
||||
},
|
||||
"chart": {
|
||||
"x_axis": "X ekseni",
|
||||
@ -2447,4 +2449,4 @@
|
||||
"plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane başlatılamadı. Bu, bir veya daha fazla Plane servisinin başlatılamaması nedeniyle olabilir.",
|
||||
"choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Emin olmak için setup.sh ve Docker loglarından View Logs'u seçin."
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -874,13 +874,15 @@
|
||||
"guests": "Гості",
|
||||
"on_track": "У межах графіку",
|
||||
"off_track": "Поза графіком",
|
||||
"at_risk": "Під загрозою",
|
||||
"timeline": "Хронологія",
|
||||
"completion": "Завершення",
|
||||
"upcoming": "Майбутнє",
|
||||
"completed": "Завершено",
|
||||
"in_progress": "В процесі",
|
||||
"planned": "Заплановано",
|
||||
"paused": "Призупинено"
|
||||
"paused": "Призупинено",
|
||||
"no_of": "Кількість {entity}"
|
||||
},
|
||||
"chart": {
|
||||
"x_axis": "Вісь X",
|
||||
@ -2468,4 +2470,4 @@
|
||||
"plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane не запустився. Це може бути через те, що один або декілька сервісів Plane не змогли запуститися.",
|
||||
"choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Виберіть View Logs з setup.sh та логів Docker, щоб переконатися."
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -873,13 +873,15 @@
|
||||
"guests": "Khách",
|
||||
"on_track": "Đúng tiến độ",
|
||||
"off_track": "Chệch hướng",
|
||||
"at_risk": "Có nguy cơ",
|
||||
"timeline": "Dòng thời gian",
|
||||
"completion": "Hoàn thành",
|
||||
"upcoming": "Sắp tới",
|
||||
"completed": "Đã hoàn thành",
|
||||
"in_progress": "Đang tiến hành",
|
||||
"planned": "Đã lên kế hoạch",
|
||||
"paused": "Tạm dừng"
|
||||
"paused": "Tạm dừng",
|
||||
"no_of": "Số lượng {entity}"
|
||||
},
|
||||
"chart": {
|
||||
"x_axis": "Trục X",
|
||||
@ -2466,4 +2468,4 @@
|
||||
"plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane không khởi động được. Điều này có thể do một hoặc nhiều dịch vụ Plane không khởi động được.",
|
||||
"choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Chọn View Logs từ setup.sh và log Docker để chắc chắn."
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -873,13 +873,15 @@
|
||||
"guests": "访客",
|
||||
"on_track": "进展顺利",
|
||||
"off_track": "偏离轨道",
|
||||
"at_risk": "有风险",
|
||||
"timeline": "时间轴",
|
||||
"completion": "完成",
|
||||
"upcoming": "即将发生",
|
||||
"completed": "已完成",
|
||||
"in_progress": "进行中",
|
||||
"planned": "已计划",
|
||||
"paused": "暂停"
|
||||
"paused": "暂停",
|
||||
"no_of": "{entity} 的数量"
|
||||
},
|
||||
"chart": {
|
||||
"x_axis": "X轴",
|
||||
@ -2448,4 +2450,4 @@
|
||||
"plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane 未能启动。这可能是因为一个或多个 Plane 服务启动失败。",
|
||||
"choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "请选择“查看日志”来查看 setup.sh 和 Docker 日志,以确认问题。"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -880,7 +880,9 @@
|
||||
"completed": "已完成",
|
||||
"in_progress": "進行中",
|
||||
"planned": "已計劃",
|
||||
"paused": "暫停"
|
||||
"paused": "暫停",
|
||||
"at_risk": "有風險",
|
||||
"no_of": "{entity} 的數量"
|
||||
},
|
||||
"chart": {
|
||||
"x_axis": "X 軸",
|
||||
@ -2465,9 +2467,8 @@
|
||||
"previously_edited_by": "先前編輯者",
|
||||
"edited_by": "編輯者"
|
||||
},
|
||||
|
||||
"self_hosted_maintenance_message": {
|
||||
"plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane 未能啟動。這可能是因為一個或多個 Plane 服務啟動失敗。",
|
||||
"choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "從 setup.sh 和 Docker 日誌中選擇 View Logs 來確認。"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -122,7 +122,7 @@ export const LineChart = React.memo(<K extends string, T extends string>(props:
|
||||
angle: -90,
|
||||
position: "bottom",
|
||||
offset: -24,
|
||||
dx: -16,
|
||||
dx: yAxis.dx ?? -16,
|
||||
className: AXIS_LABEL_CLASSNAME,
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,7 +27,6 @@ export const ScatterChart = React.memo(<K extends string, T extends string>(prop
|
||||
margin,
|
||||
xAxis,
|
||||
yAxis,
|
||||
|
||||
className,
|
||||
tickCount = {
|
||||
x: undefined,
|
||||
@ -35,6 +34,7 @@ export const ScatterChart = React.memo(<K extends string, T extends string>(prop
|
||||
},
|
||||
legend,
|
||||
showTooltip = true,
|
||||
customTooltipContent,
|
||||
} = props;
|
||||
// states
|
||||
const [activePoint, setActivePoint] = useState<string | null>(null);
|
||||
@ -107,7 +107,7 @@ export const ScatterChart = React.memo(<K extends string, T extends string>(prop
|
||||
angle: -90,
|
||||
position: "bottom",
|
||||
offset: -24,
|
||||
dx: -16,
|
||||
dx: yAxis.dx ?? -16,
|
||||
className: AXIS_LABEL_CLASSNAME,
|
||||
}
|
||||
}
|
||||
@ -133,17 +133,21 @@ export const ScatterChart = React.memo(<K extends string, T extends string>(prop
|
||||
wrapperStyle={{
|
||||
pointerEvents: "auto",
|
||||
}}
|
||||
content={({ active, label, payload }) => (
|
||||
<CustomTooltip
|
||||
active={active}
|
||||
activeKey={activePoint}
|
||||
label={label}
|
||||
payload={payload}
|
||||
itemKeys={itemKeys}
|
||||
itemLabels={itemLabels}
|
||||
itemDotColors={itemDotColors}
|
||||
/>
|
||||
)}
|
||||
content={({ active, label, payload }) =>
|
||||
customTooltipContent ? (
|
||||
customTooltipContent({ active, label, payload })
|
||||
) : (
|
||||
<CustomTooltip
|
||||
active={active}
|
||||
activeKey={activePoint}
|
||||
label={label}
|
||||
payload={payload}
|
||||
itemKeys={itemKeys}
|
||||
itemLabels={itemLabels}
|
||||
itemDotColors={itemDotColors}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{renderPoints}
|
||||
@ -152,4 +156,4 @@ export const ScatterChart = React.memo(<K extends string, T extends string>(prop
|
||||
</div>
|
||||
);
|
||||
});
|
||||
ScatterChart.displayName = "ScatterChart";
|
||||
ScatterChart.displayName = "ScatterChart";
|
||||
15
packages/types/src/analytics.d.ts
vendored
15
packages/types/src/analytics.d.ts
vendored
@ -1,5 +1,6 @@
|
||||
import { ChartXAxisProperty, ChartYAxisMetric } from "@plane/constants";
|
||||
import { TChartData } from "./charts";
|
||||
import { Row } from "@tanstack/react-table";
|
||||
|
||||
export type TAnalyticsTabsBase = "overview" | "work-items";
|
||||
export type TAnalyticsGraphsBase = "projects" | "work-items" | "custom-work-items";
|
||||
@ -20,12 +21,6 @@ export interface IAnalyticsResponseFields {
|
||||
filter_count: number;
|
||||
}
|
||||
|
||||
export interface IAnalyticsRadarEntity {
|
||||
key: string;
|
||||
name: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
// chart types
|
||||
|
||||
export interface IChartResponse {
|
||||
@ -43,7 +38,7 @@ export interface WorkItemInsightColumns {
|
||||
backlog_work_items: number;
|
||||
un_started_work_items: number;
|
||||
started_work_items: number;
|
||||
// because of the peek view, we will display the name of the project instead of project__name
|
||||
// incase of peek view, we will display the display_name instead of project__name
|
||||
display_name?: string;
|
||||
avatar_url?: string;
|
||||
assignee_id?: string;
|
||||
@ -58,3 +53,9 @@ export interface IAnalyticsParams {
|
||||
y_axis: ChartYAxisMetric;
|
||||
group_by?: ChartXAxisProperty;
|
||||
}
|
||||
|
||||
export type ExportConfig<T> = {
|
||||
key: string;
|
||||
value: (row: Row<T>) => string | number;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
29
packages/types/src/charts/index.d.ts
vendored
29
packages/types/src/charts/index.d.ts
vendored
@ -1,7 +1,5 @@
|
||||
|
||||
|
||||
// ============================================================
|
||||
// Chart Base
|
||||
// Chart Base
|
||||
// ============================================================
|
||||
export * from "./common";
|
||||
export type TChartLegend = {
|
||||
@ -48,10 +46,11 @@ type TChartProps<K extends string, T extends string> = {
|
||||
y?: number;
|
||||
};
|
||||
showTooltip?: boolean;
|
||||
customTooltipContent?: (props: { active?: boolean; label: string; payload: any }) => React.ReactNode;
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Bar Chart
|
||||
// Bar Chart
|
||||
// ============================================================
|
||||
|
||||
export type TBarItem<T extends string> = {
|
||||
@ -71,7 +70,7 @@ export type TBarChartProps<K extends string, T extends string> = TChartProps<K,
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Line Chart
|
||||
// Line Chart
|
||||
// ============================================================
|
||||
|
||||
export type TLineItem<T extends string> = {
|
||||
@ -90,7 +89,7 @@ export type TLineChartProps<K extends string, T extends string> = TChartProps<K,
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Scatter Chart
|
||||
// Scatter Chart
|
||||
// ============================================================
|
||||
|
||||
export type TScatterPointItem<T extends string> = {
|
||||
@ -105,7 +104,7 @@ export type TScatterChartProps<K extends string, T extends string> = TChartProps
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Area Chart
|
||||
// Area Chart
|
||||
// ============================================================
|
||||
|
||||
export type TAreaItem<T extends string> = {
|
||||
@ -130,7 +129,7 @@ export type TAreaChartProps<K extends string, T extends string> = TChartProps<K,
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Pie Chart
|
||||
// Pie Chart
|
||||
// ============================================================
|
||||
|
||||
export type TCellItem<T extends string> = {
|
||||
@ -161,7 +160,7 @@ export type TPieChartProps<K extends string, T extends string> = Pick<
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Tree Map
|
||||
// Tree Map
|
||||
// ============================================================
|
||||
|
||||
export type TreeMapItem = {
|
||||
@ -171,13 +170,13 @@ export type TreeMapItem = {
|
||||
textClassName?: string;
|
||||
icon?: React.ReactElement;
|
||||
} & (
|
||||
| {
|
||||
| {
|
||||
fillColor: string;
|
||||
}
|
||||
| {
|
||||
| {
|
||||
fillClassName: string;
|
||||
}
|
||||
);
|
||||
);
|
||||
|
||||
export type TreeMapChartProps = {
|
||||
data: TreeMapItem[];
|
||||
@ -217,8 +216,8 @@ export type TRadarItem<T extends string> = {
|
||||
dot?: {
|
||||
r: number;
|
||||
fillOpacity: number;
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export type TRadarChartProps<K extends string, T extends string> = Pick<
|
||||
TChartProps<K, T>,
|
||||
@ -231,4 +230,4 @@ export type TRadarChartProps<K extends string, T extends string> = Pick<
|
||||
label?: string;
|
||||
strokeColor?: string;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@ -16,3 +16,12 @@ export const CYCLE_GROUP_COLORS: {
|
||||
completed: "#16A34A",
|
||||
draft: "#525252",
|
||||
};
|
||||
|
||||
export const CYCLE_GROUP_I18N_LABELS: {
|
||||
[key in TCycleGroups]: string;
|
||||
} = {
|
||||
current: "current",
|
||||
upcoming: "common.upcoming",
|
||||
completed: "common.completed",
|
||||
draft: "project_cycles.status.draft",
|
||||
};
|
||||
|
||||
@ -3,3 +3,4 @@ export * from "./circle-dot-full-icon";
|
||||
export * from "./contrast-icon";
|
||||
export * from "./circle-dot-full-icon";
|
||||
export * from "./cycle-group-icon";
|
||||
export * from "./helper";
|
||||
|
||||
@ -5,6 +5,7 @@ export const ANALYTICS_TABS: {
|
||||
key: TAnalyticsTabsBase;
|
||||
i18nKey: string;
|
||||
content: React.FC;
|
||||
isExtended?: boolean;
|
||||
}[] = [
|
||||
{ key: "overview", i18nKey: "common.overview", content: Overview },
|
||||
{ key: "work-items", i18nKey: "sidebar.work_items", content: WorkItems },
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
import { mkConfig } from "export-to-csv";
|
||||
|
||||
export const csvConfig = (workspaceSlug: string) =>
|
||||
mkConfig({
|
||||
fieldSeparator: ",",
|
||||
filename: `${workspaceSlug}-analytics`,
|
||||
decimalSeparator: ".",
|
||||
useKeysAsHeaders: true,
|
||||
});
|
||||
@ -39,7 +39,7 @@ const AnalyticsEmptyState = ({ title, description, assetPath, className }: Props
|
||||
)}
|
||||
<div className="flex flex-shrink flex-col items-center gap-1.5 text-center">
|
||||
<h3 className={cn("text-xl font-semibold")}>{title}</h3>
|
||||
{description && <p className="text-sm text-custom-text-300">{description}</p>}
|
||||
{description && <p className="text-sm text-custom-text-300 max-w-[350px]">{description}</p>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
26
web/core/components/analytics/export.ts
Normal file
26
web/core/components/analytics/export.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { ColumnDef, Row } from "@tanstack/react-table";
|
||||
import { download, generateCsv, mkConfig } from "export-to-csv";
|
||||
|
||||
export const csvConfig = (workspaceSlug: string) =>
|
||||
mkConfig({
|
||||
fieldSeparator: ",",
|
||||
filename: `${workspaceSlug}-analytics`,
|
||||
decimalSeparator: ".",
|
||||
useKeysAsHeaders: true,
|
||||
});
|
||||
|
||||
export const exportCSV = <T>(rows: Row<T>[], columns: ColumnDef<T>[], workspaceSlug: string) => {
|
||||
const rowData = rows.map((row) => {
|
||||
const exportColumns = columns.map((col) => col.meta?.export);
|
||||
const cells = exportColumns.reduce((acc: Record<string, string | number>, col) => {
|
||||
if (col) {
|
||||
const cell = col?.value(row) ?? "-";
|
||||
acc[col.label ?? col.key] = cell;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
return cells;
|
||||
});
|
||||
const csv = generateCsv(csvConfig(workspaceSlug))(rowData);
|
||||
download(csvConfig(workspaceSlug))(csv);
|
||||
};
|
||||
@ -94,7 +94,7 @@ export function DataTable<TData, TValue>({ columns, data, searchPlaceholder, act
|
||||
ref={inputRef}
|
||||
className="w-full max-w-[234px] border-none bg-transparent text-sm text-custom-text-100 placeholder:text-custom-text-400 focus:outline-none"
|
||||
placeholder="Search"
|
||||
value={table.getColumn(table.getHeaderGroups()?.[0].headers[0].id)?.getFilterValue() as string}
|
||||
value={table.getColumn(table.getHeaderGroups()?.[0]?.headers?.[0]?.id)?.getFilterValue() as string}
|
||||
onChange={(e) => {
|
||||
const columnId = table.getHeaderGroups()?.[0]?.headers?.[0]?.id;
|
||||
if (columnId) table.getColumn(columnId)?.setFilterValue(e.target.value);
|
||||
|
||||
@ -26,24 +26,20 @@ export const InsightTable = <T extends Exclude<TAnalyticsTabsBase, "overview">>(
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
{data ? (
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={data}
|
||||
searchPlaceholder={`${data.length} ${headerText}`}
|
||||
actions={(table: Table<AnalyticsTableDataMap[T]>) => (
|
||||
<Button
|
||||
variant="accent-primary"
|
||||
prependIcon={<Download className="h-3.5 w-3.5" />}
|
||||
onClick={() => onExport?.(table.getFilteredRowModel().rows)}
|
||||
>
|
||||
<div>{t("exporter.csv.short_description")}</div>
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<div>{t("common.no_data_yet")}</div>
|
||||
)}
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={data || []}
|
||||
searchPlaceholder={`${data?.length || 0} ${headerText}`}
|
||||
actions={(table: Table<AnalyticsTableDataMap[T]>) => (
|
||||
<Button
|
||||
variant="accent-primary"
|
||||
prependIcon={<Download className="h-3.5 w-3.5" />}
|
||||
onClick={() => onExport?.(table.getFilteredRowModel().rows)}
|
||||
>
|
||||
<div>{t("exporter.csv.short_description")}</div>
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -32,7 +32,7 @@ const ProjectInsights = observer(() => {
|
||||
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics/empty-chart-radar" });
|
||||
|
||||
const { data: projectInsightsData, isLoading: isLoadingProjectInsight } = useSWR(
|
||||
`radar-chart-${workspaceSlug}-${selectedDuration}-${selectedProjects}-${selectedCycle}-${selectedModule}-${isPeekView}`,
|
||||
`radar-chart-project-insights-${workspaceSlug}-${selectedDuration}-${selectedProjects}-${selectedCycle}-${selectedModule}-${isPeekView}`,
|
||||
() =>
|
||||
analyticsService.getAdvanceAnalyticsCharts<TChartData<string, string>[]>(
|
||||
workspaceSlug,
|
||||
|
||||
@ -8,6 +8,8 @@ type Props = {
|
||||
percentage: number;
|
||||
className?: string;
|
||||
size?: "xs" | "sm" | "md" | "lg";
|
||||
trendIconVisible?: boolean;
|
||||
variant?: "simple" | "outlined" | "tinted";
|
||||
};
|
||||
|
||||
const sizeConfig = {
|
||||
@ -29,16 +31,47 @@ const sizeConfig = {
|
||||
},
|
||||
} as const;
|
||||
|
||||
const variants: Record<NonNullable<Props["variant"]>, Record<"ontrack" | "offtrack" | "atrisk", string>> = {
|
||||
simple: {
|
||||
ontrack: "text-green-500",
|
||||
offtrack: "text-yellow-500",
|
||||
atrisk: "text-red-500",
|
||||
},
|
||||
outlined: {
|
||||
ontrack: "text-green-500 border border-green-500",
|
||||
offtrack: "text-yellow-500 border border-yellow-500",
|
||||
atrisk: "text-red-500 border border-red-500",
|
||||
},
|
||||
tinted: {
|
||||
ontrack: "text-green-500 bg-green-500/10",
|
||||
offtrack: "text-yellow-500 bg-yellow-500/10",
|
||||
atrisk: "text-red-500 bg-red-500/10",
|
||||
},
|
||||
} as const;
|
||||
|
||||
const TrendPiece = (props: Props) => {
|
||||
const { percentage, className, size = "sm" } = props;
|
||||
const isPositive = percentage > 0;
|
||||
const { percentage, className, trendIconVisible = true, size = "sm", variant = "simple" } = props;
|
||||
const isOnTrack = percentage >= 66;
|
||||
const isOffTrack = percentage >= 33 && percentage < 66;
|
||||
const config = sizeConfig[size];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("flex items-center gap-1", isPositive ? "text-green-500" : "text-red-500", config.text, className)}
|
||||
className={cn(
|
||||
"flex items-center gap-1 p-1 rounded-md",
|
||||
variants[variant][isOnTrack ? "ontrack" : isOffTrack ? "offtrack" : "atrisk"],
|
||||
config.text,
|
||||
className
|
||||
)}
|
||||
>
|
||||
{isPositive ? <TrendingUp className={config.icon} /> : <TrendingDown className={config.icon} />}
|
||||
{trendIconVisible &&
|
||||
(isOnTrack ? (
|
||||
<TrendingUp className={config.icon} />
|
||||
) : isOffTrack ? (
|
||||
<TrendingDown className={config.icon} />
|
||||
) : (
|
||||
<TrendingDown className={config.icon} />
|
||||
))}
|
||||
{Math.round(Math.abs(percentage))}%
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -104,7 +104,7 @@ const CreatedVsResolved = observer(() => {
|
||||
}}
|
||||
yAxis={{
|
||||
key: "count",
|
||||
label: t("no_of", { entity: t("work_items") }),
|
||||
label: t("no_of", { entity: isEpic ? t("epics") : t("work_items") }),
|
||||
offset: -30,
|
||||
dx: -22,
|
||||
}}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useMemo } from "react";
|
||||
import { ColumnDef, Row, Table } from "@tanstack/react-table";
|
||||
import { mkConfig, generateCsv, download } from "export-to-csv";
|
||||
import { ColumnDef, RowData, Table } from "@tanstack/react-table";
|
||||
import { mkConfig } from "export-to-csv";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useTheme } from "next-themes";
|
||||
@ -18,8 +18,8 @@ import {
|
||||
} from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { BarChart } from "@plane/propel/charts/bar-chart";
|
||||
import { IChartResponse } from "@plane/types";
|
||||
import { TBarItem, TChart, TChartData, TChartDatum } from "@plane/types/src/charts";
|
||||
import { ExportConfig } from "@plane/types";
|
||||
import { TBarItem, TChart, TChartDatum } from "@plane/types/src/charts";
|
||||
// plane web components
|
||||
import { Button } from "@plane/ui";
|
||||
import { generateExtendedColors, parseChartData } from "@/components/chart/utils";
|
||||
@ -29,10 +29,17 @@ import { useAnalytics } from "@/hooks/store/use-analytics";
|
||||
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
||||
import { AnalyticsService } from "@/services/analytics.service";
|
||||
import AnalyticsEmptyState from "../empty-state";
|
||||
import { exportCSV } from "../export";
|
||||
import { DataTable } from "../insight-table/data-table";
|
||||
import { ChartLoader } from "../loaders";
|
||||
import { generateBarColor } from "./utils";
|
||||
|
||||
declare module "@tanstack/react-table" {
|
||||
interface ColumnMeta<TData extends RowData, TValue> {
|
||||
export: ExportConfig<TData>;
|
||||
}
|
||||
}
|
||||
|
||||
interface Props {
|
||||
x_axis: ChartXAxisProperty;
|
||||
y_axis: ChartYAxisMetric;
|
||||
@ -146,11 +153,25 @@ const PriorityChart = observer((props: Props) => {
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: () => xAxisLabel,
|
||||
meta: {
|
||||
export: {
|
||||
key: xAxisLabel,
|
||||
value: (row) => row.original.name,
|
||||
label: xAxisLabel,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "count",
|
||||
header: () => <div className="text-right">Count</div>,
|
||||
cell: ({ row }) => <div className="text-right">{row.original.count}</div>,
|
||||
meta: {
|
||||
export: {
|
||||
key: "Count",
|
||||
value: (row) => row.original.count,
|
||||
label: "Count",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
[xAxisLabel]
|
||||
@ -163,40 +184,18 @@ const PriorityChart = observer((props: Props) => {
|
||||
accessorKey: key,
|
||||
header: () => <div className="text-right">{parsedData.schema[key]}</div>,
|
||||
cell: ({ row }) => <div className="text-right">{row.original[key]}</div>,
|
||||
meta: {
|
||||
export: {
|
||||
key,
|
||||
value: (row) => row.original[key],
|
||||
label: parsedData.schema[key],
|
||||
},
|
||||
},
|
||||
}))
|
||||
: [],
|
||||
[parsedData]
|
||||
);
|
||||
|
||||
const csvConfig = mkConfig({
|
||||
fieldSeparator: ",",
|
||||
filename: `${workspaceSlug}-analytics`,
|
||||
decimalSeparator: ".",
|
||||
useKeysAsHeaders: true,
|
||||
});
|
||||
|
||||
const exportCSV = (rows: Row<TChartDatum>[]) => {
|
||||
const rowData = rows.map((row) => {
|
||||
const hiddenFields = ["key", "avatar_url", "assignee_id", "project_id"];
|
||||
const otherFields = Object.keys(row.original).filter(
|
||||
(key) => key !== "name" && key !== "count" && !hiddenFields.includes(key) && !key.includes("id")
|
||||
);
|
||||
return {
|
||||
name: row.original.name,
|
||||
count: row.original.count,
|
||||
...otherFields.reduce(
|
||||
(acc, key) => {
|
||||
acc[parsedData?.schema[key] ?? key] = row.original[key];
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string | number>
|
||||
),
|
||||
};
|
||||
});
|
||||
const csv = generateCsv(csvConfig)(rowData);
|
||||
download(csvConfig)(csv);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-12 ">
|
||||
{priorityChartLoading ? (
|
||||
@ -217,7 +216,7 @@ const PriorityChart = observer((props: Props) => {
|
||||
}}
|
||||
yAxis={{
|
||||
key: "count",
|
||||
label: yAxisLabel,
|
||||
label: t("no_of", { entity: yAxisLabel.replace("_", " ") }),
|
||||
offset: -40,
|
||||
dx: -26,
|
||||
}}
|
||||
@ -230,7 +229,7 @@ const PriorityChart = observer((props: Props) => {
|
||||
<Button
|
||||
variant="accent-primary"
|
||||
prependIcon={<Download className="h-3.5 w-3.5" />}
|
||||
onClick={() => exportCSV(table.getFilteredRowModel().rows)}
|
||||
onClick={() => exportCSV(table.getRowModel().rows, [...defaultColumns, ...columns], workspaceSlug)}
|
||||
>
|
||||
<div>{t("exporter.csv.short_description")}</div>
|
||||
</Button>
|
||||
|
||||
@ -1,13 +1,12 @@
|
||||
import { useMemo, useCallback } from "react";
|
||||
import { ColumnDef, Row } from "@tanstack/react-table";
|
||||
import { download, generateCsv } from "export-to-csv";
|
||||
import { useMemo } from "react";
|
||||
import { ColumnDef, Row, RowData } from "@tanstack/react-table";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
import { Briefcase, UserRound } from "lucide-react";
|
||||
// plane package imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { WorkItemInsightColumns, AnalyticsTableDataMap } from "@plane/types";
|
||||
import { WorkItemInsightColumns, AnalyticsTableDataMap, ExportConfig } from "@plane/types";
|
||||
// plane web components
|
||||
import { Avatar } from "@plane/ui";
|
||||
import { getFileURL } from "@plane/utils";
|
||||
@ -17,11 +16,17 @@ import { useAnalytics } from "@/hooks/store/use-analytics";
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
import { AnalyticsService } from "@/services/analytics.service";
|
||||
// plane web components
|
||||
import { csvConfig } from "../config";
|
||||
import { exportCSV } from "../export";
|
||||
import { InsightTable } from "../insight-table";
|
||||
|
||||
const analyticsService = new AnalyticsService();
|
||||
|
||||
declare module "@tanstack/react-table" {
|
||||
interface ColumnMeta<TData extends RowData, TValue> {
|
||||
export: ExportConfig<TData>;
|
||||
}
|
||||
}
|
||||
|
||||
const WorkItemsInsightTable = observer(() => {
|
||||
// router
|
||||
const params = useParams();
|
||||
@ -60,104 +65,125 @@ const WorkItemsInsightTable = observer(() => {
|
||||
}),
|
||||
[t]
|
||||
);
|
||||
const columns = useMemo(
|
||||
() =>
|
||||
[
|
||||
!isPeekView
|
||||
? {
|
||||
accessorKey: "project__name",
|
||||
header: () => <div className="text-left">{columnsLabels["project__name"]}</div>,
|
||||
cell: ({ row }) => {
|
||||
const project = getProjectById(row.original.project_id);
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{project?.logo_props ? (
|
||||
<Logo logo={project.logo_props} size={18} />
|
||||
) : (
|
||||
<Briefcase className="h-4 w-4" />
|
||||
)}
|
||||
{project?.name}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}
|
||||
: {
|
||||
accessorKey: "display_name",
|
||||
header: () => <div className="text-left">{columnsLabels["display_name"]}</div>,
|
||||
cell: ({ row }: { row: Row<WorkItemInsightColumns> }) => (
|
||||
<div className="text-left">
|
||||
<div className="flex items-center gap-2">
|
||||
{row.original.avatar_url && row.original.avatar_url !== "" ? (
|
||||
<Avatar
|
||||
name={row.original.display_name}
|
||||
src={getFileURL(row.original.avatar_url)}
|
||||
size={24}
|
||||
shape="circle"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full bg-custom-background-80 capitalize overflow-hidden">
|
||||
{row.original.display_name ? (
|
||||
row.original.display_name?.[0]
|
||||
) : (
|
||||
<UserRound className="text-custom-text-200 " size={12} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<span className="break-words text-custom-text-200">
|
||||
{row.original.display_name ?? t(`Unassigned`)}
|
||||
</span>
|
||||
</div>
|
||||
const columns: ColumnDef<AnalyticsTableDataMap["work-items"]>[] = useMemo(
|
||||
() => [
|
||||
!isPeekView
|
||||
? {
|
||||
accessorKey: "project__name",
|
||||
header: () => <div className="text-left">{columnsLabels["project__name"]}</div>,
|
||||
cell: ({ row }) => {
|
||||
const project = getProjectById(row.original.project_id);
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{project?.logo_props ? (
|
||||
<Logo logo={project.logo_props} size={18} />
|
||||
) : (
|
||||
<Briefcase className="h-4 w-4" />
|
||||
)}
|
||||
{project?.name}
|
||||
</div>
|
||||
),
|
||||
);
|
||||
},
|
||||
{
|
||||
accessorKey: "backlog_work_items",
|
||||
header: () => <div className="text-right">{columnsLabels["backlog_work_items"]}</div>,
|
||||
cell: ({ row }) => <div className="text-right">{row.original.backlog_work_items}</div>,
|
||||
meta: {
|
||||
export: {
|
||||
key: columnsLabels["project__name"],
|
||||
value: (row) => row.original.project__name?.toString() ?? "",
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
accessorKey: "display_name",
|
||||
header: () => <div className="text-left">{columnsLabels["display_name"]}</div>,
|
||||
cell: ({ row }: { row: Row<WorkItemInsightColumns> }) => (
|
||||
<div className="text-left">
|
||||
<div className="flex items-center gap-2">
|
||||
{row.original.avatar_url && row.original.avatar_url !== "" ? (
|
||||
<Avatar
|
||||
name={row.original.display_name}
|
||||
src={getFileURL(row.original.avatar_url)}
|
||||
size={24}
|
||||
shape="circle"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full bg-custom-background-80 capitalize overflow-hidden">
|
||||
{row.original.display_name ? (
|
||||
row.original.display_name?.[0]
|
||||
) : (
|
||||
<UserRound className="text-custom-text-200 " size={12} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<span className="break-words text-custom-text-200">
|
||||
{row.original.display_name ?? t(`Unassigned`)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
meta: {
|
||||
export: {
|
||||
key: columnsLabels["display_name"],
|
||||
value: (row) => row.original.display_name?.toString() ?? "",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "backlog_work_items",
|
||||
header: () => <div className="text-right">{columnsLabels["backlog_work_items"]}</div>,
|
||||
cell: ({ row }) => <div className="text-right">{row.original.backlog_work_items}</div>,
|
||||
meta: {
|
||||
export: {
|
||||
key: columnsLabels["backlog_work_items"],
|
||||
value: (row) => row.original.backlog_work_items.toString(),
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "started_work_items",
|
||||
header: () => <div className="text-right">{columnsLabels["started_work_items"]}</div>,
|
||||
cell: ({ row }) => <div className="text-right">{row.original.started_work_items}</div>,
|
||||
},
|
||||
{
|
||||
accessorKey: "started_work_items",
|
||||
header: () => <div className="text-right">{columnsLabels["started_work_items"]}</div>,
|
||||
cell: ({ row }) => <div className="text-right">{row.original.started_work_items}</div>,
|
||||
meta: {
|
||||
export: {
|
||||
key: columnsLabels["started_work_items"],
|
||||
value: (row) => row.original.started_work_items.toString(),
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "un_started_work_items",
|
||||
header: () => <div className="text-right">{columnsLabels["un_started_work_items"]}</div>,
|
||||
cell: ({ row }) => <div className="text-right">{row.original.un_started_work_items}</div>,
|
||||
},
|
||||
{
|
||||
accessorKey: "un_started_work_items",
|
||||
header: () => <div className="text-right">{columnsLabels["un_started_work_items"]}</div>,
|
||||
cell: ({ row }) => <div className="text-right">{row.original.un_started_work_items}</div>,
|
||||
meta: {
|
||||
export: {
|
||||
key: columnsLabels["un_started_work_items"],
|
||||
value: (row) => row.original.un_started_work_items.toString(),
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "completed_work_items",
|
||||
header: () => <div className="text-right">{columnsLabels["completed_work_items"]}</div>,
|
||||
cell: ({ row }) => <div className="text-right">{row.original.completed_work_items}</div>,
|
||||
},
|
||||
{
|
||||
accessorKey: "completed_work_items",
|
||||
header: () => <div className="text-right">{columnsLabels["completed_work_items"]}</div>,
|
||||
cell: ({ row }) => <div className="text-right">{row.original.completed_work_items}</div>,
|
||||
meta: {
|
||||
export: {
|
||||
key: columnsLabels["completed_work_items"],
|
||||
value: (row) => row.original.completed_work_items.toString(),
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "cancelled_work_items",
|
||||
header: () => <div className="text-right">{columnsLabels["cancelled_work_items"]}</div>,
|
||||
cell: ({ row }) => <div className="text-right">{row.original.cancelled_work_items}</div>,
|
||||
},
|
||||
{
|
||||
accessorKey: "cancelled_work_items",
|
||||
header: () => <div className="text-right">{columnsLabels["cancelled_work_items"]}</div>,
|
||||
cell: ({ row }) => <div className="text-right">{row.original.cancelled_work_items}</div>,
|
||||
meta: {
|
||||
export: {
|
||||
key: columnsLabels["cancelled_work_items"],
|
||||
value: (row) => row.original.cancelled_work_items.toString(),
|
||||
},
|
||||
},
|
||||
] as ColumnDef<AnalyticsTableDataMap["work-items"]>[],
|
||||
},
|
||||
],
|
||||
[columnsLabels, getProjectById, isPeekView, t]
|
||||
);
|
||||
|
||||
const exportCSV = useCallback(
|
||||
(rows: Row<AnalyticsTableDataMap["work-items"]>[]) => {
|
||||
const rowData: any = rows.map((row) => {
|
||||
const { project_id, avatar_url, assignee_id, ...exportableData } = row.original;
|
||||
return Object.fromEntries(
|
||||
Object.entries(exportableData).map(([key, value]) => {
|
||||
if (columnsLabels?.[key as keyof typeof columnsLabels]) {
|
||||
return [columnsLabels[key as keyof typeof columnsLabels], value];
|
||||
}
|
||||
return [key, value];
|
||||
})
|
||||
);
|
||||
});
|
||||
const csv = generateCsv(csvConfig(workspaceSlug))(rowData);
|
||||
download(csvConfig(workspaceSlug))(csv);
|
||||
},
|
||||
[columnsLabels, workspaceSlug]
|
||||
);
|
||||
|
||||
return (
|
||||
<InsightTable<"work-items">
|
||||
analyticsType="work-items"
|
||||
@ -166,7 +192,7 @@ const WorkItemsInsightTable = observer(() => {
|
||||
columns={columns}
|
||||
columnsLabels={columnsLabels}
|
||||
headerText={isPeekView ? t("common.assignee") : t("common.projects")}
|
||||
onExport={exportCSV}
|
||||
onExport={(rows) => workItemsData && exportCSV(rows, columns, workspaceSlug)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user