Compare commits

..
Author SHA1 Message Date
Danielv123 8229db7533 Fix plugin list and header layout on mobile 2026-11-15 18:26:41 +01:00
6 changed files with 98 additions and 26 deletions

No files matched your search

+7
View File
@@ -31,6 +31,13 @@ Many thanks to the following for contributing to this release:
[@username](https://github.com/username) [@username](https://github.com/username)
--> -->
## Unreleased
### Fixes
- Fixed cramped plugin rows and wrapping header branding on mobile screens.
## Version 2.1.0 ## Version 2.1.0
### Features ### Features
@@ -172,10 +172,12 @@ export function PluginVersions(props: { status: PluginStatus, latest?: string })
{versions.map(version => { {versions.map(version => {
const now = count(machine => machine.running === version); const now = count(machine => machine.running === version);
const after = count(machine => machine.afterRestart === version); const after = count(machine => machine.afterRestart === version);
return <div key={version}> return <div key={version} className="plugin-version">
<span>
<Text>{version}</Text> <Text>{version}</Text>
{props.latest === version && <Tag color="success" style={{ marginLeft: 6 }}>latest</Tag>} {props.latest === version && <Tag color="success" style={{ marginLeft: 6 }}>latest</Tag>}
<Text type="secondary" style={{ marginLeft: 12 }}> </span>
<Text type="secondary">
{plural(now, "machine")}{now !== after && ` → ${after} after restart`} {plural(now, "machine")}{now !== after && ` → ${after} after restart`}
</Text> </Text>
</div>; </div>;
+35 -21
View File
@@ -1,5 +1,5 @@
import React, { useContext, useEffect, useState } from "react"; import React, { useContext, useEffect, useState } from "react";
import { Space, Table, Tooltip, Typography } from "antd"; import { Grid, Space, Table, Tooltip, Typography } from "antd";
import CloseCircleFilled from "@ant-design/icons/CloseCircleFilled"; import CloseCircleFilled from "@ant-design/icons/CloseCircleFilled";
import type { PluginWebApi } from "@clusterio/lib"; import type { PluginWebApi } from "@clusterio/lib";
@@ -48,6 +48,8 @@ function WebError(props: { error: string }) {
export default function PluginsPage() { export default function PluginsPage() {
const control = useContext(ControlContext); const control = useContext(ControlContext);
const account = useAccount(); const account = useAccount();
const screens = Grid.useBreakpoint();
const isMobile = screens.md === false;
let [pluginList, setPluginList] = useState<PluginWebApi[]>([]); let [pluginList, setPluginList] = useState<PluginWebApi[]>([]);
const tableState = useTableQueryState<PluginRow>({ const tableState = useTableQueryState<PluginRow>({
namespace: "plugin", defaultSortKey: "name", namespace: "plugin", defaultSortKey: "name",
@@ -96,6 +98,24 @@ export default function PluginsPage() {
const latestOf = (plugin: PluginRow) => ( const latestOf = (plugin: PluginRow) => (
plugin.status?.npmPackage ? latestVersions[plugin.status.npmPackage] : undefined plugin.status?.npmPackage ? latestVersions[plugin.status.npmPackage] : undefined
); );
const versions = (plugin: PluginRow) => <>
{plugin.status
? <PluginVersions status={plugin.status} latest={latestOf(plugin)} />
: plugin.meta?.version}
{plugin.webError && <WebError error={plugin.webError} />}
</>;
const updateAction = (plugin: PluginRow) => (plugin.status
// Keep clicks on the update action from opening the plugin.
? <span onClick={event => { event.stopPropagation(); }}>
<PluginUpdateButton
status={plugin.status}
title={plugin.title}
latest={latestOf(plugin)}
installer={installer}
/>
</span>
: null
);
return <PageLayout nav={[{ name: "Plugins" }]}> return <PageLayout nav={[{ name: "Plugins" }]}>
<PageHeader <PageHeader
@@ -115,14 +135,22 @@ export default function PluginsPage() {
</Space>} </Space>}
/> />
<Table <Table
className="plugins-table"
tableLayout={isMobile ? "fixed" : "auto"}
columns={[ columns={[
{ {
title: "Name", title: "Name",
key: "name", key: "name",
className: "table-link-cell", className: "table-link-cell",
render: (_, plugin) => <Link to={pluginPath(plugin)} style={{ color: "inherit" }}> render: (_, plugin) => <>
<Link to={pluginPath(plugin)} style={{ color: "inherit" }}>
{plugin.title} {plugin.title}
</Link>, </Link>
{isMobile && <div className="plugin-mobile-details">
<div>{versions(plugin)}</div>
{updateAction(plugin)}
</div>}
</>,
sorter: (a, b) => strcmp(a.title, b.title), sorter: (a, b) => strcmp(a.title, b.title),
sortOrder: tableState.sortOrder("name"), sortOrder: tableState.sortOrder("name"),
filteredValue: tableState.filteredValue("name"), filteredValue: tableState.filteredValue("name"),
@@ -131,28 +159,14 @@ export default function PluginsPage() {
{ {
title: "Versions", title: "Versions",
key: "versions", key: "versions",
render: (_, plugin) => <> responsive: ["md"],
{plugin.status render: (_, plugin) => versions(plugin),
? <PluginVersions status={plugin.status} latest={latestOf(plugin)} />
: plugin.meta?.version}
{plugin.webError && <WebError error={plugin.webError} />}
</>,
}, },
{ {
key: "actions", key: "actions",
align: "right", align: "right",
render: (_, plugin) => (plugin.status responsive: ["md"],
// Keep clicks on the update action from opening the plugin render: (_, plugin) => updateAction(plugin),
? <span onClick={event => { event.stopPropagation(); }}>
<PluginUpdateButton
status={plugin.status}
title={plugin.title}
latest={latestOf(plugin)}
installer={installer}
/>
</span>
: null
),
}, },
]} ]}
dataSource={tableContents} dataSource={tableContents}
@@ -169,8 +169,10 @@ export default function SiteLayout() {
/>; />;
let accountMenu = <Dropdown menu={accountMenuProps} trigger={["click"]} placement="bottomRight"> let accountMenu = <Dropdown menu={accountMenuProps} trigger={["click"]} placement="bottomRight">
<Button type="text" icon={<UserOutlined />} style={{ marginInlineStart: "auto" }}> <Button type="text" icon={<UserOutlined />} style={{ marginInlineStart: "auto" }}
{account.name} aria-label={`Account menu for ${account.name}`}
>
<span className="site-layout-account-name">{account.name}</span>
</Button> </Button>
</Dropdown>; </Dropdown>;
@@ -228,7 +230,7 @@ export default function SiteLayout() {
<Link to="/" style={{ display: "flex", alignItems: "center" }}> <Link to="/" style={{ display: "flex", alignItems: "center" }}>
<img src={logo} width={56} height={56} alt="Clusterio logo" /> <img src={logo} width={56} height={56} alt="Clusterio logo" />
</Link> </Link>
<Flex vertical justify="center"> <Flex vertical justify="center" className="site-layout-brand">
<Typography.Title level={4} style={{ margin: 0, lineHeight: 1.2 }}>Clusterio</Typography.Title> <Typography.Title level={4} style={{ margin: 0, lineHeight: 1.2 }}>Clusterio</Typography.Title>
<Tooltip title="View changelog" placement="right"> <Tooltip title="View changelog" placement="right">
<Typography.Text <Typography.Text
+32
View File
@@ -72,6 +72,38 @@ body {
background: #001529; background: #001529;
} }
.site-layout-brand {
flex-shrink: 0;
white-space: nowrap;
}
@media (max-width: 767px) {
.site-layout-account-name {
display: none;
}
.plugins-table .table-link-cell > a {
font-weight: 600;
overflow-wrap: anywhere;
}
}
.plugin-mobile-details {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12px;
margin-top: 8px;
overflow-wrap: anywhere;
}
.plugin-version {
display: flex;
flex-wrap: wrap;
align-items: baseline;
column-gap: 12px;
}
/* Keep the sidebar fixed in place (below the header) while content scrolls. */ /* Keep the sidebar fixed in place (below the header) while content scrolls. */
.site-layout-sider { .site-layout-sider {
position: sticky; position: sticky;
+15
View File
@@ -2,6 +2,21 @@
This folder contains files used for performing manual tests that are non-trivial to automate. This folder contains files used for performing manual tests that are non-trivial to automate.
## Plugins page on small screens
Use a cluster with several plugins, including one with different running and installed versions and one with a web load error.
Compare the Plugins page at 390px and 1440px viewport widths, and check the 768px breakpoint.
1. On phones, check that each row shows its name, versions, machine counts, status and update action vertically, without clipping or breaking ordinary words.
2. Check that a long plugin name wraps and the header branding stays on one line.
3. Search and sort by name, reload, and check that the selected filter and sort order remain applied.
4. Open an update or restart checklist, then cancel it; update actions must not navigate to the plugin page.
5. Open a plugin by its name and by clicking elsewhere in its row.
6. Open the account menu using the user icon on mobile; the full account name should appear in the menu.
7. On desktop, check that the name, versions and update action remain separate columns.
## Setup test mod ## Setup test mod
Run `node packages/lib/dist/node/build_mod.js --source-dir test/manual/test_mod/ --output-dir mods/` to create the test mod. Run `node packages/lib/dist/node/build_mod.js --source-dir test/manual/test_mod/ --output-dir mods/` to create the test mod.