Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 8 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,14 @@ This is my attempt to solving that.


## Features
- GUI Editing: Edit and manage links, search engines, your wallpapers in settings via the GUI instead of relying purely on a config.
- Built-in Authentication: dashwise has Auth built right into it - and even features
- SSO via OIDC: While tested with PocketId, it should work via OIDC - which can be configured in Pocketbase directly.
- Links: store your most important links for quick access and group them into Link Groups and Folders coming. Links can also be monitored with downtime being logged.
- Glanceables: Customizable bits of one-line information next to the clock.
- Widgets: Modular blocks on the dashboard that show key info or actions at a glance. They can be moved and customized individually.
- News: Subscribe to RSS feeds to stay on top of everything.
- Notfications: Dashwise can receive notifications via PUSH requests to /api/v1/notifications/TOPIC-NAME
- Wallpapers: Upload them to dashwise directly, or even change the default one for new users by mounting one into the container
- Spotlight-like Search: Hit Ctrl+K from your dashboard, and you'll be able to search your links and integrations or use bangs for search engines specified in settings.
- Integrations: directly integrates with your favourite self hosted apps. Supported services are Karakeep, Dashdot, Beszel and Jellyfin. More integrations are planned
- **Principles**: Configure dashwise using its UI. No need to touch config files. Dashwise also has Authentication (including SSO) built in.
- **Dashboards** show Glanceables and Widgets, for example Links (which can be grouped into Link groups and folders)
- **Glanceables**: Customizable bits of one-line information next to the clock.
- **Widgets**: Modular blocks on the dashboard that show key info or actions at a glance. They can be moved and customized individually.
- **News**: Subscribe to RSS feeds to stay on top of everything.
- **Notfications** : Dashwise can receive notifications via PUSH requests to /api/v1/notifications/TOPIC-NAME
- **Spotlight-like Search**: Hit Ctrl+K from your dashboard, and you'll be able to search your links and integrations or use bangs for search engines specified in settings.
- **Integrations**: directly integrates with your favourite self hosted apps. Supported services are Karakeep, Dashdot, Beszel and Jellyfin. More integrations are planned

## Installation
For production depolyments, use the docker-compose.yaml (image is currently only built for arm, will change later).
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/components/settings/SmartFramesManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const LOCAL_WIDGET_OPTIONS = [
{ value: "progress", label: "Calendar progress" },
"countdown",
"rss-feed",
"latest-links",
"monitoring",
] as const;

