Skip to content

Commit

Permalink
feat: all time range
Browse files Browse the repository at this point in the history
Signed-off-by: Henry Gressmann <mail@henrygressmann.de>
  • Loading branch information
explodingcamera committed Nov 21, 2024
1 parent 5fad021 commit 5c069e0
Show file tree
Hide file tree
Showing 10 changed files with 128 additions and 13 deletions.
21 changes: 20 additions & 1 deletion src/app/core/reports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,12 +211,31 @@ fn metric_sql(metric: Metric) -> String {
Metric::AvgTimeOnSite => {
// avg time_until_next_event where time_until_next_event <= 1800 and time_until_next_event is not null
"--sql
avg(sd.time_until_next_event) filter (where sd.time_until_next_event is not null and sd.time_until_next_event <= 1800)"
coalesce(avg(sd.time_until_next_event) filter (where sd.time_until_next_event is not null and sd.time_until_next_event <= 1800), 0)"
}
}
.to_owned()
}

pub fn earliest_timestamp(conn: &DuckDBConn, entities: &[String]) -> Result<Option<time::OffsetDateTime>> {
if entities.is_empty() {
return Ok(None);
}

let vars = repeat_vars(entities.len());
let query = format!(
"--sql
select min(created_at) from events
where entity_id in ({vars});
"
);

let mut stmt = conn.prepare_cached(&query)?;
let rows = stmt.query_map(params_from_iter(entities), |row| row.get(0))?;
let earliest_timestamp = rows.collect::<Result<Vec<Option<time::OffsetDateTime>>, duckdb::Error>>()?;
Ok(earliest_timestamp[0])
}

