feat: refine mobile dashboard experience
This commit is contained in:
1 parent
9f0969ec22
commit
315520beca
116 files changed
+2499
-586
No files matched your search
@@ -13,9 +13,10 @@ interface ApplicationsSectionProps {
|
||||
applications: CoolifyApplicationWithContext[];
|
||||
baseUrl: string;
|
||||
isTiny: boolean;
|
||||
isMobileDetail: boolean;
|
||||
}
|
||||
|
||||
export function ApplicationsSection({ applications, baseUrl, isTiny }: ApplicationsSectionProps) {
|
||||
export function ApplicationsSection({ applications, baseUrl, isTiny, isMobileDetail }: ApplicationsSectionProps) {
|
||||
const t = useScopedI18n("widget.coolify");
|
||||
const tCommon = useScopedI18n("common");
|
||||
const runningApps = applications.filter((app) => parseStatus(app.status ?? "") === "running").length;
|
||||
@@ -34,7 +35,14 @@ export function ApplicationsSection({ applications, baseUrl, isTiny }: Applicati
|
||||
{applications.length > 0 ? (
|
||||
<Stack gap={4}>
|
||||
{applications.map((app) => (
|
||||
<ResourceRow key={app.uuid} item={app} baseUrl={baseUrl} isTiny={isTiny} resourceType="application" />
|
||||
<ResourceRow
|
||||
key={app.uuid}
|
||||
item={app}
|
||||
baseUrl={baseUrl}
|
||||
isTiny={isTiny}
|
||||
resourceType="application"
|
||||
isMobileDetail={isMobileDetail}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
) : (
|
||||
|
||||
@@ -10,7 +10,12 @@ import { createWidgetKey } from "./coolify-utils";
|
||||
import { InstanceCard } from "./instance-card";
|
||||
import { SingleInstanceLayout } from "./single-instance-layout";
|
||||
|
||||
export default function CoolifyWidget({ options, integrationIds, width }: WidgetComponentProps<"coolify">) {
|
||||
export default function CoolifyWidget({
|
||||
options,
|
||||
integrationIds,
|
||||
width,
|
||||
displayMode,
|
||||
}: WidgetComponentProps<"coolify">) {
|
||||
const t = useScopedI18n("widget.coolify");
|
||||
|
||||
if (integrationIds.length === 0) {
|
||||
@@ -21,16 +26,24 @@ export default function CoolifyWidget({ options, integrationIds, width }: Widget
|
||||
);
|
||||
}
|
||||
|
||||
return <CoolifyContent integrationIds={integrationIds} options={options} width={width} />;
|
||||
return (
|
||||
<CoolifyContent
|
||||
integrationIds={integrationIds}
|
||||
options={options}
|
||||
width={width}
|
||||
isMobileDetail={displayMode === "mobileDetail"}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface CoolifyContentProps {
|
||||
integrationIds: string[];
|
||||
options: WidgetComponentProps<"coolify">["options"];
|
||||
width: number;
|
||||
isMobileDetail: boolean;
|
||||
}
|
||||
|
||||
function CoolifyContent({ integrationIds, options, width }: CoolifyContentProps) {
|
||||
function CoolifyContent({ integrationIds, options, width, isMobileDetail }: CoolifyContentProps) {
|
||||
const { data: instancesData = [] } = clientApi.widget.coolify.getInstancesInfo.useQuery({ integrationIds });
|
||||
|
||||
const isTiny = width < 256;
|
||||
@@ -38,7 +51,15 @@ function CoolifyContent({ integrationIds, options, width }: CoolifyContentProps)
|
||||
const widgetKey = createWidgetKey(integrationIds);
|
||||
|
||||
if (instancesData.length === 1 && firstInstance) {
|
||||
return <SingleInstanceLayout instance={firstInstance} options={options} isTiny={isTiny} widgetKey={widgetKey} />;
|
||||
return (
|
||||
<SingleInstanceLayout
|
||||
instance={firstInstance}
|
||||
options={options}
|
||||
isTiny={isTiny}
|
||||
widgetKey={widgetKey}
|
||||
isMobileDetail={isMobileDetail}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -51,6 +72,7 @@ function CoolifyContent({ integrationIds, options, width }: CoolifyContentProps)
|
||||
options={options}
|
||||
isTiny={isTiny}
|
||||
widgetKey={widgetKey}
|
||||
isMobileDetail={isMobileDetail}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
@@ -5,6 +5,8 @@ import { optionsBuilder } from "../options";
|
||||
|
||||
export const { definition, componentLoader } = createWidgetDefinition("coolify", {
|
||||
icon: IconCloud,
|
||||
mobile: { supportsDetailView: true },
|
||||
queryKey: [["widget", "coolify"]],
|
||||
createOptions() {
|
||||
return optionsBuilder.from((factory) => ({
|
||||
showServers: factory.switch({
|
||||
|
||||
@@ -18,9 +18,10 @@ interface InstanceCardProps {
|
||||
options: CoolifyOptions;
|
||||
isTiny: boolean;
|
||||
widgetKey: string;
|
||||
isMobileDetail: boolean;
|
||||
}
|
||||
|
||||
export function InstanceCard({ instance, options, isTiny, widgetKey }: InstanceCardProps) {
|
||||
export function InstanceCard({ instance, options, isTiny, widgetKey, isMobileDetail }: InstanceCardProps) {
|
||||
const t = useScopedI18n("widget.coolify");
|
||||
const cardKey = `${widgetKey}-${instance.integrationId}`;
|
||||
const [showIp, setShowIp] = useLocalStorage({
|
||||
@@ -99,13 +100,24 @@ export function InstanceCard({ instance, options, isTiny, widgetKey }: InstanceC
|
||||
isTiny={isTiny}
|
||||
showIp={showIp}
|
||||
onToggleIp={() => setShowIp((prev) => !prev)}
|
||||
isMobileDetail={isMobileDetail}
|
||||
/>
|
||||
)}
|
||||
{options.showApplications && (
|
||||
<ApplicationsSection applications={instance.instanceInfo.applications} baseUrl={baseUrl} isTiny={isTiny} />
|
||||
<ApplicationsSection
|
||||
applications={instance.instanceInfo.applications}
|
||||
baseUrl={baseUrl}
|
||||
isTiny={isTiny}
|
||||
isMobileDetail={isMobileDetail}
|
||||
/>
|
||||
)}
|
||||
{options.showServices && (
|
||||
<ServicesSection services={instance.instanceInfo.services} baseUrl={baseUrl} isTiny={isTiny} />
|
||||
<ServicesSection
|
||||
services={instance.instanceInfo.services}
|
||||
baseUrl={baseUrl}
|
||||
isTiny={isTiny}
|
||||
isMobileDetail={isMobileDetail}
|
||||
/>
|
||||
)}
|
||||
</Accordion>
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import { ActionIcon, Anchor, Group, Indicator, Stack, Text } from "@mantine/core";
|
||||
import { IconExternalLink, IconFileText, IconLink } from "@tabler/icons-react";
|
||||
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { cleanFqdn, getResourceTimestamp, getStatusColor, parseStatus } from "./coolify-utils";
|
||||
|
||||
interface ResourceRowProps {
|
||||
@@ -21,11 +23,18 @@ interface ResourceRowProps {
|
||||
baseUrl: string;
|
||||
isTiny: boolean;
|
||||
resourceType: "application" | "service";
|
||||
isMobileDetail: boolean;
|
||||
}
|
||||
|
||||
export function ResourceRow({ item, baseUrl, isTiny, resourceType }: ResourceRowProps) {
|
||||
export function ResourceRow({ item, baseUrl, isTiny, resourceType, isMobileDetail }: ResourceRowProps) {
|
||||
const tCommon = useScopedI18n("common");
|
||||
const tApp = useScopedI18n("widget.app");
|
||||
const status = parseStatus(item.status ?? "");
|
||||
const statusColor = getStatusColor(status);
|
||||
const fqdn = cleanFqdn(item.fqdn);
|
||||
const actionIconSize = isMobileDetail ? 44 : "xs";
|
||||
const resourceLabel = resourceType === "application" ? tCommon("applications") : tCommon("services");
|
||||
const openInNewTabLabel = tApp("option.openInNewTab.label");
|
||||
|
||||
const resourceUrl =
|
||||
item.projectUuid && item.environmentUuid
|
||||
@@ -49,18 +58,42 @@ export function ResourceRow({ item, baseUrl, isTiny, resourceType }: ResourceRow
|
||||
)}
|
||||
</Group>
|
||||
<Group wrap="nowrap" gap={4} ml={16}>
|
||||
{cleanFqdn(item.fqdn) && (
|
||||
<ActionIcon component="a" href={cleanFqdn(item.fqdn)} target="_blank" size="xs" variant="subtle" c="dimmed">
|
||||
{fqdn && (
|
||||
<ActionIcon
|
||||
component="a"
|
||||
href={fqdn}
|
||||
target="_blank"
|
||||
size={actionIconSize}
|
||||
variant="subtle"
|
||||
c="dimmed"
|
||||
aria-label={`${openInNewTabLabel}: ${fqdn}`}
|
||||
>
|
||||
<IconLink size={12} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
{resourceUrl && (
|
||||
<ActionIcon component="a" href={resourceUrl} target="_blank" size="xs" variant="subtle" c="dimmed">
|
||||
<ActionIcon
|
||||
component="a"
|
||||
href={resourceUrl}
|
||||
target="_blank"
|
||||
size={actionIconSize}
|
||||
variant="subtle"
|
||||
c="dimmed"
|
||||
aria-label={`${openInNewTabLabel}: ${resourceLabel} ${item.name}`}
|
||||
>
|
||||
<IconExternalLink size={12} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
{logsUrl && (
|
||||
<ActionIcon component="a" href={logsUrl} target="_blank" size="xs" variant="subtle" c="dimmed">
|
||||
<ActionIcon
|
||||
component="a"
|
||||
href={logsUrl}
|
||||
target="_blank"
|
||||
size={actionIconSize}
|
||||
variant="subtle"
|
||||
c="dimmed"
|
||||
aria-label={`${tCommon("action.checkLogs")}: ${item.name}`}
|
||||
>
|
||||
<IconFileText size={12} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
|
||||
@@ -15,6 +15,7 @@ interface ServersSectionProps {
|
||||
isTiny: boolean;
|
||||
showIp: boolean;
|
||||
onToggleIp: () => void;
|
||||
isMobileDetail: boolean;
|
||||
}
|
||||
|
||||
export function ServersSection({
|
||||
@@ -24,6 +25,7 @@ export function ServersSection({
|
||||
isTiny,
|
||||
showIp,
|
||||
onToggleIp,
|
||||
isMobileDetail,
|
||||
}: ServersSectionProps) {
|
||||
const t = useScopedI18n("widget.coolify");
|
||||
const tCommon = useScopedI18n("common");
|
||||
@@ -40,9 +42,11 @@ export function ServersSection({
|
||||
</Badge>
|
||||
</Group>
|
||||
<ActionIcon
|
||||
size="xs"
|
||||
size={isMobileDetail ? 44 : "xs"}
|
||||
variant="subtle"
|
||||
c="dimmed"
|
||||
aria-label={`${showIp ? tCommon("action.hide") : tCommon("action.show")} IP`}
|
||||
aria-pressed={showIp}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onToggleIp();
|
||||
@@ -65,6 +69,7 @@ export function ServersSection({
|
||||
baseUrl={baseUrl}
|
||||
isTiny={isTiny}
|
||||
showIp={showIp}
|
||||
isMobileDetail={isMobileDetail}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
@@ -84,10 +89,12 @@ interface ServerRowProps {
|
||||
baseUrl: string;
|
||||
isTiny: boolean;
|
||||
showIp: boolean;
|
||||
isMobileDetail: boolean;
|
||||
}
|
||||
|
||||
function ServerRow({ server, counts, baseUrl, isTiny, showIp }: ServerRowProps) {
|
||||
function ServerRow({ server, counts, baseUrl, isTiny, showIp, isMobileDetail }: ServerRowProps) {
|
||||
const t = useScopedI18n("widget.coolify");
|
||||
const tApp = useScopedI18n("widget.app");
|
||||
const isBuildServer = server.settings?.is_build_server === true;
|
||||
const isOnline = server.is_reachable !== false;
|
||||
const serverUrl = `${baseUrl}/server/${server.uuid}`;
|
||||
@@ -110,7 +117,15 @@ function ServerRow({ server, counts, baseUrl, isTiny, showIp }: ServerRowProps)
|
||||
)}
|
||||
</Group>
|
||||
<Group wrap="nowrap" gap={4} ml={16}>
|
||||
<ActionIcon component="a" href={serverUrl} target="_blank" size="xs" variant="subtle" c="dimmed">
|
||||
<ActionIcon
|
||||
component="a"
|
||||
href={serverUrl}
|
||||
target="_blank"
|
||||
size={isMobileDetail ? 44 : "xs"}
|
||||
variant="subtle"
|
||||
c="dimmed"
|
||||
aria-label={`${tApp("option.openInNewTab.label")}: ${server.name}`}
|
||||
>
|
||||
<IconExternalLink size={12} />
|
||||
</ActionIcon>
|
||||
<Text fz="10px" c="dimmed">
|
||||
|
||||
@@ -13,9 +13,10 @@ interface ServicesSectionProps {
|
||||
services: CoolifyServiceWithContext[];
|
||||
baseUrl: string;
|
||||
isTiny: boolean;
|
||||
isMobileDetail: boolean;
|
||||
}
|
||||
|
||||
export function ServicesSection({ services, baseUrl, isTiny }: ServicesSectionProps) {
|
||||
export function ServicesSection({ services, baseUrl, isTiny, isMobileDetail }: ServicesSectionProps) {
|
||||
const t = useScopedI18n("widget.coolify");
|
||||
const tCommon = useScopedI18n("common");
|
||||
const runningServices = services.filter((svc) => parseStatus(svc.status ?? "") === "running").length;
|
||||
@@ -34,7 +35,14 @@ export function ServicesSection({ services, baseUrl, isTiny }: ServicesSectionPr
|
||||
{services.length > 0 ? (
|
||||
<Stack gap={4}>
|
||||
{services.map((service) => (
|
||||
<ResourceRow key={service.uuid} item={service} baseUrl={baseUrl} isTiny={isTiny} resourceType="service" />
|
||||
<ResourceRow
|
||||
key={service.uuid}
|
||||
item={service}
|
||||
baseUrl={baseUrl}
|
||||
isTiny={isTiny}
|
||||
resourceType="service"
|
||||
isMobileDetail={isMobileDetail}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
) : (
|
||||
|
||||
@@ -18,9 +18,16 @@ interface SingleInstanceLayoutProps {
|
||||
options: CoolifyOptions;
|
||||
isTiny: boolean;
|
||||
widgetKey: string;
|
||||
isMobileDetail: boolean;
|
||||
}
|
||||
|
||||
export function SingleInstanceLayout({ instance, options, isTiny, widgetKey }: SingleInstanceLayoutProps) {
|
||||
export function SingleInstanceLayout({
|
||||
instance,
|
||||
options,
|
||||
isTiny,
|
||||
widgetKey,
|
||||
isMobileDetail,
|
||||
}: SingleInstanceLayoutProps) {
|
||||
const t = useScopedI18n("widget.coolify");
|
||||
const [showIp, setShowIp] = useLocalStorage({
|
||||
key: `coolify-show-ip-${widgetKey}`,
|
||||
@@ -65,13 +72,24 @@ export function SingleInstanceLayout({ instance, options, isTiny, widgetKey }: S
|
||||
isTiny={isTiny}
|
||||
showIp={showIp}
|
||||
onToggleIp={() => setShowIp((prev) => !prev)}
|
||||
isMobileDetail={isMobileDetail}
|
||||
/>
|
||||
)}
|
||||
{options.showApplications && (
|
||||
<ApplicationsSection applications={instance.instanceInfo.applications} baseUrl={baseUrl} isTiny={isTiny} />
|
||||
<ApplicationsSection
|
||||
applications={instance.instanceInfo.applications}
|
||||
baseUrl={baseUrl}
|
||||
isTiny={isTiny}
|
||||
isMobileDetail={isMobileDetail}
|
||||
/>
|
||||
)}
|
||||
{options.showServices && (
|
||||
<ServicesSection services={instance.instanceInfo.services} baseUrl={baseUrl} isTiny={isTiny} />
|
||||
<ServicesSection
|
||||
services={instance.instanceInfo.services}
|
||||
baseUrl={baseUrl}
|
||||
isTiny={isTiny}
|
||||
isMobileDetail={isMobileDetail}
|
||||
/>
|
||||
)}
|
||||
</Accordion>
|
||||
|
||||
|
||||
Reference in new issue
Block a user