feat: /status page
This commit is contained in:
parent
396a39372d
commit
54cd3c259b
|
@ -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
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 { 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")
|
||||||
|
|
|
@ -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
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