pub fn online_users(conn: &DuckDBConn, entities: &[String]) -> Result<u64> {
if entities.is_empty() {
return Ok(0);
Expand Down
19 changes: 19 additions & 0 deletions src/web/routes/dashboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,25 @@ pub struct DashboardAPI;

#[OpenApi]
impl DashboardAPI {
#[oai(path = "/project/:project_id/earliest", method = "get")]
async fn project_earliest_handler(
&self,
Path(project_id): Path<String>,
Data(app): Data<&Liwan>,
user: Option<SessionUser>,
) -> ApiResult<Json<Option<time::OffsetDateTime>>> {
let project = app.projects.get(&project_id).http_status(StatusCode::NOT_FOUND)?;

if !can_access_project(&project, user.as_ref()) {
http_bail!(StatusCode::NOT_FOUND, "Project not found")
}

let conn = app.events_conn().http_status(StatusCode::INTERNAL_SERVER_ERROR)?;
let entities = app.projects.entity_ids(&project.id).http_status(StatusCode::INTERNAL_SERVER_ERROR)?;
let earliest = reports::earliest_timestamp(&conn, &entities).http_status(StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(earliest))
}

#[oai(path = "/project/:project_id/graph", method = "post")]
async fn project_graph_handler(
&self,
Expand Down
2 changes: 1 addition & 1 deletion web/src/api/dashboard.ts

Large diffs are not rendered by default.

45 changes: 38 additions & 7 deletions web/src/api/ranges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
addYears,
differenceInDays,
differenceInHours,
differenceInMonths,
differenceInSeconds,
differenceInYears,
endOfDay,
Expand Down Expand Up @@ -37,11 +38,11 @@ type DateRangeValue = { start: Date; end: Date };

export class DateRange {
#value: RangeName | { start: Date; end: Date };
label: string;
variant?: string;

constructor(value: RangeName | { start: Date; end: Date }) {
this.#value = value;
this.label = "";
if (typeof value === "string") this.variant = value;
}

get value(): DateRangeValue {
Expand All @@ -52,10 +53,11 @@ export class DateRange {
}

isCustom(): boolean {
return typeof this.#value !== "string";
return typeof this.#value !== "string" && !this.variant;
}

format(): string {
if (this.variant === "allTime") return "All Time";
if (typeof this.#value === "string") return wellKnownRanges[this.#value];
return formatDateRange(this.#value.start, this.#value.end);
}
Expand All @@ -66,15 +68,19 @@ export class DateRange {

serialize(): string {
if (typeof this.#value === "string") return this.#value;
return `${Number(this.#value.start)}:${Number(this.#value.end)}`;
return `${Number(this.#value.start)}:${Number(this.#value.end)}:${this.variant}`;
}

static deserialize(range: string): DateRange {
if (!range.includes(":")) {
return new DateRange(range as RangeName);
}
const [start, end] = range.split(":").map((v) => new Date(Number(v)));
return new DateRange({ start, end });
const [start, end, variant] = range.split(":");
const dr = new DateRange({ start: new Date(Number(start)), end: new Date(Number(end)) });
if (variant) {
dr.variant = variant;
}
return dr;
}

endsToday(): boolean {
Expand Down Expand Up @@ -118,6 +124,7 @@ export class DateRange {
}

previous() {
if (this.variant === "allTime") return this;
if (this.#value === "today") return new DateRange("yesterday");

if (
Expand Down Expand Up @@ -150,6 +157,18 @@ export class DateRange {
return new DateRange({ start, end });
}

if (differenceInMonths(this.value.end, this.value.start) === 12) {
// if (isSameDay(this.value.start, startOfMonth(this.value.start))) {
// const start = startOfMonth(subYears(this.value.start, 1));
// const end = endOfMonth(subYears(this.value.end, 1));
// return new DateRange({ start, end });
// }

const start = subYears(this.value.start, 1);
const end = subYears(this.value.end, 1);
return new DateRange({ start, end });
}

if (differenceInHours(this.value.end, this.value.start) < 23) {
const start = subSeconds(this.value.start, differenceInSeconds(this.value.end, this.value.start));
const end = subSeconds(this.value.end, differenceInSeconds(this.value.end, this.value.start));
Expand Down Expand Up @@ -198,6 +217,18 @@ export class DateRange {
return new DateRange({ start, end });
}

if (differenceInMonths(this.value.end, this.value.start) === 12) {
// if (isSameDay(this.value.start, startOfMonth(this.value.start))) {
// const start = startOfMonth(addYears(this.value.start, 1));
// const end = endOfMonth(addYears(this.value.end, 1));
// return new DateRange({ start, end });
// }

const start = addYears(this.value.start, 1);
const end = addYears(this.value.end, 1);
return new DateRange({ start, end });
}

if (differenceInHours(this.value.end, this.value.start) < 23) {
const start = addSeconds(this.value.start, differenceInSeconds(this.value.end, this.value.start));
const end = addSeconds(this.value.end, differenceInSeconds(this.value.end, this.value.start));
Expand Down Expand Up @@ -247,8 +278,8 @@ export const ranges: Record<RangeName, () => { range: { start: Date; end: Date }
last7Days: () => ({ range: lastXDays(7) }),
last30Days: () => ({ range: lastXDays(30) }),
last12Months: () => {
const start = startOfMonth(subYears(new Date(), 1));
const end = endOfMonth(new Date());
const start = subMonths(end, 11);
return { range: { start, end } };
},
weekToDate: () => {
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/project.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export const Project = () => {
<div>
<div className={styles.projectHeader}>
<ProjectHeader project={project} stats={stats.data} />
<SelectRange onSelect={(range) => setRangeString(range.serialize())} range={range} />
<SelectRange onSelect={(range) => setRangeString(range.serialize())} range={range} projectId={project.id} />
</div>
<SelectMetrics data={stats.data} metric={metric} setMetric={setMetric} className={styles.projectStats} />
<SelectFilters value={filters} onChange={setFilters} />
Expand Down
12 changes: 12 additions & 0 deletions web/src/components/project/range.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@
color: var(--pico-h1-background-color);
}
}

ul {
li {
padding: 0;

button {
box-sizing: border-box;
width: 100%;
padding: 0.4rem var(--pico-form-element-spacing-horizontal);
}
}
}
}

details.selectRange {
Expand Down
36 changes: 35 additions & 1 deletion web/src/components/project/range.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,38 @@ import { cls } from "../../utils";
import { Dialog } from "../dialog";
import { DatePickerRange } from "../daterange";
import { DateRange, wellKnownRanges, type RangeName } from "../../api/ranges";
import { api, useQuery } from "../../api";
import { endOfDay, startOfDay } from "date-fns";

export const SelectRange = ({ onSelect, range }: { onSelect: (range: DateRange) => void; range: DateRange }) => {
export const SelectRange = ({
onSelect,
range,
projectId,
}: { onSelect: (range: DateRange) => void; range: DateRange; projectId?: string }) => {
const detailsRef = useRef<HTMLDetailsElement>(null);

const handleSelect = (range: DateRange) => () => {
if (detailsRef.current) detailsRef.current.open = false;
onSelect(range);
};

const allTime = useQuery({
queryKey: ["allTime", projectId],
enabled: !!projectId,
staleTime: 7 * 24 * 60 * 60 * 1000,
queryFn: () =>
api["/api/dashboard/project/{project_id}/earliest"].get({ params: { project_id: projectId || "" } }).json(),
});

const selectAllTime = async () => {
if (!projectId) return;
if (!allTime.data) return;
const from = new Date(allTime.data);
const range = new DateRange({ start: startOfDay(from), end: endOfDay(new Date()) });
range.variant = "allTime";
onSelect(range);
};

return (
<div className={styles.container}>
<button type="button" className="secondary" onClick={handleSelect(range.previous())}>
Expand All @@ -38,6 +61,17 @@ export const SelectRange = ({ onSelect, range }: { onSelect: (range: DateRange)
</button>
</li>
))}
{projectId && allTime.data && (
<li>
<button
type="button"
className={range.variant === "allTime" ? styles.selected : ""}
onClick={selectAllTime}
>
All Time
</button>
</li>
)}
<li>
<Dialog
className={styles.rangeDialog}
Expand Down
1 change: 0 additions & 1 deletion web/src/components/projects.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ const NoProjects = () => {
export const Projects = () => {
const { data, isLoading, isError } = useQuery({
queryKey: ["projects"],

queryFn: () => api["/api/dashboard/projects"].get().json(),
});

Expand Down
2 changes: 1 addition & 1 deletion web/src/components/worldmap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export const WorldMap = ({
<div className={styles.tooltip} data-theme="dark">
<h2>{metricNames[metric]}</h2>
<h3>
{currentGeo.name} <span>{formatMetricVal(countries.get(currentGeo.iso) ?? 0)}</span>
{currentGeo.name} <span>{formatMetricVal(countries.get(currentGeo.iso) ?? 0, metric)}</span>
</h3>
</div>
)}
Expand Down
1 change: 1 addition & 0 deletions web/src/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
Ubuntu, Cantarell, Helvetica, Arial, "Helvetica Neue", sans-serif,
var(--pico-font-family-emoji);
--pico-font-family-sans-serif: var(--pico-font-family);
font-variant-numeric: tabular-nums;
}

:root[data-theme="dark"] {
Expand Down

0 comments on commit 5c069e0

Please sign in to comment.