feat: /status page

This commit is contained in:
Vyacheslav Matyukhin 2022-04-27 23:02:14 +04:00
parent 396a39372d
commit 54cd3c259b
No known key found for this signature in database
GPG Key ID: 3D2A774C5489F96C
9 changed files with 200 additions and 32 deletions

View File

@ -38,6 +38,28 @@ type Dashboard {
"""Date serialized as the Unix timestamp.""" """Date serialized as the Unix timestamp."""
scalar Date scalar Date
type History implements QuestionShape {
description: String!
"""History items are identified by their integer ids"""
id: ID!
options: [ProbabilityOption!]!
platform: Platform!
qualityIndicators: QualityIndicators!
"""Unique string which identifies the question"""
questionId: ID!
"""Timestamp at which metaforecast fetched the question"""
timestamp: Date!
title: String!
"""
Non-unique, a very small number of platforms have a page for more than one prediction
"""
url: String!
}
type Mutation { type Mutation {
""" """
Create a new dashboard; if the dashboard with given ids already exists then it will be returned instead. Create a new dashboard; if the dashboard with given ids already exists then it will be returned instead.
@ -63,6 +85,7 @@ type Platform {
Platform name for displaying on frontend etc., e.g. "X-risk estimates" Platform name for displaying on frontend etc., e.g. "X-risk estimates"
""" """
label: String! label: String!
lastUpdated: Date
} }
type ProbabilityOption { type ProbabilityOption {
@ -93,6 +116,7 @@ type Query {
"""Get a list of questions that are currently on the frontpage""" """Get a list of questions that are currently on the frontpage"""
frontpage: [Question!]! frontpage: [Question!]!
platforms: [Platform!]!
"""Look up a single question by its id""" """Look up a single question by its id"""
question(id: ID!): Question! question(id: ID!): Question!
@ -114,8 +138,9 @@ type QueryQuestionsConnectionEdge {
node: Question! node: Question!
} }
type Question { type Question implements QuestionShape {
description: String! description: String!
history: [History!]!
"""Unique string which identifies the question""" """Unique string which identifies the question"""
id: ID! id: ID!
@ -134,6 +159,22 @@ type Question {
visualization: String visualization: String
} }
interface QuestionShape {
description: String!
options: [ProbabilityOption!]!
platform: Platform!
qualityIndicators: QualityIndicators!
"""Timestamp at which metaforecast fetched the question"""
timestamp: Date!
title: String!
"""
Non-unique, a very small number of platforms have a page for more than one prediction
"""
url: String!
}
input SearchInput { input SearchInput {
"""List of platform ids to filter by""" """List of platform ids to filter by"""
forecastingPlatforms: [String!] forecastingPlatforms: [String!]

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,55 @@
import { prisma } from "../../backend/database/prisma";
import { platforms } from "../../backend/platforms";
import { builder } from "../builder";
export const PlatformObj = builder.objectRef<string>("Platform").implement({
description: "Forecasting platform supported by Metaforecast",
fields: (t) => ({
id: t.id({
description: 'Short unique platform name, e.g. "xrisk"',
resolve: (x) => x,
}),
label: t.string({
description:
'Platform name for displaying on frontend etc., e.g. "X-risk estimates"',
resolve: (platformName) => {
if (platformName === "metaforecast") {
return "Metaforecast";
}
if (platformName === "guesstimate") {
return "Guesstimate";
}
// kinda slow and repetitive, TODO - store a map {name => platform} somewhere and `getPlatform` util function?
const platform = platforms.find((p) => p.name === platformName);
if (!platform) {
throw new Error(`Unknown platform ${platformName}`);
}
return platform.label;
},
}),
lastUpdated: t.field({
type: "Date",
nullable: true,
resolve: async (platformName) => {
const res = await prisma.question.aggregate({
where: {
platform: platformName,
},
_max: {
timestamp: true,
},
});
return res._max.timestamp;
},
}),
}),
});
builder.queryField("platforms", (t) =>
t.field({
type: [PlatformObj],
resolve: async (parent, args) => {
return platforms.map((platform) => platform.name);
},
})
);

View File

@ -1,36 +1,9 @@
import { History, Question } from "@prisma/client"; import { History, Question } from "@prisma/client";
import { prisma } from "../../backend/database/prisma"; import { prisma } from "../../backend/database/prisma";
import { platforms, QualityIndicators } from "../../backend/platforms"; import { QualityIndicators } from "../../backend/platforms";
import { builder } from "../builder"; import { builder } from "../builder";
import { PlatformObj } from "./platforms";
const PlatformObj = builder.objectRef<string>("Platform").implement({
description: "Forecasting platform supported by Metaforecast",
fields: (t) => ({
id: t.id({
description: 'Short unique platform name, e.g. "xrisk"',
resolve: (x) => x,
}),
label: t.string({
description:
'Platform name for displaying on frontend etc., e.g. "X-risk estimates"',
resolve: (platformName) => {
if (platformName === "metaforecast") {
return "Metaforecast";
}
if (platformName === "guesstimate") {
return "Guesstimate";
}
// kinda slow and repetitive, TODO - store a map {name => platform} somewhere and `getPlatform` util function?
const platform = platforms.find((p) => p.name === platformName);
if (!platform) {
throw new Error(`Unknown platform ${platformName}`);
}
return platform.label;
},
}),
}),
});
export const QualityIndicatorsObj = builder export const QualityIndicatorsObj = builder
.objectRef<QualityIndicators>("QualityIndicators") .objectRef<QualityIndicators>("QualityIndicators")

View File

@ -43,6 +43,23 @@ export type Dashboard = {
title: Scalars['String']; title: Scalars['String'];
}; };
export type History = QuestionShape & {
__typename?: 'History';
description: Scalars['String'];
/** History items are identified by their integer ids */
id: Scalars['ID'];
options: Array<ProbabilityOption>;
platform: Platform;
qualityIndicators: QualityIndicators;
/** Unique string which identifies the question */
questionId: Scalars['ID'];
/** Timestamp at which metaforecast fetched the question */
timestamp: Scalars['Date'];
title: Scalars['String'];
/** Non-unique, a very small number of platforms have a page for more than one prediction */
url: Scalars['String'];
};
export type Mutation = { export type Mutation = {
__typename?: 'Mutation'; __typename?: 'Mutation';
/** Create a new dashboard; if the dashboard with given ids already exists then it will be returned instead. */ /** Create a new dashboard; if the dashboard with given ids already exists then it will be returned instead. */
@ -69,6 +86,7 @@ export type Platform = {
id: Scalars['ID']; id: Scalars['ID'];
/** Platform name for displaying on frontend etc., e.g. "X-risk estimates" */ /** Platform name for displaying on frontend etc., e.g. "X-risk estimates" */
label: Scalars['String']; label: Scalars['String'];
lastUpdated?: Maybe<Scalars['Date']>;
}; };
export type ProbabilityOption = { export type ProbabilityOption = {
@ -99,6 +117,7 @@ export type Query = {
dashboard: Dashboard; dashboard: Dashboard;
/** Get a list of questions that are currently on the frontpage */ /** Get a list of questions that are currently on the frontpage */
frontpage: Array<Question>; frontpage: Array<Question>;
platforms: Array<Platform>;
/** Look up a single question by its id */ /** Look up a single question by its id */
question: Question; question: Question;
questions: QueryQuestionsConnection; questions: QueryQuestionsConnection;
@ -141,9 +160,10 @@ export type QueryQuestionsConnectionEdge = {
node: Question; node: Question;
}; };
export type Question = { export type Question = QuestionShape & {
__typename?: 'Question'; __typename?: 'Question';
description: Scalars['String']; description: Scalars['String'];
history: Array<History>;
/** Unique string which identifies the question */ /** Unique string which identifies the question */
id: Scalars['ID']; id: Scalars['ID'];
options: Array<ProbabilityOption>; options: Array<ProbabilityOption>;
@ -157,6 +177,18 @@ export type Question = {
visualization?: Maybe<Scalars['String']>; visualization?: Maybe<Scalars['String']>;
}; };
export type QuestionShape = {
description: Scalars['String'];
options: Array<ProbabilityOption>;
platform: Platform;
qualityIndicators: QualityIndicators;
/** Timestamp at which metaforecast fetched the question */
timestamp: Scalars['Date'];
title: Scalars['String'];
/** Non-unique, a very small number of platforms have a page for more than one prediction */
url: Scalars['String'];
};
export type SearchInput = { export type SearchInput = {
/** List of platform ids to filter by */ /** List of platform ids to filter by */
forecastingPlatforms?: InputMaybe<Array<Scalars['String']>>; forecastingPlatforms?: InputMaybe<Array<Scalars['String']>>;

1
src/pages/status.tsx Normal file
View File

@ -0,0 +1 @@
export { default } from "../web/status/pages/StatusPage";

View File

@ -0,0 +1,49 @@
import { NextPage } from "next";
import { Query } from "../../common/Query";
import { Layout } from "../../display/Layout";
import { PlatformsStatusDocument } from "../queries.generated";
const StatusPage: NextPage = () => {
return (
<Layout page="status">
<Query document={PlatformsStatusDocument}>
{({ data }) => (
<table className="table-auto border-collapse border border-gray-200 bg-white mx-auto mb-10">
<thead>
<tr className="bg-gray-100">
<th className="border border-gray-200 p-4">Platform</th>
<th className="border border-gray-200 p-4">Last updated</th>
</tr>
</thead>
<tbody>
{data.result.map((platform) => {
const ts = platform.lastUpdated
? new Date(platform.lastUpdated * 1000)
: null;
const isStale =
!ts || new Date().getTime() - ts.getTime() < 2 * 86400 * 1000;
return (
<tr key={platform.id}>
<td
className={`border border-gray-200 p-4 ${
isStale ? "bg-red-300" : ""
}`}
>
{platform.label}
</td>
<td className="border border-gray-200 p-4">
<div className="text-sm">{ts ? String(ts) : null}</div>
</td>
</tr>
);
})}
</tbody>
</table>
)}
</Query>
</Layout>
);
};
export default StatusPage;

View File

@ -0,0 +1,10 @@
import * as Types from '../../graphql/types.generated';
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
export type PlatformsStatusQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type PlatformsStatusQuery = { __typename?: 'Query', result: Array<{ __typename?: 'Platform', id: string, label: string, lastUpdated?: number | null }> };
export const PlatformsStatusDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PlatformsStatus"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"result"},"name":{"kind":"Name","value":"platforms"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdated"}}]}}]}}]} as unknown as DocumentNode<PlatformsStatusQuery, PlatformsStatusQueryVariables>;

View File

@ -0,0 +1,7 @@
query PlatformsStatus {
result: platforms {
id
label
lastUpdated
}
}