A segunda-feira de trabalho começou com o seguinte diálogo:
Líder (P): não está claro em sua equipe quem está fazendo o que.
I (I): Sim, não temos uma ferramenta que reflita a imagem geral do trabalho nas tarefas. Existem quadros kanban no hitlab, mas eles são apenas no contexto de projetos e grupos. Um quadro Kanban comum resolveria o problema.
R: Então faça um quadro.
Eu: Pela manhã, estará pronto.
Mais cedo
ou mais tarde, chega
o momento na vida de um líder de equipe iniciante, quando ele percebe que sua equipe precisa de um quadro Kanban. Ele remove a ansiedade sobre o controle do processo de desenvolvimento e dá confiança no futuro. Normalmente, esse problema é resolvido com a menor solicitação ao gerente para comprar uma placa magnética, um conjunto de adesivos multicoloridos e alguns marcadores para a placa. Bem, ou usando serviços como o Jira. Mas nossa equipe tem um desenvolvedor no controle remoto e o gitlab em um circuito fechado (não disponível na Internet por razões de segurança da informação), então tive que escolher a mais simples das duas soluções possíveis:
a) a criação de um braço mecânico e de um controlador para ele, que recolocará remotamente os adesivos no quadro; para não complicar a decisão, teremos que escrever nos adesivos para nosso colega inacessível, o que é injusto;
b) implementação de uma placa kanban de software que coletaria todas as tarefas do nosso hitlab.
Obviamente, a alma estava contra o tubo da prancha física. Eu até comecei a pensar em usar o DualShock 4 como um controlador de braço mecânico, mas eu próprio designei o prazo na manhã seguinte, então tive que seguir com uma solução de software sem alma.
Antes de começar a descrever a parte técnica da história, vou falar sobre como usamos o gitlab no Onlanta. Os grupos no hitlab que temos correspondem ao cliente interno / externo ou a um projeto grande separado, e o projeto é um repositório para o código de um serviço específico. Por exemplo, temos um grupo de um painel de controle de cobrança no qual quatro projetos são serviços e aplicativos da web, e há um grupo de marketing no qual há um projeto para um site corporativo, um microsserviço para integração com o amoCRM, todos os tipos de páginas de destino e assim por diante. Atualmente, temos nove grupos ativos e há menos programadores, portanto todos os programadores participam de todos os grupos.
E tudo o que precisávamos era de uma página com todas as tarefas do hitlab, de todos os grupos e projetos, mas essa funcionalidade no GitLab, infelizmente, não é. O Google rápido não ajudou a encontrar uma solução autônoma, portanto
Vamos precisar de:
- backend no Node.js - para coletar tarefas com o GitLab e transferi-las para o cliente;
- cliente no Vue.js - para fazer isso de forma rápida e bonita;
- tempere tudo com o TypeScript, é claro!
- PostgreSQL - onde armazenaremos informações sobre tarefas e sua posição no quadro;
- 8 horas de tempo de trabalho;
- bom humor.
A condição mais importante é que o desenvolvimento seja fácil e divertido, vamos chamá-lo de "programação fácil".
O zoológico
Por razões óbvias, não posso usar os projetos e tarefas de nosso hitlab corporativo para ilustrar o artigo sobre Habré, então implantei meu hitlab, com macacos e crocodilos. Sim, este é um gitlab para um zoológico fictício, mas não serão definidas tarefas menos sérias do que em qualquer outro gitlab. Veja a lista de projetos:
karcass
Ao longo dos anos de desenvolvimento, desenvolvi minha própria visão de como o aplicativo de back-end no Node.js deve ser construído, e essa abordagem assumiu a forma de uma estrutura - um conjunto de arquivos. Para criar um novo projeto, apenas copiei o diretório com o modelo. Isso não pôde durar para sempre, e eu fiz o pacote
karcass npm (o nome carcaça já foi usado pelo mesmo entusiasta de bicicletas que eu), o que automatiza o processo de criação da base para o aplicativo:
Agora precisamos lidar com o recebimento e armazenamento de grupos, projetos, tarefas e, é claro, artistas.
TipoORM
"Eu vejo algumas entidades ..."
- de discussões no fórum psíquico
Como precisamos apresentar os dados do gitlab de uma forma conveniente para nós, no processo entre coletar e apresentá-los, precisamos armazená-los de alguma forma. Recentemente, descobri a incrível biblioteca para trabalhar com bancos de dados,
TypeORM, incrível para o ecossistema JS. Ainda é verde, mas tem todas as chances de expulsar Sequelize do trono.
Em algumas dezenas de minutos, migrações e classes para
usuários ,
grupos ,
projetos e
tarefas são exibidas. Aqui está um deles:
import { Entity, PrimaryColumn, Column } from 'typeorm' @Entity({ name: 'group' }) export class Group { @PrimaryColumn('integer') public id!: number @Column('varchar') public name!: string @Column('varchar') public url!: string }
E para criar migrações, fiz novamente uma
bicicleta assistente
CreateMigrationCommand . Este comando é muito simples de usar:
Na classe Issue, além dos campos que armazenam dados do Gitlab, adicionamos campos devido aos quais toda a confusão começou, ou seja, indicando a posição da tarefa no quadro Kanban:
export class Issue extends AbstractEntity { @Column('varchar', { name: 'kanban_status' }) public kanbanStatus?: 'new'|'planed'|'working'|'checking'|'done' @Column('int', { name: 'kanban_order' }) public kanbanOrder?: number }
Precisamos de informação
Quando eu comecei a me familiarizar com o GitLab, ele me pareceu um produto simples (leia-se "odiado"), mas ao trabalhar com ele todos os dias, percebi o quanto estava errado. Dessa vez, fiquei surpreso - o GitLab tem uma API muito rica que cobre quase todas as funcionalidades disponíveis na interface do usuário. Mas, para resolver nosso problema, você precisará de apenas quatro métodos, cujos nomes falam por si:
/ usuários ,
/ grupos ,
/ projetos ,
/ projetos /: id / issues . O GitlabService será responsável pela interação direta com o GitLab, e outras classes o acessarão. Por exemplo, a implementação do método updateGroups da classe GroupService se parece com isso:
export class GroupService extends AbstractService { public async updateGroups() { for (const data of await this.app.gitlabService.getGroups()) {
O comando
updateProjectsCommand será responsável pela operação de obter informações do gitab e atualizar as informações relevantes em nosso banco de dados, que podem ser chamadas no console, mas nosso aplicativo iniciará ele próprio com a frequência especificada na configuração:
export class Application { protected initCron() { if (this.config.gitlab.updateInterval) { setInterval(async () => { if (!this.updateProjectsCommand) { this.updateProjectsCommand = new UpdateProjectsCommand(this) } await this.updateProjectsCommand.execute() }, this.config.gitlab.updateInterval * 1000) } } }
Não codificaremos o número, a cor e o nome das colunas no quadro, mas as tornaremos personalizáveis no config.js. Temos cinco deles, mas para alguém, talvez apenas dois sejam necessários e cor ácida:
columns: [ { key: 'new', title: '', color: 'rgb(255, 255, 219)' }, { key: 'planed', title: '', color: 'rgb(236, 236, 191)' }, { key: 'working', title: ' ', color: 'rgb(253, 214, 162)' }, { key: 'checking', title: ' ', color: 'rgb(162, 226, 253)' }, { key: 'done', title: '', color: 'rgb(162, 253, 200)' }, ],
O principal é que deve haver pelo menos dois, caso contrário, o aplicativo não será iniciado.
Para interagir com a frente, precisamos de um método que receba tarefas do banco de dados e as classifique em "colunas":
Mostrar IssueService.ts export class IssueService extends AbstractService { public async getKanban() { let issues = await this.issueRepository.find({ where: { updatedTimestamp: MoreThanOrEqual(new Date().getTimestamp() - 60 * 60 * 24 * 30) }, order: { kanbanOrder: 'ASC', updatedTimestamp: 'DESC' }, }) const keys = this.app.config.columns.map(c => c.key) const result: { [key: string]: Issue[] } = {} for (let ki = keys.length - 1; ki >= 0; ki--) { const key = keys[ki] if (ki === keys.length - 1) {
E um controlador que salva novas posições de tarefas:
Mostrar IssueController.ts export default class IssueController extends AbstractController { public async kanbanUpdate(data: IQueryData) { const keys = this.app.config.columns.map(c => c.key) for (const c of data.params as { key: string, title: string, color: string, issues: number[] }[]) { if (keys.indexOf(c.key) < 0) { continue } let index = 0 for (const i of c.issues) { index++ const issue = await this.app.issueService.getIssue(i) if (!issue) { continue } issue.kanbanStatus = c.key issue.kanbanOrder = index this.app.issueService.issueRepository.save(issue) } } } }
Stosh. Coletamos informações do GitLab, preparamos dados para a exibição do quadro, e até sabemos como salvar posições atualizadas de tarefas no quadro. A única coisa que resta é fazer o próprio quadro. E aqui estamos armados com uma ferramenta poderosa:
Vue CLI - em vez de mil palavras
TSX
Para facilitar a refatoração, encontrar bugs, fiapos e outras tranqüilidades, usamos modelos tsx que o Vue.js suporta
quase imediatamente
* . Por exemplo, o componente mais importante em nosso quadro, o componente da apresentação da tarefa:
Mostrar código Issue.tsx import { Vue, Component, Prop, Watch } from 'vue-property-decorator' import './Issue.css' import { CreateElement, VNode } from 'vue' interface IIssue { groupId: number groupName: string groupUrl: string projectName: string projectUrl: string url: string title: string executor: { color: string, name: string } spent: number estimate: number } @Component export default class extends Vue { @Prop() public issue!: IIssue public issueValue!: IIssue public selectUser = false @Watch('issue', { immediate: true }) public onIssueChange() { this.issueValue = this.issue }
Este componente exibe uma barra de tarefas. Observe que a tarefa realmente gastou e planejou o tempo:
Também existem
componentes Column.tsx e
Kanban.tsx . Apenas três componentes fornecem a apresentação de tarefas no quadro:
Vue.Draggable , cujas pernas crescem a partir de
SortableJS , é responsável por mover as tarefas no quadro. É extremamente simples de usar:
import Draggable from 'vuedraggable' export default class extends Vue { public render(h: CreateElement): VNode { <Draggable class="kvcIssues" vModel={ this.issuesValue } group={ { name: 'issues', pull: true, put: true } } onEnd={ this.onDrag } onAdd={ this.onDrag } > { this.issuesValue.map(i => <Issue key={ i.id } issue={ i } />) } </Draggable> /* ... */ } }
Certamente você notou que cada usuário tem sua própria cor, usada para exibir o logon. Isso é muito conveniente - entre várias tarefas, você pode encontrar rapidamente as suas. Existem duas maneiras de obter a "multicoloridade": definir a cor com as mãos para cada usuário ou gerá-la com base em algo.
Dê-me seu nome de usuário e eu direi qual a cor que você tem
O getter de cores da classe User é responsável pela geração de cores. Eu implementei essa funcionalidade especificamente no back-end, porque precisava de criptografia, e é um crime arrastar uma biblioteca inteira para a frente para uma tarefa tão pequena. E é mais correto quando as costas, e não a frente, são responsáveis pelas características das entidades:
export class User extends AbstractEntity { public get color() { const hash = crypto.createHash('md5').update(this.username).digest()
Para aqueles que querem saber qual a cor destinada a eles, não tive preguiça
de cortar o trecho no CodePen .
Isso é tudo
A terça-feira de trabalho começou com o seguinte diálogo:
R: Bem, como está o quadro, pronto?
Eu: Sim, aqui.
R: Oh, legal!
O resultado da minha "jornada de trabalho" está aqui:
https://github.com/onlanta/kanban . Você não pode apenas vê-lo, tocá-lo, mas também usá-lo (instruções aqui). E você pode criar nele um rastreador em tempo integral para o hitlab, o que fizemos na empresa.
Por que eu decidi escrever esta nota? Esse é um dos momentos mais recentes da minha atividade profissional, quando a tarefa era importante, interessante e ao mesmo tempo bastante simples, e sua solução se mostrou fácil e elegante. Sugiro nos comentários que lembre tarefas semelhantes de sua prática.
Bem, onde estamos sem vagas.