Expand Down Expand Up @@ -68,6 +69,7 @@ const LOCAL_WIDGET_SCHEMAS: Record<string, Record<string, any>> = {
"calendar-upcoming": { maxEvents: 5 },
countdown: { date: "", date_format: "yyyy-MM-dd", label: "Countdown" },
"rss-feed": { feedId: "all", maxItems: 8, title: "Latest Articles" },
"latest-links": { listId: "", maxItems: 8, title: "Latest Links" },
progress: {
period: {
type: "select",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from "@/components/ui/select";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
import { GlanceableSide } from "./utils";
import { useLocalization } from "@/context/LocalizationContext";
import useAuth from "@/context/useAuth";
Expand Down
136 changes: 136 additions & 0 deletions apps/web/src/components/widgets/Widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
} from "@dashwise/integrationskit/static-widgets/CalendarWidgets";
import CountdownWidget from "@dashwise/integrationskit/static-widgets/CountdownWidget";
import RssFeedWidget from "@dashwise/integrationskit/static-widgets/RssFeedWidget";
import VerticalList from "@dashwise/integrationskit/templates/VerticalList";
import LinkView from "./LinkView";
import SearchBar from "./SearchBar";
import IframeTemplate from "@dashwise/integrationskit/templates/IFrame";
Expand All @@ -27,6 +28,8 @@ import useAuth from "@/context/useAuth";
import {
getConsumerDataAction,
getIntegrationCalendarEventsAction,
getLinksCollectionsAction,
getLinksItemsAction,
getNewsFeedAction,
} from '@/lib/apiClient';

Expand Down Expand Up @@ -91,6 +94,9 @@ export function renderWidget({
case "latest-rss-feed":
return <RssFeedWidgetWrapper className={finalClassName} {...renderParams} />;

case "latest-links":
return <LatestLinksWidgetWrapper className={finalClassName} {...renderParams} />;

case "countdown":
return <CountdownWidget className={finalClassName} {...renderParams} />;

Expand Down Expand Up @@ -123,6 +129,136 @@ export function renderWidget({
}
}

type LatestLinkItem = {
id: string;
url?: string;
title?: string;
iconUrl?: string;
description?: string;
collection?: string;
created?: string;
updated?: string;
};

function LatestLinksWidgetWrapper({
className,
listId,
collectionId,
maxItems = 8,
title = "Latest Links",
}: {
className?: string;
listId?: string;
collectionId?: string;
maxItems?: number;
title?: string;
}) {
const { withAuth } = useAuth();
const [items, setItems] = useState<LatestLinkItem[]>([]);
const [loading, setLoading] = useState(true);

useEffect(() => {
let cancelled = false;
const selectedListId = String(listId || collectionId || "").trim();

setLoading(true);

const fetchItems = async () => {
const links = await withAuth(async (auth) => {
if (selectedListId) {
const listItems = await getLinksItemsAction(auth, selectedListId);
return Array.isArray(listItems) ? listItems : [];
}

const collections = await getLinksCollectionsAction(auth);
const listIds = Array.isArray(collections)
? collections.map((collection: any) => String(collection?.id ?? "").trim()).filter(Boolean)
: [];
const groupedItems = await Promise.all(
listIds.map(async (id) => {
try {
const listItems = await getLinksItemsAction(auth, id);
return Array.isArray(listItems) ? listItems : [];
} catch {
return [];
}
}),
);

return groupedItems.flat();
});

return Array.isArray(links) ? links : [];
};

void fetchItems()
.then((links) => {
if (!cancelled) {
setItems(sortLatestLinks(links).slice(0, maxItems));
}
})
.catch((err) => {
console.error("Failed to fetch latest links", err);
if (!cancelled) setItems([]);
})
.finally(() => {
if (!cancelled) setLoading(false);
});

return () => {
cancelled = true;
};
}, [collectionId, listId, maxItems, withAuth]);

if (loading) {
return (
<div className={`rounded-lg p-2 flex flex-col ${className ?? ""}`}>
<div className="text-sm opacity-50 py-4 text-center text-foreground">
Loading...
</div>
</div>
);
}

return (
<VerticalList
className={className}
itemClassName="gap-1"
resolved={{
header: {
title,
show: true,
icon: "fa6-solid:link",
titleAction: "/apps/links",
},
list: items.map((item) => ({
title: item.title || "Untitled",
titleAction: item.url || undefined,
subtitle: [item.description || item.url, formatLatestLinkDate(item.created)].filter(Boolean) as string[],
thumbnail: item.iconUrl || undefined,
icon: "fa6-solid:link",
})),
raw: {},
}}
/>
);
}

function sortLatestLinks(items: LatestLinkItem[]) {
return [...items].sort((left, right) => {
const leftTime = new Date(left.created || left.updated || 0).getTime();
const rightTime = new Date(right.created || right.updated || 0).getTime();
return (Number.isNaN(rightTime) ? 0 : rightTime) - (Number.isNaN(leftTime) ? 0 : leftTime);
});
}

function formatLatestLinkDate(value?: string) {
if (!value) return "";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return "";
return date.toLocaleDateString([], { month: "short", day: "numeric" });
}

function resolveProgressPeriod(type: string, params?: Record<string, any>) {
const candidate = String(params?.period ?? type ?? "day").trim();
if (candidate === "year" || candidate === "month" || candidate === "day" || candidate === "week") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ function ResolvedGlanceable({
return (
<GlanceableComponent
glanceableJSON={blueprint?.glanceableJSON}
data={resolved?.data}
resolved={{
text: blueprint?.text ?? "",
icon: blueprint?.icon,
Expand Down
9 changes: 9 additions & 0 deletions packages/assets/integrations/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ configuration:
key: "link-view"
template: "link-view"
category: "Links"
- name: "Latest Links"
key: "latest-links"
template: "latest-links"
category: "Links"
data:
input:
listId: ""
maxItems: 8
title: "Latest Links"
- name: "Placeholder"
key: "placeholder"
template: "placeholder"
Expand Down
Loading
Loading