mirror of
https://github.com/gethomepage/homepage.git
synced 2026-09-27 06:21:18 -07:00
Enhancement: add additional fields to Tailscale Widget (#6589)
Docker CI / Docker Build & Push (push) Has been cancelled
Lint / Linting Checks (push) Has been cancelled
Release Drafter / Update Release Draft (push) Has been cancelled
Release Drafter / Auto Label PR (push) Has been cancelled
Tests / vitest (1) (push) Has been cancelled
Tests / vitest (2) (push) Has been cancelled
Tests / vitest (3) (push) Has been cancelled
Tests / vitest (4) (push) Has been cancelled
Docker CI / Docker Build & Push (push) Has been cancelled
Lint / Linting Checks (push) Has been cancelled
Release Drafter / Update Release Draft (push) Has been cancelled
Release Drafter / Auto Label PR (push) Has been cancelled
Tests / vitest (1) (push) Has been cancelled
Tests / vitest (2) (push) Has been cancelled
Tests / vitest (3) (push) Has been cancelled
Tests / vitest (4) (push) Has been cancelled
This commit is contained in:
@@ -9,7 +9,7 @@ You will need to generate an API access token from the [keys page](https://login
|
|||||||
|
|
||||||
To find your device ID, go to the [machine overview page](https://login.tailscale.com/admin/machines) and select your machine. In the "Machine Details" section, copy your `ID`. It will end with `CNTRL`.
|
To find your device ID, go to the [machine overview page](https://login.tailscale.com/admin/machines) and select your machine. In the "Machine Details" section, copy your `ID`. It will end with `CNTRL`.
|
||||||
|
|
||||||
Allowed fields: `["address", "last_seen", "expires"]`.
|
Allowed fields: `[ "address", "last_seen", "expires", "user", "hostname", "name", "client_version", "os", "created", "authorized", "is_external", "update_available", "tags" ]`.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
widget:
|
widget:
|
||||||
|
|||||||
@@ -344,6 +344,16 @@
|
|||||||
"address": "Address",
|
"address": "Address",
|
||||||
"expires": "Expires",
|
"expires": "Expires",
|
||||||
"never": "Never",
|
"never": "Never",
|
||||||
|
"user": "User",
|
||||||
|
"hostname": "Hostname",
|
||||||
|
"name": "Name",
|
||||||
|
"client_version": "Client Version",
|
||||||
|
"os": "OS",
|
||||||
|
"created": "Created",
|
||||||
|
"authorized": "Authorized",
|
||||||
|
"is_external": "Is External",
|
||||||
|
"update_available": "Update Available",
|
||||||
|
"tags": "Tags",
|
||||||
"last_seen": "Last Seen",
|
"last_seen": "Last Seen",
|
||||||
"now": "Now",
|
"now": "Now",
|
||||||
"years": "{{number}}y",
|
"years": "{{number}}y",
|
||||||
@@ -352,7 +362,9 @@
|
|||||||
"hours": "{{number}}h",
|
"hours": "{{number}}h",
|
||||||
"minutes": "{{number}}m",
|
"minutes": "{{number}}m",
|
||||||
"seconds": "{{number}}s",
|
"seconds": "{{number}}s",
|
||||||
"ago": "{{value}} Ago"
|
"ago": "{{value}} Ago",
|
||||||
|
"true": "Yes",
|
||||||
|
"false": "No"
|
||||||
},
|
},
|
||||||
"technitium": {
|
"technitium": {
|
||||||
"totalQueries": "Queries",
|
"totalQueries": "Queries",
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ export default function Component({ service }) {
|
|||||||
|
|
||||||
const { widget } = service;
|
const { widget } = service;
|
||||||
|
|
||||||
const { data: statsData, error: statsError } = useWidgetAPI(widget, "device");
|
const { data: tailscaleData, error: tailscaleError } = useWidgetAPI(widget, "device");
|
||||||
|
|
||||||
if (statsError || statsData?.message) {
|
if (tailscaleError || tailscaleData?.message) {
|
||||||
return <Container service={service} error={statsError ?? statsData} />;
|
return <Container service={service} error={tailscaleError ?? tailscaleData} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!statsData) {
|
if (!tailscaleData) {
|
||||||
return (
|
return (
|
||||||
<Container service={service}>
|
<Container service={service}>
|
||||||
<Block label="tailscale.address" />
|
<Block label="tailscale.address" />
|
||||||
@@ -25,12 +25,29 @@ export default function Component({ service }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MAX_ALLOWED_FIELDS = 4;
|
||||||
|
if (widget.fields?.length == 0 || !widget.fields) {
|
||||||
|
widget.fields = ["address", "last_seen", "expires"];
|
||||||
|
} else if (widget.fields?.length > MAX_ALLOWED_FIELDS) {
|
||||||
|
widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS);
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
addresses: [address],
|
addresses: [address],
|
||||||
keyExpiryDisabled,
|
keyExpiryDisabled,
|
||||||
lastSeen,
|
lastSeen,
|
||||||
expires,
|
expires,
|
||||||
} = statsData;
|
user,
|
||||||
|
hostname,
|
||||||
|
name,
|
||||||
|
clientVersion,
|
||||||
|
os,
|
||||||
|
created,
|
||||||
|
authorized,
|
||||||
|
isExternal,
|
||||||
|
updateAvailable,
|
||||||
|
tags,
|
||||||
|
} = tailscaleData;
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const compareDifferenceInTwoDates = (priorDate, futureDate) => {
|
const compareDifferenceInTwoDates = (priorDate, futureDate) => {
|
||||||
@@ -62,11 +79,28 @@ export default function Component({ service }) {
|
|||||||
return compareDifferenceInTwoDates(now, date);
|
return compareDifferenceInTwoDates(now, date);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getBooleanAsString = (value) => {
|
||||||
|
return value ? t("tailscale.true") : t("tailscale.false");
|
||||||
|
};
|
||||||
|
|
||||||
|
const clientVersionString = clientVersion ? clientVersion.toString() : "-";
|
||||||
|
const tagsString = tags && Array.isArray(tags) ? tags.join(", ") : "-";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container service={service}>
|
<Container service={service}>
|
||||||
<Block label="tailscale.address" value={address} />
|
<Block label="tailscale.address" value={address} />
|
||||||
<Block label="tailscale.last_seen" value={getLastSeen()} />
|
<Block label="tailscale.last_seen" value={getLastSeen()} />
|
||||||
<Block label="tailscale.expires" value={getExpiry()} />
|
<Block label="tailscale.expires" value={getExpiry()} />
|
||||||
|
<Block label="tailscale.user" value={user} />
|
||||||
|
<Block label="tailscale.hostname" value={hostname} />
|
||||||
|
<Block label="tailscale.name" value={name} />
|
||||||
|
<Block label="tailscale.client_version" value={clientVersionString} />
|
||||||
|
<Block label="tailscale.os" value={os} />
|
||||||
|
<Block label="tailscale.created" value={created} />
|
||||||
|
<Block label="tailscale.authorized" value={getBooleanAsString(authorized)} />
|
||||||
|
<Block label="tailscale.is_external" value={getBooleanAsString(isExternal)} />
|
||||||
|
<Block label="tailscale.update_available" value={getBooleanAsString(updateAvailable)} />
|
||||||
|
<Block label="tailscale.tags" value={tagsString} />
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,23 @@ describe("widgets/tailscale/component", () => {
|
|||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const fullData = {
|
||||||
|
addresses: ["127.0.0.1"],
|
||||||
|
keyExpiryDisabled: false,
|
||||||
|
expires: "2020-06-01T00:00:00Z",
|
||||||
|
lastSeen: "2019-12-31T23:55:00Z",
|
||||||
|
user: "fin@example.com",
|
||||||
|
hostname: "localhost",
|
||||||
|
name: "localhost.tail1234.ts.net",
|
||||||
|
clientVersion: "1.1.0",
|
||||||
|
os: "linux",
|
||||||
|
created: "2019-06-01T00:00:00Z",
|
||||||
|
authorized: true,
|
||||||
|
isExternal: false,
|
||||||
|
updateAvailable: true,
|
||||||
|
tags: ["server", "prod"],
|
||||||
|
};
|
||||||
|
|
||||||
it("renders placeholders while loading", () => {
|
it("renders placeholders while loading", () => {
|
||||||
useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
|
useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
|
||||||
|
|
||||||
@@ -35,14 +52,108 @@ describe("widgets/tailscale/component", () => {
|
|||||||
expect(screen.getByText("tailscale.expires")).toBeInTheDocument();
|
expect(screen.getByText("tailscale.expires")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders address and expiry/last-seen strings when loaded", () => {
|
describe("fields group: address, last_seen, expires, user", () => {
|
||||||
|
it("renders only the specified 4 fields", () => {
|
||||||
|
useWidgetAPI.mockReturnValue({ data: fullData, error: undefined });
|
||||||
|
|
||||||
|
const { container } = renderWithProviders(
|
||||||
|
<Component service={{ widget: { type: "tailscale", fields: ["address", "last_seen", "expires", "user"] } }} />,
|
||||||
|
{ settings: { hideErrors: false } },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
|
||||||
|
expectBlockValue(container, "tailscale.address", "127.0.0.1");
|
||||||
|
expectBlockValue(container, "tailscale.last_seen", "tailscale.ago");
|
||||||
|
expectBlockValue(container, "tailscale.expires", "tailscale.weeks");
|
||||||
|
expectBlockValue(container, "tailscale.user", "fin@example.com");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("fields group: hostname, name, client_version, os", () => {
|
||||||
|
it("renders only the specified 4 fields", () => {
|
||||||
|
useWidgetAPI.mockReturnValue({ data: fullData, error: undefined });
|
||||||
|
|
||||||
|
const { container } = renderWithProviders(
|
||||||
|
<Component service={{ widget: { type: "tailscale", fields: ["hostname", "name", "client_version", "os"] } }} />,
|
||||||
|
{ settings: { hideErrors: false } },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
|
||||||
|
expectBlockValue(container, "tailscale.hostname", "localhost");
|
||||||
|
expectBlockValue(container, "tailscale.name", "localhost.tail1234.ts.net");
|
||||||
|
expectBlockValue(container, "tailscale.client_version", "1.1.0");
|
||||||
|
expectBlockValue(container, "tailscale.os", "linux");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("fields group: created, authorized, is_external, update_available", () => {
|
||||||
|
it("renders only the specified 4 fields", () => {
|
||||||
|
useWidgetAPI.mockReturnValue({ data: fullData, error: undefined });
|
||||||
|
|
||||||
|
const { container } = renderWithProviders(
|
||||||
|
<Component
|
||||||
|
service={{
|
||||||
|
widget: { type: "tailscale", fields: ["created", "authorized", "is_external", "update_available"] },
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
{ settings: { hideErrors: false } },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
|
||||||
|
expectBlockValue(container, "tailscale.created", "2019-06-01T00:00:00Z");
|
||||||
|
expectBlockValue(container, "tailscale.authorized", "tailscale.true");
|
||||||
|
expectBlockValue(container, "tailscale.is_external", "tailscale.false");
|
||||||
|
expectBlockValue(container, "tailscale.update_available", "tailscale.true");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("fields group: tags with defaults", () => {
|
||||||
|
it("renders tags alongside default fields", () => {
|
||||||
|
useWidgetAPI.mockReturnValue({ data: fullData, error: undefined });
|
||||||
|
|
||||||
|
const { container } = renderWithProviders(
|
||||||
|
<Component service={{ widget: { type: "tailscale", fields: ["address", "last_seen", "expires", "tags"] } }} />,
|
||||||
|
{ settings: { hideErrors: false } },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
|
||||||
|
expectBlockValue(container, "tailscale.address", "127.0.0.1");
|
||||||
|
expectBlockValue(container, "tailscale.tags", "server, prod");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("fields truncation", () => {
|
||||||
|
it("truncates to 4 fields when more than 4 are specified", () => {
|
||||||
|
useWidgetAPI.mockReturnValue({ data: fullData, error: undefined });
|
||||||
|
|
||||||
|
const { container } = renderWithProviders(
|
||||||
|
<Component
|
||||||
|
service={{ widget: { type: "tailscale", fields: ["address", "last_seen", "expires", "user", "hostname"] } }}
|
||||||
|
/>,
|
||||||
|
{ settings: { hideErrors: false } },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
|
||||||
|
expect(findServiceBlockByLabel(container, "tailscale.hostname")).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("defaults to address, last_seen, expires when fields is empty", () => {
|
||||||
|
useWidgetAPI.mockReturnValue({ data: fullData, error: undefined });
|
||||||
|
|
||||||
|
const { container } = renderWithProviders(<Component service={{ widget: { type: "tailscale", fields: [] } }} />, {
|
||||||
|
settings: { hideErrors: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(container.querySelectorAll(".service-block")).toHaveLength(3);
|
||||||
|
expectBlockValue(container, "tailscale.address", "127.0.0.1");
|
||||||
|
expectBlockValue(container, "tailscale.last_seen", "tailscale.ago");
|
||||||
|
expectBlockValue(container, "tailscale.expires", "tailscale.weeks");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders never for expires if key expiry is disabled", () => {
|
||||||
useWidgetAPI.mockReturnValue({
|
useWidgetAPI.mockReturnValue({
|
||||||
data: {
|
data: { ...fullData, keyExpiryDisabled: true },
|
||||||
addresses: ["100.64.0.1"],
|
|
||||||
keyExpiryDisabled: true,
|
|
||||||
lastSeen: "2019-12-31T23:00:00Z",
|
|
||||||
expires: "2021-01-01T00:00:00Z",
|
|
||||||
},
|
|
||||||
error: undefined,
|
error: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -50,8 +161,17 @@ describe("widgets/tailscale/component", () => {
|
|||||||
settings: { hideErrors: false },
|
settings: { hideErrors: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
expectBlockValue(container, "tailscale.address", "100.64.0.1");
|
|
||||||
expect(findServiceBlockByLabel(container, "tailscale.last_seen")?.textContent).toContain("tailscale.ago");
|
|
||||||
expectBlockValue(container, "tailscale.expires", "tailscale.never");
|
expectBlockValue(container, "tailscale.expires", "tailscale.never");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders error message when API returns an error", () => {
|
||||||
|
useWidgetAPI.mockReturnValue({ data: undefined, error: { message: "API error" } });
|
||||||
|
|
||||||
|
const { container } = renderWithProviders(<Component service={{ widget: { type: "tailscale" } }} />, {
|
||||||
|
settings: { hideErrors: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(container.querySelectorAll(".service-block")).toHaveLength(0);
|
||||||
|
expect(container.textContent).toContain("API error");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user