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
16 changes: 15 additions & 1 deletion messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
"nav_billing": "Billing",
"nav_settings": "Settings",
"nav_logout": "Logout",
"nav_group_info_management": "Info and Management",
"nav_group_connectivity_hardware": "Connectivity & Hardware",
"header_privacy_mode": "Privacy Mode",
"header_logout_error": "An error occurred while logging out. Please try again.",
"status_online": "Online",
Expand Down Expand Up @@ -891,11 +893,23 @@
"stat_aboveAvg": "Above Avg",
"stat_belowAvg": "Below Avg",
"stat_atAvg": "At Avg",
"stat_expand": "Expand",
"stat_collapse": "Collapse",
"traffic_metric_bicycle_count": "Bicycle Count",
"traffic_metric_bus_count": "Bus Count",
"traffic_metric_car_count": "Car Count",
"traffic_metric_motorcycle_count": "Motorcycle Count",
"traffic_metric_people_count": "People Count",
"traffic_metric_train_count": "Train Count",
"traffic_metric_truck_count": "Truck Count"
"traffic_metric_truck_count": "Truck Count",
"nav_gateways": "Gateways",
"gateways_page_title": "Gateways",
"gateways_your_gateways": "Your Gateways",
"gateways_gateway_id": "Gateway ID",
"gateways_gateway_name": "Gateway Name",
"gateways_status": "Status",
"gateways_public": "Public",
"gateways_online": "Online",
"gateways_offline": "Offline",
"gateways_private": "Private"
}
14 changes: 13 additions & 1 deletion messages/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
"nav_billing": "請求",
"nav_settings": "設定",
"nav_logout": "ログアウト",
"nav_group_info_management": "情報と管理",
"nav_group_connectivity_hardware": "接続とハードウェア",
"header_privacy_mode": "プライバシーモード",
"header_logout_error": "ログアウト中にエラーが発生しました。もう一度お試しください。",
"status_online": "オンライン",
Expand Down Expand Up @@ -899,5 +901,15 @@
"traffic_metric_motorcycle_count": "バイク台数",
"traffic_metric_people_count": "人数",
"traffic_metric_train_count": "電車本数",
"traffic_metric_truck_count": "トラック台数"
"traffic_metric_truck_count": "トラック台数",
"nav_gateways": "ゲートウェイ",
"gateways_page_title": "ゲートウェイ",
"gateways_your_gateways": "あなたのゲートウェイ",
"gateways_gateway_id": "ゲートウェイ ID",
"gateways_gateway_name": "ゲートウェイ名",
"gateways_status": "状態",
"gateways_public": "公開",
"gateways_online": "オンライン",
"gateways_offline": "オフライン",
"gateways_private": "非公開"
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
},
"packageManager": "pnpm@10.18.2+sha512.9fb969fa749b3ade6035e0f109f0b8a60b5d08a1a87fdf72e337da90dcc93336e2280ca4e44f2358a649b83c17959e9993e777c2080879f3801e6f0d999ad3dd",
"dependencies": {
"@cropwatchdevelopment/cwui": "0.1.82",
"@cropwatchdevelopment/cwui": "0.1.84",
"@supabase/supabase-js": "^2.98.0"
}
}
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions src/lib/api/api.dtos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,3 +409,21 @@ export type UpdateLocationRequest = {
description?: string | null;
group?: string | null;
};

export interface GatewayOwnerDto {
created_at: string;
gateway_id: number;
id: number;
user_id: string;
}

export interface GatewayDto {
created_at: string;
gateway_id: string;
gateway_name: string;
id: number;
is_online: boolean;
is_public: boolean;
updated_at: string | null;
cw_gateways_owners?: GatewayOwnerDto[];
}
10 changes: 9 additions & 1 deletion src/lib/api/api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ import type {
UpdateLocationOwnerRequest,
UpdateReportRequest,
UpdateRuleRequest,
WaterDataPoint
WaterDataPoint,
GatewayDto
} from './api.dtos';

type FetchLike = typeof fetch;
Expand Down Expand Up @@ -142,6 +143,7 @@ const TRAFFIC_ENDPOINT = '/traffic/{dev_eui}';
const TRAFFIC_MONTHLY_ENDPOINT = '/traffic/{dev_eui}/monthly';
const WATER_ENDPOINT = '/water/{dev_eui}';
const DEVICE_PERMISSION_LEVEL_ENDPOINT = '/devices/{dev_eui}/permission-level';
const GATEWAYS_ENDPOINT = '/gateway';
const DEVICE_LIST_PAGE_SIZE = 1000;

function toIsoIfDate(value: string | Date | undefined): string | undefined {
Expand Down Expand Up @@ -1130,6 +1132,12 @@ export class ApiService {
});
}

public getGateways(): Promise<GatewayDto[]> {
return this.request<GatewayDto[]>(GATEWAYS_ENDPOINT, {
method: 'GET'
});
}

