Skip to content

Commit

Permalink
Add version filter on project revision/revisions and add version and …
Browse files Browse the repository at this point in the history
…check-reviewed flags on CLI
  • Loading branch information
simonprev committed Dec 15, 2023
1 parent d1f7a0b commit 42a7b4b
Show file tree
Hide file tree
Showing 7 changed files with 127 additions and 59 deletions.
55 changes: 50 additions & 5 deletions cli/src/commands/stats.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,65 @@
// Vendor
import * as chalk from 'chalk';

// Command
import {flags} from '@oclif/command';
import Command, {configFlag} from '../base';
import {CLIError} from '@oclif/errors';

// Services
import Formatter from '../services/formatters/project-stats';
import ProjectFetcher from '../services/project-fetcher';
import {Revision} from '../types/project';

export default class Stats extends Command {
static description = 'Fetch stats from the API and display it beautifully';
static description = 'Fetch stats from the API and display them beautifully';

static examples = [`$ accent stats`];
static flags = {config: configFlag};
static flags = {
version: flags.string({
default: undefined,
description: 'View stats for a specific version',
}),
'check-reviewed': flags.boolean({
description: 'Exit 1 when reviewed percentage is not 100%',
}),
config: configFlag,
};

/* eslint-disable @typescript-eslint/require-await */
async run() {
const formatter = new Formatter(this.project!, this.projectConfig.config);
const {flags} = this.parse(Stats);

if (flags.version) {
const config = this.projectConfig.config;
const fetcher = new ProjectFetcher();
const response = await fetcher.fetch(config, {versionId: flags.version});

this.project = response.project;
}

const formatter = new Formatter(
this.project!,
this.projectConfig.config,
flags.version
);

formatter.log();

if (flags['check-reviewed']) {
const conflictsCount = this.project!.revisions.reduce(
(memo, revision: Revision) => memo + revision.conflictsCount,
0
);

if (conflictsCount !== 0) {
const versionFormat = flags.version ? ` ${flags.version}` : '';
throw new CLIError(
chalk.red(
`Project${versionFormat} has ${conflictsCount} strings to be reviewed`
),
{exit: 1}
);
}
}
}
/* eslint-enable @typescript-eslint/require-await */
}
83 changes: 47 additions & 36 deletions cli/src/services/formatters/project-stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,43 @@ import {
} from '../revision-slug-fetcher';
import Base from './base';

const TITLE_LENGTH_PADDING = 4;

