0%

用React重寫Elmah畫面

我長期以來一直習慣用Elmah來紀錄錯誤日誌(但沒有用elmah.io),而它本身雖然有提供檢視頁面,但功能卻十分基本,甚至連搜索功能都沒有,既然如此,今天就來自己寫一個吧。

學習目標

  • React : 一直沒有時間學,終於找到這次的機會了,就結合.Net Core的React專案一起來研究吧
  • Material-UI : 畫面的部份當然也要找個React的知名UI Component來減少設計時間
  • 簡單的搜尋功能 : 我想要為Elmah加入的功能

架構

整個環境大致如下圖。

假設有多個站台,發生錯誤時將資訊記錄到DB,而要重作畫面的話就必須重寫存取這些資料的邏輯以及畫面,也就是圖片右半部的WebApi與React View,而這部分我們可以直接使用Vsiaul Studio提供的.Net Core React Project來完成。

讀取錯誤日誌DB

這裡用EntityFramework來做到簡易的串接DB,跟上面架構圖比較不同的是,因為我自己維護的錯誤日誌散落在不同架構的DB內(SqlServer、Postgres),所以這裡多了一層Factory來幫助我建立不同DB的Context。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ElmahDbContextFactory : IDesignTimeDbContextFactory<ElmahDbContext>
{
public ElmahDbContext CreateDbContext(string[] args)
{
var dataSource = args[0];
var catalog = args[1];
var userId = args[2];
var password = args[3];
var provider = args[4];
var optionsBuilder = new DbContextOptionsBuilder<ElmahDbContext>();
if (DatabaseProviderTypes.SqlServer.ToString() == provider)
{
optionsBuilder.UseSqlServer($"Data Source={dataSource};Initial Catalog={catalog};User ID={userId};Password={password};multipleactiveresultsets=True;");
}
if (DatabaseProviderTypes.PostgresQL.ToString() == provider)
{
optionsBuilder.UseNpgsql($"Server={dataSource};Database={catalog};User Id={userId};Password={password};");
}
return new ElmahDbContext(optionsBuilder.Options);
}
}

接下來實作查詢錯誤日誌列表的Api,這裡可以用剛剛的Factory產生Context讀取錯誤日誌,除了能搜尋ErrorMessage之外,我還想要有分頁功能,所以Model中加入了一些必要參數。

1
2
3
4
5
6
7
8
9
10
public class ReceiveGetLogsModel
{
public int EntityId { get; set; }

public int Limit { get; set; }

public int CurrentPage { get; set; }

public string SearchMessage { get; set; }
}
1
2
3
4
5
6
public class ResponseGetLogsModel
{
public List<ResponseGetLogEntity> Datas { get; set; }

public int Count { get; set; }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
[HttpPost("[action]")]
public ResponseGetLogsModel GetLogs([FromBody]ReceiveGetLogsModel model)
{
var entity = Db.Elmahs.FirstOrDefault(s => s.Id == model.EntityId);
var factory = new ElmahDbContextFactory();
var elmah = factory.CreateDbContext(new string[] {
entity.DataSource,
entity.CataLog,
entity.UserId,
entity.Password,
entity.ProviderType.ToString()
});
var query = elmah.Errors.AsQueryable();
if (!string.IsNullOrEmpty(model.SearchMessage))
{
query = query.Where(s => s.Message.Contains(model.SearchMessage));
}
var count = query.Count();
var pageDatas = query.Skip((model.CurrentPage - 1) * model.Limit)
.Take(model.Limit)
.Select(s => new ResponseGetLogEntity()
{
ErrorId = s.ErrorId,
Application = s.Application,
Host = s.Host,
Message = s.Message,
Sequence = s.Sequence,
Source = s.Source,
StatusCode = s.StatusCode,
TimeUtc = s.TimeUtc,
Type = s.Type,
User = s.User
})
.ToList();
return new ResponseGetLogsModel()
{
Datas = pageDatas,
Count = count
};
}

這樣就簡單的完成該功能了,實際用Postman測試一下看是否運作正常。

設計畫面

Material-UI官方推薦了一些不錯的Theme,這裡也是直接選擇免費的Material Dashboard,下載完後把相關東西放進我們的.Net Core專案的ClientApp目錄下,應該就可以正常執行了。

把專案站台啟動起來,應該可以看到Preview中的漂亮Dashboard,React專案中也提供了許多現成的View來教開發者如何使用它們提供的UI Component,稍微看一下後就可以開始修改和去除我們不需要的東西,大致結果如下圖。

串接資料

前端的大致框架搞定後,接下來要把資料接回來並顯示在畫面上,這裡可以順道學習一下Redux,建立Store來處理這件事情。

state

1
2
3
4
5
6
7
8
9
10
11
12
const requestGetLogs = 'REQUEST_GET_LOGS';
const receiveGetLogs = 'RECEIVE_GET_LOGS';
const initialState = {
logs: {
count: 0,
currentPage: 1,
searchMessage: '',
limit: 10,
data: []
},
isLoading: false
};

action

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
export const actionCreators = {
requestGetLogs: (currentPage = 1, limit = 10, searchMessage = '') => async (dispatch, getState) => {
dispatch({ type: requestGetLogs });
const url = '/api/Elmah/GetLogs';
const response = await fetch(url, {
method: 'POST',
body: JSON.stringify({
EntityId: getState().elmah.activeEntityId,
CurrentPage: currentPage,
Limit: limit,
SearchMessage: searchMessage
}),
headers: {
'content-type': 'application/json'
}
});
const data = await response.json();
dispatch({
type: receiveGetLogs,
datas: data.datas,
count: data.count,
currentPage: currentPage,
searchMessage: searchMessage,
limit: limit });
}
};

reducer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
export const reducer = (state, action) => {
state = state || initialState;
if (action.type === requestGetLogs) {
return {
...state,
logs: {
data: []
},
isLoading: true
};
}
if (action.type === receiveGetLogs) {
return {
...state,
logs: {
data: action.datas,
currentPage: action.currentPage,
limit: action.limit,
count: action.count,
searchMessage: action.searchMessage
},
isLoading: false
};
}
return state;
};

再將State和Dispatche綁到我們自己寫的View Component上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Logs extends React.Component {
componentWillMount() {
let page = this.props.match.params.page || 1;
let limit = this.props.match.params.limit || 10;
let message = this.props.match.params.message || '';
this
.props
.requestGetLogs(parseInt(page), parseInt(limit), message);
}
//此處過長,在文章中略過
}
const mapStateToProps = state => ({elmah: state.elmah});
const mapDispatchToProps = dispatch => bindActionCreators(actionCreators, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(Logs);

資料就可以正常顯示了。

詳情頁我們也可以依樣畫葫蘆,這樣近似於原本Elmah的功能就完成了。

至於搜尋功能,包含API和畫面都是我們自己做的,未來擴充也相當方便。

結語

所有學習目標應該都完成了,目前Angular、Vue、React通通都寫過,個人覺得React相較於前兩者真的是有比較難一點,而這個專案未來我也會抽時間繼續開發,希望有機會能幫到同為使用Elmah的大家。