mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
* Wip: Initial commit * Refactor: Adds support in Loki datasource for streaming * Refactor: Adds Live option to RefreshInterval * Refactor: Adds styles to logrows * Style: Reverses the order of Explore layout on Live * Refactor: Adds LiveLogs component * Tests: Adds tests for epics * Style: Adds animation to Live in RefreshPicker * Refactor: Adds ElapsedTime and progress line to LiveLogs * Style: Adds specific colors to each theme * Refactor: Adds support for Lokis new API * Fix: Adds null to resulting empty array * Refactor: Limits the rate of incoming messages from websockets * Refactor: Throttles messages instead for simplicity * Refactor: Optimizes row processing performance * Refactor: Adds stop live button * Fix: Fixes so that RefreshPicker shows the correct value when called programmatically * Refactor: Merges with master and removes a console.log * Refactor: Sorts rows in correct order and fixes minor UI issues * Refactor: Adds minor improvments to sorting and container size
551 lines
20 KiB
TypeScript
551 lines
20 KiB
TypeScript
import { liveOption } from '@grafana/ui/src/components/RefreshPicker/RefreshPicker';
|
|
import { DataSourceApi, DataQuery } from '@grafana/ui/src/types/datasource';
|
|
|
|
import { ExploreId, ExploreState } from 'app/types';
|
|
import { actionCreatorFactory } from 'app/core/redux/actionCreatorFactory';
|
|
import {
|
|
startSubscriptionsEpic,
|
|
startSubscriptionsAction,
|
|
SubscriptionDataReceivedPayload,
|
|
startSubscriptionAction,
|
|
startSubscriptionEpic,
|
|
limitMessageRatePayloadAction,
|
|
} from './epics';
|
|
import { makeExploreItemState } from './reducers';
|
|
import { epicTester } from 'test/core/redux/epicTester';
|
|
import {
|
|
resetExploreAction,
|
|
updateDatasourceInstanceAction,
|
|
changeRefreshIntervalAction,
|
|
clearQueriesAction,
|
|
} from './actionTypes';
|
|
|
|
const setup = (options: any = {}) => {
|
|
const url = '/api/datasources/proxy/20/api/prom/tail?query=%7Bfilename%3D%22%2Fvar%2Flog%2Fdocker.log%22%7D';
|
|
const webSocketUrl = 'ws://localhost' + url;
|
|
const refId = options.refId || 'A';
|
|
const exploreId = ExploreId.left;
|
|
const datasourceInstance: DataSourceApi = options.datasourceInstance || {
|
|
id: 1337,
|
|
query: jest.fn(),
|
|
name: 'test',
|
|
testDatasource: jest.fn(),
|
|
convertToStreamTargets: () => [
|
|
{
|
|
url,
|
|
refId,
|
|
},
|
|
],
|
|
resultToSeriesData: data => [data],
|
|
};
|
|
const itemState = makeExploreItemState();
|
|
const explore: Partial<ExploreState> = {
|
|
[exploreId]: {
|
|
...itemState,
|
|
datasourceInstance,
|
|
refreshInterval: options.refreshInterval || liveOption.value,
|
|
queries: [{} as DataQuery],
|
|
},
|
|
};
|
|
const state: any = {
|
|
explore,
|
|
};
|
|
|
|
return { url, state, refId, webSocketUrl, exploreId };
|
|
};
|
|
|
|
const dataReceivedActionCreator = actionCreatorFactory<SubscriptionDataReceivedPayload>('test').create();
|
|
|
|
describe('startSubscriptionsEpic', () => {
|
|
describe('when startSubscriptionsAction is dispatched', () => {
|
|
describe('and datasource supports convertToStreamTargets', () => {
|
|
describe('and explore is Live', () => {
|
|
it('then correct actions should be dispatched', () => {
|
|
const { state, refId, webSocketUrl, exploreId } = setup();
|
|
|
|
epicTester(startSubscriptionsEpic, state)
|
|
.whenActionIsDispatched(startSubscriptionsAction({ exploreId, dataReceivedActionCreator }))
|
|
.thenResultingActionsEqual(
|
|
startSubscriptionAction({
|
|
exploreId,
|
|
refId,
|
|
url: webSocketUrl,
|
|
dataReceivedActionCreator,
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('and explore is not Live', () => {
|
|
it('then no actions should be dispatched', () => {
|
|
const { state, exploreId } = setup({ refreshInterval: '10s' });
|
|
|
|
epicTester(startSubscriptionsEpic, state)
|
|
.whenActionIsDispatched(startSubscriptionsAction({ exploreId, dataReceivedActionCreator }))
|
|
.thenNoActionsWhereDispatched();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('and datasource does not support streaming', () => {
|
|
it('then no actions should be dispatched', () => {
|
|
const { state, exploreId } = setup({ datasourceInstance: {} });
|
|
|
|
epicTester(startSubscriptionsEpic, state)
|
|
.whenActionIsDispatched(startSubscriptionsAction({ exploreId, dataReceivedActionCreator }))
|
|
.thenNoActionsWhereDispatched();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('startSubscriptionEpic', () => {
|
|
describe('when startSubscriptionAction is dispatched', () => {
|
|
describe('and datasource supports resultToSeriesData', () => {
|
|
it('then correct actions should be dispatched', () => {
|
|
const { state, webSocketUrl, refId, exploreId } = setup();
|
|
|
|
epicTester(startSubscriptionEpic, state)
|
|
.whenActionIsDispatched(
|
|
startSubscriptionAction({ url: webSocketUrl, refId, exploreId, dataReceivedActionCreator })
|
|
)
|
|
.thenNoActionsWhereDispatched()
|
|
.whenWebSocketReceivesData({ data: [1, 2, 3] })
|
|
.thenResultingActionsEqual(
|
|
limitMessageRatePayloadAction({
|
|
exploreId,
|
|
data: { data: [1, 2, 3] } as any,
|
|
dataReceivedActionCreator,
|
|
})
|
|
)
|
|
.whenWebSocketReceivesData({ data: [4, 5, 6] })
|
|
.thenResultingActionsEqual(
|
|
limitMessageRatePayloadAction({
|
|
exploreId,
|
|
data: { data: [1, 2, 3] } as any,
|
|
dataReceivedActionCreator,
|
|
}),
|
|
limitMessageRatePayloadAction({
|
|
exploreId,
|
|
data: { data: [4, 5, 6] } as any,
|
|
dataReceivedActionCreator,
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('and datasource does not support resultToSeriesData', () => {
|
|
it('then no actions should be dispatched', () => {
|
|
const { state, webSocketUrl, refId, exploreId } = setup({ datasourceInstance: {} });
|
|
|
|
epicTester(startSubscriptionEpic, state)
|
|
.whenActionIsDispatched(
|
|
startSubscriptionAction({ url: webSocketUrl, refId, exploreId, dataReceivedActionCreator })
|
|
)
|
|
.thenNoActionsWhereDispatched()
|
|
.whenWebSocketReceivesData({ data: [1, 2, 3] })
|
|
.thenNoActionsWhereDispatched();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('when an subscription is active', () => {
|
|
describe('and resetExploreAction is dispatched', () => {
|
|
it('then subscription should be unsubscribed', () => {
|
|
const { state, webSocketUrl, refId, exploreId } = setup();
|
|
|
|
epicTester(startSubscriptionEpic, state)
|
|
.whenActionIsDispatched(
|
|
startSubscriptionAction({ url: webSocketUrl, refId, exploreId, dataReceivedActionCreator })
|
|
)
|
|
.thenNoActionsWhereDispatched()
|
|
.whenWebSocketReceivesData({ data: [1, 2, 3] })
|
|
.thenResultingActionsEqual(
|
|
limitMessageRatePayloadAction({
|
|
exploreId,
|
|
data: { data: [1, 2, 3] } as any,
|
|
dataReceivedActionCreator,
|
|
})
|
|
)
|
|
.whenActionIsDispatched(resetExploreAction())
|
|
.whenWebSocketReceivesData({ data: [4, 5, 6] })
|
|
.thenResultingActionsEqual(
|
|
limitMessageRatePayloadAction({
|
|
exploreId,
|
|
data: { data: [1, 2, 3] } as any,
|
|
dataReceivedActionCreator,
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('and updateDatasourceInstanceAction is dispatched', () => {
|
|
describe('and exploreId matches the websockets', () => {
|
|
it('then subscription should be unsubscribed', () => {
|
|
const { state, webSocketUrl, refId, exploreId } = setup();
|
|
|
|
epicTester(startSubscriptionEpic, state)
|
|
.whenActionIsDispatched(
|
|
startSubscriptionAction({
|
|
url: webSocketUrl,
|
|
refId,
|
|
exploreId,
|
|
dataReceivedActionCreator,
|
|
})
|
|
)
|
|
.thenNoActionsWhereDispatched()
|
|
.whenWebSocketReceivesData({ data: [1, 2, 3] })
|
|
.thenResultingActionsEqual(
|
|
limitMessageRatePayloadAction({
|
|
exploreId,
|
|
data: { data: [1, 2, 3] } as any,
|
|
dataReceivedActionCreator,
|
|
})
|
|
)
|
|
.whenActionIsDispatched(updateDatasourceInstanceAction({ exploreId, datasourceInstance: null }))
|
|
.whenWebSocketReceivesData({ data: [4, 5, 6] })
|
|
.thenResultingActionsEqual(
|
|
limitMessageRatePayloadAction({
|
|
exploreId,
|
|
data: { data: [1, 2, 3] } as any,
|
|
dataReceivedActionCreator,
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('and exploreId does not match the websockets', () => {
|
|
it('then subscription should not be unsubscribed', () => {
|
|
const { state, webSocketUrl, refId, exploreId } = setup();
|
|
|
|
epicTester(startSubscriptionEpic, state)
|
|
.whenActionIsDispatched(
|
|
startSubscriptionAction({
|
|
url: webSocketUrl,
|
|
refId,
|
|
exploreId,
|
|
dataReceivedActionCreator,
|
|
})
|
|
)
|
|
.thenNoActionsWhereDispatched()
|
|
.whenWebSocketReceivesData({ data: [1, 2, 3] })
|
|
.thenResultingActionsEqual(
|
|
limitMessageRatePayloadAction({
|
|
exploreId,
|
|
data: { data: [1, 2, 3] } as any,
|
|
dataReceivedActionCreator,
|
|
})
|
|
)
|
|
.whenActionIsDispatched(
|
|
updateDatasourceInstanceAction({ exploreId: ExploreId.right, datasourceInstance: null })
|
|
)
|
|
.whenWebSocketReceivesData({ data: [4, 5, 6] })
|
|
.thenResultingActionsEqual(
|
|
limitMessageRatePayloadAction({
|
|
exploreId,
|
|
data: { data: [1, 2, 3] } as any,
|
|
dataReceivedActionCreator,
|
|
}),
|
|
limitMessageRatePayloadAction({
|
|
exploreId,
|
|
data: { data: [4, 5, 6] } as any,
|
|
dataReceivedActionCreator,
|
|
})
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('and changeRefreshIntervalAction is dispatched', () => {
|
|
describe('and exploreId matches the websockets', () => {
|
|
describe('and refreshinterval is not "Live"', () => {
|
|
it('then subscription should be unsubscribed', () => {
|
|
const { state, webSocketUrl, refId, exploreId } = setup();
|
|
|
|
epicTester(startSubscriptionEpic, state)
|
|
.whenActionIsDispatched(
|
|
startSubscriptionAction({
|
|
url: webSocketUrl,
|
|
refId,
|
|
exploreId,
|
|
dataReceivedActionCreator,
|
|
})
|
|
)
|
|
.thenNoActionsWhereDispatched()
|
|
.whenWebSocketReceivesData({ data: [1, 2, 3] })
|
|
.thenResultingActionsEqual(
|
|
limitMessageRatePayloadAction({
|
|
exploreId,
|
|
data: { data: [1, 2, 3] } as any,
|
|
dataReceivedActionCreator,
|
|
})
|
|
)
|
|
.whenActionIsDispatched(changeRefreshIntervalAction({ exploreId, refreshInterval: '10s' }))
|
|
.whenWebSocketReceivesData({ data: [4, 5, 6] })
|
|
.thenResultingActionsEqual(
|
|
limitMessageRatePayloadAction({
|
|
exploreId,
|
|
data: { data: [1, 2, 3] } as any,
|
|
dataReceivedActionCreator,
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('and refreshinterval is "Live"', () => {
|
|
it('then subscription should not be unsubscribed', () => {
|
|
const { state, webSocketUrl, refId, exploreId } = setup();
|
|
|
|
epicTester(startSubscriptionEpic, state)
|
|
.whenActionIsDispatched(
|
|
startSubscriptionAction({
|
|
url: webSocketUrl,
|
|
refId,
|
|
exploreId,
|
|
dataReceivedActionCreator,
|
|
})
|
|
)
|
|
.thenNoActionsWhereDispatched()
|
|
.whenWebSocketReceivesData({ data: [1, 2, 3] })
|
|
.thenResultingActionsEqual(
|
|
limitMessageRatePayloadAction({
|
|
exploreId,
|
|
data: { data: [1, 2, 3] } as any,
|
|
dataReceivedActionCreator,
|
|
})
|
|
)
|
|
.whenActionIsDispatched(changeRefreshIntervalAction({ exploreId, refreshInterval: liveOption.value }))
|
|
.whenWebSocketReceivesData({ data: [4, 5, 6] })
|
|
.thenResultingActionsEqual(
|
|
limitMessageRatePayloadAction({
|
|
exploreId,
|
|
data: { data: [1, 2, 3] } as any,
|
|
dataReceivedActionCreator,
|
|
}),
|
|
limitMessageRatePayloadAction({
|
|
exploreId,
|
|
data: { data: [4, 5, 6] } as any,
|
|
dataReceivedActionCreator,
|
|
})
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('and exploreId does not match the websockets', () => {
|
|
it('then subscription should not be unsubscribed', () => {
|
|
const { state, webSocketUrl, refId, exploreId } = setup();
|
|
|
|
epicTester(startSubscriptionEpic, state)
|
|
.whenActionIsDispatched(
|
|
startSubscriptionAction({
|
|
url: webSocketUrl,
|
|
refId,
|
|
exploreId,
|
|
dataReceivedActionCreator,
|
|
})
|
|
)
|
|
.thenNoActionsWhereDispatched()
|
|
.whenWebSocketReceivesData({ data: [1, 2, 3] })
|
|
.thenResultingActionsEqual(
|
|
limitMessageRatePayloadAction({
|
|
exploreId,
|
|
data: { data: [1, 2, 3] } as any,
|
|
dataReceivedActionCreator,
|
|
})
|
|
)
|
|
.whenActionIsDispatched(changeRefreshIntervalAction({ exploreId: ExploreId.right, refreshInterval: '10s' }))
|
|
.whenWebSocketReceivesData({ data: [4, 5, 6] })
|
|
.thenResultingActionsEqual(
|
|
limitMessageRatePayloadAction({
|
|
exploreId,
|
|
data: { data: [1, 2, 3] } as any,
|
|
dataReceivedActionCreator,
|
|
}),
|
|
limitMessageRatePayloadAction({
|
|
exploreId,
|
|
data: { data: [4, 5, 6] } as any,
|
|
dataReceivedActionCreator,
|
|
})
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('and clearQueriesAction is dispatched', () => {
|
|
describe('and exploreId matches the websockets', () => {
|
|
it('then subscription should be unsubscribed', () => {
|
|
const { state, webSocketUrl, refId, exploreId } = setup();
|
|
|
|
epicTester(startSubscriptionEpic, state)
|
|
.whenActionIsDispatched(
|
|
startSubscriptionAction({
|
|
url: webSocketUrl,
|
|
refId,
|
|
exploreId,
|
|
dataReceivedActionCreator,
|
|
})
|
|
)
|
|
.thenNoActionsWhereDispatched()
|
|
.whenWebSocketReceivesData({ data: [1, 2, 3] })
|
|
.thenResultingActionsEqual(
|
|
limitMessageRatePayloadAction({
|
|
exploreId,
|
|
data: { data: [1, 2, 3] } as any,
|
|
dataReceivedActionCreator,
|
|
})
|
|
)
|
|
.whenActionIsDispatched(clearQueriesAction({ exploreId }))
|
|
.whenWebSocketReceivesData({ data: [4, 5, 6] })
|
|
.thenResultingActionsEqual(
|
|
limitMessageRatePayloadAction({
|
|
exploreId,
|
|
data: { data: [1, 2, 3] } as any,
|
|
dataReceivedActionCreator,
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('and exploreId does not match the websockets', () => {
|
|
it('then subscription should not be unsubscribed', () => {
|
|
const { state, webSocketUrl, refId, exploreId } = setup();
|
|
|
|
epicTester(startSubscriptionEpic, state)
|
|
.whenActionIsDispatched(
|
|
startSubscriptionAction({
|
|
url: webSocketUrl,
|
|
refId,
|
|
exploreId,
|
|
dataReceivedActionCreator,
|
|
})
|
|
)
|
|
.thenNoActionsWhereDispatched()
|
|
.whenWebSocketReceivesData({ data: [1, 2, 3] })
|
|
.thenResultingActionsEqual(
|
|
limitMessageRatePayloadAction({
|
|
exploreId,
|
|
data: { data: [1, 2, 3] } as any,
|
|
dataReceivedActionCreator,
|
|
})
|
|
)
|
|
.whenActionIsDispatched(clearQueriesAction({ exploreId: ExploreId.right }))
|
|
.whenWebSocketReceivesData({ data: [4, 5, 6] })
|
|
.thenResultingActionsEqual(
|
|
limitMessageRatePayloadAction({
|
|
exploreId,
|
|
data: { data: [1, 2, 3] } as any,
|
|
dataReceivedActionCreator,
|
|
}),
|
|
limitMessageRatePayloadAction({
|
|
exploreId,
|
|
data: { data: [4, 5, 6] } as any,
|
|
dataReceivedActionCreator,
|
|
})
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('and startSubscriptionAction is dispatched', () => {
|
|
describe('and exploreId and refId matches the websockets', () => {
|
|
it('then subscription should be unsubscribed', () => {
|
|
const { state, webSocketUrl, refId, exploreId } = setup();
|
|
|
|
epicTester(startSubscriptionEpic, state)
|
|
.whenActionIsDispatched(
|
|
startSubscriptionAction({
|
|
url: webSocketUrl,
|
|
refId,
|
|
exploreId,
|
|
dataReceivedActionCreator,
|
|
})
|
|
)
|
|
.thenNoActionsWhereDispatched()
|
|
.whenWebSocketReceivesData({ data: [1, 2, 3] })
|
|
.thenResultingActionsEqual(
|
|
limitMessageRatePayloadAction({
|
|
exploreId,
|
|
data: { data: [1, 2, 3] } as any,
|
|
dataReceivedActionCreator,
|
|
})
|
|
)
|
|
.whenActionIsDispatched(
|
|
startSubscriptionAction({
|
|
url: webSocketUrl,
|
|
refId,
|
|
exploreId,
|
|
dataReceivedActionCreator,
|
|
})
|
|
)
|
|
.whenWebSocketReceivesData({ data: [4, 5, 6] })
|
|
.thenResultingActionsEqual(
|
|
limitMessageRatePayloadAction({
|
|
exploreId,
|
|
data: { data: [1, 2, 3] } as any,
|
|
dataReceivedActionCreator,
|
|
}),
|
|
limitMessageRatePayloadAction({
|
|
exploreId,
|
|
data: { data: [4, 5, 6] } as any,
|
|
dataReceivedActionCreator,
|
|
})
|
|
// This looks like we haven't stopped the subscription but we actually started the same again
|
|
);
|
|
});
|
|
|
|
describe('and exploreId or refId does not match the websockets', () => {
|
|
it('then subscription should not be unsubscribed and another websocket is started', () => {
|
|
const { state, webSocketUrl, refId, exploreId } = setup();
|
|
|
|
epicTester(startSubscriptionEpic, state)
|
|
.whenActionIsDispatched(
|
|
startSubscriptionAction({
|
|
url: webSocketUrl,
|
|
refId,
|
|
exploreId,
|
|
dataReceivedActionCreator,
|
|
})
|
|
)
|
|
.thenNoActionsWhereDispatched()
|
|
.whenWebSocketReceivesData({ data: [1, 2, 3] })
|
|
.thenResultingActionsEqual(
|
|
limitMessageRatePayloadAction({
|
|
exploreId,
|
|
data: { data: [1, 2, 3] } as any,
|
|
dataReceivedActionCreator,
|
|
})
|
|
)
|
|
.whenActionIsDispatched(
|
|
startSubscriptionAction({
|
|
url: webSocketUrl,
|
|
refId: 'B',
|
|
exploreId,
|
|
dataReceivedActionCreator,
|
|
})
|
|
)
|
|
.whenWebSocketReceivesData({ data: [4, 5, 6] })
|
|
.thenResultingActionsEqual(
|
|
limitMessageRatePayloadAction({
|
|
exploreId,
|
|
data: { data: [1, 2, 3] } as any,
|
|
dataReceivedActionCreator,
|
|
}),
|
|
limitMessageRatePayloadAction({
|
|
exploreId,
|
|
data: { data: [4, 5, 6] } as any,
|
|
dataReceivedActionCreator,
|
|
}),
|
|
limitMessageRatePayloadAction({
|
|
exploreId,
|
|
data: { data: [4, 5, 6] } as any,
|
|
dataReceivedActionCreator,
|
|
})
|
|
);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|