feat: a dashboard for clients, behind a secret link
Owners and admins turn it on in Configuración → General, pick what it
shows (tickets per status, the sprint under way, release forecasts,
open epics and activity per day) and can renew or drop the link. Whoever
has it sees /share/{token} without an account: only those blocks, no
people nor single tickets but the epics, no links into the app, read
again every minute. A renewed or dropped link says it no longer works.
REST (GET/PUT/DELETE /api/projects/{project}/share and the public
GET /api/shared/dashboards/{token}) and GraphQL (dashboardShare,
shareDashboard, unshareDashboard, sharedDashboard). The token is kept
in dashboard_shares and goes with the project.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -329,6 +329,15 @@ export type SavedFilterCriteriaInput = {
|
||||
unassigned?: boolean | null | undefined;
|
||||
};
|
||||
|
||||
/** The project's link for clients */
|
||||
export type ShareDashboardInput = {
|
||||
/** Blocks to show: flow, sprint, releases, epics, activity */
|
||||
blocks: Array<string>;
|
||||
projectId: string;
|
||||
/** A new secret: the old link stops working */
|
||||
renew?: boolean | null | undefined;
|
||||
};
|
||||
|
||||
/** PLANNED, ACTIVE (one per project at a time) or CLOSED */
|
||||
export type SprintStatus = 'ACTIVE' | 'CLOSED' | 'PLANNED';
|
||||
|
||||
@@ -2437,6 +2446,98 @@ export type DeleteMyViewMutationVariables = Exact<{
|
||||
|
||||
export type DeleteMyViewMutation = { deleteMyView: boolean };
|
||||
|
||||
export type DashboardShareFieldsFragment = {
|
||||
projectId: string;
|
||||
token: string;
|
||||
blocks: Array<string>;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
export type DashboardShareQueryVariables = Exact<{
|
||||
projectId: string;
|
||||
}>;
|
||||
|
||||
export type DashboardShareQuery = {
|
||||
dashboardShare: {
|
||||
projectId: string;
|
||||
token: string;
|
||||
blocks: Array<string>;
|
||||
createdAt: string;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type ShareDashboardMutationVariables = Exact<{
|
||||
input: ShareDashboardInput;
|
||||
}>;
|
||||
|
||||
export type ShareDashboardMutation = {
|
||||
shareDashboard: {
|
||||
projectId: string;
|
||||
token: string;
|
||||
blocks: Array<string>;
|
||||
createdAt: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type UnshareDashboardMutationVariables = Exact<{
|
||||
projectId: string;
|
||||
}>;
|
||||
|
||||
export type UnshareDashboardMutation = { unshareDashboard: boolean };
|
||||
|
||||
export type SharedDashboardQueryVariables = Exact<{
|
||||
token: string;
|
||||
}>;
|
||||
|
||||
export type SharedDashboardQuery = {
|
||||
sharedDashboard: {
|
||||
projectKey: string;
|
||||
projectName: string;
|
||||
generatedAt: string;
|
||||
blocks: Array<string>;
|
||||
flow: Array<{
|
||||
status: string;
|
||||
name: string;
|
||||
color: string;
|
||||
count: number;
|
||||
}> | null;
|
||||
sprint: {
|
||||
sprintId: string;
|
||||
name: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
totalDays: number;
|
||||
daysLeft: number;
|
||||
tickets: number;
|
||||
doneTickets: number;
|
||||
points: number;
|
||||
donePoints: number;
|
||||
pacePerDay: number | null;
|
||||
neededPerDay: number | null;
|
||||
behind: boolean;
|
||||
} | null;
|
||||
releases: Array<{
|
||||
releaseId: string;
|
||||
name: string;
|
||||
targetDate: string | null;
|
||||
total: number;
|
||||
finished: number;
|
||||
likelyDate: string | null;
|
||||
safeDate: string | null;
|
||||
verdict: string;
|
||||
}> | null;
|
||||
epics: Array<{
|
||||
key: string;
|
||||
title: string;
|
||||
dueDate: string | null;
|
||||
done: number;
|
||||
total: number;
|
||||
percent: number;
|
||||
}> | null;
|
||||
activity: Array<{ day: string; count: number }> | null;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type SprintFieldsFragment = {
|
||||
id: string;
|
||||
projectId: string;
|
||||
@@ -3948,6 +4049,28 @@ export const SavedFilterFieldsFragmentDoc = {
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<SavedFilterFieldsFragment, unknown>;
|
||||
export const DashboardShareFieldsFragmentDoc = {
|
||||
kind: 'Document',
|
||||
definitions: [
|
||||
{
|
||||
kind: 'FragmentDefinition',
|
||||
name: { kind: 'Name', value: 'DashboardShareFields' },
|
||||
typeCondition: {
|
||||
kind: 'NamedType',
|
||||
name: { kind: 'Name', value: 'DashboardShare' },
|
||||
},
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'projectId' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'token' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'blocks' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'createdAt' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<DashboardShareFieldsFragment, unknown>;
|
||||
export const SprintFieldsFragmentDoc = {
|
||||
kind: 'Document',
|
||||
definitions: [
|
||||
@@ -14377,6 +14500,391 @@ export const DeleteMyViewDocument = {
|
||||
DeleteMyViewMutation,
|
||||
DeleteMyViewMutationVariables
|
||||
>;
|
||||
export const DashboardShareDocument = {
|
||||
kind: 'Document',
|
||||
definitions: [
|
||||
{
|
||||
kind: 'OperationDefinition',
|
||||
operation: 'query',
|
||||
name: { kind: 'Name', value: 'DashboardShare' },
|
||||
variableDefinitions: [
|
||||
{
|
||||
kind: 'VariableDefinition',
|
||||
variable: {
|
||||
kind: 'Variable',
|
||||
name: { kind: 'Name', value: 'projectId' },
|
||||
},
|
||||
type: {
|
||||
kind: 'NonNullType',
|
||||
type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } },
|
||||
},
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'dashboardShare' },
|
||||
arguments: [
|
||||
{
|
||||
kind: 'Argument',
|
||||
name: { kind: 'Name', value: 'projectId' },
|
||||
value: {
|
||||
kind: 'Variable',
|
||||
name: { kind: 'Name', value: 'projectId' },
|
||||
},
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'FragmentSpread',
|
||||
name: { kind: 'Name', value: 'DashboardShareFields' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'FragmentDefinition',
|
||||
name: { kind: 'Name', value: 'DashboardShareFields' },
|
||||
typeCondition: {
|
||||
kind: 'NamedType',
|
||||
name: { kind: 'Name', value: 'DashboardShare' },
|
||||
},
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'projectId' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'token' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'blocks' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'createdAt' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<DashboardShareQuery, DashboardShareQueryVariables>;
|
||||
export const ShareDashboardDocument = {
|
||||
kind: 'Document',
|
||||
definitions: [
|
||||
{
|
||||
kind: 'OperationDefinition',
|
||||
operation: 'mutation',
|
||||
name: { kind: 'Name', value: 'ShareDashboard' },
|
||||
variableDefinitions: [
|
||||
{
|
||||
kind: 'VariableDefinition',
|
||||
variable: {
|
||||
kind: 'Variable',
|
||||
name: { kind: 'Name', value: 'input' },
|
||||
},
|
||||
type: {
|
||||
kind: 'NonNullType',
|
||||
type: {
|
||||
kind: 'NamedType',
|
||||
name: { kind: 'Name', value: 'ShareDashboardInput' },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'shareDashboard' },
|
||||
arguments: [
|
||||
{
|
||||
kind: 'Argument',
|
||||
name: { kind: 'Name', value: 'input' },
|
||||
value: {
|
||||
kind: 'Variable',
|
||||
name: { kind: 'Name', value: 'input' },
|
||||
},
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'FragmentSpread',
|
||||
name: { kind: 'Name', value: 'DashboardShareFields' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'FragmentDefinition',
|
||||
name: { kind: 'Name', value: 'DashboardShareFields' },
|
||||
typeCondition: {
|
||||
kind: 'NamedType',
|
||||
name: { kind: 'Name', value: 'DashboardShare' },
|
||||
},
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'projectId' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'token' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'blocks' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'createdAt' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<
|
||||
ShareDashboardMutation,
|
||||
ShareDashboardMutationVariables
|
||||
>;
|
||||
export const UnshareDashboardDocument = {
|
||||
kind: 'Document',
|
||||
definitions: [
|
||||
{
|
||||
kind: 'OperationDefinition',
|
||||
operation: 'mutation',
|
||||
name: { kind: 'Name', value: 'UnshareDashboard' },
|
||||
variableDefinitions: [
|
||||
{
|
||||
kind: 'VariableDefinition',
|
||||
variable: {
|
||||
kind: 'Variable',
|
||||
name: { kind: 'Name', value: 'projectId' },
|
||||
},
|
||||
type: {
|
||||
kind: 'NonNullType',
|
||||
type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } },
|
||||
},
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'unshareDashboard' },
|
||||
arguments: [
|
||||
{
|
||||
kind: 'Argument',
|
||||
name: { kind: 'Name', value: 'projectId' },
|
||||
value: {
|
||||
kind: 'Variable',
|
||||
name: { kind: 'Name', value: 'projectId' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<
|
||||
UnshareDashboardMutation,
|
||||
UnshareDashboardMutationVariables
|
||||
>;
|
||||
export const SharedDashboardDocument = {
|
||||
kind: 'Document',
|
||||
definitions: [
|
||||
{
|
||||
kind: 'OperationDefinition',
|
||||
operation: 'query',
|
||||
name: { kind: 'Name', value: 'SharedDashboard' },
|
||||
variableDefinitions: [
|
||||
{
|
||||
kind: 'VariableDefinition',
|
||||
variable: {
|
||||
kind: 'Variable',
|
||||
name: { kind: 'Name', value: 'token' },
|
||||
},
|
||||
type: {
|
||||
kind: 'NonNullType',
|
||||
type: {
|
||||
kind: 'NamedType',
|
||||
name: { kind: 'Name', value: 'String' },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'sharedDashboard' },
|
||||
arguments: [
|
||||
{
|
||||
kind: 'Argument',
|
||||
name: { kind: 'Name', value: 'token' },
|
||||
value: {
|
||||
kind: 'Variable',
|
||||
name: { kind: 'Name', value: 'token' },
|
||||
},
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'projectKey' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'projectName' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'generatedAt' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'blocks' } },
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'flow' },
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'status' },
|
||||
},
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'color' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'count' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'sprint' },
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'sprintId' },
|
||||
},
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'startDate' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'endDate' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'totalDays' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'daysLeft' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'tickets' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'doneTickets' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'points' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'donePoints' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'pacePerDay' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'neededPerDay' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'behind' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'releases' },
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'releaseId' },
|
||||
},
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'targetDate' },
|
||||
},
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'total' } },
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'finished' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'likelyDate' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'safeDate' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'verdict' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'epics' },
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'key' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'title' } },
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'dueDate' },
|
||||
},
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'done' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'total' } },
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'percent' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'activity' },
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'day' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'count' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<
|
||||
SharedDashboardQuery,
|
||||
SharedDashboardQueryVariables
|
||||
>;
|
||||
export const SprintsDocument = {
|
||||
kind: 'Document',
|
||||
definitions: [
|
||||
|
||||
Reference in New Issue
Block a user