Skip to content

Commit

Permalink
Adds support for custom context path on Jira Server (#12)
Browse files Browse the repository at this point in the history
  • Loading branch information
mediabounds authored Dec 23, 2024
1 parent 7922f6a commit 092ad7a
Show file tree
Hide file tree
Showing 12 changed files with 177 additions and 26 deletions.
9 changes: 7 additions & 2 deletions src/JiraConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,17 @@ export class JiraConnection {
* @returns A configured Client.
*/
public static getClient(settings: DefaultPluginSettings): Client {
const {domain, email: username, token: key, strategy} = settings;
const {domain, context, email: username, token: key, strategy} = settings;

if (!domain) {
throw new Error('A domain must be set');
}

let endpoint = `https://${domain}`;
if (context) {
endpoint = `${endpoint}/${context}`;
}

if (!key) {
throw new Error('An API token must be set');
}
Expand All @@ -29,6 +34,6 @@ export class JiraConnection {
authenticator = new BasicAuth(username, key);
}

return new Client(`https://${domain}`, authenticator);
return new Client(endpoint, authenticator);
}
}
8 changes: 8 additions & 0 deletions src/JiraPluginSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,14 @@ export interface DefaultPluginSettings {
* The base product URL of the Jira instance.
*/
domain: string;

/**
* For Jira Server instances, they may have a custom context path defined.
*
* @see https://confluence.atlassian.com/jirakb/change-the-context-path-used-to-access-jira-server-225119408.html
*/
context: string;

/**
* The email address of the user to use when using the Jira API (i.e. requesting issues).
*/
Expand Down
39 changes: 39 additions & 0 deletions src/actions/BaseConfluenceAction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import Client from "../Client";
import { CommonSettings, DefaultPluginSettings } from "../JiraPluginSettings";
import BaseJiraAction, { CountableResponse } from "./BaseJiraAction";

/**
* Base class for actions that periodically pull data from Confluence.
*/
export default abstract class BaseConfluenceAction<ResponseType extends CountableResponse<unknown>, SettingsType extends CommonSettings> extends BaseJiraAction<ResponseType, SettingsType> {
/**
* {@inheritDoc}
*/
protected override getJiraClient(settings: DefaultPluginSettings): Client {
this.validateSettings(settings);
return super.getJiraClient(settings);
}

/**
* {@inheritDoc}
*/
protected override getUrl(settings: DefaultPluginSettings): string | null {
this.validateSettings(settings);
return super.getUrl(settings);
}

/**
* Ensures that the settings have a proper context defined.
*
* When using JIRA Cloud, the context (as far as I know) is always `wiki`.
* But when using JIRA Server, the context could be anything.
*
* @param settings - The current plugin settings.
*/
private validateSettings(settings: DefaultPluginSettings) {
// Ensure that the default context is set to `wiki` for JIRA Cloud.
if (!this.isJiraServer(settings) && !settings.context) {
settings.context = 'wiki';
}
}
}
42 changes: 41 additions & 1 deletion src/actions/BaseJiraAction.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { DidReceiveSettingsEvent } from "@fnando/streamdeck";
import Icon, { BadgeOptions } from "../Icon";
import { IconSettings, BadgeType, CommonSettings } from "../JiraPluginSettings";
import { IconSettings, BadgeType, CommonSettings, DefaultPluginSettings } from "../JiraPluginSettings";
import { PollingErrorEvent, PollingResponseEvent } from "../PollingClient";
import PollingAction, { ActionPollingContext } from "./PollingAction";
import Client from "../Client";
import { JiraConnection } from "../JiraConnection";

/**
* A generic API response that has a countable number of results.
Expand Down Expand Up @@ -119,4 +121,42 @@ export default abstract class BaseJiraAction<ResponseType extends CountableRespo
protected getDefaultImage(): string {
return `images/actions/${this.constructor.name}/${this.states[0].image}@2x.png`;
}

/**
* Determines whether the plugin is configured for JIRA Server.
*
* @param settings - The plugin settings.
* @returns `true` if the plugin settings indicate it is configured for JIRA Server.
*/
protected isJiraServer(settings: DefaultPluginSettings): boolean {
return settings.strategy === 'PAT';
}

/**
* Gets an HTTP client for making API calls to JIRA.
*
* @param settings - The plugin settings.
* @returns A configured HTTP client for making API calls.
*/
protected getJiraClient(settings: DefaultPluginSettings): Client {
return JiraConnection.getClient(settings);
}

/**
* Retrieves the base URL to the Atlassian instance.
* @param settings - The plugin settings.
* @returns The base URL to the Atlassian instance.
*/
protected getUrl(settings: DefaultPluginSettings): string|null {
if (!settings.domain) {
return null;
}

let url = `https://${settings.domain}`;
if (settings.context) {
url = `${url}/${settings.context}`;
}

return url;
}
}
28 changes: 18 additions & 10 deletions src/actions/ConfluenceSearch.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { KeyDownEvent } from "@fnando/streamdeck";
import { JiraConnection } from "../JiraConnection";
import { ConfluenceSearchSettings } from "../JiraPluginSettings";
import BaseJiraAction, { CountableResponse } from "./BaseJiraAction";
import { CountableResponse } from "./BaseJiraAction";
import { ActionPollingContext } from "./PollingAction";
import BaseConfluenceAction from "./BaseConfluenceAction";

/**
* The response to the search query.
Expand Down Expand Up @@ -63,7 +63,7 @@ interface Content {
*
* @see https://developer.atlassian.com/cloud/confluence/advanced-searching-using-cql/
*/
class ConfluenceSearch extends BaseJiraAction<CountableResponse<CQLResponse>, ConfluenceSearchSettings> {
class ConfluenceSearch extends BaseConfluenceAction<CountableResponse<CQLResponse>, ConfluenceSearchSettings> {
/**
* {@inheritDoc}
*/
Expand All @@ -77,9 +77,19 @@ class ConfluenceSearch extends BaseJiraAction<CountableResponse<CQLResponse>, Co
return;
}
}

const apiContext = event.settings.strategy === 'PAT' ? '' : '/wiki';
this.openURL(`https://${event.settings.domain}${apiContext}/search?cql=${encodeURIComponent(event.settings.cql)}`);

if (this.isJiraServer(event.settings)) {
this.openURL(`${this.getUrl(event.settings)}/dosearchsite.action?cql=${encodeURIComponent(event.settings.cql)}`);
}
else {
const cql = encodeURIComponent(event.settings.cql);
if (cql) {
this.openURL(`${this.getUrl(event.settings)}/search?cql=${cql}`);
}
else {
this.openURL(`${this.getUrl(event.settings)}/home`);
}
}
}

