feat: /status page
This commit is contained in:
parent
396a39372d
commit
54cd3c259b
|
@ -38,6 +38,28 @@ type Dashboard {
|
|||
"""Date serialized as the Unix timestamp."""
|
||||
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 {
|
||||
"""
|
||||
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"
|
||||
"""
|
||||
label: String!
|
||||
lastUpdated: Date
|
||||
}
|
||||
|
||||
type ProbabilityOption {
|
||||
|
@ -93,6 +116,7 @@ type Query {
|
|||
|
||||
"""Get a list of questions that are currently on the frontpage"""
|
||||
frontpage: [Question!]!
|
||||
platforms: [Platform!]!
|
||||
|
||||
"""Look up a single question by its id"""
|
||||
question(id: ID!): Question!
|
||||
|
@ -114,8 +138,9 @@ type QueryQuestionsConnectionEdge {
|
|||
node: Question!
|
||||
}
|
||||
|
||||
type Question {
|
||||
type Question implements QuestionShape {
|
||||
description: String!
|
||||
history: [History!]!
|
||||
|
||||
"""Unique string which identifies the question"""
|
||||
id: ID!
|
||||
|
@ -134,6 +159,22 @@ type Question {
|
|||
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 {
|
||||
"""List of platform ids to filter by"""
|
||||
forecastingPlatforms: [String!]
|
||||
|
|
File diff suppressed because one or more lines are too long
55
src/graphql/schema/platforms.ts
Normal file
55
src/graphql/schema/platforms.ts
Normal 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);
|
||||
},
|
||||
})
|
||||
);
|
|
@ -1,36 +1,9 @@
|
|||
import { History, Question } from "@prisma/client";
|
||||
|
||||
import { prisma } from "../../backend/database/prisma";
|
||||
import { platforms, QualityIndicators } from "../../backend/platforms";
|
||||
import { QualityIndicators } from "../../backend/platforms";
|
||||
import { builder } from "../builder";
|
||||
|
||||
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;
|
||||
},
|
||||
}),
|
||||
}),
|
||||
});
|
||||
import { PlatformObj } from "./platforms";
|
||||
|
||||
export const QualityIndicatorsObj = builder
|
||||
.objectRef<QualityIndicators>("QualityIndicators")
|
||||
|
|
|
@ -43,6 +43,23 @@ export type Dashboard = {
|
|||
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 = {
|
||||
__typename?: 'Mutation';
|
||||
/** 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'];
|
||||
/** Platform name for displaying on frontend etc., e.g. "X-risk estimates" */
|
||||
label: Scalars['String'];
|
||||
lastUpdated?: Maybe<Scalars['Date']>;
|
||||
};
|
||||
|
||||
export type ProbabilityOption = {
|
||||
|
@ -99,6 +117,7 @@ export type Query = {
|
|||
dashboard: Dashboard;
|
||||
/** Get a list of questions that are currently on the frontpage */
|
||||
frontpage: Array<Question>;
|
||||
platforms: Array<Platform>;
|
||||
/** Look up a single question by its id */
|
||||
question: Question;
|
||||
questions: QueryQuestionsConnection;
|
||||
|
@ -141,9 +160,10 @@ export type QueryQuestionsConnectionEdge = {
|
|||
node: Question;
|
||||
};
|
||||
|
||||
export type Question = {
|
||||
export type Question = QuestionShape & {
|
||||
__typename?: 'Question';
|
||||
description: Scalars['String'];
|
||||
history: Array<History>;
|
||||
/** Unique string which identifies the question */
|
||||
id: Scalars['ID'];
|
||||
options: Array<ProbabilityOption>;
|
||||
|
@ -157,6 +177,18 @@ export type Question = {
|
|||
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 = {
|
||||
/** List of platform ids to filter by */
|
||||
forecastingPlatforms?: InputMaybe<Array<Scalars['String']>>;
|
||||
|
|
1
src/pages/status.tsx
Normal file
1
src/pages/status.tsx
Normal file
|
@ -0,0 +1 @@
|
|||
export { default } from "../web/status/pages/StatusPage";
|
49
src/web/status/pages/StatusPage.tsx
Normal file
49
src/web/status/pages/StatusPage.tsx
Normal 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;
|
10
src/web/status/queries.generated.tsx
Normal file
10
src/web/status/queries.generated.tsx
Normal 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>;
|
7
src/web/status/queries.graphql
Normal file
7
src/web/status/queries.graphql
Normal file
|
@ -0,0 +1,7 @@
|
|||
query PlatformsStatus {
|
||||
result: platforms {
|
||||
id
|
||||
label
|
||||
lastUpdated
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user