Skip to content

Commit 874a539

Browse files
authored
Merge pull request #47 from devaslanphp/dev
Add jira integration
2 parents e556943 + 644fa75 commit 874a539

14 files changed

Lines changed: 482 additions & 5 deletions

File tree

README.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ All this made with the best technologies.
6464
<img src="github-contents/22.png" width="20%"></img>
6565
<img src="github-contents/23.png" width="20%"></img>
6666
<img src="github-contents/24.png" width="20%"></img>
67+
<img src="github-contents/25.png" width="20%"></img>
68+
<img src="github-contents/26.png" width="20%"></img>
6769
</div>
6870

6971
## Documentation
@@ -122,9 +124,12 @@ The MIT License (MIT). Please see [License File](LICENSE.md) for more informatio
122124
- #32 Default user seeder enhancement
123125
- #31 Issue resolved
124126
- **Release 1.2.0**
125-
- Scrum module #28
126-
- Design enhancement (Kanban / Scrum boards)
127-
- Referential updates
127+
- Scrum module #28
128+
- Design enhancement (Kanban / Scrum boards)
129+
- Referential updates
130+
- **Release 1.2.1**
131+
- Add jira integration #36
132+
- New feature: Import jira projects / tickets
128133

129134
## Support us
130135

app/Filament/Pages/JiraImport.php

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
<?php
2+
3+
namespace App\Filament\Pages;
4+
5+
use App\Helpers\JiraHelper;
6+
use App\Jobs\ImportJiraTicketsJob;
7+
use Filament\Forms\Components\Card;
8+
use Filament\Forms\Components\Checkbox;
9+
use Filament\Forms\Components\CheckboxList;
10+
use Filament\Forms\Components\Grid;
11+
use Filament\Forms\Components\Placeholder;
12+
use Filament\Forms\Components\TextInput;
13+
use Filament\Forms\Components\Wizard;
14+
use Filament\Forms\Concerns\InteractsWithForms;
15+
use Filament\Forms\Contracts\HasForms;
16+
use Filament\Pages\Page;
17+
use Illuminate\Contracts\Support\Htmlable;
18+
use Illuminate\Support\HtmlString;
19+
use Illuminate\Support\Str;
20+
21+
class JiraImport extends Page implements HasForms
22+
{
23+
use InteractsWithForms, JiraHelper;
24+
25+
protected static ?string $navigationIcon = 'heroicon-o-cloud-download';
26+
27+
protected static string $view = 'filament.pages.jira-import';
28+
29+
protected static ?string $slug = 'jira-import';
30+
31+
protected static ?int $navigationSort = 2;
32+
33+
protected $listeners = [
34+
'updateJiraProjects',
35+
'updateJiraTickets'
36+
];
37+
38+
public $host;
39+
public $username;
40+
public $token;
41+
private $loadingProjects;
42+
private $projects;
43+
public $selected_projects;
44+
private $loadingTickets;
45+
private $tickets;
46+
public $selected_tickets;
47+
public $data = [];
48+
public $ticketsDataApi;
49+
50+
public function mount(): void
51+
{
52+
$this->form->fill();
53+
}
54+
55+
protected static function shouldRegisterNavigation(): bool
56+
{
57+
return auth()->user()->can('Import from Jira');
58+
}
59+
60+
protected function getSubheading(): string|Htmlable|null
61+
{
62+
return __('Use this section to login into your jira account and import tickets to this application');
63+
}
64+
65+
protected static function getNavigationLabel(): string
66+
{
67+
return __('Jira import');
68+
}
69+
70+
protected static function getNavigationGroup(): ?string
71+
{
72+
return __('Settings');
73+
}
74+
75+
protected function getFormSchema(): array
76+
{
77+
return [
78+
Card::make()
79+
->schema([
80+
Wizard::make([
81+
Wizard\Step::make(__('Jira login'))
82+
->schema([
83+
Placeholder::make('info')
84+
->extraAttributes([
85+
'class' => 'bg-primary-500 rounded-lg border border-primary-600 text-white font-medium text-sm py-3 px-4'
86+
])
87+
->disableLabel()
88+
->content(__('Important: Your jira credentials are only used to communicate with jira REST API, and will not be stored in this application')),
89+
90+
Grid::make()
91+
->schema([
92+
TextInput::make('host')
93+
->label(__('Host'))
94+
->helperText(__('The url used to access your jira account'))
95+
->required(),
96+
97+
TextInput::make('username')
98+
->label(__('Username'))
99+
->helperText(__('Your jira account username'))
100+
->required(),
101+
102+
TextInput::make('token')
103+
->label(__('API Token'))
104+
->helperText(__('Your jira account API Token'))
105+
->password()
106+
->required(),
107+
]),
108+
])
109+
->afterValidation(function () {
110+
$this->loadingProjects = true;
111+
$this->emit('updateJiraProjects');
112+
}),
113+
114+
Wizard\Step::make(__('Jira projects'))
115+
->schema([
116+
Placeholder::make('hint')
117+
->extraAttributes([
118+
'class' => 'bg-primary-500 rounded-lg border border-primary-600 text-white font-medium text-sm py-3 px-4'
119+
])
120+
->disableLabel()
121+
->visible(fn() => !$this->loadingProjects && $this->projects)
122+
->content(__('Choose your jira projects to import')),
123+
124+
Placeholder::make('loading')
125+
->extraAttributes([
126+
'class' => 'bg-warning-500 rounded-lg border border-warning-600 text-white font-medium text-sm py-3 px-4'
127+
])
128+
->disableLabel()
129+
->visible(fn() => $this->loadingProjects)
130+
->content(__('Loading projects, please wait...')),
131+
132+
Placeholder::make('info')
133+
->extraAttributes([
134+
'class' => 'bg-danger-500 rounded-lg border border-danger-600 text-white font-medium text-sm py-3 px-4'
135+
])
136+
->disableLabel()
137+
->visible(fn() => !$this->loadingProjects && !$this->projects)
138+
->content(__('Your jira credentials are incorrect, please go to previous step and re-enter your jira credentials')),
139+
140+
CheckboxList::make('selected_projects')
141+
->label(__('Jira projects'))
142+
->required()
143+
->visible(fn() => $this->projects)
144+
->options(function () {
145+
$list = [];
146+
if ($this->projects) {
147+
foreach ($this->projects as $project) {
148+
$list[$project->key] = new HtmlString(
149+
"<div class='w-full flex flex-col gap-1'>"
150+
. "<div class='w-full flex items-center gap-1'>"
151+
. "<img src='" . $project->avatarUrls->{'16x16'} . "' class='rounded-full w-8 h-8 shadow' />"
152+
. "<span class='font-medium text-gray-700 text-base'>" . $project->name . "</span>"
153+
. "<div class='text-gray-700 text-xs font-light'><span class='font-medium uppercase'>/</span> " . $project->key . "</div>"
154+
. "</div>"
155+
. "</div>"
156+
);
157+
}
158+
}
159+
return $list;
160+
}),
161+
162+
])
163+
->afterValidation(function () {
164+
$this->loadingTickets = true;
165+
$this->emit('updateJiraTickets');
166+
}),
167+
168+
Wizard\Step::make(__('Jira tickets'))
169+
->schema(function () {
170+
$fields = [];
171+
172+
$fields[] = Placeholder::make('hint')
173+
->extraAttributes([
174+
'class' => 'bg-primary-500 rounded-lg border border-primary-600 text-white font-medium text-sm py-3 px-4'
175+
])
176+
->disableLabel()
177+
->visible(fn() => !$this->loadingTickets && $this->tickets)
178+
->content(__('Choose your jira projects to import'));
179+
180+
$fields[] = Placeholder::make('loading')
181+
->extraAttributes([
182+
'class' => 'bg-warning-500 rounded-lg border border-warning-600 text-white font-medium text-sm py-3 px-4'
183+
])
184+
->disableLabel()
185+
->visible(fn() => $this->loadingTickets)
186+
->content(__('Loading tickets, please wait...'));
187+
188+
if (!$this->loadingTickets) {
189+
if ($this->tickets) {
190+
foreach ($this->tickets as $projectKey => $ticket) {
191+
if ($ticket['total'] > 0) {
192+
$fields[] = Placeholder::make('tickets_' . Str::slug($projectKey))
193+
->label(__('Tickets for the project:') . ' ' . $projectKey)
194+
->extraAttributes([
195+
'style' => 'margin-bottom: -15px;'
196+
])
197+
->content('');
198+
199+
foreach ($ticket['issues'] as $issue) {
200+
$fields[] = Checkbox::make('data.' . Str::slug($projectKey) . '_' . Str::slug($issue['code']))
201+
->label(function () use ($issue) {
202+
return new HtmlString(
203+
"<div class='w-full flex flex-col gap-1'>"
204+
. "<div class='w-full flex items-center gap-1'>"
205+
. "<div class='text-gray-700 text-xs font-light'><span class='font-medium uppercase'>" . $issue['code'] . "</span> " . $issue['name'] . "</div>"
206+
. "</div>"
207+
. "</div>"
208+
);
209+
});
210+
}
211+
} else {
212+
$fields[] = Placeholder::make('no_tickets_' . Str::slug($projectKey))
213+
->label(__('Tickets for the project:') . ' ' . $projectKey)
214+
->content(__('No tickets found!'));
215+
}
216+
}
217+
} else {
218+
$fields[] = Placeholder::make('info')
219+
->extraAttributes([
220+
'class' => 'bg-warning-500 rounded-lg border border-warning-600 text-white font-medium text-sm py-3 px-4'
221+
])
222+
->disableLabel()
223+
->visible(fn() => !$this->projects)
224+
->content(__('No tickets found!'));
225+
}
226+
}
227+
return $fields;
228+
}),
229+
])
230+
->submitAction(new HtmlString("<button type='submit' class='px-3 py-2 bg-primary-500 hover:bg-primary-600 text-white rounded'>" . __('Import') . "</button>")),
231+
]),
232+
];
233+
}
234+
235+
public function import(): void
236+
{
237+
if ($this->data && sizeof($this->data)) {
238+
$tickets = [];
239+
foreach (array_keys($this->data) as $item) {
240+
$url = $this->ticketsDataApi[$item];
241+
$tickets[] = $this->getJiraTicketDetails($this->host, $this->username, $this->token, $url);
242+
}
243+
dispatch(new ImportJiraTicketsJob($tickets, auth()->user()));
244+
$this->notify('success', __('The importation job is started, when finished you will be notified'), true);
245+
$this->redirect(route('filament.pages.jira-import'));
246+
} else {
247+
$this->notify('warning', __('Please choose at least a jira ticket to import'));
248+
}
249+
}
250+
251+
public function updateJiraProjects(): void
252+
{
253+
$client = $this->connectToJira($this->host, $this->username, $this->token);
254+
$this->projects = $this->getJiraProjects($client);
255+
$this->loadingProjects = false;
256+
}
257+
258+
public function updateJiraTickets(): void
259+
{
260+
$this->ticketsDataApi = [];
261+
$client = $this->connectToJira($this->host, $this->username, $this->token);
262+
$this->tickets = $this->getJiraTicketsByProject($client, $this->selected_projects);
263+
foreach ($this->tickets as $projectKey => $ticket) {
264+
foreach ($ticket['issues'] as $issue) {
265+
$this->ticketsDataApi[Str::slug($projectKey) . '_' . Str::slug($issue['code'])] = $issue['data']->self;
266+
}
267+
}
268+
$this->loadingTickets = false;
269+
}
270+
}