export default class ProjectStatsFormatter extends Base {
private readonly project: Project;
private readonly config: Config;
private readonly version: string | undefined;

constructor(project: Project, config: Config) {
constructor(project: Project, config: Config, version?: string) {
super();
this.project = project;
this.config = config;
this.version = version;
}

percentageReviewedString(number: number, translationsCount: number) {
const prettyFloat = (number: string) => {
if (number.endsWith('.00')) {
return parseInt(number, 10).toString();
} else {
return number;
}
};

const percentageReviewedString = `${prettyFloat(
number.toFixed(2)
)}% reviewed`;
let percentageReviewedFormat = chalk.green(percentageReviewedString);

if (number === 100) {
percentageReviewedFormat = chalk.green(percentageReviewedString);
} else if (number > 100 / 2) {
percentageReviewedFormat = chalk.yellow(percentageReviewedString);
} else if (number <= 0 && translationsCount === 0) {
percentageReviewedFormat = chalk.dim('No strings');
} else {
percentageReviewedFormat = chalk.red(percentageReviewedString);
}

return percentageReviewedFormat;
}

log() {
Expand All @@ -44,20 +71,12 @@ export default class ProjectStatsFormatter extends Base {
0
);
const percentageReviewed =
translationsCount > 0 ? reviewedCount / translationsCount : 0;
translationsCount > 0 ? (reviewedCount / translationsCount) * 100 : 0;

const percentageReviewedString = `${percentageReviewed}% reviewed`;
let percentageReviewedFormat = chalk.green(percentageReviewedString);

if (percentageReviewed === 100) {
percentageReviewedFormat = chalk.green(percentageReviewedString);
} else if (percentageReviewed > 100 / 2) {
percentageReviewedFormat = chalk.yellow(percentageReviewedString);
} else if (percentageReviewed <= 0 && translationsCount === 0) {
percentageReviewedFormat = chalk.dim('No strings');
} else {
percentageReviewedFormat = chalk.red(percentageReviewedString);
}
const percentageReviewedFormat = this.percentageReviewedString(
percentageReviewed,
translationsCount
);

console.log(
this.project.logo
Expand All @@ -67,12 +86,7 @@ export default class ProjectStatsFormatter extends Base {
chalk.dim(' • '),
percentageReviewedFormat
);
const titleLength =
(this.project.logo ? this.project.logo.length + 1 : 0) +
this.project.name.length +
percentageReviewedString.length +
TITLE_LENGTH_PADDING;
console.log(chalk.gray.dim('⎯'.repeat(titleLength)));
console.log(chalk.gray.dim('⎯'));

console.log(chalk.magenta('Last synced'));
if (this.project.lastSyncedAt) {
Expand All @@ -99,18 +113,12 @@ export default class ProjectStatsFormatter extends Base {
this.project.revisions.forEach((revision: Revision) => {
if (this.project.masterRevision.id !== revision.id) {
const percentageReviewed =
revision.reviewedCount / revision.translationsCount;

const percentageReviewedString = `${percentageReviewed}% reviewed`;
let percentageReviewedFormat = chalk.green(percentageReviewedString);
(revision.reviewedCount / revision.translationsCount) * 100;

if (percentageReviewed === 100) {
percentageReviewedFormat = chalk.green(percentageReviewedString);
} else if (percentageReviewed > 100 / 2) {
percentageReviewedFormat = chalk.yellow(percentageReviewedString);
} else {
percentageReviewedFormat = chalk.red(percentageReviewedString);
}
const percentageReviewedFormat = this.percentageReviewedString(
percentageReviewed,
translationsCount
);

console.log(
`${chalk.white.bold(
Expand Down Expand Up @@ -147,13 +155,16 @@ export default class ProjectStatsFormatter extends Base {
)
);
this.project.versions.entries.forEach((version: Version) => {
console.log(chalk.bgBlack.white(` ${version.tag} `));
if (version.tag === this.version) {
console.log(chalk.bgBlack.whiteBright(` ${version.tag} `));
} else {
console.log(chalk.white(`${version.tag}`));
}
});
console.log('');
}

console.log(chalk.magenta('Strings'));
console.log(chalk.white('# Strings:'), chalk.white(`${translationsCount}`));
console.log(chalk.magenta(`Strings (${translationsCount})`));
console.log(chalk.green('✓ Reviewed:'), chalk.green(`${reviewedCount}`));
console.log(chalk.red('× In review:'), chalk.red(`${conflictsCount}`));
console.log('');
Expand Down
16 changes: 8 additions & 8 deletions cli/src/services/project-fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import {Config} from '../types/config';
import {ProjectViewer} from '../types/project';

export default class ProjectFetcher {
async fetch(config: Config): Promise<ProjectViewer> {
const response = await this.graphql(config);
async fetch(config: Config, params?: object): Promise<ProjectViewer> {
const response = await this.graphql(config, params || {});
try {
const data = (await response.json()) as {data: any};

Expand All @@ -31,14 +31,14 @@ export default class ProjectFetcher {
}
}

private async graphql(config: Config) {
const query = `query ProjectDetails($project_id: ID!) {
private async graphql(config: Config, params: object) {
const query = `query ProjectDetails($projectId: ID! $versionId: ID) {
viewer {
user {
fullname
}
project(id: $project_id) {
project(id: $projectId) {
id
name
logo
Expand Down Expand Up @@ -88,7 +88,7 @@ export default class ProjectFetcher {
}
}
revisions {
revisions(versionId: $versionId) {
id
isMaster
translationsCount
Expand All @@ -106,8 +106,8 @@ export default class ProjectFetcher {
}
}`;

// eslint-disable-next-line camelcase
const variables = config.project ? {project_id: config.project} : {};
const configParams = config.project ? {projectId: config.project} : {};
const variables = {...configParams, ...params};

return await fetch(`${config.apiUrl}/graphql`, {
body: JSON.stringify({query, variables}),
Expand Down
6 changes: 3 additions & 3 deletions lib/accent/scopes/revision.ex
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ defmodule Accent.Scopes.Revision do
@doc """
Fill `translations_count`, `conflicts_count` and `reviewed_count` for revisions.
"""
@spec with_stats(Ecto.Queryable.t()) :: Ecto.Queryable.t()
def with_stats(query) do
Accent.Scopes.TranslationsCount.with_stats(query, :revision_id)
@spec with_stats(Ecto.Queryable.t(), Keyword.t() | nil) :: Ecto.Queryable.t()
def with_stats(query, options \\ []) do
Accent.Scopes.TranslationsCount.with_stats(query, :revision_id, options)
end
end
12 changes: 11 additions & 1 deletion lib/accent/scopes/translations_count.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,26 @@ defmodule Accent.Scopes.TranslationsCount do

def with_stats(query, column, options \\ []) do
exclude_empty_translations = Keyword.get(options, :exclude_empty_translations, false)
version_id = Keyword.get(options, :version_id, nil)

translations =
from(
t in Accent.Translation,
select: %{field_id: field(t, ^column), count: count(t)},
where: [removed: false, locked: false],
where: is_nil(t.version_id),
group_by: field(t, ^column)
)

translations =
if version_id do
from(t in translations,
inner_join: versions in assoc(t, :version),
where: versions.tag == ^version_id
)
else
from(t in translations, where: is_nil(t.version_id))
end

query =
query
|> count_translations(translations, exclude_empty_translations)
Expand Down
12 changes: 6 additions & 6 deletions lib/graphql/resolvers/revision.ex
Original file line number Diff line number Diff line change
Expand Up @@ -113,31 +113,31 @@ defmodule Accent.GraphQL.Resolvers.Revision do
end

@spec show_project(Project.t(), %{id: String.t()}, GraphQLContext.t()) :: {:ok, Revision.t() | nil}
def show_project(project, %{id: id}, _) do
def show_project(project, %{id: id} = args, _) do
Revision
|> RevisionScope.from_project(project.id)
|> RevisionScope.with_stats()
|> RevisionScope.with_stats(version_id: args[:version_id])
|> Query.where(id: ^id)
|> Repo.one()
|> then(&{:ok, &1})
end

def show_project(project, _, _) do
def show_project(project, args, _) do
Revision
|> RevisionScope.from_project(project.id)
|> RevisionScope.with_stats()
|> RevisionScope.with_stats(version_id: args[:version_id])
|> RevisionScope.master()
|> Repo.one()
|> then(&{:ok, &1})
end

@spec list_project(Project.t(), any(), GraphQLContext.t()) :: {:ok, [Revision.t()]}
def list_project(project, _, _) do
def list_project(project, args, _) do
project
|> Ecto.assoc(:revisions)
|> Query.join(:inner, [revisions], languages in assoc(revisions, :language), as: :languages)
|> Query.order_by([revisions, languages: languages], desc: :master, asc: revisions.name, asc: languages.name)
|> RevisionScope.with_stats()
|> RevisionScope.with_stats(version_id: args[:version_id])
|> Repo.all()
|> then(&{:ok, &1})
end
Expand Down
2 changes: 2 additions & 0 deletions lib/graphql/types/project.ex
Original file line number Diff line number Diff line change
Expand Up @@ -192,11 +192,13 @@ defmodule Accent.GraphQL.Types.Project do

field :revision, :revision do
arg(:id, :id)
arg(:version_id, :id)

resolve(project_authorize(:show_revision, &Accent.GraphQL.Resolvers.Revision.show_project/3))
end

field :revisions, list_of(:revision) do
arg(:version_id, :id)
resolve(project_authorize(:index_revisions, &Accent.GraphQL.Resolvers.Revision.list_project/3))
end

Expand Down

0 comments on commit 42a7b4b

Please sign in to comment.