diff --git a/assets/src/components/detourListPage.tsx b/assets/src/components/detourListPage.tsx
index 0193a2513..fa605fecc 100644
--- a/assets/src/components/detourListPage.tsx
+++ b/assets/src/components/detourListPage.tsx
@@ -1,7 +1,11 @@
import React, { useCallback, useState } from "react"
-import { DetoursTable, DetourStatus } from "./detoursTable"
+import {
+ DetoursTable,
+ DetourStatus,
+ timestampLabelFromStatus,
+} from "./detoursTable"
import userInTestGroup, { TestGroups } from "../userInTestGroup"
-import { Button } from "react-bootstrap"
+import { Button, Table } from "react-bootstrap"
import {
GlobeAmericas,
LockFill,
@@ -16,6 +20,9 @@ import { isErr, isOk } from "../util/result"
import { isValidSnapshot } from "../util/isValidSnapshot"
import { createDetourMachine } from "../models/createDetourMachine"
import { joinClasses } from "../helpers/dom"
+import { RoutePill } from "./routePill"
+import { timeAgoLabelForDates } from "../util/dateTime"
+import { useCurrentTime } from "../hooks/useCurrentTime"
export const DetourListPage = () => {
const [showDetourModal, setShowDetourModal] = useState(false)
@@ -55,6 +62,8 @@ export const DetourListPage = () => {
setShowDetourModal(false)
}
+ const epochNow = useCurrentTime()
+
return (
{userInTestGroup(TestGroups.DetoursPilot) && (
@@ -75,12 +84,53 @@ export const DetourListPage = () => {
visibility="All Skate users"
classNames={["d-flex"]}
/>
-
+
+
+
+ Route and direction |
+
+ Starting Intersection
+ |
+
+ {timestampLabelFromStatus(DetourStatus.Active)}
+ |
+
+
+
+ {detours.active
+ ? detours.active.map((detour, index) => (
+ onOpenDetour(detour.details.id)}
+ >
+
+
+
+
+
+ {detour.details.name}
+
+
+ {detour.details.direction}
+
+
+
+ |
+
+ {detour.details.intersection}
+ |
+
+ {timeAgoLabelForDates(detour.activatedAt, epochNow)}
+ |
+
+ ))
+ : null}
+
+
{userInTestGroup(TestGroups.DetoursPilot) && (
<>
{
+export const useCurrentTime = (): Date => {
const [currentTime, setCurrentTime] = useState(now())
useInterval(() => setCurrentTime(now()), 1000)
return currentTime
diff --git a/assets/src/models/detoursList.ts b/assets/src/models/detoursList.ts
index 8df56eb96..1d004ea7a 100644
--- a/assets/src/models/detoursList.ts
+++ b/assets/src/models/detoursList.ts
@@ -1,4 +1,13 @@
-import { array, Infer, nullable, number, string, type } from "superstruct"
+import {
+ array,
+ coerce,
+ date,
+ Infer,
+ nullable,
+ number,
+ string,
+ type,
+} from "superstruct"
export type DetourId = number
export interface SimpleDetour {
@@ -10,6 +19,11 @@ export interface SimpleDetour {
updatedAt: number
}
+export interface ActivatedDetour {
+ activatedAt: Date
+ details: SimpleDetour
+}
+
export const detourId = number()
export const SimpleDetourData = type({
id: detourId,
@@ -20,6 +34,11 @@ export const SimpleDetourData = type({
updated_at: number(),
})
+export const ActivatedDetourData = type({
+ activated_at: coerce(date(), string(), (dateStr) => new Date(dateStr)),
+ details: SimpleDetourData,
+})
+
export type SimpleDetourData = Infer
export const simpleDetourFromData = (
@@ -34,13 +53,13 @@ export const simpleDetourFromData = (
})
export interface GroupedSimpleDetours {
- active?: SimpleDetour[]
+ active?: ActivatedDetour[]
draft?: SimpleDetour[]
past?: SimpleDetour[]
}
export const GroupedDetoursData = type({
- active: nullable(array(SimpleDetourData)),
+ active: nullable(array(ActivatedDetourData)),
draft: nullable(array(SimpleDetourData)),
past: nullable(array(SimpleDetourData)),
})
@@ -50,7 +69,10 @@ export type GroupedDetoursData = Infer
export const groupedDetoursFromData = (
groupedDetours: GroupedDetoursData
): GroupedSimpleDetours => ({
- active: groupedDetours.active?.map((detour) => simpleDetourFromData(detour)),
+ active: groupedDetours.active?.map((detour) => ({
+ details: simpleDetourFromData(detour.details),
+ activatedAt: detour.activated_at,
+ })),
draft: groupedDetours.draft?.map((detour) => simpleDetourFromData(detour)),
past: groupedDetours.past?.map((detour) => simpleDetourFromData(detour)),
})
diff --git a/assets/src/util/dateTime.ts b/assets/src/util/dateTime.ts
index 305331ede..6a0bf472a 100644
--- a/assets/src/util/dateTime.ts
+++ b/assets/src/util/dateTime.ts
@@ -104,3 +104,21 @@ export const timeAgoLabel = (
return diffHours >= 1 ? `${diffHours} hours ago` : `${diffMinutes} min ago`
}
+
+const second = 1000
+const minute = second * 60
+const hour = minute * 60
+
+export const timeAgoLabelForDates = (start: Date, end: Date) => {
+ const length = end.valueOf() - start.valueOf()
+
+ if (length > 1 * hour) {
+ return `${Math.floor(length / hour)} hours ago`
+ }
+
+ if (length > 1 * minute) {
+ return `${Math.floor(length / minute)} min ago`
+ }
+
+ return "Just now"
+}
diff --git a/assets/tests/components/detours/__snapshots__/detoursListPage.openDetour.test.tsx.snap b/assets/tests/components/detours/__snapshots__/detoursListPage.openDetour.test.tsx.snap
index 75670605e..aff965085 100644
--- a/assets/tests/components/detours/__snapshots__/detoursListPage.openDetour.test.tsx.snap
+++ b/assets/tests/components/detours/__snapshots__/detoursListPage.openDetour.test.tsx.snap
@@ -124,7 +124,7 @@ exports[`Detours Page: Open a Detour renders detour details in an open drawer on
- 26 hours ago
+ Just now
|
@@ -163,7 +163,7 @@ exports[`Detours Page: Open a Detour renders detour details in an open drawer on
- 26 hours ago
+ Just now
|
@@ -971,7 +971,7 @@ exports[`Detours Page: Open a Detour renders detour details modal to match mocke
- 26 hours ago
+ Just now
|
@@ -1010,7 +1010,7 @@ exports[`Detours Page: Open a Detour renders detour details modal to match mocke
- 26 hours ago
+ Just now
|
diff --git a/assets/tests/components/detours/detourListPage.test.tsx b/assets/tests/components/detours/detourListPage.test.tsx
index d0774286b..3f1a37785 100644
--- a/assets/tests/components/detours/detourListPage.test.tsx
+++ b/assets/tests/components/detours/detourListPage.test.tsx
@@ -35,20 +35,26 @@ describe("DetourListPage", () => {
Ok({
active: [
{
- id: 1,
- route: "1",
- direction: "Inbound",
- name: "Headsign A",
- intersection: "Street A & Avenue B",
- updatedAt: 1724866392,
+ details: {
+ id: 1,
+ route: "1",
+ direction: "Inbound",
+ name: "Headsign A",
+ intersection: "Street A & Avenue B",
+ updatedAt: 1724866392,
+ },
+ activatedAt: new Date(1724866392000),
},
{
- id: 8,
- route: "2",
- direction: "Outbound",
- name: "Headsign B",
- intersection: "Street C & Avenue D",
- updatedAt: 1724856392,
+ details: {
+ id: 8,
+ route: "2",
+ direction: "Outbound",
+ name: "Headsign B",
+ intersection: "Street C & Avenue D",
+ updatedAt: 1724856392,
+ },
+ activatedAt: new Date(1724856392000),
},
],
draft: undefined,
@@ -93,20 +99,26 @@ describe("DetourListPage", () => {
Ok({
active: [
{
- id: 1,
- route: "1",
- direction: "Inbound",
- name: "Headsign A",
- intersection: "Street A & Avenue B",
- updatedAt: 1724866392,
+ details: {
+ id: 1,
+ route: "1",
+ direction: "Inbound",
+ name: "Headsign A",
+ intersection: "Street A & Avenue B",
+ updatedAt: 1724866392,
+ },
+ activatedAt: new Date(1724866392000),
},
{
- id: 8,
- route: "2",
- direction: "Outbound",
- name: "Headsign B",
- intersection: "Street C & Avenue D",
- updatedAt: 1724856392,
+ details: {
+ id: 8,
+ route: "2",
+ direction: "Outbound",
+ name: "Headsign B",
+ intersection: "Street C & Avenue D",
+ updatedAt: 1724856392,
+ },
+ activatedAt: new Date(1724856392000),
},
],
draft: undefined,
diff --git a/assets/tests/factories/detourListFactory.ts b/assets/tests/factories/detourListFactory.ts
index d90ba8315..08dbaedd0 100644
--- a/assets/tests/factories/detourListFactory.ts
+++ b/assets/tests/factories/detourListFactory.ts
@@ -1,5 +1,6 @@
import { Factory } from "fishery"
import {
+ ActivatedDetour,
GroupedSimpleDetours,
SimpleDetour,
} from "../../src/models/detoursList"
@@ -7,8 +8,8 @@ import {
export const detourListFactory = Factory.define(() => {
return {
active: [
- simpleDetourFactory.build(),
- simpleDetourFactory.build({ direction: "Outbound" }),
+ activeDetourFactory.build(),
+ activeDetourFactory.build({ details: { direction: "Outbound" } }),
],
draft: undefined,
past: [simpleDetourFactory.build({ name: "Headsign Z" })],
@@ -23,3 +24,8 @@ const simpleDetourFactory = Factory.define(({ sequence }) => ({
intersection: `Street A${sequence} & Avenue B${sequence}`,
updatedAt: 1724866392,
}))
+
+const activeDetourFactory = Factory.define(() => ({
+ activatedAt: new Date(),
+ details: simpleDetourFactory.build(),
+}))
diff --git a/lib/skate/detours/detour.ex b/lib/skate/detours/detour.ex
index 53581c82c..d7758dce5 100644
--- a/lib/skate/detours/detour.ex
+++ b/lib/skate/detours/detour.ex
@@ -30,6 +30,59 @@ defmodule Skate.Detours.Detour do
:author_id,
:status
]
+
+ def from(
+ status,
+ %{
+ state: %{
+ "context" => %{
+ "route" => %{"name" => route_name, "directionNames" => direction_names},
+ "routePattern" => %{"headsign" => headsign, "directionId" => direction_id},
+ "nearestIntersection" => nearest_intersection
+ }
+ }
+ } = db_detour
+ ) do
+ direction = Map.get(direction_names, Integer.to_string(direction_id))
+
+ %__MODULE__{
+ id: db_detour.id,
+ route: route_name,
+ direction: direction,
+ name: headsign,
+ intersection: nearest_intersection,
+ updated_at: timestamp_to_unix(db_detour.updated_at),
+ author_id: db_detour.author_id,
+ status: status
+ }
+ end
+
+ def from(_status, _attrs), do: nil
+
+ # Converts the db timestamp to unix
+ defp timestamp_to_unix(db_date) do
+ db_date
+ |> DateTime.from_naive!("Etc/UTC")
+ |> DateTime.to_unix()
+ end
+ end
+
+ defmodule ActivatedDetourDetails do
+ @moduledoc """
+ Extended information for active detours
+ """
+
+ @type t :: %__MODULE__{
+ activated_at: DateTime.t(),
+ details: Detailed.t()
+ }
+
+ @derive Jason.Encoder
+
+ defstruct [
+ :activated_at,
+ :details
+ ]
end
defmodule WithState do
diff --git a/lib/skate/detours/detours.ex b/lib/skate/detours/detours.ex
index 53e0b2fc1..6d12bb2d1 100644
--- a/lib/skate/detours/detours.ex
+++ b/lib/skate/detours/detours.ex
@@ -4,6 +4,7 @@ defmodule Skate.Detours.Detours do
"""
import Ecto.Query, warn: false
+ alias Skate.Detours.Detour.ActivatedDetourDetails
alias Skate.Repo
alias Skate.Detours.Db.Detour
alias Skate.Detours.SnapshotSerde
@@ -50,7 +51,10 @@ defmodule Skate.Detours.Detours do
list_detours()
|> Enum.map(&db_detour_to_detour/1)
|> Enum.filter(& &1)
- |> Enum.group_by(fn detour -> detour.status end)
+ |> Enum.group_by(fn
+ %ActivatedDetourDetails{details: %{status: status}} -> status
+ %{status: status} -> status
+ end)
%{
active: Map.get(detours, :active),
@@ -61,29 +65,9 @@ defmodule Skate.Detours.Detours do
end
@spec db_detour_to_detour(Detour.t()) :: DetailedDetour.t() | nil
- def db_detour_to_detour(
- %{
- state: %{
- "context" => %{
- "route" => %{"name" => route_name, "directionNames" => direction_names},
- "routePattern" => %{"headsign" => headsign, "directionId" => direction_id},
- "nearestIntersection" => nearest_intersection
- }
- }
- } = db_detour
- ) do
- direction = Map.get(direction_names, Integer.to_string(direction_id))
-
- %DetailedDetour{
- id: db_detour.id,
- route: route_name,
- direction: direction,
- name: headsign,
- intersection: nearest_intersection,
- updated_at: timestamp_to_unix(db_detour.updated_at),
- author_id: db_detour.author_id,
- status: categorize_detour(db_detour)
- }
+ @spec db_detour_to_detour(status :: detour_type(), Detour.t()) :: DetailedDetour.t() | nil
+ def db_detour_to_detour(%{} = db_detour) do
+ db_detour_to_detour(categorize_detour(db_detour), db_detour)
end
def db_detour_to_detour(invalid_detour) do
@@ -94,6 +78,34 @@ defmodule Skate.Detours.Detours do
nil
end
+ def db_detour_to_detour(
+ :active,
+ %Detour{activated_at: activated_at} = db_detour
+ ) do
+ details = DetailedDetour.from(:active, db_detour)
+
+ details &&
+ %ActivatedDetourDetails{
+ activated_at: activated_at,
+ details: details
+ }
+ end
+
+ def db_detour_to_detour(
+ status,
+ %{} = db_detour
+ ) do
+ DetailedDetour.from(status, db_detour)
+ end
+
+ def db_detour_to_detour(state, invalid_detour) do
+ Sentry.capture_message("Detour error: the detour has an outdated schema",
+ extra: %{error: invalid_detour, state: state}
+ )
+
+ nil
+ end
+
@type detour_type :: :active | :draft | :past
@doc """
diff --git a/test/skate_web/controllers/detours_controller_test.exs b/test/skate_web/controllers/detours_controller_test.exs
index bc2097dba..84363d12d 100644
--- a/test/skate_web/controllers/detours_controller_test.exs
+++ b/test/skate_web/controllers/detours_controller_test.exs
@@ -146,24 +146,28 @@ defmodule SkateWeb.DetoursControllerTest do
defp populate_db_and_get_user(conn) do
# Active detour
put(conn, "/api/detours/update_snapshot", %{
- "snapshot" => %{
- "context" => %{
- "route" => %{
- "name" => "23",
- "directionNames" => %{
- "0" => "Outbound",
- "1" => "Inbound"
- }
- },
- "routePattern" => %{
- "headsign" => "Headsign",
- "directionId" => 0
+ "snapshot" =>
+ activated(
+ %{
+ "context" => %{
+ "route" => %{
+ "name" => "23",
+ "directionNames" => %{
+ "0" => "Outbound",
+ "1" => "Inbound"
+ }
+ },
+ "routePattern" => %{
+ "headsign" => "Headsign",
+ "directionId" => 0
+ },
+ "nearestIntersection" => "Street A & Avenue B",
+ "uuid" => 1
+ },
+ "value" => %{"Detour Drawing" => %{"Active" => "Reviewing"}}
},
- "nearestIntersection" => "Street A & Avenue B",
- "uuid" => 1
- },
- "value" => %{"Detour Drawing" => %{"Active" => "Reviewing"}}
- }
+ ~U[2024-01-01 13:00:00.000000Z]
+ )
})
# Past detour
@@ -364,13 +368,15 @@ defmodule SkateWeb.DetoursControllerTest do
"data" => %{
"active" => [
%{
- "author_id" => ^author_id,
- "direction" => "Outbound",
- "intersection" => "Street A & Avenue B",
- "name" => "Headsign",
- "route" => "23",
- "status" => "active",
- "updated_at" => _
+ "details" => %{
+ "author_id" => ^author_id,
+ "direction" => "Outbound",
+ "intersection" => "Street A & Avenue B",
+ "name" => "Headsign",
+ "route" => "23",
+ "status" => "active",
+ "updated_at" => _
+ }
}
],
"draft" => [
@@ -429,15 +435,7 @@ defmodule SkateWeb.DetoursControllerTest do
assert %{
"data" => %{
"active" => [
- %{
- "author_id" => ^current_user_id,
- "direction" => "Outbound",
- "intersection" => "Street A & Avenue B",
- "name" => "Headsign",
- "route" => "23",
- "status" => "active",
- "updated_at" => _
- }
+ _
],
"draft" => [
%{
@@ -514,13 +512,16 @@ defmodule SkateWeb.DetoursControllerTest do
"data" => %{
"active" => [
%{
- "author_id" => ^author_id,
- "direction" => "Outbound",
- "intersection" => "Street A & Avenue B",
- "name" => "Headsign",
- "route" => "23",
- "status" => "active",
- "updated_at" => _
+ "activated_at" => "2024-01-01T13:00:00.000000Z",
+ "details" => %{
+ "author_id" => ^author_id,
+ "direction" => "Outbound",
+ "intersection" => "Street A & Avenue B",
+ "name" => "Headsign",
+ "route" => "23",
+ "status" => "active",
+ "updated_at" => _
+ }
}
],
"draft" => [