public cancelPaymentsSubscription(subscriptionId: string): Promise<unknown> {
return this.request<unknown>(
`${PAYMENTS_SUBSCRIPTIONS_ENDPOINT}/${encodeURIComponent(subscriptionId)}`,
Expand Down
4 changes: 2 additions & 2 deletions src/routes/Header.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,12 @@
{/snippet}

{#snippet actions()}
<div class="app-header__actions-group">
<!-- <div class="app-header__actions-group">
<div class="app-header__utility-group">
<LanguageSwitcher compact />
<CwThemePicker />
</div>
</div>
</div> -->
<CwProfileMenu
class="app-header__profile-menu"
name={profileMenuName}
Expand Down
10 changes: 5 additions & 5 deletions src/routes/Sidebar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -54,21 +54,21 @@
label: m.nav_rules(),
href: '/rules',
icon: { path: RULES_ICON_PATH },
group: 'Info and Management'
group: m.nav_group_info_management()
},
{
id: 'reports',
label: m.nav_reports(),
href: '/reports',
icon: { path: REPORTS_ICON_PATH },
group: 'Info and Management'
group: m.nav_group_info_management()
},
{
id: 'gateways',
label: 'Gateways',
label: m.nav_gateways(),
href: '/gateways',
icon: { path: GATEWAYS_ICON_PATH },
group: 'Connectivity & Hardware'
group: m.nav_group_connectivity_hardware()
}
]);

Expand Down Expand Up @@ -138,7 +138,7 @@
params.searchParams.delete(key);
}
const qs = params.searchParams.toString();
goto(resolve(qs ? `/?${qs}` : '/'), {
goto(resolve((qs ? `/?${qs}` : '/') as '/'), {
replaceState: true,
keepFocus: true,
noScroll: true
Expand Down
7 changes: 7 additions & 0 deletions src/routes/gateways/+page.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { PageServerLoad } from './$types';

// The gateways list page uses CwDataTable with a client-side loadData callback.
// The root layout supplies authToken and session to every route via layout data inheritance.
export const load: PageServerLoad = async () => {
return {};
};
110 changes: 110 additions & 0 deletions src/routes/gateways/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<script lang="ts">
import Icon from '$lib/components/Icon.svelte';
import { AppPage } from '$lib/components/layout';
import {
CwButton,
CwCard,
CwDataTable,
type CwColumnDef,
type CwTableQuery,
type CwTableResult
} from '@cropwatchdevelopment/cwui';
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
import { m } from '$lib/paraglide/messages.js';
import { getAppContext } from '$lib/appContext.svelte';
import { ApiService, type GatewayDto } from '$lib/api/api.service';
import { formatDateTime } from '$lib/i18n/format';
import CHECK_ICON from '$lib/images/icons/check_circle.svg';
import NO_ICON from '$lib/images/icons/no.svg';

let loading = $state(false);
let app = getAppContext();

const columns: CwColumnDef<GatewayDto>[] = [
{ key: 'gateway_name', header: m.gateways_gateway_name(), sortable: true },
{ key: 'gateway_id', header: m.gateways_gateway_id() },
{ key: 'is_online', header: m.gateways_status(), sortable: true },
{ key: 'is_public', header: m.gateways_public(), sortable: true },
{ key: 'created_at', header: m.common_created(), sortable: true }
];

function sortGateways(
rows: GatewayDto[],
column: string,
direction: 'asc' | 'desc'
): GatewayDto[] {
const dir = direction === 'asc' ? 1 : -1;
return [...rows].sort((a, b) => {
const aVal = (a as unknown as Record<string, unknown>)[column];
const bVal = (b as unknown as Record<string, unknown>)[column];
if (aVal == null && bVal == null) return 0;
if (aVal == null) return dir;
if (bVal == null) return -dir;
if (typeof aVal === 'boolean') return (Number(aVal) - Number(bVal)) * dir;
return String(aVal).localeCompare(String(bVal)) * dir;
});
}

async function loadData(query: CwTableQuery): Promise<CwTableResult<GatewayDto>> {
const search = query.search?.trim().toLowerCase() || '';
const api = new ApiService({ authToken: app.accessToken });
const gateways = await api.getGateways();

let rows = search
? gateways.filter(
(gw) =>
gw.gateway_name.toLowerCase().includes(search) ||
gw.gateway_id.toLowerCase().includes(search)
)
: gateways;

if (query.sort) {
rows = sortGateways(rows, query.sort.column, query.sort.direction);
}

const total = rows.length;
const skip = (query.page - 1) * query.pageSize;
rows = rows.slice(skip, skip + query.pageSize);

return { rows, total };
}
</script>

<svelte:head>
<title>{m.gateways_page_title()}</title>
</svelte:head>

<AppPage>
<CwButton variant="secondary" onclick={() => goto(resolve('/'))}>
&larr; {m.action_back_to_dashboard()}
</CwButton>

<CwCard title={m.gateways_your_gateways()}>
<CwDataTable {columns} {loadData} {loading} rowKey="id" class="w-full">
{#snippet cell(row: GatewayDto, col: CwColumnDef<GatewayDto>, defaultValue: string)}
{#if col.key === 'is_online'}
{#if row.is_online}
<Icon src={CHECK_ICON} alt={m.gateways_online()} preserveColor />
{:else}
<span class="text-red-500">
<Icon src={NO_ICON} alt={m.gateways_offline()} />
</span>
{/if}
{:else if col.key === 'is_public'}
{#if row.is_public}
<Icon src={CHECK_ICON} alt={m.gateways_public()} preserveColor />
{:else}
<span class="text-red-500">
<Icon src={NO_ICON} alt={m.gateways_private()} />
</span>
{/if}
{:else if col.key === 'created_at'}
{formatDateTime(row.created_at, undefined, m.common_not_available())}
{:else}
{defaultValue}
{/if}
{/snippet}
</CwDataTable>
</CwCard>
</AppPage>
Loading