app/Helpers/JiraHelper.php

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
namespace App\Helpers;
4+
5+
use GuzzleHttp\Client;
6+
use GuzzleHttp\Exception\GuzzleException;
7+
use Illuminate\Support\Facades\Log;
8+
9+
trait JiraHelper
10+
{
11+
12+
public function connectToJira($host, $username, $token): Client|null
13+
{
14+
return new Client([
15+
'base_uri' => $host,
16+
'headers' => [
17+
'Content-Type' => 'application/json',
18+
'Accept' => 'application/json',
19+
'Authorization' => 'Basic ' . base64_encode($username . ":" . $token)
20+
]
21+
]);
22+
}
23+
24+
public function getJiraProjects(Client $client): array|null
25+
{
26+
try {
27+
$response = $client->get('/rest/api/2/project');
28+
return json_decode($response->getBody()->getContents());
29+
} catch (GuzzleException $e) {
30+
Log::error($e->getTraceAsString());
31+
return null;
32+
}
33+
}
34+
35+
public function getJiraTicketsByProject(Client $client, $projectKeys): array|null
36+
{
37+
try {
38+
$formatIssues = function ($issues) {
39+
$results = [];
40+
foreach ($issues as $issue) {
41+
$results[] = [
42+
'code' => $issue->key,
43+
'name' => $issue->fields->summary,
44+
'data' => $issue
45+
];
46+
}
47+
return $results;
48+
};
49+
$results = [];
50+
foreach ($projectKeys as $projectKey) {
51+
$response = $client->get('/rest/api/2/search?jql=project=' . $projectKey);
52+
$data = json_decode($response->getBody()->getContents());
53+
$results[$projectKey] = [
54+
'total' => $data->total,
55+
'issues' => $formatIssues($data->issues)
56+
];
57+
}
58+
return $results;
59+
} catch (GuzzleException $e) {
60+
Log::error($e->getTraceAsString());
61+
return null;
62+
}
63+
}
64+
65+
public function getJiraTicketDetails($host, $username, $token, $url)
66+
{
67+
try {
68+
$client = $this->connectToJira($host, $username, $token);
69+
$url = explode('/', $url);
70+
$response = $client->get('/rest/api/2/issue/' . $url[sizeof($url) - 1]);
71+
return json_decode($response->getBody()->getContents());
72+
} catch (GuzzleException $e) {
73+
Log::error($e->getTraceAsString());
74+
return null;
75+
}
76+
}
77+
78+
}

0 commit comments

Comments
 (0)