/**
Expand All @@ -94,12 +104,10 @@ class ConfluenceSearch extends BaseJiraAction<CountableResponse<CQLResponse>, Co
};
}

const client = JiraConnection.getClient(context.settings);

const apiContext = context.settings.strategy === 'PAT' ? '' : 'wiki';
const client = this.getJiraClient(context.settings);

const response = await client.request<CQLResponse>({
endpoint: `${apiContext}/rest/api/search`,
endpoint: `rest/api/search`,
query: {
cql: cql,
limit: '5',
Expand Down
37 changes: 26 additions & 11 deletions src/actions/ConfluenceTasks.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { KeyDownEvent } from "@fnando/streamdeck";
import { JiraConnection } from "../JiraConnection";
import { ConfluenceTasksSettings } from "../JiraPluginSettings";
import BaseJiraAction, { CountableResponse } from "./BaseJiraAction";
import { CountableResponse } from "./BaseJiraAction";
import { ActionPollingContext } from "./PollingAction";
import BaseConfluenceAction from "./BaseConfluenceAction";

/**
* Query parameters for filtering inline tasks.
Expand Down Expand Up @@ -77,15 +77,13 @@ interface ConfluenceUser {
/**
* Periodically polls Confluence to get the number of inline tasks assigned to the current user.
*/
class ConfluenceTasks extends BaseJiraAction<CountableResponse<InlineTasksResponse>, ConfluenceTasksSettings> {
class ConfluenceTasks extends BaseConfluenceAction<CountableResponse<InlineTasksResponse>, ConfluenceTasksSettings> {
/**
* {@inheritDoc}
*/
handleKeyDown(event: KeyDownEvent<ConfluenceTasksSettings>): void {
super.handleKeyDown(event);

const apiContext = event.settings.strategy === 'PAT' ? '' : '/wiki';
this.openURL(`https://${event.settings.domain}${apiContext}/plugins/inlinetasks/mytasks.action`);
this.openURL(`${this.getUrl(event.settings)}/plugins/inlinetasks/mytasks.action`);
}

/**
Expand All @@ -96,15 +94,31 @@ class ConfluenceTasks extends BaseJiraAction<CountableResponse<InlineTasksRespon

if (!domain) {
return {
count: 0
count: 0,
};
}

const client = JiraConnection.getClient(context.settings);
const client = this.getJiraClient(context.settings);

if (this.isJiraServer(context.settings)) {
// In JIRA Server, getting the count of inline tasks
// is part of Notifications and Tasks REST API.
// There doesn't seem to be a way to filter by due date.
const response = await client.request<number>({
endpoint: 'rest/mywork/task/count',
query: {
completed: 'false',
},
});

return {
count: response.body,
};
}

const apiContext = context.settings.strategy === 'PAT' ? '' : 'wiki';
// https://developer.atlassian.com/cloud/confluence/rest/v1/api-group-users/#api-wiki-rest-api-user-current-get
const currentUserResponse = await client.request<ConfluenceUser>({
endpoint: `${apiContext}/rest/api/user/current`
endpoint: `rest/api/user/current`
});

const filter: InlineTaskFilter = {
Expand All @@ -120,8 +134,9 @@ class ConfluenceTasks extends BaseJiraAction<CountableResponse<InlineTasksRespon
filter.duedateTo = Date.parse(dueDateTo);
}

// https://developer.atlassian.com/cloud/confluence/rest/v1/api-group-inline-tasks/#api-wiki-rest-api-inlinetasks-search-get
const response = await client.request<InlineTasksResponse>({
endpoint: `${apiContext}/rest/api/inlinetasks/search`,
endpoint: `rest/api/inlinetasks/search`,
query: {...filter},
});

Expand Down
4 changes: 2 additions & 2 deletions src/actions/Query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class Query extends BaseJiraAction<CountableResponse<SearchResponse>, JQLQuerySe
this.getPollingClient()?.poll();
break;
case 'ViewFilter':
this.openURL(`https://${event.settings.domain}/issues/?jql=${encodeURIComponent(event.settings.jql)}`);
this.openURL(`${this.getUrl(event.settings)}/issues/?jql=${encodeURIComponent(event.settings.jql)}`);
break;
default: {
const action = event.settings.keyAction;
Expand Down Expand Up @@ -93,7 +93,7 @@ class Query extends BaseJiraAction<CountableResponse<SearchResponse>, JQLQuerySe
* @returns The URL to the issue.
*/
protected getIssueUrl(issue: Issue, settings: JQLQuerySettings): string {
return `https://${settings.domain}/browse/${issue.key}`;
return `${this.getUrl(settings)}/browse/${issue.key}`;
}

}
Expand Down
15 changes: 15 additions & 0 deletions src/inspectors/Components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export class AuthenticationComponent extends PropertyInspectorComponent<DefaultP
private email: HTMLInputElement;
private token: HTMLInputElement;
private tokenType: HTMLSelectElement;
private contextPath: HTMLInputElement;

/**
* {@inheritDoc}
Expand All @@ -91,11 +92,23 @@ export class AuthenticationComponent extends PropertyInspectorComponent<DefaultP
* {@inheritDoc}
*/
get value(): DefaultPluginSettings {
// The PAT authentication strategy is only used for JIRA Server.
// A custom context path is only supported for JIRA Server,
// so if the authentication method is not PAT, then there can be
// no custom context path.
if (this.tokenType.value !== 'PAT') {
this.contextPath.value = '';
}

return {
domain: this.domain.value
.replace(/^https?:\/\//, '')
.replace(/\/.*$/, '')
.trim(),
context: this.contextPath.value
// Remove leading and trailing slashes
.replace(/^\/+|\/+$/g, '')
.trim(),
email: this.email.value.trim(),
token: this.token.value.trim(),
strategy: <'APIToken'|'PAT'>this.tokenType.value,
Expand All @@ -107,6 +120,7 @@ export class AuthenticationComponent extends PropertyInspectorComponent<DefaultP
*/
set value(newValue: DefaultPluginSettings) {
this.domain.value = newValue.domain;
this.contextPath.value = newValue.context;
this.email.value = newValue.email;
this.token.value = newValue.token;
this.tokenType.value = newValue.strategy;
Expand All @@ -122,6 +136,7 @@ export class AuthenticationComponent extends PropertyInspectorComponent<DefaultP
this.email = this.querySelector('#email');
this.token = this.querySelector('#token');
this.tokenType = this.querySelector('#token-type');
this.contextPath = this.querySelector('#context-path');
}

/**
Expand Down
7 changes: 7 additions & 0 deletions src/inspectors/ConfluenceSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,15 @@ class ConfluenceSearchActionPropertyInspector extends PollingActionInspector<Con
...this.icon.value,
};

if (settings.strategy === 'APIToken' && !settings.context) {
// On JIRA Cloud, all Confluence-related APIs use the `wiki` context path.
settings.context = 'wiki';
}

this.setSettings(settings);
this.setGlobalSettings({
domain: settings.domain,
context: settings.context,
email: settings.email,
token: settings.token,
strategy: settings.strategy,
Expand All @@ -57,6 +63,7 @@ class ConfluenceSearchActionPropertyInspector extends PollingActionInspector<Con
protected getDefaultSettings(): ConfluenceSearchSettings {
return {
domain: this.globalSettings.domain ?? '',
context: this.globalSettings.context ?? 'wiki',
email: this.globalSettings.email ?? '',
token: this.globalSettings.token ?? '',
strategy: this.globalSettings.strategy ?? 'APIToken',
Expand Down
7 changes: 7 additions & 0 deletions src/inspectors/ConfluenceTasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,15 @@ class ConfluenceTasksActionPropertyInspector extends PollingActionInspector<Conf
...this.icon.value,
};

if (settings.strategy === 'APIToken' && !settings.context) {
// On JIRA Cloud, all Confluence-related APIs use the `wiki` context path.
settings.context = 'wiki';
}

this.setSettings(settings);
this.setGlobalSettings({
domain: settings.domain,
context: settings.context,
email: settings.email,
token: settings.token,
strategy: settings.strategy,
Expand All @@ -60,6 +66,7 @@ class ConfluenceTasksActionPropertyInspector extends PollingActionInspector<Conf
protected getDefaultSettings(): ConfluenceTasksSettings {
return {
domain: this.globalSettings.domain ?? '',
context: this.globalSettings.context ?? 'wiki',
email: this.globalSettings.email ?? '',
token: this.globalSettings.token ?? '',
strategy: this.globalSettings.strategy ?? 'APIToken',
Expand Down
Loading

0 comments on commit 092ad7a

Please sign in to comment.